Statistics
| Branch: | Tag: | Revision:

mininet / mininet / cli.py @ b1ec912d

History | View | Annotate | Download (14.6 KB)

1
"""
2
A simple command-line interface for Mininet.
3

4
The Mininet CLI provides a simple control console which
5
makes it easy to talk to nodes. For example, the command
6

7
mininet> h27 ifconfig
8

9
runs 'ifconfig' on host h27.
10

11
Having a single console rather than, for example, an xterm for each
12
node is particularly convenient for networks of any reasonable
13
size.
14

15
The CLI automatically substitutes IP addresses for node names,
16
so commands like
17

18
mininet> h2 ping h3
19

20
should work correctly and allow host h2 to ping host h3
21

22
Several useful commands are provided, including the ability to
23
list all nodes ('nodes'), to print out the network topology
24
('net') and to check connectivity ('pingall', 'pingpair')
25
and bandwidth ('iperf'.)
26
"""
27

    
28
from subprocess import call
29
from cmd import Cmd
30
from os import isatty
31
from select import poll, POLLIN
32
import sys
33
import time
34
import os
35
import atexit
36

    
37
from mininet.log import info, output, error
38
from mininet.term import makeTerms, runX11
39
from mininet.util import ( quietRun, dumpNodeConnections,
40
                         dumpPorts )
41

    
42
class CLI( Cmd ):
43
    "Simple command-line interface to talk to nodes."
44

    
45
    prompt = 'mininet> '
46

    
47
    def __init__( self, mininet, stdin=sys.stdin, script=None ):
48
        self.mn = mininet
49
        # Local variable bindings for py command
50
        self.locals = { 'net': mininet }
51
        # Attempt to handle input
52
        self.stdin = stdin
53
        self.inPoller = poll()
54
        self.inPoller.register( stdin )
55
        self.inputFile = script
56
        Cmd.__init__( self )
57
        info( '*** Starting CLI:\n' )
58

    
59
        # Set up history if readline is available
60
        try:
61
            import readline
62
        except ImportError:
63
            pass
64
        else:
65
            history_path = os.path.expanduser('~/.mininet_history')
66
            if os.path.isfile(history_path):
67
                readline.read_history_file(history_path)
68
            atexit.register(lambda: readline.write_history_file(history_path))
69

    
70
        if self.inputFile:
71
            self.do_source( self.inputFile )
72
            return
73
        while True:
74
            try:
75
                # Make sure no nodes are still waiting
76
                for node in self.mn.values():
77
                    while node.waiting:
78
                        node.sendInt()
79
                        node.waitOutput()
80
                if self.isatty():
81
                    quietRun( 'stty echo sane intr "^C"' )
82
                self.cmdloop()
83
                break
84
            except KeyboardInterrupt:
85
                output( '\nInterrupt\n' )
86

    
87
    def emptyline( self ):
88
        "Don't repeat last command when you hit return."
89
        pass
90

    
91
    def getLocals( self ):
92
        "Local variable bindings for py command"
93
        self.locals.update( self.mn )
94
        return self.locals
95

    
96
    # Disable pylint "Unused argument: 'arg's'" messages, as well as
97
    # "method could be a function" warning, since each CLI function
98
    # must have the same interface
99
    # pylint: disable=R0201
100

    
101
    helpStr = (
102
        'You may also send a command to a node using:\n'
103
        '  <node> command {args}\n'
104
        'For example:\n'
105
        '  mininet> h1 ifconfig\n'
106
        '\n'
107
        'The interpreter automatically substitutes IP addresses\n'
108
        'for node names when a node is the first arg, so commands\n'
109
        'like\n'
110
        '  mininet> h2 ping h3\n'
111
        'should work.\n'
112
        '\n'
113
        'Some character-oriented interactive commands require\n'
114
        'noecho:\n'
115
        '  mininet> noecho h2 vi foo.py\n'
116
        'However, starting up an xterm/gterm is generally better:\n'
117
        '  mininet> xterm h2\n\n'
118
    )
119

    
120
    def do_help( self, line ):
