Statistics
| Branch: | Tag: | Revision:

mininet / examples / consoles.py @ 8a130dea

History | View | Annotate | Download (15 KB)

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
        if self.outputHook:
111
            self.outputHook( self, text )
112

    
113
    def handleKey( self, event ):
114
        "If it's an interactive command, send it to the node."
115
        char = event.char
116
        if self.node.waiting:
117
            self.node.write( char )
118

    
119
    def handleReturn( self, event ):
120
        "Handle a carriage return."
121
        cmd = self.text.get( 'insert linestart', 'insert lineend' )
122
        # Send it immediately, if "interactive" command
123
        if self.node.waiting:
124
            self.node.write( event.char )
125
            return
126
        # Otherwise send the whole line to the shell
127
        pos = cmd.find( self.prompt )
128
        if pos >= 0:
129
            cmd = cmd[ pos + len( self.prompt ): ]
130
        self.sendCmd( cmd )
131

    
132
    # Callback ignores event
133
    # pylint: disable-msg=W0613
134
    def handleInt( self, event=None ):
135
        "Handle control-c."
136
        self.node.sendInt()
137
    # pylint: enable-msg=W0613
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
    # Callback ignores fds
145
    # pylint: disable-msg=W0613
146
    def handleReadable( self, fds, timeoutms=None ):
147
        "Handle file readable event."
148
        data = self.node.monitor( timeoutms )
149
        self.append( data )
150
        if not self.node.waiting:
151
            # Print prompt
152
            self.append( self.prompt )
153
    # pylint: enable-msg=W0613
154

    
155
    def waiting( self ):
156
        "Are we waiting for output?"
157
        return self.node.waiting
158

    
159
    def waitOutput( self ):
160
        "Wait for any remaining output."
161
        while self.node.waiting:
162
            # A bit of a trade-off here...
163
            self.handleReadable( self, timeoutms=1000)
164
            self.update()
165

    
166
    def clear( self ):
167
        "Clear all of our text."
168
        self.text.delete( '1.0', 'end' )
169

    
170

    
171
class Graph( Frame ):
172

    
173
    "Graph that we can add bars to over time."
174

    
175
    def __init__( self, parent=None,
176
        bg = 'white',
177
        gheight=200, gwidth=500,
178
        barwidth=10,
179
        ymax=3.5,):
180

    
181
        Frame.__init__( self, parent )
182

    
183
        self.bg = bg
184
        self.gheight = gheight
185
        self.gwidth = gwidth
186
        self.barwidth = barwidth
187
        self.ymax = float( ymax )
188
        self.xpos = 0
189

    
190
        # Create everything
191
        self.title, self.scale, self.graph = self.createWidgets()
192
        self.updateScrollRegions()
193
        self.yview( 'moveto', '1.0' )
194

    
195
    def createScale( self ):
196
        "Create a and return a new canvas with scale markers."
197
        height = float( self.gheight )
198
        width = 25
199
        ymax = self.ymax
200
        scale = Canvas( self, width=width, height=height,
201
            background=self.bg )
202
        opts = { 'fill': 'red' }
203
        # Draw scale line
204
        scale.create_line( width - 1, height, width - 1, 0, **opts )
205
        # Draw ticks and numbers
206
        for y in range( 0, int( ymax + 1 ) ):
207
            ypos = height * (1 - float( y ) / ymax )
208
            scale.create_line( width, ypos, width - 10, ypos, **opts )
209
            scale.create_text( 10, ypos, text=str( y ), **opts )
210
        return scale
211

    
212
    def updateScrollRegions( self ):
213
        "Update graph and scale scroll regions."
214
        ofs = 20
215
        height = self.gheight + ofs
216
        self.graph.configure( scrollregion=( 0, -ofs,
217
            self.xpos * self.barwidth, height ) )
218
        self.scale.configure( scrollregion=( 0, -ofs, 0, height ) )
219

    
220
    def yview( self, *args ):
221
        "Scroll both scale and graph."
222
        self.graph.yview( *args )
223
        self.scale.yview( *args )
224

    
225
    def createWidgets( self ):
226
        "Create initial widget set."
227

    
228
        # Objects
229
        title = Label( self, text='Bandwidth (Gb/s)', bg=self.bg )
230
        width = self.gwidth
231
        height = self.gheight
232
        scale = self.createScale()
233
        graph = Canvas( self, width=width, height=height, background=self.bg)
