4 consoles.py: bring up a bunch of miniature consoles on a virtual network
6 This demo shows how to monitor a set of nodes by using
7 Node's monitor() and Tkinter's createfilehandler().
9 We monitor nodes in a couple of ways:
11 - First, each individual node is monitored, and its output is added
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.
18 The consoles also support limited interaction:
20 - Pressing "return" in a console will send a command to it
22 - Pressing the console's title button will open up an xterm
30 from Tkinter import Frame, Button, Label, Text, Scrollbar, Canvas, Wm, READABLE
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
37 class Console( Frame ):
38 "A simple console on a host."
40 def __init__( self, parent, net, node, height=10, width=32, title='Node' ):
41 Frame.__init__( self, parent )
45 self.prompt = node.name + '# '
46 self.height, self.width, self.title = height, width, title
48 # Initialize widget styles
49 self.buttonStyle = { 'font': 'Monaco 7' }
55 'height': self.height,
57 'insertbackground': 'green',
58 'highlightcolor': 'green',
59 'selectforeground': 'black',
60 'selectbackground': 'green'
64 self.text = self.makeWidgets( )
66 self.sendCmd( 'export TERM=dumb' )
68 self.outputHook = None
70 def makeWidgets( self ):
71 "Make a label, a text area, and a scroll bar."
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,
78 label.pack( side='top', fill='x' )
79 text = Text( self, wrap='word', **self.textStyle )
80 ybar = Scrollbar( self, orient='vertical', width=7,
82 text.configure( yscrollcommand=ybar.set )
83 text.pack( side='left', expand=True, fill='both' )
84 ybar.pack( side='right', fill='y' )
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
97 self.tk.createfilehandler( self.node.stdout, READABLE,
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]+' )
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
112 outputHook = self.outputHook
113 outputHook( self, text )
115 def handleKey( self, event ):
116 "If it's an interactive command, send it to the node."
118 if self.node.waiting:
119 self.node.write( char )
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 )
128 # Otherwise send the whole line to the shell
129 pos = cmd.find( self.prompt )
131 cmd = cmd[ pos + len( self.prompt ): ]
134 # Callback ignores event
135 def handleInt( self, _event=None ):
139 def sendCmd( self, cmd ):
140 "Send a command to our node."
141 if not self.node.waiting:
142 self.node.sendCmd( cmd )
144 def handleReadable( self, _fds, timeoutms=None ):
145 "Handle file readable event."
146 data = self.node.monitor( timeoutms )
148 if not self.node.waiting:
150 self.append( self.prompt )
153 "Are we waiting for output?"
154 return self.node.waiting
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)
164 "Clear all of our text."
165 self.text.delete( '1.0', 'end' )
168 class Graph( Frame ):
170 "Graph that we can add bars to over time."
172 def __init__( self, parent=None, bg = 'white', gheight=200, gwidth=500,
173 barwidth=10, ymax=3.5,):
175 Frame.__init__( self, parent )
178 self.gheight = gheight
180 self.barwidth = barwidth
181 self.ymax = float( ymax )
185 self.title, self.scale, self.graph = self.createWidgets()
186 self.updateScrollRegions()
187 self.yview( 'moveto', '1.0' )
189 def createScale( self ):
190 "Create a and return a new canvas with scale markers."
191 height = float( self.gheight )
194 scale = Canvas( self, width=width, height=height,
196 opts = { 'fill': 'red' }
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 )
206 def updateScrollRegions( self ):
207 "Update graph and scale scroll regions."
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 ) )
214 def yview( self, *args ):
215 "Scroll both scale and graph."
216 self.graph.yview( *args )
217 self.scale.yview( *args )
219 def createWidgets( self ):
220 "Create initial widget set."
223 title = Label( self, text='Bandwidth (Gb/s)', bg=self.bg )
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 )
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
244 def addBar( self, yval ):
245 "Add a new bar to our graph."
246 percent = yval / self.ymax
248 x0 = self.xpos * self.barwidth
249 x1 = x0 + self.barwidth
251 y1 = ( 1 - percent ) * self.gheight
252 c.create_rectangle( x0, y0, x1, y1, fill='green' )
254 self.updateScrollRegions()
255 self.graph.xview( 'moveto', '1.0' )
258 "Clear graph contents."
259 self.graph.delete( 'all' )
263 "Add a bar for testing purposes."
266 self.addBar( self.xpos / 10 * self.ymax )
267 self.after( ms, self.test )
269 def setTitle( self, text ):
271 self.title.configure( text=text, font='Helvetica 9 bold' )
274 class ConsoleApp( Frame ):
276 "Simple Tk consoles for Mininet."
278 menuStyle = { 'font': 'Geneva 7 bold' }
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' )
285 self.menubar = self.createMenuBar()
286 cframe = self.cframe = Frame( self )
287 self.consoles = {} # consoles themselves
290 'switches': 'Switch',
291 'controllers': 'Controller'
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 )
299 self.select( 'hosts' )
300 self.cframe.pack( expand=True, fill='both' )
302 # Close window gracefully
303 Wm.wm_protocol( self.top, name='WM_DELETE_WINDOW', func=self.quit )
306 graph = Graph( cframe )
307 self.consoles[ 'graph' ] = Object( frame=graph, consoles=[ graph ] )
309 self.graphVisible = False
311 self.hostCount = len( self.consoles[ 'hosts' ].consoles )
314 self.pack( expand=True, fill='both' )
316 def updateGraph( self, _console, output ):
318 m = re.search( r'(\d+.?\d*) ([KMG]?bits)/sec', output )
321 val, units = float( m.group( 1 ) ), m.group( 2 )
325 elif units[0] == 'K':
327 elif units[0] == 'b':
331 if self.updates >= self.hostCount:
332 self.graph.addBar( self.bw )
336 def setOutputHook( self, fn=None, consoles=None ):
337 "Register fn as output hook [on specific consoles.]"
339 consoles = self.consoles[ 'hosts' ].consoles
340 for console in consoles:
341 console.outputHook = fn
343 def createConsoles( self, parent, nodes, width, title ):
344 "Create a grid of consoles in a frame."
350 console = Console( f, self.net, node, title=title )
351 consoles.append( console )
353 column = index % width
354 console.grid( row=row, column=column, sticky='nsew' )
356 f.rowconfigure( row, weight=1 )
357 f.columnconfigure( column, weight=1 )
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' )
367 def createMenuBar( self ):
368 "Create and return a menu (really button) bar."
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 )
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' )
389 for console in self.selected.consoles:
392 def waiting( self, consoles=None ):
393 "Are any of our hosts waiting for output?"
395 consoles = self.consoles[ 'hosts' ].consoles
396 for console in consoles:
397 if console.waiting():
402 "Tell each host to ping the next one."
403 consoles = self.consoles[ 'hosts' ].consoles
404 if self.waiting( consoles ):
406 count = len( consoles )
408 for console in consoles:
409 i = ( i + 1 ) % count
410 ip = consoles[ i ].node.IP()
411 console.sendCmd( 'ping ' + ip )
414 "Tell each host to iperf to the next one."
415 consoles = self.consoles[ 'hosts' ].consoles
416 if self.waiting( consoles ):
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 &' )
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 )
430 def stop( self, wait=True ):
431 "Interrupt all hosts."
432 consoles = self.consoles[ 'hosts' ].consoles
433 for console in consoles:
436 for console in consoles:
438 self.setOutputHook( None )
439 # Shut down any iperfs that might still be running
440 quietRun( 'killall -9 iperf' )
443 "Stop everything and quit."
444 self.stop( wait=False)
448 # Make it easier to construct and assign objects
450 def assign( obj, **kwargs ):
451 "Set a bunch of fields in an object."
452 obj.__dict__.update( kwargs )
454 class Object( object ):
455 "Generic object you can stuff junk into."
456 def __init__( self, **kwargs ):
457 assign( self, **kwargs )
460 if __name__ == '__main__':
461 setLogLevel( 'info' )
462 network = TreeNet( depth=2, fanout=4 )
464 app = ConsoleApp( network, width=4 )