121
        "Describe available CLI commands."
122
        Cmd.do_help( self, line )
123
        if line is '':
124
            output( self.helpStr )
125

    
126
    def do_nodes( self, _line ):
127
        "List all nodes."
128
        nodes = ' '.join( sorted( self.mn ) )
129
        output( 'available nodes are: \n%s\n' % nodes )
130

    
131
    def do_ports( self, _line ):
132
        "display ports and interfaces for each switch"
133
        dumpPorts( self.mn.switches )
134

    
135
    def do_net( self, _line ):
136
        "List network connections."
137
        dumpNodeConnections( self.mn.values() )
138

    
139
    def do_sh( self, line ):
140
        """Run an external shell command
141
           Usage: sh [cmd args]"""
142
        call( line, shell=True )
143

    
144
    # do_py() and do_px() need to catch any exception during eval()/exec()
145
    # pylint: disable=W0703
146

    
147
    def do_py( self, line ):
148
        """Evaluate a Python expression.
149
           Node names may be used, e.g.: py h1.cmd('ls')"""
150
        try:
151
            result = eval( line, globals(), self.getLocals() )
152
            if not result:
153
                return
154
            elif isinstance( result, str ):
155
                output( result + '\n' )
156
            else:
157
                output( repr( result ) + '\n' )
158
        except Exception, e:
159
            output( str( e ) + '\n' )
160

    
161
    # We are in fact using the exec() pseudo-function
162
    # pylint: disable=W0122
163

    
164
    def do_px( self, line ):
165
        """Execute a Python statement.
166
            Node names may be used, e.g.: px print h1.cmd('ls')"""
167
        try:
168
            exec( line, globals(), self.getLocals() )
169
        except Exception, e:
170
            output( str( e ) + '\n' )
171

    
172
    # pylint: enable=W0703,W0122
173

    
174
    def do_pingall( self, line ):
175
        "Ping between all hosts."
176
        self.mn.pingAll( line )
177

    
178
    def do_pingpair( self, _line ):
179
        "Ping between first two hosts, useful for testing."
180
        self.mn.pingPair()
181

    
182
    def do_pingallfull( self, _line ):
183
        "Ping between all hosts, returns all ping results."
184
        self.mn.pingAllFull()
185

    
186
    def do_pingpairfull( self, _line ):
187
        "Ping between first two hosts, returns all ping results."
188
        self.mn.pingPairFull()
189

    
190
    def do_iperf( self, line ):
191
        """Simple iperf TCP test between two (optionally specified) hosts.
192
           Usage: iperf node1 node2"""
193
        args = line.split()
194
        if not args:
195
            self.mn.iperf()
196
        elif len(args) == 2:
197
            hosts = []
198
            err = False
199
            for arg in args:
200
                if arg not in self.mn:
201
                    err = True
202
                    error( "node '%s' not in network\n" % arg )
203
                else:
204
                    hosts.append( self.mn[ arg ] )
205
            if not err:
206
                self.mn.iperf( hosts )
207
        else:
208
            error( 'invalid number of args: iperf src dst\n' )
209

    
210
    def do_iperfudp( self, line ):
211
        """Simple iperf UDP test between two (optionally specified) hosts.
212
           Usage: iperfudp bw node1 node2"""
213
        args = line.split()
214
        if not args:
215
            self.mn.iperf( l4Type='UDP' )
216
        elif len(args) == 3:
217
            udpBw = args[ 0 ]
218
            hosts = []
219
            err = False
220
            for arg in args[ 1:3 ]:
221
                if arg not in self.mn:
222
                    err = True
223
                    error( "node '%s' not in network\n" % arg )
224
                else:
225
                    hosts.append( self.mn[ arg ] )
226
            if not err:
227
                self.mn.iperf( hosts, l4Type='UDP', udpBw=udpBw )
228
        else:
229
            error( 'invalid number of args: iperfudp bw src dst\n' +
230
                   'bw examples: 10M\n' )
231

    
232
    def do_intfs( self, _line ):
233
        "List interfaces."
