mininet / mininet / cli.py @ cac98f5f
History | View | Annotate | Download (13.9 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, isShellBuiltin, 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-msg=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-msg=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-msg=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-msg=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 default( self, line ): |
355 |
"""Called on an input line when the command prefix is not recognized.
|
356 |
Overridden to run shell commands when a node is the first CLI argument.
|
357 |
Past the first CLI argument, node names are automatically replaced with
|
358 |
corresponding IP addrs."""
|
359 |
|
360 |
first, args, line = self.parseline( line )
|
361 |
|
362 |
if first in self.mn: |
363 |
if not args: |
364 |
print "*** Enter a command for node: %s <cmd>" % first |
365 |
return
|
366 |
node = self.mn[ first ]
|
367 |
rest = args.split( ' ' )
|
368 |
# Substitute IP addresses for node names in command
|
369 |
# If updateIP() returns None, then use node name
|
370 |
rest = [ self.mn[ arg ].defaultIntf().updateIP() or arg |
371 |
if arg in self.mn else arg |
372 |
for arg in rest ] |
373 |
rest = ' '.join( rest )
|
374 |
# Run cmd on node:
|
375 |
node.sendCmd( rest ) |
376 |
self.waitForNode( node )
|
377 |
else:
|
378 |
error( '*** Unknown command: %s\n' % line )
|
379 |
|
380 |
# pylint: enable-msg=R0201
|
381 |
|
382 |
def waitForNode( self, node ): |
383 |
"Wait for a node to finish, and print its output."
|
384 |
# Pollers
|
385 |
nodePoller = poll() |
386 |
nodePoller.register( node.stdout ) |
387 |
bothPoller = poll() |
388 |
bothPoller.register( self.stdin, POLLIN )
|
389 |
bothPoller.register( node.stdout, POLLIN ) |
390 |
if self.isatty(): |
391 |
# Buffer by character, so that interactive
|
392 |
# commands sort of work
|
393 |
quietRun( 'stty -icanon min 1' )
|
394 |
while True: |
395 |
try:
|
396 |
bothPoller.poll() |
397 |
# XXX BL: this doesn't quite do what we want.
|
398 |
if False and self.inputFile: |
399 |
key = self.inputFile.read( 1 ) |
400 |
if key is not '': |
401 |
node.write( key ) |
402 |
else:
|
403 |
self.inputFile = None |
404 |
if isReadable( self.inPoller ): |
405 |
key = self.stdin.read( 1 ) |
406 |
node.write( key ) |
407 |
if isReadable( nodePoller ):
|
408 |
data = node.monitor() |
409 |
output( data ) |
410 |
if not node.waiting: |
411 |
break
|
412 |
except KeyboardInterrupt: |
413 |
# There is an at least one race condition here, since
|
414 |
# it's possible to interrupt ourselves after we've
|
415 |
# read data but before it has been printed.
|
416 |
node.sendInt() |
417 |
|
418 |
def precmd( self, line ): |
419 |
"allow for comments in the cli"
|
420 |
if '#' in line: |
421 |
line = line.split( '#' )[ 0 ] |
422 |
return line
|
423 |
|
424 |
|
425 |
# Helper functions
|
426 |
|
427 |
def isReadable( poller ): |
428 |
"Check whether a Poll object has a readable fd."
|
429 |
for fdmask in poller.poll( 0 ): |
430 |
mask = fdmask[ 1 ]
|
431 |
if mask & POLLIN:
|
432 |
return True |