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
  
- 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

from Tkinter import *

Bob Lantz's avatar
Bob Lantz committed
from mininet.log import setLogLevel,info
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
    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
                
        # 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' )
Bob Lantz's avatar
Bob Lantz committed
        
        self.outputHook = None
        
    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 )
        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."
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,
            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' )
Bob Lantz's avatar
Bob Lantz committed
        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()
Bob Lantz's avatar
Bob Lantz committed

    def sendCmd( self, cmd ):
        "Send a command to our node."
        text, node = self.text, self.node
Bob Lantz's avatar
Bob Lantz committed
        if not node.waiting:
            node.sendCmd( cmd )
    def handleReadable( self, file=None, mask=None, 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

            
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' )
        
Bob Lantz's avatar
Bob Lantz committed
        
Bob Lantz's avatar
Bob Lantz committed
    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
Bob Lantz's avatar
Bob Lantz committed
        titles = { 
            'hosts': 'Host', 
            'switches': 'Switch',
            'controllers': 'Controller'
        }
        for name in titles:
            nodes = getattr( net, name )
Bob Lantz's avatar
Bob Lantz committed
            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()
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
        # 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
            
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, 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' ) ),
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
    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
    
    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:
            console.node.cmd( 'iperf -sD' )
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 -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 ):
        "Stope 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."
    for name, value in kwargs.items():
        setattr( obj, name, value )

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
    net = TreeNet( depth=2, fanout=4 )
    net.start()
    app = ConsoleApp( net, width=4 )
    app.mainloop()
    net.stop()