234
        for node in self.mn.values():
235
            output( '%s: %s\n' %
236
                    ( node.name, ','.join( node.intfNames() ) ) )
237

    
238
    def do_dump( self, _line ):
239
        "Dump node info."
240
        for node in self.mn.values():
241
            output( '%s\n' % repr( node ) )
242

    
243
    def do_link( self, line ):
244
        """Bring link(s) between two nodes up or down.
245
           Usage: link node1 node2 [up/down]"""
246
        args = line.split()
247
        if len(args) != 3:
248
            error( 'invalid number of args: link end1 end2 [up down]\n' )
249
        elif args[ 2 ] not in [ 'up', 'down' ]:
250
            error( 'invalid type: link end1 end2 [up down]\n' )
251
        else:
252
            self.mn.configLinkStatus( *args )
253

    
254
    def do_xterm( self, line, term='xterm' ):
255
        """Spawn xterm(s) for the given node(s).
256
           Usage: xterm node1 node2 ..."""
257
        args = line.split()
258
        if not args:
259
            error( 'usage: %s node1 node2 ...\n' % term )
260
        else:
261
            for arg in args:
262
                if arg not in self.mn:
263
                    error( "node '%s' not in network\n" % arg )
264
                else:
265
                    node = self.mn[ arg ]
266
                    self.mn.terms += makeTerms( [ node ], term = term )
267

    
268
    def do_x( self, line ):
269
        """Create an X11 tunnel to the given node,
270
           optionally starting a client.
271
           Usage: x node [cmd args]"""
272
        args = line.split()
273
        if not args:
274
            error( 'usage: x node [cmd args]...\n' )
275
        else:
276
            node = self.mn[ args[ 0 ] ]
277
            cmd = args[ 1: ]
278
            self.mn.terms += runX11( node, cmd )
279

    
280
    def do_gterm( self, line ):
281
        """Spawn gnome-terminal(s) for the given node(s).
282
           Usage: gterm node1 node2 ..."""
283
        self.do_xterm( line, term='gterm' )
284

    
285
    def do_exit( self, _line ):
286
        "Exit"
287
        return 'exited by user command'
288

    
289
    def do_quit( self, line ):
290
        "Exit"
291
        return self.do_exit( line )
292

    
293
    def do_EOF( self, line ):
294
        "Exit"
295
        output( '\n' )
296
        return self.do_exit( line )
297

    
298
    def isatty( self ):
299
        "Is our standard input a tty?"
300
        return isatty( self.stdin.fileno() )
301

    
302
    def do_noecho( self, line ):
303
        """Run an interactive command with echoing turned off.
304
           Usage: noecho [cmd args]"""
305
        if self.isatty():
306
            quietRun( 'stty -echo' )
307
        self.default( line )
308
        if self.isatty():
309
            quietRun( 'stty echo' )
310

    
311
    def do_source( self, line ):
312
        """Read commands from an input file.
313
           Usage: source <file>"""
314
        args = line.split()
315
        if len(args) != 1:
316
            error( 'usage: source <file>\n' )
317
            return
318
        try:
319
            self.inputFile = open( args[ 0 ] )
320
            while True:
321
                line = self.inputFile.readline()
322
                if len( line ) > 0:
323
                    self.onecmd( line )
324
                else:
325
                    break
326
        except IOError:
327
            error( 'error reading file %s\n' % args[ 0 ] )
328
        self.inputFile.close()
329
        self.inputFile = None
330

    
331
    def do_dpctl( self, line ):
332
        """Run dpctl (or ovs-ofctl) command on all switches.
333
           Usage: dpctl command [arg1] [arg2] ..."""
334
        args = line.split()
335
        if len(args) < 1:
336
            error( 'usage: dpctl command [arg1] [arg2] ...\n' )
337
            return
338
        for sw in self.mn.switches:
339
            output( '*** ' + sw.name + ' ' + ('-' * 72) + '\n' )
340
            output( sw.dpctl( *args ) )
341

    
342
    def do_time( self, line ):
