Skip to content
Snippets Groups Projects
consoles.py 15 KiB
Newer Older
#!/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().
Bob Lantz's avatar
Bob Lantz committed

We monitor nodes in a couple of ways:

- First, each individual node is monitored, and its output is added
  to its console window
Bob Lantz's avatar
Bob Lantz committed

Bob Lantz's avatar
Bob Lantz committed
- 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

Bob Lantz's avatar
Bob Lantz committed
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
Bob Lantz's avatar
Bob Lantz committed
from mininet.util import quietRun

class Console( Frame ):
    "A simple console on a host."
Bob Lantz's avatar
Bob Lantz committed

Bob Lantz's avatar
Bob Lantz committed
    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 + '# '
Bob Lantz's avatar
Bob Lantz committed
        self.height, self.width, self.title = height, width, title
Bob Lantz's avatar
Bob Lantz committed

        # Initialize widget styles
        self.buttonStyle = { 'font': 'Monaco 7' }
Bob Lantz's avatar
Bob Lantz committed
        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'
Bob Lantz's avatar
Bob Lantz committed
        }

        # Set up widgets
        self.text = self.makeWidgets( )
        self.bindEvents()
        self.sendCmd( 'export TERM=dumb' )
Bob Lantz's avatar
Bob Lantz committed

Bob Lantz's avatar
Bob Lantz committed
        self.outputHook = None
Bob Lantz's avatar
Bob Lantz committed

    def makeWidgets( self ):
        "Make a label, a text area, and a scroll bar."

Bob Lantz's avatar
Bob Lantz committed
        def newTerm( net=self.net, node=self.node, title=self.title ):
            "Pop up a new terminal window for a node."
Bob Lantz's avatar
Bob Lantz committed
            net.terms += makeTerms( [ node ], title )
Bob Lantz's avatar
Bob Lantz committed
        label = Button( self, text=self.node.name, command=newTerm,
        label.pack( side='top', fill='x' )
        text = Text( self, wrap='word', **self.textStyle )
Bob Lantz's avatar
Bob Lantz committed
        ybar = Scrollbar( self, orient='vertical', width=7,
        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."
Bob Lantz's avatar
Bob Lantz committed
        # 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,
    # We're not a terminal (yet?), so we ignore the following
    # control characters other than [\b\n\r]
Bob Lantz's avatar
Bob Lantz committed
    ignoreChars = re.compile( r'[\x00-\x07\x09\x0b\x0c\x0e-\x1f]+' )
Bob Lantz's avatar
Bob Lantz committed

    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
Bob Lantz's avatar
Bob Lantz committed
        if self.outputHook:
Bob Lantz's avatar
Bob Lantz committed
            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 )
Bob Lantz's avatar
Bob Lantz committed

    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 )
Bob Lantz's avatar
Bob Lantz committed

    # Callback ignores event
    def handleInt( self, _event=None ):
        "Handle control-c."
        self.node.sendInt()
Bob Lantz's avatar
Bob Lantz committed

    def sendCmd( self, cmd ):
        "Send a command to our node."
Bob Lantz's avatar
Bob Lantz committed
        if not self.node.waiting:
            self.node.sendCmd( cmd )
    def handleReadable( self, _fds, timeoutms=None ):
Bob Lantz's avatar
Bob Lantz committed
        "Handle file readable event."
        data = self.node.monitor( timeoutms )
        self.append( data )
        if not self.node.waiting:
Bob Lantz's avatar
Bob Lantz committed
            # Print prompt
            self.append( self.prompt )
Bob Lantz's avatar
Bob Lantz committed

    def waiting( self ):
        "Are we waiting for output?"
        return self.node.waiting

Bob Lantz's avatar
Bob Lantz committed
    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()
Bob Lantz's avatar
Bob Lantz committed
    def clear( self ):
        "Clear all of our text."
        self.text.delete( '1.0', 'end' )
Bob Lantz's avatar
Bob Lantz committed

Bob Lantz's avatar
Bob Lantz committed
class Graph( Frame ):

    "Graph that we can add bars to over time."
Bob Lantz's avatar
Bob Lantz committed

    def __init__( self, parent=None, bg = 'white', gheight=200, gwidth=500,
                  barwidth=10, ymax=3.5,):
Bob Lantz's avatar
Bob Lantz committed

Bob Lantz's avatar
Bob Lantz committed
        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
Bob Lantz's avatar
Bob Lantz committed
        self.title, self.scale, self.graph = self.createWidgets()
