carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 1 | #!/usr/bin/python |
| 2 | |
| 3 | """ |
| 4 | consoles.py: bring up a bunch of miniature consoles on a virtual network |
| 5 | |
| 6 | This demo shows how to monitor a set of nodes by using |
| 7 | Node's monitor() and Tkinter's createfilehandler(). |
| 8 | |
| 9 | We monitor nodes in a couple of ways: |
| 10 | |
| 11 | - First, each individual node is monitored, and its output is added |
| 12 | to its console window |
| 13 | |
| 14 | - Second, each time a console window gets iperf output, it is parsed |
| 15 | and accumulated. Once we have output for all consoles, a bar is |
| 16 | added to the bandwidth graph. |
| 17 | |
| 18 | The consoles also support limited interaction: |
| 19 | |
| 20 | - Pressing "return" in a console will send a command to it |
| 21 | |
| 22 | - Pressing the console's title button will open up an xterm |
| 23 | |
| 24 | Bob Lantz, April 2010 |
| 25 | |
| 26 | """ |
| 27 | |
| 28 | import re |
| 29 | |
| 30 | from Tkinter import Frame, Button, Label, Text, Scrollbar, Canvas, Wm, READABLE |
| 31 | |
| 32 | from mininet.log import setLogLevel |
| 33 | from mininet.topolib import TreeNet |
| 34 | from mininet.term import makeTerms, cleanUpScreens |
| 35 | from mininet.util import quietRun |
| 36 | |
| 37 | class Console( Frame ): |
| 38 | "A simple console on a host." |
| 39 | |
| 40 | def __init__( self, parent, net, node, height=10, width=32, title='Node' ): |
| 41 | Frame.__init__( self, parent ) |
| 42 | |
| 43 | self.net = net |
| 44 | self.node = node |
| 45 | self.prompt = node.name + '# ' |
| 46 | self.height, self.width, self.title = height, width, title |
| 47 | |
| 48 | # Initialize widget styles |
| 49 | self.buttonStyle = { 'font': 'Monaco 7' } |
| 50 | self.textStyle = { |
| 51 | 'font': 'Monaco 7', |
| 52 | 'bg': 'black', |
| 53 | 'fg': 'green', |
| 54 | 'width': self.width, |
| 55 | 'height': self.height, |
| 56 | 'relief': 'sunken', |
| 57 | 'insertbackground': 'green', |
| 58 | 'highlightcolor': 'green', |
| 59 | 'selectforeground': 'black', |
| 60 | 'selectbackground': 'green' |
| 61 | } |
| 62 | |
| 63 | # Set up widgets |
| 64 | self.text = self.makeWidgets( ) |
| 65 | self.bindEvents() |
| 66 | self.sendCmd( 'export TERM=dumb' ) |
| 67 | |
| 68 | self.outputHook = None |
| 69 | |
| 70 | def makeWidgets( self ): |
| 71 | "Make a label, a text area, and a scroll bar." |
| 72 | |
| 73 | def newTerm( net=self.net, node=self.node, title=self.title ): |
| 74 | "Pop up a new terminal window for a node." |
| 75 | net.terms += makeTerms( [ node ], title ) |
| 76 | label = Button( self, text=self.node.name, command=newTerm, |
| 77 | **self.buttonStyle ) |
| 78 | label.pack( side='top', fill='x' ) |
| 79 | text = Text( self, wrap='word', **self.textStyle ) |
| 80 | ybar = Scrollbar( self, orient='vertical', width=7, |
| 81 | command=text.yview ) |
| 82 | text.configure( yscrollcommand=ybar.set ) |
| 83 | text.pack( side='left', expand=True, fill='both' ) |
| 84 | ybar.pack( side='right', fill='y' ) |
| 85 | return text |
| 86 | |
| 87 | def bindEvents( self ): |
| 88 | "Bind keyboard and file events." |
| 89 | # The text widget handles regular key presses, but we |
| 90 | # use special handlers for the following: |
| 91 | self.text.bind( '<Return>', self.handleReturn ) |
| 92 | self.text.bind( '<Control-c>', self.handleInt ) |
| 93 | self.text.bind( '<KeyPress>', self.handleKey ) |
| 94 | # This is not well-documented, but it is the correct |
| 95 | # way to trigger a file event handler from Tk's |
| 96 | # event loop! |
| 97 | self.tk.createfilehandler( self.node.stdout, READABLE, |
| 98 | self.handleReadable ) |
| 99 | |
| 100 | # We're not a terminal (yet?), so we ignore the following |
| 101 | # control characters other than [\b\n\r] |
| 102 | ignoreChars = re.compile( r'[\x00-\x07\x09\x0b\x0c\x0e-\x1f]+' ) |
| 103 | |
| 104 | def append( self, text ): |
| 105 | "Append something to our text frame." |
| 106 | text = self.ignoreChars.sub( '', text ) |
| 107 | self.text.insert( 'end', text ) |
| 108 | self.text.mark_set( 'insert', 'end' ) |
| 109 | self.text.see( 'insert' ) |
| 110 | outputHook = lambda x, y: True # make pylint happier |
| 111 | if self.outputHook: |
| 112 | outputHook = self.outputHook |
| 113 | outputHook( self, text ) |
| 114 | |
| 115 | def handleKey( self, event ): |
| 116 | "If it's an interactive command, send it to the node." |
| 117 | char = event.char |
| 118 | if self.node.waiting: |
| 119 | self.node.write( char ) |
| 120 | |
| 121 | def handleReturn( self, event ): |
| 122 | "Handle a carriage return." |
| 123 | cmd = self.text.get( 'insert linestart', 'insert lineend' ) |
| 124 | # Send it immediately, if "interactive" command |
| 125 | if self.node.waiting: |
| 126 | self.node.write( event.char ) |
| 127 | return |
| 128 | # Otherwise send the whole line to the shell |
| 129 | pos = cmd.find( self.prompt ) |
| 130 | if pos >= 0: |
| 131 | cmd = cmd[ pos + len( self.prompt ): ] |
| 132 | self.sendCmd( cmd ) |
| 133 | |
| 134 | # Callback ignores event |
| 135 | def handleInt( self, _event=None ): |
| 136 | "Handle control-c." |
| 137 | self.node.sendInt() |
| 138 | |
| 139 | def sendCmd( self, cmd ): |
| 140 | "Send a command to our node." |
| 141 | if not self.node.waiting: |
| 142 | self.node.sendCmd( cmd ) |
| 143 | |
| 144 | def handleReadable( self, _fds, timeoutms=None ): |
| 145 | "Handle file readable event." |
| 146 | data = self.node.monitor( timeoutms ) |
| 147 | self.append( data ) |
| 148 | if not self.node.waiting: |
| 149 | # Print prompt |
| 150 | self.append( self.prompt ) |
| 151 | |
| 152 | def waiting( self ): |
| 153 | "Are we waiting for output?" |
| 154 | return self.node.waiting |
| 155 | |
| 156 | def waitOutput( self ): |
| 157 | "Wait for any remaining output." |
| 158 | while self.node.waiting: |
| 159 | # A bit of a trade-off here... |
| 160 | self.handleReadable( self, timeoutms=1000) |
| 161 | self.update() |
| 162 | |
| 163 | def clear( self ): |
| 164 | "Clear all of our text." |
| 165 | self.text.delete( '1.0', 'end' ) |
| 166 | |
| 167 | |
| 168 | class Graph( Frame ): |
| 169 | |
| 170 | "Graph that we can add bars to over time." |
| 171 | |
| 172 | def __init__( self, parent=None, bg = 'white', gheight=200, gwidth=500, |
| 173 | barwidth=10, ymax=3.5,): |
| 174 | |
| 175 | Frame.__init__( self, parent ) |
| 176 | |
| 177 | self.bg = bg |
| 178 | self.gheight = gheight |
| 179 | self.gwidth = gwidth |
| 180 | self.barwidth = barwidth |
| 181 | self.ymax = float( ymax ) |
| 182 | self.xpos = 0 |
| 183 | |
| 184 | # Create everything |
| 185 | self.title, self.scale, self.graph = self.createWidgets() |
| 186 | self.updateScrollRegions() |
| 187 | self.yview( 'moveto', '1.0' ) |
| 188 | |
| 189 | def createScale( self ): |
| 190 | "Create a and return a new canvas with scale markers." |
| 191 | height = float( self.gheight ) |
| 192 | width = 25 |
| 193 | ymax = self.ymax |
| 194 | scale = Canvas( self, width=width, height=height, |
| 195 | background=self.bg ) |
| 196 | opts = { 'fill': 'red' } |
| 197 | # Draw scale line |
| 198 | scale.create_line( width - 1, height, width - 1, 0, **opts ) |
| 199 | # Draw ticks and numbers |
| 200 | for y in range( 0, int( ymax + 1 ) ): |
| 201 | ypos = height * (1 - float( y ) / ymax ) |
| 202 | scale.create_line( width, ypos, width - 10, ypos, **opts ) |
| 203 | scale.create_text( 10, ypos, text=str( y ), **opts ) |
| 204 | return scale |
| 205 | |
| 206 | def updateScrollRegions( self ): |
| 207 | "Update graph and scale scroll regions." |
| 208 | ofs = 20 |
| 209 | height = self.gheight + ofs |
| 210 | self.graph.configure( scrollregion=( 0, -ofs, |
| 211 | self.xpos * self.barwidth, height ) ) |
| 212 | self.scale.configure( scrollregion=( 0, -ofs, 0, height ) ) |
| 213 | |
| 214 | def yview( self, *args ): |
| 215 | "Scroll both scale and graph." |
| 216 | self.graph.yview( *args ) |
| 217 | self.scale.yview( *args ) |
| 218 | |
| 219 | def createWidgets( self ): |
| 220 | "Create initial widget set." |
| 221 | |
| 222 | # Objects |
| 223 | title = Label( self, text='Bandwidth (Gb/s)', bg=self.bg ) |
| 224 | width = self.gwidth |
| 225 | height = self.gheight |
| 226 | scale = self.createScale() |
| 227 | graph = Canvas( self, width=width, height=height, background=self.bg) |
| 228 | xbar = Scrollbar( self, orient='horizontal', command=graph.xview ) |
| 229 | ybar = Scrollbar( self, orient='vertical', command=self.yview ) |
| 230 | graph.configure( xscrollcommand=xbar.set, yscrollcommand=ybar.set, |
| 231 | scrollregion=(0, 0, width, height ) ) |
| 232 | scale.configure( yscrollcommand=ybar.set ) |
| 233 | |
| 234 | # Layout |
| 235 | title.grid( row=0, columnspan=3, sticky='new') |
| 236 | scale.grid( row=1, column=0, sticky='nsew' ) |
| 237 | graph.grid( row=1, column=1, sticky='nsew' ) |
| 238 | ybar.grid( row=1, column=2, sticky='ns' ) |
| 239 | xbar.grid( row=2, column=0, columnspan=2, sticky='ew' ) |
| 240 | self.rowconfigure( 1, weight=1 ) |
| 241 | self.columnconfigure( 1, weight=1 ) |
| 242 | return title, scale, graph |
| 243 | |
| 244 | def addBar( self, yval ): |
| 245 | "Add a new bar to our graph." |
| 246 | percent = yval / self.ymax |
| 247 | c = self.graph |
| 248 | x0 = self.xpos * self.barwidth |
| 249 | x1 = x0 + self.barwidth |
| 250 | y0 = self.gheight |
| 251 | y1 = ( 1 - percent ) * self.gheight |
| 252 | c.create_rectangle( x0, y0, x1, y1, fill='green' ) |
| 253 | self.xpos += 1 |
| 254 | self.updateScrollRegions() |
| 255 | self.graph.xview( 'moveto', '1.0' ) |
| 256 | |
| 257 | def clear( self ): |
| 258 | "Clear graph contents." |
| 259 | self.graph.delete( 'all' ) |
| 260 | self.xpos = 0 |
| 261 | |
| 262 | def test( self ): |
| 263 | "Add a bar for testing purposes." |
| 264 | ms = 1000 |
| 265 | if self.xpos < 10: |
| 266 | self.addBar( self.xpos / 10 * self.ymax ) |
| 267 | self.after( ms, self.test ) |
| 268 | |
| 269 | def setTitle( self, text ): |
| 270 | "Set graph title" |
| 271 | self.title.configure( text=text, font='Helvetica 9 bold' ) |
| 272 | |
| 273 | |
| 274 | class ConsoleApp( Frame ): |
| 275 | |
| 276 | "Simple Tk consoles for Mininet." |
| 277 | |
| 278 | menuStyle = { 'font': 'Geneva 7 bold' } |
| 279 | |
| 280 | def __init__( self, net, parent=None, width=4 ): |
| 281 | Frame.__init__( self, parent ) |
| 282 | self.top = self.winfo_toplevel() |
| 283 | self.top.title( 'Mininet' ) |
| 284 | self.net = net |
| 285 | self.menubar = self.createMenuBar() |
| 286 | cframe = self.cframe = Frame( self ) |
| 287 | self.consoles = {} # consoles themselves |
| 288 | titles = { |
| 289 | 'hosts': 'Host', |
| 290 | 'switches': 'Switch', |
| 291 | 'controllers': 'Controller' |
| 292 | } |
| 293 | for name in titles: |
| 294 | nodes = getattr( net, name ) |
| 295 | frame, consoles = self.createConsoles( |
| 296 | cframe, nodes, width, titles[ name ] ) |
| 297 | self.consoles[ name ] = Object( frame=frame, consoles=consoles ) |
| 298 | self.selected = None |
| 299 | self.select( 'hosts' ) |
| 300 | self.cframe.pack( expand=True, fill='both' ) |
| 301 | cleanUpScreens() |
| 302 | # Close window gracefully |
| 303 | Wm.wm_protocol( self.top, name='WM_DELETE_WINDOW', func=self.quit ) |
| 304 | |
| 305 | # Initialize graph |
| 306 | graph = Graph( cframe ) |
| 307 | self.consoles[ 'graph' ] = Object( frame=graph, consoles=[ graph ] ) |
| 308 | self.graph = graph |
| 309 | self.graphVisible = False |
| 310 | self.updates = 0 |
| 311 | self.hostCount = len( self.consoles[ 'hosts' ].consoles ) |
| 312 | self.bw = 0 |
| 313 | |
| 314 | self.pack( expand=True, fill='both' ) |
| 315 | |
| 316 | def updateGraph( self, _console, output ): |
| 317 | "Update our graph." |
| 318 | m = re.search( r'(\d+) Mbits/sec', output ) |
| 319 | if not m: |
| 320 | return |
| 321 | self.updates += 1 |
| 322 | self.bw += .001 * float( m.group( 1 ) ) |
| 323 | if self.updates >= self.hostCount: |
| 324 | self.graph.addBar( self.bw ) |
| 325 | self.bw = 0 |
| 326 | self.updates = 0 |
| 327 | |
| 328 | def setOutputHook( self, fn=None, consoles=None ): |
| 329 | "Register fn as output hook [on specific consoles.]" |
| 330 | if consoles is None: |
| 331 | consoles = self.consoles[ 'hosts' ].consoles |
| 332 | for console in consoles: |
| 333 | console.outputHook = fn |
| 334 | |
| 335 | def createConsoles( self, parent, nodes, width, title ): |
| 336 | "Create a grid of consoles in a frame." |
| 337 | f = Frame( parent ) |
| 338 | # Create consoles |
| 339 | consoles = [] |
| 340 | index = 0 |
| 341 | for node in nodes: |
| 342 | console = Console( f, self.net, node, title=title ) |
| 343 | consoles.append( console ) |
| 344 | row = index / width |
| 345 | column = index % width |
| 346 | console.grid( row=row, column=column, sticky='nsew' ) |
| 347 | index += 1 |
| 348 | f.rowconfigure( row, weight=1 ) |
| 349 | f.columnconfigure( column, weight=1 ) |
| 350 | return f, consoles |
| 351 | |
| 352 | def select( self, groupName ): |
| 353 | "Select a group of consoles to display." |
| 354 | if self.selected is not None: |
| 355 | self.selected.frame.pack_forget() |
| 356 | self.selected = self.consoles[ groupName ] |
| 357 | self.selected.frame.pack( expand=True, fill='both' ) |
| 358 | |
| 359 | def createMenuBar( self ): |
| 360 | "Create and return a menu (really button) bar." |
| 361 | f = Frame( self ) |
| 362 | buttons = [ |
| 363 | ( 'Hosts', lambda: self.select( 'hosts' ) ), |
| 364 | ( 'Switches', lambda: self.select( 'switches' ) ), |
| 365 | ( 'Controllers', lambda: self.select( 'controllers' ) ), |
| 366 | ( 'Graph', lambda: self.select( 'graph' ) ), |
| 367 | ( 'Ping', self.ping ), |
| 368 | ( 'Iperf', self.iperf ), |
| 369 | ( 'Interrupt', self.stop ), |
| 370 | ( 'Clear', self.clear ), |
| 371 | ( 'Quit', self.quit ) |
| 372 | ] |
| 373 | for name, cmd in buttons: |
| 374 | b = Button( f, text=name, command=cmd, **self.menuStyle ) |
| 375 | b.pack( side='left' ) |
| 376 | f.pack( padx=4, pady=4, fill='x' ) |
| 377 | return f |
| 378 | |
| 379 | def clear( self ): |
| 380 | "Clear selection." |
| 381 | for console in self.selected.consoles: |
| 382 | console.clear() |
| 383 | |
| 384 | def waiting( self, consoles=None ): |
| 385 | "Are any of our hosts waiting for output?" |
| 386 | if consoles is None: |
| 387 | consoles = self.consoles[ 'hosts' ].consoles |
| 388 | for console in consoles: |
| 389 | if console.waiting(): |
| 390 | return True |
| 391 | return False |
| 392 | |
| 393 | def ping( self ): |
| 394 | "Tell each host to ping the next one." |
| 395 | consoles = self.consoles[ 'hosts' ].consoles |
| 396 | if self.waiting( consoles ): |
| 397 | return |
| 398 | count = len( consoles ) |
| 399 | i = 0 |
| 400 | for console in consoles: |
| 401 | i = ( i + 1 ) % count |
| 402 | ip = consoles[ i ].node.IP() |
| 403 | console.sendCmd( 'ping ' + ip ) |
| 404 | |
| 405 | def iperf( self ): |
| 406 | "Tell each host to iperf to the next one." |
| 407 | consoles = self.consoles[ 'hosts' ].consoles |
| 408 | if self.waiting( consoles ): |
| 409 | return |
| 410 | count = len( consoles ) |
| 411 | self.setOutputHook( self.updateGraph ) |
| 412 | for console in consoles: |
| 413 | console.node.cmd( 'iperf -sD' ) |
| 414 | i = 0 |
| 415 | for console in consoles: |
| 416 | i = ( i + 1 ) % count |
| 417 | ip = consoles[ i ].node.IP() |
| 418 | console.sendCmd( 'iperf -t 99999 -i 1 -c ' + ip ) |
| 419 | |
| 420 | def stop( self, wait=True ): |
| 421 | "Interrupt all hosts." |
| 422 | consoles = self.consoles[ 'hosts' ].consoles |
| 423 | for console in consoles: |
| 424 | console.handleInt() |
| 425 | if wait: |
| 426 | for console in consoles: |
| 427 | console.waitOutput() |
| 428 | self.setOutputHook( None ) |
| 429 | # Shut down any iperfs that might still be running |
| 430 | quietRun( 'killall -9 iperf' ) |
| 431 | |
| 432 | def quit( self ): |
| 433 | "Stop everything and quit." |
| 434 | self.stop( wait=False) |
| 435 | Frame.quit( self ) |
| 436 | |
| 437 | |
| 438 | # Make it easier to construct and assign objects |
| 439 | |
| 440 | def assign( obj, **kwargs ): |
| 441 | "Set a bunch of fields in an object." |
| 442 | obj.__dict__.update( kwargs ) |
| 443 | |
| 444 | class Object( object ): |
| 445 | "Generic object you can stuff junk into." |
| 446 | def __init__( self, **kwargs ): |
| 447 | assign( self, **kwargs ) |
| 448 | |
| 449 | |
| 450 | if __name__ == '__main__': |
| 451 | setLogLevel( 'info' ) |
| 452 | network = TreeNet( depth=2, fanout=4 ) |
| 453 | network.start() |
| 454 | app = ConsoleApp( network, width=4 ) |
| 455 | app.mainloop() |
| 456 | network.stop() |