467 lines
15 KiB
Python
Executable File
467 lines
15 KiB
Python
Executable File
#!/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+.?\d*) ([KMG]?bits)/sec', output )
|
|
if not m:
|
|
return
|
|
val, units = float( m.group( 1 ) ), m.group( 2 )
|
|
#convert to Gbps
|
|
if units[0] == 'M':
|
|
val *= 10 ** -3
|
|
elif units[0] == 'K':
|
|
val *= 10 ** -6
|
|
elif units[0] == 'b':
|
|
val *= 10 ** -9
|
|
self.updates += 1
|
|
self.bw += val
|
|
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 -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()
|