commented unused variables
[vsorcdistro/.git] / mininet / examples / consoles.py
1 #!/usr/bin/python
2
3 """
4 consoles.py: bring up a bunch of miniature consoles on a virtual network
5
6 This demo shows how to monitor a set of nodes by using
7 Node's monitor() and Tkinter's createfilehandler().
8
9 We monitor nodes in a couple of ways:
10
11 - First, each individual node is monitored, and its output is added
12   to its console window
13
14 - Second, each time a console window gets iperf output, it is parsed
15   and accumulated. Once we have output for all consoles, a bar is
16   added to the bandwidth graph.
17
18 The consoles also support limited interaction:
19
20 - Pressing "return" in a console will send a command to it
21
22 - Pressing the console's title button will open up an xterm
23
24 Bob Lantz, April 2010
25
26 """
27
28 import re
29
30 from Tkinter import Frame, Button, Label, Text, Scrollbar, Canvas, Wm, READABLE
31
32 from mininet.log import setLogLevel
33 from mininet.topolib import TreeNet
34 from mininet.term import makeTerms, cleanUpScreens
35 from mininet.util import quietRun
36
37 class Console( Frame ):
38     "A simple console on a host."
39
40     def __init__( self, parent, net, node, height=10, width=32, title='Node' ):
41         Frame.__init__( self, parent )
42
43         self.net = net
44         self.node = node
45         self.prompt = node.name + '# '
46         self.height, self.width, self.title = height, width, title
47
48         # Initialize widget styles
49         self.buttonStyle = { 'font': 'Monaco 7' }
50         self.textStyle = {
51             'font': 'Monaco 7',
52             'bg': 'black',
53             'fg': 'green',
54             'width': self.width,
55             'height': self.height,
56             'relief': 'sunken',
57             'insertbackground': 'green',
58             'highlightcolor': 'green',
59             'selectforeground': 'black',
60             'selectbackground': 'green'
61         }
62
63         # Set up widgets
64         self.text = self.makeWidgets( )
65         self.bindEvents()
66         self.sendCmd( 'export TERM=dumb' )
67
68         self.outputHook = None
69
70     def makeWidgets( self ):
71         "Make a label, a text area, and a scroll bar."
72
73         def newTerm( net=self.net, node=self.node, title=self.title ):
74             "Pop up a new terminal window for a node."
75             net.terms += makeTerms( [ node ], title )
76         label = Button( self, text=self.node.name, command=newTerm,
77                         **self.buttonStyle )
78         label.pack( side='top', fill='x' )
79         text = Text( self, wrap='word', **self.textStyle )
80         ybar = Scrollbar( self, orient='vertical', width=7,
81                           command=text.yview )
82         text.configure( yscrollcommand=ybar.set )
83         text.pack( side='left', expand=True, fill='both' )
84         ybar.pack( side='right', fill='y' )
85         return text
86
87     def bindEvents( self ):
88         "Bind keyboard and file events."
89         # The text widget handles regular key presses, but we
90         # use special handlers for the following:
91         self.text.bind( '<Return>', self.handleReturn )
92         self.text.bind( '<Control-c>', self.handleInt )
93         self.text.bind( '<KeyPress>', self.handleKey )
94         # This is not well-documented, but it is the correct
95         # way to trigger a file event handler from Tk's
96         # event loop!
97         self.tk.createfilehandler( self.node.stdout, READABLE,
98                                    self.handleReadable )
99
100     # We're not a terminal (yet?), so we ignore the following
101     # control characters other than [\b\n\r]
102     ignoreChars = re.compile( r'[\x00-\x07\x09\x0b\x0c\x0e-\x1f]+' )
103
104     def append( self, text ):
105         "Append something to our text frame."
106         text = self.ignoreChars.sub( '', text )
107         self.text.insert( 'end', text )
108         self.text.mark_set( 'insert', 'end' )
109         self.text.see( 'insert' )
110         outputHook = lambda x, y: True  # make pylint happier
111         if self.outputHook:
112             outputHook = self.outputHook
113         outputHook( self, text )
114
115     def handleKey( self, event ):
116         "If it's an interactive command, send it to the node."
117         char = event.char
118         if self.node.waiting:
119             self.node.write( char )
120
121     def handleReturn( self, event ):
122         "Handle a carriage return."
123         cmd = self.text.get( 'insert linestart', 'insert lineend' )
124         # Send it immediately, if "interactive" command
125         if self.node.waiting:
126             self.node.write( event.char )
127             return
128         # Otherwise send the whole line to the shell
129         pos = cmd.find( self.prompt )
130         if pos >= 0:
131             cmd = cmd[ pos + len( self.prompt ): ]
132         self.sendCmd( cmd )
133
134     # Callback ignores event
135     def handleInt( self, _event=None ):
136         "Handle control-c."
137         self.node.sendInt()
138
139     def sendCmd( self, cmd ):
140         "Send a command to our node."
141         if not self.node.waiting:
142             self.node.sendCmd( cmd )
143
144     def handleReadable( self, _fds, timeoutms=None ):
145         "Handle file readable event."
146         data = self.node.monitor( timeoutms )
147         self.append( data )
148         if not self.node.waiting:
149             # Print prompt
150             self.append( self.prompt )
151
152     def waiting( self ):
153         "Are we waiting for output?"
154         return self.node.waiting
155
156     def waitOutput( self ):
157         "Wait for any remaining output."
158         while self.node.waiting:
159             # A bit of a trade-off here...
160             self.handleReadable( self, timeoutms=1000)
161             self.update()
162
163     def clear( self ):
164         "Clear all of our text."
165         self.text.delete( '1.0', 'end' )
166
167
168 class Graph( Frame ):
169
170     "Graph that we can add bars to over time."
171
172     def __init__( self, parent=None, bg = 'white', gheight=200, gwidth=500,
173                   barwidth=10, ymax=3.5,):
174
175         Frame.__init__( self, parent )
176
177         self.bg = bg
178         self.gheight = gheight
179         self.gwidth = gwidth
180         self.barwidth = barwidth
181         self.ymax = float( ymax )
182         self.xpos = 0
183
184         # Create everything
185         self.title, self.scale, self.graph = self.createWidgets()
186         self.updateScrollRegions()
187         self.yview( 'moveto', '1.0' )
188
189     def createScale( self ):
190         "Create a and return a new canvas with scale markers."
191         height = float( self.gheight )
192         width = 25
193         ymax = self.ymax
194         scale = Canvas( self, width=width, height=height,
195                         background=self.bg )
196         opts = { 'fill': 'red' }
197         # Draw scale line
198         scale.create_line( width - 1, height, width - 1, 0, **opts )
199         # Draw ticks and numbers
200         for y in range( 0, int( ymax + 1 ) ):
201             ypos = height * (1 - float( y ) / ymax )
202             scale.create_line( width, ypos, width - 10, ypos, **opts )
203             scale.create_text( 10, ypos, text=str( y ), **opts )
204         return scale
205
206     def updateScrollRegions( self ):
207         "Update graph and scale scroll regions."
208         ofs = 20
209         height = self.gheight + ofs
210         self.graph.configure( scrollregion=( 0, -ofs,
211                               self.xpos * self.barwidth, height ) )
212         self.scale.configure( scrollregion=( 0, -ofs, 0, height ) )
213
214     def yview( self, *args ):
215         "Scroll both scale and graph."
216         self.graph.yview( *args )
217         self.scale.yview( *args )
218
219     def createWidgets( self ):
220         "Create initial widget set."
221
222         # Objects
223         title = Label( self, text='Bandwidth (Gb/s)', bg=self.bg )
224         width = self.gwidth
225         height = self.gheight
226         scale = self.createScale()
227         graph = Canvas( self, width=width, height=height, background=self.bg)
228         xbar = Scrollbar( self, orient='horizontal', command=graph.xview )
229         ybar = Scrollbar( self, orient='vertical', command=self.yview )
230         graph.configure( xscrollcommand=xbar.set, yscrollcommand=ybar.set,
231                          scrollregion=(0, 0, width, height ) )
232         scale.configure( yscrollcommand=ybar.set )
233
234         # Layout
235         title.grid( row=0, columnspan=3, sticky='new')
236         scale.grid( row=1, column=0, sticky='nsew' )
237         graph.grid( row=1, column=1, sticky='nsew' )
238         ybar.grid( row=1, column=2, sticky='ns' )
239         xbar.grid( row=2, column=0, columnspan=2, sticky='ew' )
240         self.rowconfigure( 1, weight=1 )
241         self.columnconfigure( 1, weight=1 )
242         return title, scale, graph
243
244     def addBar( self, yval ):
245         "Add a new bar to our graph."
246         percent = yval / self.ymax
247         c = self.graph
248         x0 = self.xpos * self.barwidth
249         x1 = x0 + self.barwidth
250         y0 = self.gheight
251         y1 = ( 1 - percent ) * self.gheight
252         c.create_rectangle( x0, y0, x1, y1, fill='green' )
253         self.xpos += 1
254         self.updateScrollRegions()
255         self.graph.xview( 'moveto', '1.0' )
256
257     def clear( self ):
258         "Clear graph contents."
259         self.graph.delete( 'all' )
260         self.xpos = 0
261
262     def test( self ):
263         "Add a bar for testing purposes."
264         ms = 1000
265         if self.xpos < 10:
266             self.addBar( self.xpos / 10 * self.ymax  )
267             self.after( ms, self.test )
268
269     def setTitle( self, text ):
270         "Set graph title"
271         self.title.configure( text=text, font='Helvetica 9 bold' )
272
273
274 class ConsoleApp( Frame ):
275
276     "Simple Tk consoles for Mininet."
277
278     menuStyle = { 'font': 'Geneva 7 bold' }
279
280     def __init__( self, net, parent=None, width=4 ):
281         Frame.__init__( self, parent )
282         self.top = self.winfo_toplevel()
283         self.top.title( 'Mininet' )
284         self.net = net
285         self.menubar = self.createMenuBar()
286         cframe = self.cframe = Frame( self )
287         self.consoles = {}  # consoles themselves
288         titles = {
289             'hosts': 'Host',
290             'switches': 'Switch',
291             'controllers': 'Controller'
292         }
293         for name in titles:
294             nodes = getattr( net, name )
295             frame, consoles = self.createConsoles(
296                 cframe, nodes, width, titles[ name ] )
297             self.consoles[ name ] = Object( frame=frame, consoles=consoles )
298         self.selected = None
299         self.select( 'hosts' )
300         self.cframe.pack( expand=True, fill='both' )
301         cleanUpScreens()
302         # Close window gracefully
303         Wm.wm_protocol( self.top, name='WM_DELETE_WINDOW', func=self.quit )
304
305         # Initialize graph
306         graph = Graph( cframe )
307         self.consoles[ 'graph' ] = Object( frame=graph, consoles=[ graph ] )
308         self.graph = graph
309         self.graphVisible = False
310         self.updates = 0
311         self.hostCount = len( self.consoles[ 'hosts' ].consoles )
312         self.bw = 0
313
314         self.pack( expand=True, fill='both' )
315
316     def updateGraph( self, _console, output ):
317         "Update our graph."
318         m = re.search( r'(\d+.?\d*) ([KMG]?bits)/sec', output )
319         if not m:
320             return
321         val, units = float( m.group( 1 ) ), m.group( 2 )
322         #convert to Gbps
323         if units[0] == 'M':
324             val *= 10 ** -3
325         elif units[0] == 'K':
326             val *= 10 ** -6
327         elif units[0] == 'b':
328             val *= 10 ** -9
329         self.updates += 1
330         self.bw += val
331         if self.updates >= self.hostCount:
332             self.graph.addBar( self.bw )
333             self.bw = 0
334             self.updates = 0
335
336     def setOutputHook( self, fn=None, consoles=None ):
337         "Register fn as output hook [on specific consoles.]"
338         if consoles is None:
339             consoles = self.consoles[ 'hosts' ].consoles
340         for console in consoles:
341             console.outputHook = fn
342
343     def createConsoles( self, parent, nodes, width, title ):
344         "Create a grid of consoles in a frame."
345         f = Frame( parent )
346         # Create consoles
347         consoles = []
348         index = 0
349         for node in nodes:
350             console = Console( f, self.net, node, title=title )
351             consoles.append( console )
352             row = index / width
353             column = index % width
354             console.grid( row=row, column=column, sticky='nsew' )
355             index += 1
356             f.rowconfigure( row, weight=1 )
357             f.columnconfigure( column, weight=1 )
358         return f, consoles
359
360     def select( self, groupName ):
361         "Select a group of consoles to display."
362         if self.selected is not None:
363             self.selected.frame.pack_forget()
364         self.selected = self.consoles[ groupName ]
365         self.selected.frame.pack( expand=True, fill='both' )
366
367     def createMenuBar( self ):
368         "Create and return a menu (really button) bar."
369         f = Frame( self )
370         buttons = [
371             ( 'Hosts', lambda: self.select( 'hosts' ) ),
372             ( 'Switches', lambda: self.select( 'switches' ) ),
373             ( 'Controllers', lambda: self.select( 'controllers' ) ),
374             ( 'Graph', lambda: self.select( 'graph' ) ),
375             ( 'Ping', self.ping ),
376             ( 'Iperf', self.iperf ),
377             ( 'Interrupt', self.stop ),
378             ( 'Clear', self.clear ),
379             ( 'Quit', self.quit )
380         ]
381         for name, cmd in buttons:
382             b = Button( f, text=name, command=cmd, **self.menuStyle )
383             b.pack( side='left' )
384         f.pack( padx=4, pady=4, fill='x' )
385         return f
386
387     def clear( self ):
388         "Clear selection."
389         for console in self.selected.consoles:
390             console.clear()
391
392     def waiting( self, consoles=None ):
393         "Are any of our hosts waiting for output?"
394         if consoles is None:
395             consoles = self.consoles[ 'hosts' ].consoles
396         for console in consoles:
397             if console.waiting():
398                 return True
399         return False
400
401     def ping( self ):
402         "Tell each host to ping the next one."
403         consoles = self.consoles[ 'hosts' ].consoles
404         if self.waiting( consoles ):
405             return
406         count = len( consoles )
407         i = 0
408         for console in consoles:
409             i = ( i + 1 ) % count
410             ip = consoles[ i ].node.IP()
411             console.sendCmd( 'ping ' + ip )
412
413     def iperf( self ):
414         "Tell each host to iperf to the next one."
415         consoles = self.consoles[ 'hosts' ].consoles
416         if self.waiting( consoles ):
417             return
418         count = len( consoles )
419         self.setOutputHook( self.updateGraph )
420         for console in consoles:
421             # Sometimes iperf -sD doesn't return,
422             # so we run it in the background instead
423             console.node.cmd( 'iperf -s &' )
424         i = 0
425         for console in consoles:
426             i = ( i + 1 ) % count
427             ip = consoles[ i ].node.IP()
428             console.sendCmd( 'iperf -t 99999 -i 1 -c ' + ip )
429
430     def stop( self, wait=True ):
431         "Interrupt all hosts."
432         consoles = self.consoles[ 'hosts' ].consoles
433         for console in consoles:
434             console.handleInt()
435         if wait:
436             for console in consoles:
437                 console.waitOutput()
438         self.setOutputHook( None )
439         # Shut down any iperfs that might still be running
440         quietRun( 'killall -9 iperf' )
441
442     def quit( self ):
443         "Stop everything and quit."
444         self.stop( wait=False)
445         Frame.quit( self )
446
447
448 # Make it easier to construct and assign objects
449
450 def assign( obj, **kwargs ):
451     "Set a bunch of fields in an object."
452     obj.__dict__.update( kwargs )
453
454 class Object( object ):
455     "Generic object you can stuff junk into."
456     def __init__( self, **kwargs ):
457         assign( self, **kwargs )
458
459
460 if __name__ == '__main__':
461     setLogLevel( 'info' )
462     network = TreeNet( depth=2, fanout=4 )
463     network.start()
464     app = ConsoleApp( network, width=4 )
465     app.mainloop()
466     network.stop()