234
        xbar = Scrollbar( self, orient='horizontal', command=graph.xview )
235
        ybar = Scrollbar( self, orient='vertical', command=self.yview )
236
        graph.configure( xscrollcommand=xbar.set, yscrollcommand=ybar.set,
237
            scrollregion=(0, 0, width, height ) )
238
        scale.configure( yscrollcommand=ybar.set )
239

    
240
        # Layout
241
        title.grid( row=0, columnspan=3, sticky='new')
242
        scale.grid( row=1, column=0, sticky='nsew' )
243
        graph.grid( row=1, column=1, sticky='nsew' )
244
        ybar.grid( row=1, column=2, sticky='ns' )
245
        xbar.grid( row=2, column=0, columnspan=2, sticky='ew' )
246
        self.rowconfigure( 1, weight=1 )
247
        self.columnconfigure( 1, weight=1 )
248
        return title, scale, graph
249

    
250
    def addBar( self, yval ):
251
        "Add a new bar to our graph."
252
        percent = yval / self.ymax
253
        c = self.graph
254
        x0 = self.xpos * self.barwidth
255
        x1 = x0 + self.barwidth
256
        y0 = self.gheight
257
        y1 = ( 1 - percent ) * self.gheight
258
        c.create_rectangle( x0 , y0, x1, y1, fill='green' )
259
        self.xpos += 1
260
        self.updateScrollRegions()
261
        self.graph.xview( 'moveto', '1.0' )
262

    
263
    def clear( self ):
264
        "Clear graph contents."
265
        self.graph.delete( 'all' )
266
        self.xpos = 0
267

    
268
    def test( self ):
269
        "Add a bar for testing purposes."
270
        ms = 1000
271
        if self.xpos < 10:
272
            self.addBar( self.xpos / 10 * self.ymax  )
273
            self.after( ms, self.test )
274

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

    
279

    
280
class ConsoleApp( Frame ):
281

    
282
    "Simple Tk consoles for Mininet."
283

    
284
    menuStyle = { 'font': 'Geneva 7 bold' }
285

    
286
    def __init__( self, net, parent=None, width=4 ):
287
        Frame.__init__( self, parent )
288
        self.top = self.winfo_toplevel()
289
        self.top.title( 'Mininet' )
290
        self.net = net
291
        self.menubar = self.createMenuBar()
292
        cframe = self.cframe = Frame( self )
293
        self.consoles = {}  # consoles themselves
294
        titles = {
295
            'hosts': 'Host',
296
            'switches': 'Switch',
297
            'controllers': 'Controller'
298
        }
299
        for name in titles:
300
            nodes = getattr( net, name )
301
            frame, consoles = self.createConsoles(
302
                cframe, nodes, width, titles[ name ] )
303
            self.consoles[ name ] = Object( frame=frame, consoles=consoles )
304
        self.selected = None
305
        self.select( 'hosts' )
306
        self.cframe.pack( expand=True, fill='both' )
307
        cleanUpScreens()
308
        # Close window gracefully
309
        Wm.wm_protocol( self.top, name='WM_DELETE_WINDOW', func=self.quit )
310

    
311
        # Initialize graph
312
        graph = Graph( cframe )
313
        self.consoles[ 'graph' ] = Object( frame=graph, consoles=[ graph ] )
314
        self.graph = graph
315
        self.graphVisible = False
316
        self.updates = 0
317
        self.hostCount = len( self.consoles[ 'hosts' ].consoles )
318
        self.bw = 0
319

    
320
        self.pack( expand=True, fill='both' )
321

    
322
    # Update callback doesn't use console arg
323
    # pylint: disable-msg=W0613
324
    def updateGraph( self, console, output ):
325
        "Update our graph."
326
        m = re.search( r'(\d+) Mbits/sec', output )
327
        if not m:
328
            return
329
        self.updates += 1
330
        self.bw += .001 * float( m.group( 1 ) )
331
        if self.updates >= self.hostCount:
332
            self.graph.addBar( self.bw )
333
            self.bw = 0
334
            self.updates = 0
335
    # pylint: enable-msg=W0613
336

    
337
    def setOutputHook( self, fn=None, consoles=None ):
338
        "Register fn as output hook [on specific consoles.]"
339
        if consoles is None:
340
            consoles = self.consoles[ 'hosts' ].consoles
341
        for console in consoles:
342
            console.outputHook = fn
343

    
344
    def createConsoles( self, parent, nodes, width, title ):