Bob Lantz's avatar
Bob Lantz committed
        self.updateScrollRegions()
        self.yview( 'moveto', '1.0' )
Bob Lantz's avatar
Bob Lantz committed

    def createScale( self ):
Bob Lantz's avatar
Bob Lantz committed
        "Create a and return a new canvas with scale markers."
        height = float( self.gheight )
        width = 25
        ymax = self.ymax
Bob Lantz's avatar
Bob Lantz committed
        scale = Canvas( self, width=width, height=height,
Bob Lantz's avatar
Bob Lantz committed
        opts = { 'fill': 'red' }
Bob Lantz's avatar
Bob Lantz committed
        # Draw scale line
Bob Lantz's avatar
Bob Lantz committed
        scale.create_line( width - 1, height, width - 1, 0, **opts )
Bob Lantz's avatar
Bob Lantz committed
        # Draw ticks and numbers
        for y in range( 0, int( ymax + 1 ) ):
            ypos = height * (1 - float( y ) / ymax )
Bob Lantz's avatar
Bob Lantz committed
            scale.create_line( width, ypos, width - 10, ypos, **opts )
            scale.create_text( 10, ypos, text=str( y ), **opts )
Bob Lantz's avatar
Bob Lantz committed
        return scale
Bob Lantz's avatar
Bob Lantz committed

Bob Lantz's avatar
Bob Lantz committed
    def updateScrollRegions( self ):
        "Update graph and scale scroll regions."
        ofs = 20
        height = self.gheight + ofs
Bob Lantz's avatar
Bob Lantz committed
        self.graph.configure( scrollregion=( 0, -ofs,
                              self.xpos * self.barwidth, height ) )
Bob Lantz's avatar
Bob Lantz committed
        self.scale.configure( scrollregion=( 0, -ofs, 0, height ) )
Bob Lantz's avatar
Bob Lantz committed

Bob Lantz's avatar
Bob Lantz committed
    def yview( self, *args ):
Bob Lantz's avatar
Bob Lantz committed
        "Scroll both scale and graph."
        self.graph.yview( *args )
        self.scale.yview( *args )

Bob Lantz's avatar
Bob Lantz committed
    def createWidgets( self ):
        "Create initial widget set."

        # Objects
Bob Lantz's avatar
Bob Lantz committed
        title = Label( self, text='Bandwidth (Gb/s)', bg=self.bg )
Bob Lantz's avatar
Bob Lantz committed
        width = self.gwidth
        height = self.gheight
Bob Lantz's avatar
Bob Lantz committed
        scale = self.createScale()
Bob Lantz's avatar
Bob Lantz committed
        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 ) )
Bob Lantz's avatar
Bob Lantz committed
        scale.configure( yscrollcommand=ybar.set )
Bob Lantz's avatar
Bob Lantz committed

Bob Lantz's avatar
Bob Lantz committed
        # Layout
Bob Lantz's avatar
Bob Lantz committed
        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' )
Bob Lantz's avatar
Bob Lantz committed
        self.rowconfigure( 1, weight=1 )
        self.columnconfigure( 1, weight=1 )
Bob Lantz's avatar
Bob Lantz committed
        return title, scale, graph

Bob Lantz's avatar
Bob Lantz committed
    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' )
Bob Lantz's avatar
Bob Lantz committed
        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:
Bob Lantz's avatar
Bob Lantz committed
            self.addBar( self.xpos / 10 * self.ymax  )
Bob Lantz's avatar
Bob Lantz committed
            self.after( ms, self.test )

    def setTitle( self, text ):
        "Set graph title"
        self.title.configure( text=text, font='Helvetica 9 bold' )


class ConsoleApp( Frame ):
Bob Lantz's avatar
Bob Lantz committed
    "Simple Tk consoles for Mininet."
Bob Lantz's avatar
Bob Lantz committed

    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
Bob Lantz's avatar
Bob Lantz committed
        titles = {
            'hosts': 'Host',
Bob Lantz's avatar
Bob Lantz committed
            'switches': 'Switch',
            'controllers': 'Controller'
        }
        for name in titles:
            nodes = getattr( net, name )
