| #!/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." |
| 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 ): |
| "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: |
| 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 ): |
| "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() |