Statistics
| Branch: | Tag: | Revision:

mininet / examples / consoles.py @ 32de4c9e

History | View | Annotate | Download (15.2 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
        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+.?\d*) ([KMG]?bits)/sec', output )
319
        if not m:
320
            return
321
        val, units = float( m.group( 1 ) ), m.group( 2 )
322
        #convert to Gbps
323
        if units[0] == 'M':
324
            val *= 10 ** -3
325
        elif units[0] == 'K':
326
            val *= 10 ** -6
327
        elif units[0] == 'b':
328
            val *= 10 ** -9
329
        self.updates += 1
330
        self.bw +=  val
331
        if self.updates >= self.hostCount:
332
            self.graph.addBar( self.bw )
333
            self.bw = 0
334
            self.updates = 0
335

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

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

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

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

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

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

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

    
413
    def iperf( self ):
414
        "Tell each host to iperf to the next one."
415
        consoles = self.consoles[ 'hosts' ].consoles
416
        if self.waiting( consoles ):
417
            return
418
        count = len( consoles )
419
        self.setOutputHook( self.updateGraph )
420
        for console in consoles:
421
            #sometimes iperf -sD doesn't return, so we run it in the background instead
422
            console.node.cmd( 'iperf -s &' )
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()