#!/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 Frame, Button, Label, Text, Scrollbar, Canvas, Wm, READABLE from mininet.log import setLogLevel 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' ) outputHook = lambda x, y: True # make pylint happier if self.outputHook: outputHook = self.outputHook 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 ) # Callback ignores event def handleInt( self, _event=None ): "Handle control-c." self.node.sendInt() def sendCmd( self, cmd ): "Send a command to our node." if not self.node.waiting: self.node.sendCmd( cmd ) def handleReadable( self, _fds, 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.scale, self.graph = self.createWidgets() self.updateScrollRegions() self.yview( 'moveto', '1.0' ) def createScale( 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 ) opts = { 'fill': 'red' } # Draw scale line scale.create_line( width - 1, height, width - 1, 0, **opts ) # 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, **opts ) scale.create_text( 10, ypos, text=str( y ), **opts ) 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.createScale() 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='new') scale.grid( row=1, column=0, sticky='nsew' ) graph.grid( row=1, column=1, sticky='nsew' ) ybar.grid( row=1, column=2, sticky='ns' ) xbar.grid( row=2, column=0, columnspan=2, sticky='ew' ) self.rowconfigure( 1, weight=1 ) self.columnconfigure( 1, weight=1 ) return title, scale, graph def addBar( self, yval ): "Add a new bar to our graph." percent = yval / self.ymax 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 ): "Simple Tk consoles for Mininet." 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." vals = output.split(',') if not len(vals) == 9: return self.updates += 1 #convert to Gbps self.bw += int( vals[-1] ) * ( 10 ** -9 ) if self.updates >= self.hostCount: self.graph.addBar( self.bw ) self.bw = 0 self.updates = 0 def setOutputHook( self, fn=None, consoles=None ): "Register fn as output hook [on specific consoles.]" 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, self.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, groupName ): "Select a group of consoles to display." if self.selected is not None: self.selected.frame.pack_forget() self.selected = self.consoles[ groupName ] 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: #sometimes iperf -sD doesn't return, so we run it in the background instead console.node.cmd( 'iperf -s &' ) i = 0 for console in consoles: i = ( i + 1 ) % count ip = consoles[ i ].node.IP() console.sendCmd( 'iperf -t 99999 -i 1 -y c -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 ): "Stop 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." obj.__dict__.update( kwargs ) class Object( object ): "Generic object you can stuff junk into." def __init__( self, **kwargs ): assign( self, **kwargs ) if __name__ == '__main__': setLogLevel( 'info' ) network = TreeNet( depth=2, fanout=4 ) network.start() app = ConsoleApp( network, width=4 ) app.mainloop() network.stop()