345
        "Create a grid of consoles in a frame."
346
        f = Frame( parent )
347
        # Create consoles
348
        consoles = []
349
        index = 0
350
        for node in nodes:
351
            console = Console( f, self.net, node, title=title )
352
            consoles.append( console )
353
            row = index / width
354
            column = index % width
355
            console.grid( row=row, column=column, sticky='nsew' )
356
            index += 1
357
            f.rowconfigure( row, weight=1 )
358
            f.columnconfigure( column, weight=1 )
359
        return f, consoles
360

    
361
    def select( self, groupName ):
362
        "Select a group of consoles to display."
363
        if self.selected is not None:
364
            self.selected.frame.pack_forget()
365
        self.selected = self.consoles[ groupName ]
366
        self.selected.frame.pack( expand=True, fill='both' )
367

    
368
    def createMenuBar( self ):
369
        "Create and return a menu (really button) bar."
370
        f = Frame( self )
371
        buttons = [
372
            ( 'Hosts', lambda: self.select( 'hosts' ) ),
373
            ( 'Switches', lambda: self.select( 'switches' ) ),
374
            ( 'Controllers', lambda: self.select( 'controllers' ) ),
375
            ( 'Graph', lambda: self.select( 'graph' ) ),
376
            ( 'Ping', self.ping ),
377
            ( 'Iperf', self.iperf ),
378
            ( 'Interrupt', self.stop ),
379
            ( 'Clear', self.clear ),
380
            ( 'Quit', self.quit )
381
        ]
382
        for name, cmd in buttons:
383
            b = Button( f, text=name, command=cmd, **self.menuStyle )
384
            b.pack( side='left' )
385
        f.pack( padx=4, pady=4, fill='x' )
386
        return f
387

    
388
    def clear( self ):
389
        "Clear selection."
390
        for console in self.selected.consoles:
391
            console.clear()
392

    
393
    def waiting( self, consoles=None ):
394
        "Are any of our hosts waiting for output?"
395
        if consoles is None:
396
            consoles = self.consoles[ 'hosts' ].consoles
397
        for console in consoles:
398
            if console.waiting():
399
                return True
400
        return False
401

    
402
    def ping( self ):
403
        "Tell each host to ping the next one."
404
        consoles = self.consoles[ 'hosts' ].consoles
405
        if self.waiting( consoles ):
406
            return
407
        count = len( consoles )
408
        i = 0
409
        for console in consoles:
410
            i = ( i + 1 ) % count
411
            ip = consoles[ i ].node.IP()
412
            console.sendCmd( 'ping ' + ip )
413

    
414
    def iperf( self ):
415
        "Tell each host to iperf to the next one."
416
        consoles = self.consoles[ 'hosts' ].consoles
417
        if self.waiting( consoles ):
418
            return
419
        count = len( consoles )
420
        self.setOutputHook( self.updateGraph )
421
        for console in consoles:
422
            console.node.cmd( 'iperf -sD' )
423
        i = 0
424
        for console in consoles:
425
            i = ( i + 1 ) % count
426
            ip = consoles[ i ].node.IP()
427
            console.sendCmd( 'iperf -t 99999 -i 1 -c ' + ip )
428

    
429
    def stop( self, wait=True ):
430
        "Interrupt all hosts."
431
        consoles = self.consoles[ 'hosts' ].consoles
432
        for console in consoles:
433
            console.handleInt()
434
        if wait:
435
            for console in consoles:
436
                console.waitOutput()
437
        self.setOutputHook( None )
438
        # Shut down any iperfs that might still be running
439
        quietRun( 'killall -9 iperf' )
440

    
441
    def quit( self ):
442
        "Stop everything and quit."
443
        self.stop( wait=False)
444
        Frame.quit( self )
445

    
446

    
447
# Make it easier to construct and assign objects
448

    
449
def assign( obj, **kwargs ):
450
    "Set a bunch of fields in an object."
451
    obj.__dict__.update( kwargs )
452

    
453
class Object( object ):
454
    "Generic object you can stuff junk into."
455
    def __init__( self, **kwargs ):
456
        assign( self, **kwargs )
457

    
458

    
459
if __name__ == '__main__':
460
    setLogLevel( 'info' )
461
    network = TreeNet( depth=2, fanout=4 )
462
    network.start()
463
    app = ConsoleApp( network, width=4 )
464
    app.mainloop()
465
    network.stop()