carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 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 | |
| 35 | from mininet.log import info, output, error |
| 36 | from mininet.term import makeTerms |
| 37 | from mininet.util import quietRun, isShellBuiltin, dumpNodeConnections |
| 38 | |
| 39 | class CLI( Cmd ): |
| 40 | "Simple command-line interface to talk to nodes." |
| 41 | |
ashu | 01b62f7 | 2015-03-12 15:16:11 -0500 | [diff] [blame] | 42 | prompt = 'minindn> ' |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 43 | |
| 44 | def __init__( self, mininet, stdin=sys.stdin, script=None ): |
| 45 | self.mn = mininet |
| 46 | self.nodelist = self.mn.controllers + self.mn.switches + self.mn.hosts |
| 47 | self.nodemap = {} # map names to Node objects |
| 48 | for node in self.nodelist: |
| 49 | self.nodemap[ node.name ] = node |
| 50 | # Local variable bindings for py command |
| 51 | self.locals = { 'net': mininet } |
| 52 | self.locals.update( self.nodemap ) |
| 53 | # Attempt to handle input |
| 54 | self.stdin = stdin |
| 55 | self.inPoller = poll() |
| 56 | self.inPoller.register( stdin ) |
| 57 | self.inputFile = script |
| 58 | Cmd.__init__( self ) |
| 59 | info( '*** Starting CLI:\n' ) |
| 60 | if self.inputFile: |
| 61 | self.do_source( self.inputFile ) |
| 62 | return |
| 63 | while True: |
| 64 | try: |
| 65 | # Make sure no nodes are still waiting |
| 66 | for node in self.nodelist: |
| 67 | while node.waiting: |
| 68 | node.sendInt() |
| 69 | node.monitor() |
| 70 | if self.isatty(): |
| 71 | quietRun( 'stty sane' ) |
| 72 | self.cmdloop() |
| 73 | break |
| 74 | except KeyboardInterrupt: |
| 75 | output( '\nInterrupt\n' ) |
| 76 | |
| 77 | def emptyline( self ): |
| 78 | "Don't repeat last command when you hit return." |
| 79 | pass |
| 80 | |
| 81 | # Disable pylint "Unused argument: 'arg's'" messages, as well as |
| 82 | # "method could be a function" warning, since each CLI function |
| 83 | # must have the same interface |
| 84 | # pylint: disable-msg=R0201 |
| 85 | |
| 86 | helpStr = ( |
| 87 | 'You may also send a command to a node using:\n' |
| 88 | ' <node> command {args}\n' |
| 89 | 'For example:\n' |
| 90 | ' mininet> h1 ifconfig\n' |
| 91 | '\n' |
| 92 | 'The interpreter automatically substitutes IP addresses\n' |
| 93 | 'for node names when a node is the first arg, so commands\n' |
| 94 | 'like\n' |
| 95 | ' mininet> h2 ping h3\n' |
| 96 | 'should work.\n' |
| 97 | '\n' |
| 98 | 'Some character-oriented interactive commands require\n' |
| 99 | 'noecho:\n' |
| 100 | ' mininet> noecho h2 vi foo.py\n' |
| 101 | 'However, starting up an xterm/gterm is generally better:\n' |
| 102 | ' mininet> xterm h2\n\n' |
| 103 | ) |
| 104 | |
| 105 | def do_help( self, line ): |
| 106 | "Describe available CLI commands." |
| 107 | Cmd.do_help( self, line ) |
| 108 | if line is '': |
| 109 | output( self.helpStr ) |
| 110 | |
| 111 | def do_nodes( self, _line ): |
| 112 | "List all nodes." |
| 113 | nodes = ' '.join( [ node.name for node in sorted( self.nodelist ) ] ) |
| 114 | output( 'available nodes are: \n%s\n' % nodes ) |
| 115 | |
| 116 | def do_net( self, _line ): |
| 117 | "List network connections." |
| 118 | dumpNodeConnections( self.nodelist ) |
| 119 | |
| 120 | def do_sh( self, line ): |
| 121 | "Run an external shell command" |
| 122 | call( line, shell=True ) |
| 123 | |
| 124 | # do_py() needs to catch any exception during eval() |
| 125 | # pylint: disable-msg=W0703 |
| 126 | |
| 127 | def do_py( self, line ): |
| 128 | """Evaluate a Python expression. |
| 129 | Node names may be used, e.g.: h1.cmd('ls')""" |
| 130 | try: |
| 131 | result = eval( line, globals(), self.locals ) |
| 132 | if not result: |
| 133 | return |
| 134 | elif isinstance( result, str ): |
| 135 | output( result + '\n' ) |
| 136 | else: |
| 137 | output( repr( result ) + '\n' ) |
| 138 | except Exception, e: |
| 139 | output( str( e ) + '\n' ) |
| 140 | |
| 141 | # pylint: enable-msg=W0703 |
| 142 | |
| 143 | def do_pingall( self, _line ): |
| 144 | "Ping between all hosts." |
| 145 | self.mn.pingAll() |
| 146 | |
| 147 | def do_pingpair( self, _line ): |
| 148 | "Ping between first two hosts, useful for testing." |
| 149 | self.mn.pingPair() |
| 150 | |
| 151 | def do_pingallfull( self, _line ): |
| 152 | "Ping between first two hosts, returns all ping results." |
| 153 | self.mn.pingAllFull() |
| 154 | |
| 155 | def do_pingpairfull( self, _line ): |
| 156 | "Ping between first two hosts, returns all ping results." |
| 157 | self.mn.pingPairFull() |
| 158 | |
| 159 | def do_iperf( self, line ): |
| 160 | "Simple iperf TCP test between two (optionally specified) hosts." |
| 161 | args = line.split() |
| 162 | if not args: |
| 163 | self.mn.iperf() |
| 164 | elif len(args) == 2: |
| 165 | hosts = [] |
| 166 | err = False |
| 167 | for arg in args: |
| 168 | if arg not in self.nodemap: |
| 169 | err = True |
| 170 | error( "node '%s' not in network\n" % arg ) |
| 171 | else: |
| 172 | hosts.append( self.nodemap[ arg ] ) |
| 173 | if not err: |
| 174 | self.mn.iperf( hosts ) |
| 175 | else: |
| 176 | error( 'invalid number of args: iperf src dst\n' ) |
| 177 | |
| 178 | def do_iperfudp( self, line ): |
| 179 | "Simple iperf TCP test between two (optionally specified) hosts." |
| 180 | args = line.split() |
| 181 | if not args: |
| 182 | self.mn.iperf( l4Type='UDP' ) |
| 183 | elif len(args) == 3: |
| 184 | udpBw = args[ 0 ] |
| 185 | hosts = [] |
| 186 | err = False |
| 187 | for arg in args[ 1:3 ]: |
| 188 | if arg not in self.nodemap: |
| 189 | err = True |
| 190 | error( "node '%s' not in network\n" % arg ) |
| 191 | else: |
| 192 | hosts.append( self.nodemap[ arg ] ) |
| 193 | if not err: |
| 194 | self.mn.iperf( hosts, l4Type='UDP', udpBw=udpBw ) |
| 195 | else: |
| 196 | error( 'invalid number of args: iperfudp bw src dst\n' + |
| 197 | 'bw examples: 10M\n' ) |
| 198 | |
| 199 | def do_intfs( self, _line ): |
| 200 | "List interfaces." |
| 201 | for node in self.nodelist: |
| 202 | output( '%s: %s\n' % |
| 203 | ( node.name, ','.join( node.intfNames() ) ) ) |
carlosmscabral | e121a7b | 2013-02-18 18:14:53 -0300 | [diff] [blame] | 204 | |
| 205 | def do_ccndump(self, _line): |
| 206 | "Dump FIB entries" |
| 207 | for node in self.nodelist: |
| 208 | if 'fib' in node.params: |
| 209 | output(node.name + ': ') |
| 210 | for name in node.params['fib']: |
| 211 | output(str(name) + ' ') |
| 212 | output('\n') |
| 213 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 214 | |
| 215 | def do_dump( self, _line ): |
| 216 | "Dump node info." |
| 217 | for node in self.nodelist: |
| 218 | output( '%s\n' % repr( node ) ) |
| 219 | |
| 220 | def do_link( self, line ): |
| 221 | "Bring link(s) between two nodes up or down." |
| 222 | args = line.split() |
| 223 | if len(args) != 3: |
| 224 | error( 'invalid number of args: link end1 end2 [up down]\n' ) |
| 225 | elif args[ 2 ] not in [ 'up', 'down' ]: |
| 226 | error( 'invalid type: link end1 end2 [up down]\n' ) |
| 227 | else: |
| 228 | self.mn.configLinkStatus( *args ) |
| 229 | |
| 230 | def do_xterm( self, line, term='xterm' ): |
| 231 | "Spawn xterm(s) for the given node(s)." |
| 232 | args = line.split() |
| 233 | if not args: |
| 234 | error( 'usage: %s node1 node2 ...\n' % term ) |
| 235 | else: |
| 236 | for arg in args: |
| 237 | if arg not in self.nodemap: |
| 238 | error( "node '%s' not in network\n" % arg ) |
| 239 | else: |
| 240 | node = self.nodemap[ arg ] |
| 241 | self.mn.terms += makeTerms( [ node ], term = term ) |
| 242 | |
| 243 | def do_gterm( self, line ): |
| 244 | "Spawn gnome-terminal(s) for the given node(s)." |
| 245 | self.do_xterm( line, term='gterm' ) |
| 246 | |
| 247 | def do_exit( self, _line ): |
| 248 | "Exit" |
| 249 | return 'exited by user command' |
| 250 | |
| 251 | def do_quit( self, line ): |
| 252 | "Exit" |
| 253 | return self.do_exit( line ) |
| 254 | |
| 255 | def do_EOF( self, line ): |
| 256 | "Exit" |
| 257 | output( '\n' ) |
| 258 | return self.do_exit( line ) |
| 259 | |
| 260 | def isatty( self ): |
| 261 | "Is our standard input a tty?" |
| 262 | return isatty( self.stdin.fileno() ) |
| 263 | |
| 264 | def do_noecho( self, line ): |
| 265 | "Run an interactive command with echoing turned off." |
| 266 | if self.isatty(): |
| 267 | quietRun( 'stty -echo' ) |
| 268 | self.default( line ) |
| 269 | if self.isatty(): |
| 270 | quietRun( 'stty echo' ) |
| 271 | |
| 272 | def do_source( self, line ): |
| 273 | "Read commands from an input file." |
| 274 | args = line.split() |
| 275 | if len(args) != 1: |
| 276 | error( 'usage: source <file>\n' ) |
| 277 | return |
| 278 | try: |
| 279 | self.inputFile = open( args[ 0 ] ) |
| 280 | while True: |
| 281 | line = self.inputFile.readline() |
| 282 | if len( line ) > 0: |
| 283 | self.onecmd( line ) |
| 284 | else: |
| 285 | break |
| 286 | except IOError: |
| 287 | error( 'error reading file %s\n' % args[ 0 ] ) |
| 288 | self.inputFile = None |
| 289 | |
| 290 | def do_dpctl( self, line ): |
| 291 | "Run dpctl command on all switches." |
| 292 | args = line.split() |
| 293 | if len(args) < 1: |
| 294 | error( 'usage: dpctl command [arg1] [arg2] ...\n' ) |
| 295 | return |
| 296 | for sw in self.mn.switches: |
| 297 | output( '*** ' + sw.name + ' ' + ('-' * 72) + '\n' ) |
| 298 | output( sw.dpctl( *args ) ) |
| 299 | |
| 300 | def do_time( self, line ): |
| 301 | "Measure time taken for any command in Mininet." |
| 302 | start = time.time() |
| 303 | self.onecmd(line) |
| 304 | elapsed = time.time() - start |
| 305 | self.stdout.write("*** Elapsed time: %0.6f secs\n" % elapsed) |
| 306 | |
| 307 | def default( self, line ): |
| 308 | """Called on an input line when the command prefix is not recognized. |
| 309 | Overridden to run shell commands when a node is the first CLI argument. |
| 310 | Past the first CLI argument, node names are automatically replaced with |
| 311 | corresponding IP addrs.""" |
| 312 | |
| 313 | first, args, line = self.parseline( line ) |
| 314 | if not args: |
| 315 | return |
| 316 | if args and len(args) > 0 and args[ -1 ] == '\n': |
| 317 | args = args[ :-1 ] |
| 318 | rest = args.split( ' ' ) |
| 319 | |
| 320 | if first in self.nodemap: |
| 321 | node = self.nodemap[ first ] |
| 322 | # Substitute IP addresses for node names in command |
| 323 | rest = [ self.nodemap[ arg ].IP() |
| 324 | if arg in self.nodemap else arg |
| 325 | for arg in rest ] |
| 326 | rest = ' '.join( rest ) |
| 327 | # Run cmd on node: |
| 328 | builtin = isShellBuiltin( first ) |
| 329 | node.sendCmd( rest, printPid=( not builtin ) ) |
| 330 | self.waitForNode( node ) |
| 331 | else: |
| 332 | error( '*** Unknown command: %s\n' % first ) |
| 333 | |
| 334 | # pylint: enable-msg=R0201 |
| 335 | |
| 336 | def waitForNode( self, node ): |
| 337 | "Wait for a node to finish, and print its output." |
| 338 | # Pollers |
| 339 | nodePoller = poll() |
| 340 | nodePoller.register( node.stdout ) |
| 341 | bothPoller = poll() |
| 342 | bothPoller.register( self.stdin, POLLIN ) |
| 343 | bothPoller.register( node.stdout, POLLIN ) |
| 344 | if self.isatty(): |
| 345 | # Buffer by character, so that interactive |
| 346 | # commands sort of work |
| 347 | quietRun( 'stty -icanon min 1' ) |
| 348 | while True: |
| 349 | try: |
| 350 | bothPoller.poll() |
| 351 | # XXX BL: this doesn't quite do what we want. |
| 352 | if False and self.inputFile: |
| 353 | key = self.inputFile.read( 1 ) |
| 354 | if key is not '': |
| 355 | node.write(key) |
| 356 | else: |
| 357 | self.inputFile = None |
| 358 | if isReadable( self.inPoller ): |
| 359 | key = self.stdin.read( 1 ) |
| 360 | node.write( key ) |
| 361 | if isReadable( nodePoller ): |
| 362 | data = node.monitor() |
| 363 | output( data ) |
| 364 | if not node.waiting: |
| 365 | break |
| 366 | except KeyboardInterrupt: |
| 367 | node.sendInt() |
| 368 | |
| 369 | # Helper functions |
| 370 | |
| 371 | def isReadable( poller ): |
| 372 | "Check whether a Poll object has a readable fd." |
| 373 | for fdmask in poller.poll( 0 ): |
| 374 | mask = fdmask[ 1 ] |
| 375 | if mask & POLLIN: |
| 376 | return True |