#!/usr/bin/python """ consoles.py: bring up a bunch of miniature consoles on a virtual network This demo shows how to monitor a set of nodes by using Node's monitor() and Tkinter's createfilehandler(). We monitor nodes in a couple of ways: - First, each individual node is monitored, and its output is added to its console window - Second, each time a console window gets iperf output, it is parsed and accumulated. Once we have output for all consoles, a bar is added to the bandwidth graph. The consoles also support limited interaction: - Pressing "return" in a console will send a command to it - Pressing the console's title button will open up an xterm Bob Lantz, April 2010 """ import re from Tkinter import * from mininet.log import setLogLevel,info from mininet.topolib import TreeNet from mininet.term import makeTerms, cleanUpScreens from mininet.util import quietRun class Console( Frame ): "A simple console on a host." def __init__( self, parent, net, node, height=10, width=32, title='Node' ): Frame.__init__( self, parent ) self.net = net self.node = node self.prompt = node.name + '# ' self.height, self.width, self.title = height, width, title # Initialize widget styles self.buttonStyle = { 'font': 'Monaco 7' } self.textStyle = { 'font': 'Monaco 7', 'bg': 'black', 'fg': 'green', 'width': self.width, 'height': self.height, 'relief': 'sunken', 'insertbackground': 'green', 'highlightcolor': 'green', 'selectforeground': 'black', 'selectbackground': 'green' } # Set up widgets self.text = self.makeWidgets( ) self.bindEvents() self.sendCmd( 'export TERM=dumb' ) self.outputHook = None def makeWidgets( self ): "Make a label, a text area, and a scroll bar." def newTerm( net=self.net, node=self.node, title=self.title ): "Pop up a new terminal window for a node." net.terms += makeTerms( [ node ], title ) label = Button( self, text=self.node.name, command=newTerm, **self.buttonStyle ) label.pack( side='top', fill='x' ) text = Text( self, wrap='word', **self.textStyle ) ybar = Scrollbar( self, orient='vertical', width=7, command=text.yview ) text.configure( yscrollcommand=ybar.set ) text.pack( side='left', expand=True, fill='both' ) ybar.pack( side='right', fill='y' ) return text def bindEvents( self ): "Bind keyboard and file events." # The text widget handles regular key presses, but we # use special handlers for the following: self.text.bind( '<Return>', self.handleReturn ) self.text.bind( '<Control-c>', self.handleInt ) self.text.bind( '<KeyPress>', self.handleKey ) # This is not well-documented, but it is the correct # way to trigger a file event handler from Tk's # event loop! self.tk.createfilehandler( self.node.stdout, READABLE, self.handleReadable ) # We're not a terminal (yet?), so we ignore the following # control characters other than [\b\n\r] ignoreChars = re.compile( r'[\x00-\x07\x09\x0b\x0c\x0e-\x1f]+' ) def append( self, text ): "Append something to our text frame." text = self.ignoreChars.sub( '', text ) self.text.insert( 'end', text ) self.text.mark_set( 'insert', 'end' ) self.text.see( 'insert' ) if self.outputHook: self.outputHook( self, text ) def handleKey( self, event ): "If it's an interactive command, send it to the node." char = event.char if self.node.waiting: self.node.write( char ) def handleReturn( self, event ): "Handle a carriage return." cmd = self.text.get( 'insert linestart', 'insert lineend' ) # Send it immediately, if "interactive" command if self.node.waiting: self.node.write( event.char ) return # Otherwise send the whole line to the shell pos = cmd.find( self.prompt ) if pos >= 0: cmd = cmd[ pos + len( self.prompt ): ] self.sendCmd( cmd ) def handleInt( self, event=None ): "Handle control-c." self.node.sendInt() def sendCmd( self, cmd ): "Send a command to our node." text, node = self.text, self.node if not node.waiting: node.sendCmd( cmd ) def handleReadable( self, file=None, mask=None, timeoutms=None ): "Handle file readable event." data = self.node.monitor( timeoutms ) self.append( data ) if not self.node.waiting: # Print prompt self.append( self.prompt ) def waiting( self ): "Are we waiting for output?" return self.node.waiting def waitOutput( self ): "Wait for any remaining output." while self.node.waiting: # A bit of a trade-off here... self.handleReadable( self, timeoutms=1000) self.update() def clear( self ): "Clear all of our text." self.text.delete( '1.0', 'end' ) class Graph( Frame ): "Graph that we can add bars to over time." def __init__( self, parent=None, bg = 'white', gheight=200, gwidth=500, barwidth=10, ymax=3.5,): Frame.__init__( self, parent ) self.bg = bg self.gheight = gheight self.gwidth = gwidth self.barwidth = barwidth self.ymax = float( ymax ) self.xpos = 0 # Create everything self.title = self.graph = None self.createWidgets() self.updateScrollRegions() self.yview( 'moveto', '1.0' ) def scale( self ): "Create a and return a new canvas with scale markers." height = float( self.gheight ) width = 25 ymax = self.ymax scale = Canvas( self, width=width, height=height, background=self.bg ) fill = 'red' # Draw scale line scale.create_line( width - 1, height, width - 1, 0, fill=fill ) # Draw ticks and numbers for y in range( 0, int( ymax + 1 ) ): ypos = height * (1 - float( y ) / ymax ) scale.create_line( width, ypos, width - 10, ypos, fill=fill ) scale.create_text( 10, ypos, text=str( y ), fill=fill ) return scale def updateScrollRegions( self ): "Update graph and scale scroll regions." ofs = 20 height = self.gheight + ofs self.graph.configure( scrollregion=( 0, -ofs, self.xpos * self.barwidth, height ) ) self.scale.configure( scrollregion=( 0, -ofs, 0, height ) ) def yview( self, *args ): "Scroll both scale and graph." self.graph.yview( *args ) self.scale.yview( *args ) def createWidgets( self ): "Create initial widget set." # Objects title = Label( self, text="Bandwidth (Gb/s)", bg=self.bg ) width = self.gwidth height = self.gheight scale = self.scale() graph = Canvas( self, width=width, height=height, background=self.bg) xbar = Scrollbar( self, orient='horizontal', command=graph.xview ) ybar = Scrollbar( self, orient='vertical', command=self.yview ) graph.configure( xscrollcommand=xbar.set, yscrollcommand=ybar.set, scrollregion=(0, 0, width, height ) ) scale.configure( yscrollcommand=ybar.set ) # Layout title.grid( row=0, columnspan=3, sticky=N+E+W) scale.grid( row=1, column=0, sticky=N+S+E+W ) graph.grid( row=1, column=1, sticky=N+S+E+W ) ybar.grid( row=1, column=2, sticky=N+S ) xbar.grid( row=2, column=0, columnspan=2, sticky=E+W ) self.rowconfigure( 1, weight=1 ) self.columnconfigure( 1, weight=1 ) # Save for future reference self.title = title self.scale = scale self.graph = graph return graph def addBar( self, yval ): "Add a new bar to our graph." percent = yval / self.ymax height = percent * self.gheight c = self.graph x0 = self.xpos * self.barwidth x1 = x0 + self.barwidth y0 = self.gheight y1 = ( 1 - percent ) * self.gheight c.create_rectangle( x0 , y0, x1, y1, fill='green' ) self.xpos += 1 self.updateScrollRegions() self.graph.xview( 'moveto', '1.0' ) def clear( self ): "Clear graph contents." self.graph.delete( 'all' ) self.xpos = 0 def test( self ): "Add a bar for testing purposes." ms = 1000 if self.xpos < 10: self.addBar( self.xpos/10 * self.ymax ) self.after( ms, self.test ) def setTitle( self, text ): "Set graph title" self.title.configure( text=text, font='Helvetica 9 bold' ) class ConsoleApp( Frame ): menuStyle = { 'font': 'Geneva 7 bold' } def __init__( self, net, parent=None, width=4 ): Frame.__init__( self, parent ) self.top = self.winfo_toplevel() self.top.title( 'Mininet' ) self.net = net self.menubar = self.createMenuBar() cframe = self.cframe = Frame( self ) self.consoles = {} # consoles themselves titles = { 'hosts': 'Host', 'switches': 'Switch', 'controllers': 'Controller' } for name in titles: nodes = getattr( net, name ) frame, consoles = self.createConsoles( cframe, nodes, width, titles[ name ] ) self.consoles[ name ] = Object( frame=frame, consoles=consoles ) self.selected = None self.select( 'hosts' ) self.cframe.pack( expand=True, fill='both' ) cleanUpScreens() # Close window gracefully Wm.wm_protocol( self.top, name='WM_DELETE_WINDOW', func=self.quit ) # Initialize graph graph = Graph( cframe ) self.consoles[ 'graph' ] = Object( frame=graph, consoles=[ graph ] ) self.graph = graph self.graphVisible = False self.updates = 0 self.hostCount = len( self.consoles[ 'hosts' ].consoles ) self.bw = 0 self.pack( expand=True, fill='both' ) def updateGraph( self, console, output ): "Update our graph." m = re.search( r'(\d+) Mbits/sec', output ) if not m: return self.updates += 1 self.bw += .001 * float( m.group( 1 ) ) if self.updates >= self.hostCount: self.graph.addBar( self.bw ) self.bw = 0 self.updates = 0 def setOutputHook( self, fn=None, consoles=None ): if consoles is None: consoles = self.consoles[ 'hosts' ].consoles for console in consoles: console.outputHook = fn def createConsoles( self, parent, nodes, width, title ): "Create a grid of consoles in a frame." f = Frame( parent ) # Create consoles consoles = [] index = 0 for node in nodes: console = Console( f, net, node, title=title ) consoles.append( console ) row = index / width column = index % width console.grid( row=row, column=column, sticky='nsew' ) index += 1 f.rowconfigure( row, weight=1 ) f.columnconfigure( column, weight=1 ) return f, consoles def select( self, set ): "Select a set of consoles to display." if self.selected is not None: self.selected.frame.pack_forget() self.selected = self.consoles[ set ] self.selected.frame.pack( expand=True, fill='both' ) def createMenuBar( self ): "Create and return a menu (really button) bar." f = Frame( self ) buttons = [ ( 'Hosts', lambda: self.select( 'hosts' ) ), ( 'Switches', lambda: self.select( 'switches' ) ), ( 'Controllers', lambda: self.select( 'controllers' ) ), ( 'Graph', lambda: self.select( 'graph' ) ), ( 'Ping', self.ping ), ( 'Iperf', self.iperf ), ( 'Interrupt', self.stop ), ( 'Clear', self.clear ), ( 'Quit', self.quit ) ] for name, cmd in buttons: b = Button( f, text=name, command=cmd, **self.menuStyle ) b.pack( side='left' ) f.pack( padx=4, pady=4, fill='x' ) return f def clear( self ): "Clear selection." for console in self.selected.consoles: console.clear() def waiting( self, consoles=None ): "Are any of our hosts waiting for output?" if consoles is None: consoles = self.consoles[ 'hosts' ].consoles for console in consoles: if console.waiting(): return True return False def ping( self ): "Tell each host to ping the next one." consoles = self.consoles[ 'hosts' ].consoles if self.waiting( consoles ): return count = len( consoles ) i = 0 for console in consoles: i = ( i + 1 ) % count ip = consoles[ i ].node.IP() console.sendCmd( 'ping ' + ip ) def iperf( self ): "Tell each host to iperf to the next one." consoles = self.consoles[ 'hosts' ].consoles if self.waiting( consoles ): return count = len( consoles ) self.setOutputHook( self.updateGraph ) for console in consoles: console.node.cmd( 'iperf -sD' ) i = 0 for console in consoles: i = ( i + 1 ) % count ip = consoles[ i ].node.IP() console.sendCmd( 'iperf -t 99999 -i 1 -c ' + ip ) def stop( self, wait=True ): "Interrupt all hosts." consoles = self.consoles[ 'hosts' ].consoles for console in consoles: console.handleInt() if wait: for console in consoles: console.waitOutput() self.setOutputHook( None ) # Shut down any iperfs that might still be running quietRun( 'killall -9 iperf' ) def quit( self ): "Stope everything and quit." self.stop( wait=False) Frame.quit( self ) # Make it easier to construct and assign objects def assign( obj, **kwargs): "Set a bunch of fields in an object." for name, value in kwargs.items(): setattr( obj, name, value ) class Object( object ): "Generic object you can stuff junk into." def __init__( self, **kwargs ): assign( self, **kwargs ) if __name__ == '__main__': setLogLevel( 'info' ) net = TreeNet( depth=2, fanout=4 ) net.start() app = ConsoleApp( net, width=4 ) app.mainloop() net.stop()