343
        "Measure time taken for any command in Mininet."
344
        start = time.time()
345
        self.onecmd(line)
346
        elapsed = time.time() - start
347
        self.stdout.write("*** Elapsed time: %0.6f secs\n" % elapsed)
348

    
349
    def do_links( self, _line ):
350
        "Report on links"
351
        for link in self.mn.links:
352
            print link, link.status()
353

    
354
    def do_switch( self, line ):
355
        "Starts or stops a switch"
356
        args = line.split()
357
        if len(args) != 2:
358
            error( 'invalid number of args: switch <switch name>'
359
                   '{start, stop}\n' )
360
            return
361
        sw = args[ 0 ]
362
        command = args[ 1 ]
363
        if sw not in self.mn or self.mn.get( sw ) not in self.mn.switches :
364
            error( 'invalid switch: %s\n' % args[ 1 ] )
365
        else:
366
            sw = args[ 0 ]
367
            command = args[ 1 ]
368
            if command == 'start':
369
                self.mn.get( sw ).start( self.mn.controllers )
370
            elif command == 'stop':
371
                self.mn.get( sw ).stop( deleteIntfs=False )
372
            else:
373
                error( 'invalid command: switch <switch name> {start, stop}\n' )
374

    
375
    def default( self, line ):
376
        """Called on an input line when the command prefix is not recognized.
377
        Overridden to run shell commands when a node is the first CLI argument.
378
        Past the first CLI argument, node names are automatically replaced with
379
        corresponding IP addrs."""
380

    
381
        first, args, line = self.parseline( line )
382

    
383
        if first in self.mn:
384
            if not args:
385
                print "*** Enter a command for node: %s <cmd>" % first
386
                return
387
            node = self.mn[ first ]
388
            rest = args.split( ' ' )
389
            # Substitute IP addresses for node names in command
390
            # If updateIP() returns None, then use node name
391
            rest = [ self.mn[ arg ].defaultIntf().updateIP() or arg
392
                     if arg in self.mn else arg
393
                     for arg in rest ]
394
            rest = ' '.join( rest )
395
            # Run cmd on node:
396
            node.sendCmd( rest )
397
            self.waitForNode( node )
398
        else:
399
            error( '*** Unknown command: %s\n' % line )
400

    
401
    # pylint: enable=R0201
402

    
403
    def waitForNode( self, node ):
404
        "Wait for a node to finish, and print its output."
405
        # Pollers
406
        nodePoller = poll()
407
        nodePoller.register( node.stdout )
408
        bothPoller = poll()
409
        bothPoller.register( self.stdin, POLLIN )
410
        bothPoller.register( node.stdout, POLLIN )
411
        if self.isatty():
412
            # Buffer by character, so that interactive
413
            # commands sort of work
414
            quietRun( 'stty -icanon min 1' )
415
        while True:
416
            try:
417
                bothPoller.poll()
418
                # XXX BL: this doesn't quite do what we want.
419
                if False and self.inputFile:
420
                    key = self.inputFile.read( 1 )
421
                    if key is not '':
422
                        node.write( key )
423
                    else:
424
                        self.inputFile = None
425
                if isReadable( self.inPoller ):
426
                    key = self.stdin.read( 1 )
427
                    node.write( key )
428
                if isReadable( nodePoller ):
429
                    data = node.monitor()
430
                    output( data )
431
                if not node.waiting:
432
                    break
433
            except KeyboardInterrupt:
434
                # There is an at least one race condition here, since
435
                # it's possible to interrupt ourselves after we've
436
                # read data but before it has been printed.
437
                node.sendInt()
438

    
439
    def precmd( self, line ):
440
        "allow for comments in the cli"
441
        if '#' in line:
442
            line = line.split( '#' )[ 0 ]
443
        return line
444

    
445

    
446
# Helper functions
447

    
448
def isReadable( poller ):
449
    "Check whether a Poll object has a readable fd."
450
    for fdmask in poller.poll( 0 ):
451
        mask = fdmask[ 1 ]
452
        if mask & POLLIN:
453
            return True