Bob Lantz's avatar
Bob Lantz committed
            frame, consoles = self.createConsoles(
Bob Lantz's avatar
Bob Lantz committed
                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()
Bob Lantz's avatar
Bob Lantz committed
        # Close window gracefully
        Wm.wm_protocol( self.top, name='WM_DELETE_WINDOW', func=self.quit )
Bob Lantz's avatar
Bob Lantz committed

Bob Lantz's avatar
Bob Lantz committed
        # 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' )
Bob Lantz's avatar
Bob Lantz committed

    def updateGraph( self, _console, output ):
Bob Lantz's avatar
Bob Lantz committed
        "Update our graph."
        vals = output.split(',')
        if not len(vals) == 9:
Bob Lantz's avatar
Bob Lantz committed
            return
        self.updates += 1
        #convert to Gbps
        self.bw +=  int( vals[-1] ) * ( 10 ** -9 )
Bob Lantz's avatar
Bob Lantz committed
        if self.updates >= self.hostCount:
            self.graph.addBar( self.bw )
            self.bw = 0
            self.updates = 0

    def setOutputHook( self, fn=None, consoles=None ):
Bob Lantz's avatar
Bob Lantz committed
        "Register fn as output hook [on specific consoles.]"
Bob Lantz's avatar
Bob Lantz committed
        if consoles is None:
            consoles = self.consoles[ 'hosts' ].consoles
        for console in consoles:
            console.outputHook = fn
Bob Lantz's avatar
Bob Lantz committed

Bob Lantz's avatar
Bob Lantz committed
    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:
Bob Lantz's avatar
Bob Lantz committed
            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
Bob Lantz's avatar
Bob Lantz committed

    def select( self, groupName ):
        "Select a group of consoles to display."
        if self.selected is not None:
            self.selected.frame.pack_forget()
Bob Lantz's avatar
Bob Lantz committed
        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' ) ),
Bob Lantz's avatar
Bob Lantz committed
            ( 'Controllers', lambda: self.select( 'controllers' ) ),
            ( 'Graph', lambda: self.select( 'graph' ) ),
            ( 'Ping', self.ping ),
Bob Lantz's avatar
Bob Lantz committed
            ( 'Iperf', self.iperf ),
            ( 'Interrupt', self.stop ),
Bob Lantz's avatar
Bob Lantz committed
            ( '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
Bob Lantz's avatar
Bob Lantz committed

Bob Lantz's avatar
Bob Lantz committed
    def clear( self ):
Bob Lantz's avatar
Bob Lantz committed
        "Clear selection."
        for console in self.selected.consoles:
Bob Lantz's avatar
Bob Lantz committed
            console.clear()
Bob Lantz's avatar
Bob Lantz committed

Bob Lantz's avatar
Bob Lantz committed
    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
Bob Lantz's avatar
Bob Lantz committed
        if self.waiting( consoles ):
            return
        count = len( consoles )
        for console in consoles:
            i = ( i + 1 ) % count
            ip = consoles[ i ].node.IP()
            console.sendCmd( 'ping ' + ip )
Bob Lantz's avatar
Bob Lantz committed

    def iperf( self ):
        "Tell each host to iperf to the next one."
        consoles = self.consoles[ 'hosts' ].consoles
Bob Lantz's avatar
Bob Lantz committed
        if self.waiting( consoles ):
            return
Bob Lantz's avatar
Bob Lantz committed
        count = len( consoles )
Bob Lantz's avatar
Bob Lantz committed
        self.setOutputHook( self.updateGraph )
Bob Lantz's avatar
Bob Lantz committed
        for console in consoles:
            #sometimes iperf -sD doesn't return, so we run it in the background instead
            console.node.cmd( 'iperf -s &' )
Bob Lantz's avatar
Bob Lantz committed
        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 )
Bob Lantz's avatar
Bob Lantz committed

    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()
Bob Lantz's avatar
Bob Lantz committed
        self.setOutputHook( None )
Bob Lantz's avatar
Bob Lantz committed
        # Shut down any iperfs that might still be running
        quietRun( 'killall -9 iperf' )

    def quit( self ):
Bob Lantz's avatar
Bob Lantz committed
        "Stop everything and quit."
        self.stop( wait=False)
Bob Lantz's avatar
Bob Lantz committed
        Frame.quit( self )
Bob Lantz's avatar
Bob Lantz committed

# Make it easier to construct and assign objects

Bob Lantz's avatar
Bob Lantz committed
def assign( obj, **kwargs ):
Bob Lantz's avatar
Bob Lantz committed
    "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 ):
Bob Lantz's avatar
Bob Lantz committed
        assign( self, **kwargs )


if __name__ == '__main__':
    setLogLevel( 'info' )
Bob Lantz's avatar
Bob Lantz committed
    network = TreeNet( depth=2, fanout=4 )
    network.start()
    app = ConsoleApp( network, width=4 )
    app.mainloop()
Bob Lantz's avatar
Bob Lantz committed
    network.stop()