blob: ea2e28dbafb15c430c0ea25c4875e5e015bd396e [file] [log] [blame]
carlosmscabralf40ecd12013-02-01 18:15:58 -02001#!/usr/bin/python
2
3"""
4consoles.py: bring up a bunch of miniature consoles on a virtual network
5
6This demo shows how to monitor a set of nodes by using
7Node's monitor() and Tkinter's createfilehandler().
8
9We 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
18The 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
24Bob Lantz, April 2010
25
26"""
27
28import re
29
30from Tkinter import Frame, Button, Label, Text, Scrollbar, Canvas, Wm, READABLE
31
32from mininet.log import setLogLevel
33from mininet.topolib import TreeNet
34from mininet.term import makeTerms, cleanUpScreens
35from mininet.util import quietRun
36
37class 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
168class 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
274class 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
440def assign( obj, **kwargs ):
441 "Set a bunch of fields in an object."
442 obj.__dict__.update( kwargs )
443
444class Object( object ):
445 "Generic object you can stuff junk into."
446 def __init__( self, **kwargs ):
447 assign( self, **kwargs )
448
449
450if __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()