Statistics
| Branch: | Tag: | Revision:

mininet / examples / consoles.py @ edf60032

History | View | Annotate | Download (14.9 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+) 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()