blob: 7f3c4c7e5aae9670698ab31ddca8a6e074608406 [file] [log] [blame]
carlosmscabralf40ecd12013-02-01 18:15:58 -02001"""
2Node objects for Mininet.
3
4Nodes provide a simple abstraction for interacting with hosts, switches
5and controllers. Local nodes are simply one or more processes on the local
6machine.
7
8Node: superclass for all (primarily local) network nodes.
9
10Host: a virtual host. By default, a host is simply a shell; commands
11 may be sent using Cmd (which waits for output), or using sendCmd(),
12 which returns immediately, allowing subsequent monitoring using
13 monitor(). Examples of how to run experiments using this
14 functionality are provided in the examples/ directory.
15
16CPULimitedHost: a virtual host whose CPU bandwidth is limited by
17 RT or CFS bandwidth limiting.
18
19Switch: superclass for switch nodes.
20
21UserSwitch: a switch using the user-space switch from the OpenFlow
22 reference implementation.
23
24KernelSwitch: a switch using the kernel switch from the OpenFlow reference
25 implementation.
26
27OVSSwitch: a switch using the OpenVSwitch OpenFlow-compatible switch
28 implementation (openvswitch.org).
29
30Controller: superclass for OpenFlow controllers. The default controller
31 is controller(8) from the reference implementation.
32
33NOXController: a controller node using NOX (noxrepo.org).
34
35RemoteController: a remote controller node, which may use any
36 arbitrary OpenFlow-compatible controller, and which is not
37 created or managed by mininet.
38
39Future enhancements:
40
41- Possibly make Node, Switch and Controller more abstract so that
42 they can be used for both local and remote nodes
43
44- Create proxy objects for remote nodes (Mininet: Cluster Edition)
45"""
46
47import os
48import re
49import signal
50import select
51from subprocess import Popen, PIPE, STDOUT
52
53from mininet.log import info, error, warn, debug
54from mininet.util import ( quietRun, errRun, errFail, moveIntf, isShellBuiltin,
carlosmscabrale121a7b2013-02-18 18:14:53 -030055 numCores, retry, mountCgroups, run )
carlosmscabralf40ecd12013-02-01 18:15:58 -020056from mininet.moduledeps import moduleDeps, pathCheck, OVS_KMOD, OF_KMOD, TUN
57from mininet.link import Link, Intf, TCIntf
58
59class Node( object ):
60 """A virtual network node is simply a shell in a network namespace.
61 We communicate with it using pipes."""
62
63 portBase = 0 # Nodes always start with eth0/port0, even in OF 1.0
64
65 def __init__( self, name, inNamespace=True, **params ):
66 """name: name of node
67 inNamespace: in network namespace?
68 params: Node parameters (see config() for details)"""
69
70 # Make sure class actually works
71 self.checkSetup()
72
73 self.name = name
74 self.inNamespace = inNamespace
75
76 # Stash configuration parameters for future reference
77 self.params = params
78
79 self.intfs = {} # dict of port numbers to interfaces
80 self.ports = {} # dict of interfaces to port numbers
81 # replace with Port objects, eventually ?
82 self.nameToIntf = {} # dict of interface names to Intfs
83
84 # Make pylint happy
85 ( self.shell, self.execed, self.pid, self.stdin, self.stdout,
86 self.lastPid, self.lastCmd, self.pollOut ) = (
87 None, None, None, None, None, None, None, None )
88 self.waiting = False
89 self.readbuf = ''
90
91 # Start command interpreter shell
92 self.startShell()
93
94 # File descriptor to node mapping support
95 # Class variables and methods
96
97 inToNode = {} # mapping of input fds to nodes
98 outToNode = {} # mapping of output fds to nodes
99
100 @classmethod
101 def fdToNode( cls, fd ):
102 """Return node corresponding to given file descriptor.
103 fd: file descriptor
104 returns: node"""
105 node = cls.outToNode.get( fd )
106 return node or cls.inToNode.get( fd )
107
108 # Command support via shell process in namespace
109
110 def startShell( self ):
111 "Start a shell process for running commands"
112 if self.shell:
113 error( "%s: shell is already running" )
114 return
115 # mnexec: (c)lose descriptors, (d)etach from tty,
116 # (p)rint pid, and run in (n)amespace
117 opts = '-cdp'
118 if self.inNamespace:
119 opts += 'n'
120 # bash -m: enable job control
121 cmd = [ 'mnexec', opts, 'bash', '-m' ]
122 self.shell = Popen( cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT,
123 close_fds=True )
124 self.stdin = self.shell.stdin
125 self.stdout = self.shell.stdout
126 self.pid = self.shell.pid
127 self.pollOut = select.poll()
128 self.pollOut.register( self.stdout )
129 # Maintain mapping between file descriptors and nodes
130 # This is useful for monitoring multiple nodes
131 # using select.poll()
132 self.outToNode[ self.stdout.fileno() ] = self
133 self.inToNode[ self.stdin.fileno() ] = self
134 self.execed = False
135 self.lastCmd = None
136 self.lastPid = None
137 self.readbuf = ''
138 self.waiting = False
139
140 def cleanup( self ):
141 "Help python collect its garbage."
142 if not self.inNamespace:
143 for intfName in self.intfNames():
144 if self.name in intfName:
145 quietRun( 'ip link del ' + intfName )
146 self.shell = None
147
148 # Subshell I/O, commands and control
149
150 def read( self, maxbytes=1024 ):
151 """Buffered read from node, non-blocking.
152 maxbytes: maximum number of bytes to return"""
153 count = len( self.readbuf )
154 if count < maxbytes:
155 data = os.read( self.stdout.fileno(), maxbytes - count )
156 self.readbuf += data
157 if maxbytes >= len( self.readbuf ):
158 result = self.readbuf
159 self.readbuf = ''
160 else:
161 result = self.readbuf[ :maxbytes ]
162 self.readbuf = self.readbuf[ maxbytes: ]
163 return result
164
165 def readline( self ):
166 """Buffered readline from node, non-blocking.
167 returns: line (minus newline) or None"""
168 self.readbuf += self.read( 1024 )
169 if '\n' not in self.readbuf:
170 return None
171 pos = self.readbuf.find( '\n' )
172 line = self.readbuf[ 0: pos ]
173 self.readbuf = self.readbuf[ pos + 1: ]
174 return line
175
176 def write( self, data ):
177 """Write data to node.
178 data: string"""
179 os.write( self.stdin.fileno(), data )
180
181 def terminate( self ):
182 "Send kill signal to Node and clean up after it."
183 os.kill( self.pid, signal.SIGKILL )
184 self.cleanup()
185
186 def stop( self ):
187 "Stop node."
188 self.terminate()
189
190 def waitReadable( self, timeoutms=None ):
191 """Wait until node's output is readable.
192 timeoutms: timeout in ms or None to wait indefinitely."""
193 if len( self.readbuf ) == 0:
194 self.pollOut.poll( timeoutms )
195
196 def sendCmd( self, *args, **kwargs ):
197 """Send a command, followed by a command to echo a sentinel,
198 and return without waiting for the command to complete.
199 args: command and arguments, or string
200 printPid: print command's PID?"""
201 assert not self.waiting
202 printPid = kwargs.get( 'printPid', True )
203 # Allow sendCmd( [ list ] )
204 if len( args ) == 1 and type( args[ 0 ] ) is list:
205 cmd = args[ 0 ]
206 # Allow sendCmd( cmd, arg1, arg2... )
207 elif len( args ) > 0:
208 cmd = args
209 # Convert to string
210 if not isinstance( cmd, str ):
211 cmd = ' '.join( [ str( c ) for c in cmd ] )
212 if not re.search( r'\w', cmd ):
213 # Replace empty commands with something harmless
214 cmd = 'echo -n'
215 self.lastCmd = cmd
216 printPid = printPid and not isShellBuiltin( cmd )
217 if len( cmd ) > 0 and cmd[ -1 ] == '&':
218 # print ^A{pid}\n{sentinel}
219 cmd += ' printf "\\001%d\n\\177" $! \n'
220 else:
221 # print sentinel
222 cmd += '; printf "\\177"'
223 if printPid and not isShellBuiltin( cmd ):
224 cmd = 'mnexec -p ' + cmd
225 self.write( cmd + '\n' )
226 self.lastPid = None
227 self.waiting = True
228
229 def sendInt( self, sig=signal.SIGINT ):
230 "Interrupt running command."
231 if self.lastPid:
232 try:
233 os.kill( self.lastPid, sig )
234 except OSError:
235 pass
236
237 def monitor( self, timeoutms=None ):
238 """Monitor and return the output of a command.
239 Set self.waiting to False if command has completed.
240 timeoutms: timeout in ms or None to wait indefinitely."""
241 self.waitReadable( timeoutms )
242 data = self.read( 1024 )
243 # Look for PID
244 marker = chr( 1 ) + r'\d+\n'
245 if chr( 1 ) in data:
246 markers = re.findall( marker, data )
247 if markers:
248 self.lastPid = int( markers[ 0 ][ 1: ] )
249 data = re.sub( marker, '', data )
250 # Look for sentinel/EOF
251 if len( data ) > 0 and data[ -1 ] == chr( 127 ):
252 self.waiting = False
253 data = data[ :-1 ]
254 elif chr( 127 ) in data:
255 self.waiting = False
256 data = data.replace( chr( 127 ), '' )
257 return data
258
259 def waitOutput( self, verbose=False ):
260 """Wait for a command to complete.
261 Completion is signaled by a sentinel character, ASCII(127)
262 appearing in the output stream. Wait for the sentinel and return
263 the output, including trailing newline.
264 verbose: print output interactively"""
265 log = info if verbose else debug
266 output = ''
267 while self.waiting:
268 data = self.monitor()
269 output += data
270 log( data )
271 return output
272
273 def cmd( self, *args, **kwargs ):
274 """Send a command, wait for output, and return it.
275 cmd: string"""
276 verbose = kwargs.get( 'verbose', False )
277 log = info if verbose else debug
278 log( '*** %s : %s\n' % ( self.name, args ) )
279 self.sendCmd( *args, **kwargs )
280 return self.waitOutput( verbose )
281
282 def cmdPrint( self, *args):
283 """Call cmd and printing its output
284 cmd: string"""
285 return self.cmd( *args, **{ 'verbose': True } )
286
287 def popen( self, *args, **kwargs ):
288 """Return a Popen() object in our namespace
289 args: Popen() args, single list, or string
290 kwargs: Popen() keyword args"""
291 defaults = { 'stdout': PIPE, 'stderr': PIPE,
292 'mncmd':
293 [ 'mnexec', '-a', str( self.pid ) ] }
294 defaults.update( kwargs )
295 if len( args ) == 1:
296 if type( args[ 0 ] ) is list:
297 # popen([cmd, arg1, arg2...])
298 cmd = args[ 0 ]
299 elif type( args[ 0 ] ) is str:
300 # popen("cmd arg1 arg2...")
301 cmd = args[ 0 ].split()
302 else:
303 raise Exception( 'popen() requires a string or list' )
304 elif len( args ) > 0:
305 # popen( cmd, arg1, arg2... )
306 cmd = list( args )
307 # Attach to our namespace using mnexec -a
308 mncmd = defaults[ 'mncmd' ]
309 del defaults[ 'mncmd' ]
310 cmd = mncmd + cmd
311 # Shell requires a string, not a list!
312 if defaults.get( 'shell', False ):
313 cmd = ' '.join( cmd )
314 return Popen( cmd, **defaults )
315
316 def pexec( self, *args, **kwargs ):
317 """Execute a command using popen
318 returns: out, err, exitcode"""
319 popen = self.popen( *args, **kwargs)
320 out, err = popen.communicate()
321 exitcode = popen.wait()
322 return out, err, exitcode
323
324 # Interface management, configuration, and routing
325
326 # BL notes: This might be a bit redundant or over-complicated.
327 # However, it does allow a bit of specialization, including
328 # changing the canonical interface names. It's also tricky since
329 # the real interfaces are created as veth pairs, so we can't
330 # make a single interface at a time.
331
332 def newPort( self ):
333 "Return the next port number to allocate."
334 if len( self.ports ) > 0:
335 return max( self.ports.values() ) + 1
336 return self.portBase
337
338 def addIntf( self, intf, port=None ):
339 """Add an interface.
340 intf: interface
341 port: port number (optional, typically OpenFlow port number)"""
342 if port is None:
343 port = self.newPort()
344 self.intfs[ port ] = intf
345 self.ports[ intf ] = port
346 self.nameToIntf[ intf.name ] = intf
347 debug( '\n' )
348 debug( 'added intf %s:%d to node %s\n' % ( intf, port, self.name ) )
349 if self.inNamespace:
350 debug( 'moving', intf, 'into namespace for', self.name, '\n' )
351 moveIntf( intf.name, self )
352
353 def defaultIntf( self ):
354 "Return interface for lowest port"
355 ports = self.intfs.keys()
356 if ports:
357 return self.intfs[ min( ports ) ]
358 else:
359 warn( '*** defaultIntf: warning:', self.name,
360 'has no interfaces\n' )
361
362 def intf( self, intf='' ):
363 """Return our interface object with given string name,
364 default intf if name is falsy (None, empty string, etc).
365 or the input intf arg.
366
367 Having this fcn return its arg for Intf objects makes it
368 easier to construct functions with flexible input args for
369 interfaces (those that accept both string names and Intf objects).
370 """
371 if not intf:
372 return self.defaultIntf()
373 elif type( intf) is str:
374 return self.nameToIntf[ intf ]
375 else:
376 return intf
377
378 def connectionsTo( self, node):
379 "Return [ intf1, intf2... ] for all intfs that connect self to node."
380 # We could optimize this if it is important
381 connections = []
382 for intf in self.intfList():
383 link = intf.link
384 if link:
385 node1, node2 = link.intf1.node, link.intf2.node
386 if node1 == self and node2 == node:
387 connections += [ ( intf, link.intf2 ) ]
388 elif node1 == node and node2 == self:
389 connections += [ ( intf, link.intf1 ) ]
390 return connections
391
392 def deleteIntfs( self ):
393 "Delete all of our interfaces."
394 # In theory the interfaces should go away after we shut down.
395 # However, this takes time, so we're better off removing them
396 # explicitly so that we won't get errors if we run before they
397 # have been removed by the kernel. Unfortunately this is very slow,
398 # at least with Linux kernels before 2.6.33
399 for intf in self.intfs.values():
400 intf.delete()
401 info( '.' )
402
403 # Routing support
404
405 def setARP( self, ip, mac ):
406 """Add an ARP entry.
407 ip: IP address as string
408 mac: MAC address as string"""
409 result = self.cmd( 'arp', '-s', ip, mac )
410 return result
411
412 def setHostRoute( self, ip, intf ):
413 """Add route to host.
414 ip: IP address as dotted decimal
415 intf: string, interface name"""
416 return self.cmd( 'route add -host', ip, 'dev', intf )
417
418 def setDefaultRoute( self, intf=None ):
419 """Set the default route to go through intf.
420 intf: string, interface name"""
421 if not intf:
422 intf = self.defaultIntf()
423 self.cmd( 'ip route flush root 0/0' )
424 return self.cmd( 'route add default %s' % intf )
425
426 # Convenience and configuration methods
427
428 def setMAC( self, mac, intf=None ):
429 """Set the MAC address for an interface.
430 intf: intf or intf name
431 mac: MAC address as string"""
432 return self.intf( intf ).setMAC( mac )
433
434 def setIP( self, ip, prefixLen=8, intf=None ):
435 """Set the IP address for an interface.
436 intf: intf or intf name
437 ip: IP address as a string
438 prefixLen: prefix length, e.g. 8 for /8 or 16M addrs"""
439 # This should probably be rethought
440 if '/' not in ip:
441 ip = '%s/%s' % ( ip, prefixLen )
442 return self.intf( intf ).setIP( ip )
443
444 def IP( self, intf=None ):
445 "Return IP address of a node or specific interface."
446 return self.intf( intf ).IP()
447
448 def MAC( self, intf=None ):
449 "Return MAC address of a node or specific interface."
450 return self.intf( intf ).MAC()
451
452 def intfIsUp( self, intf=None ):
453 "Check if an interface is up."
454 return self.intf( intf ).isUp()
455
456 # The reason why we configure things in this way is so
457 # That the parameters can be listed and documented in
458 # the config method.
459 # Dealing with subclasses and superclasses is slightly
460 # annoying, but at least the information is there!
461
462 def setParam( self, results, method, **param ):
463 """Internal method: configure a *single* parameter
464 results: dict of results to update
465 method: config method name
466 param: arg=value (ignore if value=None)
467 value may also be list or dict"""
468 name, value = param.items()[ 0 ]
469 f = getattr( self, method, None )
470 if not f or value is None:
471 return
472 if type( value ) is list:
473 result = f( *value )
474 elif type( value ) is dict:
475 result = f( **value )
476 else:
477 result = f( value )
478 results[ name ] = result
479 return result
480
481 def config( self, mac=None, ip=None,
482 defaultRoute=None, lo='up', **_params ):
483 """Configure Node according to (optional) parameters:
484 mac: MAC address for default interface
485 ip: IP address for default interface
486 ifconfig: arbitrary interface configuration
487 Subclasses should override this method and call
488 the parent class's config(**params)"""
489 # If we were overriding this method, we would call
490 # the superclass config method here as follows:
491 # r = Parent.config( **_params )
492 r = {}
493 self.setParam( r, 'setMAC', mac=mac )
494 self.setParam( r, 'setIP', ip=ip )
495 self.setParam( r, 'defaultRoute', defaultRoute=defaultRoute )
496 # This should be examined
497 self.cmd( 'ifconfig lo ' + lo )
498 return r
499
500 def configDefault( self, **moreParams ):
501 "Configure with default parameters"
502 self.params.update( moreParams )
503 self.config( **self.params )
504
505 # This is here for backward compatibility
506 def linkTo( self, node, link=Link ):
507 """(Deprecated) Link to another node
508 replace with Link( node1, node2)"""
509 return link( self, node )
510
511 # Other methods
512
513 def intfList( self ):
514 "List of our interfaces sorted by port number"
515 return [ self.intfs[ p ] for p in sorted( self.intfs.iterkeys() ) ]
516
517 def intfNames( self ):
518 "The names of our interfaces sorted by port number"
519 return [ str( i ) for i in self.intfList() ]
520
521 def __repr__( self ):
522 "More informative string representation"
523 intfs = ( ','.join( [ '%s:%s' % ( i.name, i.IP() )
524 for i in self.intfList() ] ) )
525 return '<%s %s: %s pid=%s> ' % (
526 self.__class__.__name__, self.name, intfs, self.pid )
527
528 def __str__( self ):
529 "Abbreviated string representation"
530 return self.name
531
532 # Automatic class setup support
533
534 isSetup = False
535
536 @classmethod
537 def checkSetup( cls ):
538 "Make sure our class and superclasses are set up"
539 while cls and not getattr( cls, 'isSetup', True ):
540 cls.setup()
541 cls.isSetup = True
542 # Make pylint happy
543 cls = getattr( type( cls ), '__base__', None )
544
545 @classmethod
546 def setup( cls ):
547 "Make sure our class dependencies are available"
548 pathCheck( 'mnexec', 'ifconfig', moduleName='Mininet')
549
550
551class Host( Node ):
552 "A host is simply a Node"
553 pass
554
555
carlosmscabral6d3dd602013-03-23 11:12:34 -0300556
carlosmscabralf40ecd12013-02-01 18:15:58 -0200557class CPULimitedHost( Host ):
558
559 "CPU limited host"
560
561 def __init__( self, name, sched='cfs', **kwargs ):
562 Host.__init__( self, name, **kwargs )
563 # Initialize class if necessary
564 if not CPULimitedHost.inited:
565 CPULimitedHost.init()
566 # Create a cgroup and move shell into it
567 self.cgroup = 'cpu,cpuacct,cpuset:/' + self.name
568 errFail( 'cgcreate -g ' + self.cgroup )
569 # We don't add ourselves to a cpuset because you must
570 # specify the cpu and memory placement first
571 errFail( 'cgclassify -g cpu,cpuacct:/%s %s' % ( self.name, self.pid ) )
572 # BL: Setting the correct period/quota is tricky, particularly
573 # for RT. RT allows very small quotas, but the overhead
574 # seems to be high. CFS has a mininimum quota of 1 ms, but
575 # still does better with larger period values.
576 self.period_us = kwargs.get( 'period_us', 100000 )
577 self.sched = sched
578 self.rtprio = 20
579
580 def cgroupSet( self, param, value, resource='cpu' ):
581 "Set a cgroup parameter and return its value"
582 cmd = 'cgset -r %s.%s=%s /%s' % (
583 resource, param, value, self.name )
584 quietRun( cmd )
585 nvalue = int( self.cgroupGet( param, resource ) )
586 if nvalue != value:
587 error( '*** error: cgroupSet: %s set to %s instead of %s\n'
588 % ( param, nvalue, value ) )
589 return nvalue
590
591 def cgroupGet( self, param, resource='cpu' ):
592 "Return value of cgroup parameter"
593 cmd = 'cgget -r %s.%s /%s' % (
594 resource, param, self.name )
carlosmscabral6d3dd602013-03-23 11:12:34 -0300595
carlosmscabrale121a7b2013-02-18 18:14:53 -0300596 return int(quietRun( cmd ).split()[ -1 ] )
carlosmscabralf40ecd12013-02-01 18:15:58 -0200597
598 def cgroupDel( self ):
599 "Clean up our cgroup"
600 # info( '*** deleting cgroup', self.cgroup, '\n' )
601 _out, _err, exitcode = errRun( 'cgdelete -r ' + self.cgroup )
602 return exitcode != 0
603
604 def popen( self, *args, **kwargs ):
605 """Return a Popen() object in node's namespace
606 args: Popen() args, single list, or string
607 kwargs: Popen() keyword args"""
608 # Tell mnexec to execute command in our cgroup
609 mncmd = [ 'mnexec', '-a', str( self.pid ),
610 '-g', self.name ]
611 if self.sched == 'rt':
612 mncmd += [ '-r', str( self.rtprio ) ]
613 return Host.popen( self, *args, mncmd=mncmd, **kwargs )
614
615 def cleanup( self ):
616 "Clean up our cgroup"
617 retry( retries=3, delaySecs=1, fn=self.cgroupDel )
618
619 def chrt( self ):
620 "Set RT scheduling priority"
621 quietRun( 'chrt -p %s %s' % ( self.rtprio, self.pid ) )
622 result = quietRun( 'chrt -p %s' % self.pid )
623 firstline = result.split( '\n' )[ 0 ]
624 lastword = firstline.split( ' ' )[ -1 ]
625 if lastword != 'SCHED_RR':
626 error( '*** error: could not assign SCHED_RR to %s\n' % self.name )
627 return lastword
628
629 def rtInfo( self, f ):
630 "Internal method: return parameters for RT bandwidth"
631 pstr, qstr = 'rt_period_us', 'rt_runtime_us'
632 # RT uses wall clock time for period and quota
633 quota = int( self.period_us * f * numCores() )
634 return pstr, qstr, self.period_us, quota
635
636 def cfsInfo( self, f):
637 "Internal method: return parameters for CFS bandwidth"
638 pstr, qstr = 'cfs_period_us', 'cfs_quota_us'
639 # CFS uses wall clock time for period and CPU time for quota.
640 quota = int( self.period_us * f * numCores() )
641 period = self.period_us
642 if f > 0 and quota < 1000:
643 debug( '(cfsInfo: increasing default period) ' )
644 quota = 1000
645 period = int( quota / f / numCores() )
646 return pstr, qstr, period, quota
647
648 # BL comment:
649 # This may not be the right API,
650 # since it doesn't specify CPU bandwidth in "absolute"
651 # units the way link bandwidth is specified.
652 # We should use MIPS or SPECINT or something instead.
653 # Alternatively, we should change from system fraction
654 # to CPU seconds per second, essentially assuming that
655 # all CPUs are the same.
656
657 def setCPUFrac( self, f=-1, sched=None):
658 """Set overall CPU fraction for this host
659 f: CPU bandwidth limit (fraction)
660 sched: 'rt' or 'cfs'
661 Note 'cfs' requires CONFIG_CFS_BANDWIDTH"""
662 if not f:
663 return
664 if not sched:
665 sched = self.sched
666 if sched == 'rt':
667 pstr, qstr, period, quota = self.rtInfo( f )
668 elif sched == 'cfs':
669 pstr, qstr, period, quota = self.cfsInfo( f )
670 else:
671 return
672 if quota < 0:
673 # Reset to unlimited
674 quota = -1
675 # Set cgroup's period and quota
676 self.cgroupSet( pstr, period )
677 self.cgroupSet( qstr, quota )
678 if sched == 'rt':
679 # Set RT priority if necessary
680 self.chrt()
681 info( '(%s %d/%dus) ' % ( sched, quota, period ) )
682
683 def setCPUs( self, cores, mems=0 ):
684 "Specify (real) cores that our cgroup can run on"
685 if type( cores ) is list:
686 cores = ','.join( [ str( c ) for c in cores ] )
687 self.cgroupSet( resource='cpuset', param='cpus',
688 value=cores )
689 # Memory placement is probably not relevant, but we
690 # must specify it anyway
691 self.cgroupSet( resource='cpuset', param='mems',
692 value=mems)
693 # We have to do this here after we've specified
694 # cpus and mems
695 errFail( 'cgclassify -g cpuset:/%s %s' % (
696 self.name, self.pid ) )
697
698 def config( self, cpu=None, cores=None, **params ):
699 """cpu: desired overall system CPU fraction
700 cores: (real) core(s) this host can run on
701 params: parameters for Node.config()"""
702 r = Node.config( self, **params )
703 # Was considering cpu={'cpu': cpu , 'sched': sched}, but
704 # that seems redundant
705
706 self.setParam( r, 'setCPUFrac', cpu=cpu )
707 self.setParam( r, 'setCPUs', cores=cores )
708
709 return r
710
711 inited = False
712
713 @classmethod
714 def init( cls ):
715 "Initialization for CPULimitedHost class"
716 mountCgroups()
717 cls.inited = True
718
719class CCNHost( Host ):
720 "CCNHost is a Host that always runs the ccnd daemon"
721
722 def __init__( self, name, **kwargs ):
723
carlosmscabral6d3dd602013-03-23 11:12:34 -0300724
carlosmscabralf40ecd12013-02-01 18:15:58 -0200725 Host.__init__( self, name, **kwargs )
726 if not CCNHost.inited:
727 CCNHost.init()
carlosmscabral6d3dd602013-03-23 11:12:34 -0300728
729 # self.cmd("export CCND_DEBUG=6")
730 # self.cmd("export CCND_LOG=./log.{0}".format(self.name))
carlosmscabralf40ecd12013-02-01 18:15:58 -0200731 self.cmd("export CCN_LOCAL_SOCKNAME=/tmp/.sock.ccnx.{0}".format(self.name))
732 self.cmd("ccndstart")
733 self.peerList = {}
734
735 def config( self, fib=None, app=None, **params ):
carlosmscabral6d3dd602013-03-23 11:12:34 -0300736
carlosmscabralf40ecd12013-02-01 18:15:58 -0200737 r = Node.config( self, **params )
carlosmscabral6d3dd602013-03-23 11:12:34 -0300738
carlosmscabralf40ecd12013-02-01 18:15:58 -0200739 self.setParam( r, 'app', fib=fib )
740 self.setParam( r, 'fib', app=app)
carlosmscabral6d3dd602013-03-23 11:12:34 -0300741
carlosmscabralf40ecd12013-02-01 18:15:58 -0200742 return r
carlosmscabral6d3dd602013-03-23 11:12:34 -0300743
carlosmscabralf40ecd12013-02-01 18:15:58 -0200744 def configCCN(self):
carlosmscabral6d3dd602013-03-23 11:12:34 -0300745
carlosmscabralf40ecd12013-02-01 18:15:58 -0200746 self.buildPeerIP()
747 self.setFIB()
carlosmscabral6d3dd602013-03-23 11:12:34 -0300748
carlosmscabralf40ecd12013-02-01 18:15:58 -0200749 def buildPeerIP(self):
750 for iface in self.intfList():
751 link = iface.link
752 if link:
753 node1, node2 = link.intf1.node, link.intf2.node
754 if node1 == self:
755 self.peerList[node2.name] = link.intf2.node.IP(link.intf2)
756 else:
757 self.peerList[node1.name] = link.intf1.node.IP(link.intf1)
carlosmscabral6d3dd602013-03-23 11:12:34 -0300758
759
carlosmscabralf40ecd12013-02-01 18:15:58 -0200760 def setFIB(self):
761
762 for name in self.params['fib']:
763 if not name:
764 pass
765 else:
766 self.insert_fib(name[0],self.peerList[name[1]])
carlosmscabral6d3dd602013-03-23 11:12:34 -0300767
768
carlosmscabralf40ecd12013-02-01 18:15:58 -0200769 def insert_fib(self, uri, host):
770 self.cmd('ccndc add {0} tcp {1}'.format(uri,host))
carlosmscabral6d3dd602013-03-23 11:12:34 -0300771 # self.cmd('ccndc add {0} udp {1}'.format(uri,host))
772
carlosmscabralf40ecd12013-02-01 18:15:58 -0200773 def terminate( self ):
774 "Stop node."
775 self.cmd('ccndstop')
carlosmscabral6d3dd602013-03-23 11:12:34 -0300776 self.cmd('killall -r zebra ospf')
carlosmscabralf40ecd12013-02-01 18:15:58 -0200777 Host.terminate(self)
carlosmscabral6d3dd602013-03-23 11:12:34 -0300778
carlosmscabralf40ecd12013-02-01 18:15:58 -0200779 inited = False
780
carlosmscabral6d3dd602013-03-23 11:12:34 -0300781
carlosmscabralf40ecd12013-02-01 18:15:58 -0200782 @classmethod
783 def init( cls ):
784 "Initialization for CCNHost class"
785 cls.inited = True
carlosmscabral6d3dd602013-03-23 11:12:34 -0300786
carlosmscabralf40ecd12013-02-01 18:15:58 -0200787class CPULimitedCCNHost( CPULimitedHost ):
788 '''CPULimitedCCNHost is a Host that always runs the ccnd daemon and extends CPULimitedHost.
789 It should be used when one wants to limit the resources of CCN routers and hosts '''
carlosmscabral6d3dd602013-03-23 11:12:34 -0300790
carlosmscabralf40ecd12013-02-01 18:15:58 -0200791
792 def __init__( self, name, sched='cfs', **kwargs ):
carlosmscabral6d3dd602013-03-23 11:12:34 -0300793
carlosmscabralf40ecd12013-02-01 18:15:58 -0200794 CPULimitedHost.__init__( self, name, sched, **kwargs )
795 if not CCNHost.inited:
796 CCNHost.init()
carlosmscabral6d3dd602013-03-23 11:12:34 -0300797
798 # self.cmd("export CCND_DEBUG=6")
799 # self.cmd("export CCND_LOG=./log.{0}".format(self.name))
800 self.cmd("export CCN_LOCAL_SOCKNAME=/tmp/.sock.ccnx.{0}".format(self.name))
carlosmscabralf40ecd12013-02-01 18:15:58 -0200801 self.cmd("ccndstart")
802 self.peerList = {}
803
carlosmscabral6d3dd602013-03-23 11:12:34 -0300804 def config( self, fib=None, app=None, cpu=None, cores=None, **params):
carlosmscabralf40ecd12013-02-01 18:15:58 -0200805
806 r = CPULimitedHost.config(self,cpu,cores, **params)
carlosmscabral6d3dd602013-03-23 11:12:34 -0300807
carlosmscabralf40ecd12013-02-01 18:15:58 -0200808 self.setParam( r, 'app', fib=fib )
809 self.setParam( r, 'fib', app=app)
carlosmscabral6d3dd602013-03-23 11:12:34 -0300810
carlosmscabralf40ecd12013-02-01 18:15:58 -0200811 return r
carlosmscabral6d3dd602013-03-23 11:12:34 -0300812
carlosmscabralf40ecd12013-02-01 18:15:58 -0200813 def configCCN(self):
carlosmscabral6d3dd602013-03-23 11:12:34 -0300814
carlosmscabralf40ecd12013-02-01 18:15:58 -0200815 self.buildPeerIP()
816 self.setFIB()
carlosmscabral6d3dd602013-03-23 11:12:34 -0300817
carlosmscabralf40ecd12013-02-01 18:15:58 -0200818 def buildPeerIP(self):
819 for iface in self.intfList():
820 link = iface.link
821 if link:
822 node1, node2 = link.intf1.node, link.intf2.node
823 if node1 == self:
824 self.peerList[node2.name] = link.intf2.node.IP(link.intf2)
825 else:
826 self.peerList[node1.name] = link.intf1.node.IP(link.intf1)
carlosmscabral6d3dd602013-03-23 11:12:34 -0300827
828
carlosmscabralf40ecd12013-02-01 18:15:58 -0200829 def setFIB(self):
830
831 for name in self.params['fib']:
832 if not name:
833 pass
834 else:
835 self.insert_fib(name[0],self.peerList[name[1]])
carlosmscabral6d3dd602013-03-23 11:12:34 -0300836
837
carlosmscabralf40ecd12013-02-01 18:15:58 -0200838 def insert_fib(self, uri, host):
839 self.cmd('ccndc add {0} tcp {1}'.format(uri,host))
carlosmscabral6d3dd602013-03-23 11:12:34 -0300840# self.cmd('ccndc add {0} udp {1}'.format(uri,host))
841
carlosmscabralf40ecd12013-02-01 18:15:58 -0200842 def terminate( self ):
843 "Stop node."
844 self.cmd('ccndstop')
carlosmscabral6d3dd602013-03-23 11:12:34 -0300845 self.cmd('killall -r zebra ospf')
carlosmscabralf40ecd12013-02-01 18:15:58 -0200846 Host.terminate(self)
carlosmscabral6d3dd602013-03-23 11:12:34 -0300847
carlosmscabralf40ecd12013-02-01 18:15:58 -0200848 inited = False
849
carlosmscabral6d3dd602013-03-23 11:12:34 -0300850
carlosmscabralf40ecd12013-02-01 18:15:58 -0200851 @classmethod
852 def init( cls ):
853 "Initialization for CCNHost class"
854 cls.inited = True
855
856# Some important things to note:
857#
858# The "IP" address which setIP() assigns to the switch is not
859# an "IP address for the switch" in the sense of IP routing.
860# Rather, it is the IP address for the control interface,
861# on the control network, and it is only relevant to the
862# controller. If you are running in the root namespace
863# (which is the only way to run OVS at the moment), the
864# control interface is the loopback interface, and you
865# normally never want to change its IP address!
866#
867# In general, you NEVER want to attempt to use Linux's
868# network stack (i.e. ifconfig) to "assign" an IP address or
869# MAC address to a switch data port. Instead, you "assign"
870# the IP and MAC addresses in the controller by specifying
871# packets that you want to receive or send. The "MAC" address
872# reported by ifconfig for a switch data port is essentially
873# meaningless. It is important to understand this if you
874# want to create a functional router using OpenFlow.
875
876class Switch( Node ):
877 """A Switch is a Node that is running (or has execed?)
878 an OpenFlow switch."""
879
880 portBase = 1 # Switches start with port 1 in OpenFlow
881 dpidLen = 16 # digits in dpid passed to switch
882
883 def __init__( self, name, dpid=None, opts='', listenPort=None, **params):
884 """dpid: dpid for switch (or None to derive from name, e.g. s1 -> 1)
885 opts: additional switch options
886 listenPort: port to listen on for dpctl connections"""
887 Node.__init__( self, name, **params )
888 self.dpid = dpid if dpid else self.defaultDpid()
889 self.opts = opts
890 self.listenPort = listenPort
891 if not self.inNamespace:
892 self.controlIntf = Intf( 'lo', self, port=0 )
893
894 def defaultDpid( self ):
895 "Derive dpid from switch name, s1 -> 1"
896 try:
897 dpid = int( re.findall( '\d+', self.name )[ 0 ] )
898 dpid = hex( dpid )[ 2: ]
899 dpid = '0' * ( self.dpidLen - len( dpid ) ) + dpid
900 return dpid
901 except IndexError:
902 raise Exception( 'Unable to derive default datapath ID - '
903 'please either specify a dpid or use a '
904 'canonical switch name such as s23.' )
905
906 def defaultIntf( self ):
907 "Return control interface"
908 if self.controlIntf:
909 return self.controlIntf
910 else:
911 return Node.defaultIntf( self )
912
913 def sendCmd( self, *cmd, **kwargs ):
914 """Send command to Node.
915 cmd: string"""
916 kwargs.setdefault( 'printPid', False )
917 if not self.execed:
918 return Node.sendCmd( self, *cmd, **kwargs )
919 else:
920 error( '*** Error: %s has execed and cannot accept commands' %
921 self.name )
922
923 def __repr__( self ):
924 "More informative string representation"
925 intfs = ( ','.join( [ '%s:%s' % ( i.name, i.IP() )
926 for i in self.intfList() ] ) )
927 return '<%s %s: %s pid=%s> ' % (
928 self.__class__.__name__, self.name, intfs, self.pid )
929
930class UserSwitch( Switch ):
931 "User-space switch."
932
933 dpidLen = 12
934
935 def __init__( self, name, **kwargs ):
936 """Init.
937 name: name for the switch"""
938 Switch.__init__( self, name, **kwargs )
939 pathCheck( 'ofdatapath', 'ofprotocol',
940 moduleName='the OpenFlow reference user switch' +
941 '(openflow.org)' )
942 if self.listenPort:
943 self.opts += ' --listen=ptcp:%i ' % self.listenPort
944
945 @classmethod
946 def setup( cls ):
947 "Ensure any dependencies are loaded; if not, try to load them."
948 if not os.path.exists( '/dev/net/tun' ):
949 moduleDeps( add=TUN )
950
951 def dpctl( self, *args ):
952 "Run dpctl command"
953 if not self.listenPort:
954 return "can't run dpctl without passive listening port"
955 return self.cmd( 'dpctl ' + ' '.join( args ) +
956 ' tcp:127.0.0.1:%i' % self.listenPort )
957
958 def start( self, controllers ):
959 """Start OpenFlow reference user datapath.
960 Log to /tmp/sN-{ofd,ofp}.log.
961 controllers: list of controller objects"""
962 # Add controllers
963 clist = ','.join( [ 'tcp:%s:%d' % ( c.IP(), c.port )
964 for c in controllers ] )
965 ofdlog = '/tmp/' + self.name + '-ofd.log'
966 ofplog = '/tmp/' + self.name + '-ofp.log'
967 self.cmd( 'ifconfig lo up' )
968 intfs = [ str( i ) for i in self.intfList() if not i.IP() ]
969 self.cmd( 'ofdatapath -i ' + ','.join( intfs ) +
970 ' punix:/tmp/' + self.name + ' -d ' + self.dpid +
971 ' 1> ' + ofdlog + ' 2> ' + ofdlog + ' &' )
972 self.cmd( 'ofprotocol unix:/tmp/' + self.name +
973 ' ' + clist +
974 ' --fail=closed ' + self.opts +
975 ' 1> ' + ofplog + ' 2>' + ofplog + ' &' )
976
977 def stop( self ):
978 "Stop OpenFlow reference user datapath."
979 self.cmd( 'kill %ofdatapath' )
980 self.cmd( 'kill %ofprotocol' )
981 self.deleteIntfs()
982
983
984class OVSLegacyKernelSwitch( Switch ):
985 """Open VSwitch legacy kernel-space switch using ovs-openflowd.
986 Currently only works in the root namespace."""
987
988 def __init__( self, name, dp=None, **kwargs ):
989 """Init.
990 name: name for switch
991 dp: netlink id (0, 1, 2, ...)
992 defaultMAC: default MAC as unsigned int; random value if None"""
993 Switch.__init__( self, name, **kwargs )
994 self.dp = dp if dp else self.name
995 self.intf = self.dp
996 if self.inNamespace:
997 error( "OVSKernelSwitch currently only works"
998 " in the root namespace.\n" )
999 exit( 1 )
1000
1001 @classmethod
1002 def setup( cls ):
1003 "Ensure any dependencies are loaded; if not, try to load them."
1004 pathCheck( 'ovs-dpctl', 'ovs-openflowd',
1005 moduleName='Open vSwitch (openvswitch.org)')
1006 moduleDeps( subtract=OF_KMOD, add=OVS_KMOD )
1007
1008 def start( self, controllers ):
1009 "Start up kernel datapath."
1010 ofplog = '/tmp/' + self.name + '-ofp.log'
1011 quietRun( 'ifconfig lo up' )
1012 # Delete local datapath if it exists;
1013 # then create a new one monitoring the given interfaces
1014 self.cmd( 'ovs-dpctl del-dp ' + self.dp )
1015 self.cmd( 'ovs-dpctl add-dp ' + self.dp )
1016 intfs = [ str( i ) for i in self.intfList() if not i.IP() ]
1017 self.cmd( 'ovs-dpctl', 'add-if', self.dp, ' '.join( intfs ) )
1018 # Run protocol daemon
1019 clist = ','.join( [ 'tcp:%s:%d' % ( c.IP(), c.port )
1020 for c in controllers ] )
1021 self.cmd( 'ovs-openflowd ' + self.dp +
1022 ' ' + clist +
1023 ' --fail=secure ' + self.opts +
1024 ' --datapath-id=' + self.dpid +
1025 ' 1>' + ofplog + ' 2>' + ofplog + '&' )
1026 self.execed = False
1027
1028 def stop( self ):
1029 "Terminate kernel datapath."
1030 quietRun( 'ovs-dpctl del-dp ' + self.dp )
1031 self.cmd( 'kill %ovs-openflowd' )
1032 self.deleteIntfs()
1033
1034
1035class OVSSwitch( Switch ):
1036 "Open vSwitch switch. Depends on ovs-vsctl."
1037
1038 def __init__( self, name, failMode='secure', **params ):
1039 """Init.
1040 name: name for switch
1041 failMode: controller loss behavior (secure|open)"""
1042 Switch.__init__( self, name, **params )
1043 self.failMode = failMode
1044
1045 @classmethod
1046 def setup( cls ):
1047 "Make sure Open vSwitch is installed and working"
1048 pathCheck( 'ovs-vsctl',
1049 moduleName='Open vSwitch (openvswitch.org)')
1050 # This should no longer be needed, and it breaks
1051 # with OVS 1.7 which has renamed the kernel module:
1052 # moduleDeps( subtract=OF_KMOD, add=OVS_KMOD )
1053 out, err, exitcode = errRun( 'ovs-vsctl -t 1 show' )
1054 if exitcode:
1055 error( out + err +
1056 'ovs-vsctl exited with code %d\n' % exitcode +
1057 '*** Error connecting to ovs-db with ovs-vsctl\n'
1058 'Make sure that Open vSwitch is installed, '
1059 'that ovsdb-server is running, and that\n'
1060 '"ovs-vsctl show" works correctly.\n'
1061 'You may wish to try '
1062 '"service openvswitch-switch start".\n' )
1063 exit( 1 )
1064
1065 def dpctl( self, *args ):
1066 "Run ovs-dpctl command"
1067 return self.cmd( 'ovs-dpctl', args[ 0 ], self, *args[ 1: ] )
1068
1069 @staticmethod
1070 def TCReapply( intf ):
1071 """Unfortunately OVS and Mininet are fighting
1072 over tc queuing disciplines. As a quick hack/
1073 workaround, we clear OVS's and reapply our own."""
1074 if type( intf ) is TCIntf:
1075 intf.config( **intf.params )
1076
1077 def attach( self, intf ):
1078 "Connect a data port"
1079 self.cmd( 'ovs-vsctl add-port', self, intf )
1080 self.cmd( 'ifconfig', intf, 'up' )
1081 self.TCReapply( intf )
1082
1083 def detach( self, intf ):
1084 "Disconnect a data port"
1085 self.cmd( 'ovs-vsctl del-port', self, intf )
1086
1087 def start( self, controllers ):
1088 "Start up a new OVS OpenFlow switch using ovs-vsctl"
1089 if self.inNamespace:
1090 raise Exception(
1091 'OVS kernel switch does not work in a namespace' )
1092 # We should probably call config instead, but this
1093 # requires some rethinking...
1094 self.cmd( 'ifconfig lo up' )
1095 # Annoyingly, --if-exists option seems not to work
1096 self.cmd( 'ovs-vsctl del-br', self )
1097 self.cmd( 'ovs-vsctl add-br', self )
1098 self.cmd( 'ovs-vsctl -- set Bridge', self,
1099 'other_config:datapath-id=' + self.dpid )
1100 self.cmd( 'ovs-vsctl set-fail-mode', self, self.failMode )
1101 for intf in self.intfList():
1102 if not intf.IP():
1103 self.attach( intf )
1104 # Add controllers
1105 clist = ' '.join( [ 'tcp:%s:%d' % ( c.IP(), c.port )
1106 for c in controllers ] )
1107 if self.listenPort:
1108 clist += ' ptcp:%s' % self.listenPort
1109 self.cmd( 'ovs-vsctl set-controller', self, clist )
1110
1111 def stop( self ):
1112 "Terminate OVS switch."
1113 self.cmd( 'ovs-vsctl del-br', self )
1114 self.deleteIntfs()
1115
1116OVSKernelSwitch = OVSSwitch
1117
1118
1119class Controller( Node ):
1120 """A Controller is a Node that is running (or has execed?) an
1121 OpenFlow controller."""
1122
1123 def __init__( self, name, inNamespace=False, command='controller',
1124 cargs='-v ptcp:%d', cdir=None, ip="127.0.0.1",
1125 port=6633, **params ):
1126 self.command = command
1127 self.cargs = cargs
1128 self.cdir = cdir
1129 self.ip = ip
1130 self.port = port
1131 Node.__init__( self, name, inNamespace=inNamespace,
1132 ip=ip, **params )
1133 self.cmd( 'ifconfig lo up' ) # Shouldn't be necessary
1134 self.checkListening()
1135
1136 def checkListening( self ):
1137 "Make sure no controllers are running on our port"
1138 # Verify that Telnet is installed first:
1139 out, _err, returnCode = errRun( "which telnet" )
1140 if 'telnet' not in out or returnCode != 0:
1141 raise Exception( "Error running telnet to check for listening "
1142 "controllers; please check that it is "
1143 "installed." )
1144 listening = self.cmd( "echo A | telnet -e A %s %d" %
1145 ( self.ip, self.port ) )
1146 if 'Unable' not in listening:
1147 servers = self.cmd( 'netstat -atp' ).split( '\n' )
1148 pstr = ':%d ' % self.port
1149 clist = servers[ 0:1 ] + [ s for s in servers if pstr in s ]
1150 raise Exception( "Please shut down the controller which is"
1151 " running on port %d:\n" % self.port +
1152 '\n'.join( clist ) )
1153
1154 def start( self ):
1155 """Start <controller> <args> on controller.
1156 Log to /tmp/cN.log"""
1157 pathCheck( self.command )
1158 cout = '/tmp/' + self.name + '.log'
1159 if self.cdir is not None:
1160 self.cmd( 'cd ' + self.cdir )
1161 self.cmd( self.command + ' ' + self.cargs % self.port +
1162 ' 1>' + cout + ' 2>' + cout + '&' )
1163 self.execed = False
1164
1165 def stop( self ):
1166 "Stop controller."
1167 self.cmd( 'kill %' + self.command )
1168 self.terminate()
1169
1170 def IP( self, intf=None ):
1171 "Return IP address of the Controller"
1172 if self.intfs:
1173 ip = Node.IP( self, intf )
1174 else:
1175 ip = self.ip
1176 return ip
1177
1178 def __repr__( self ):
1179 "More informative string representation"
1180 return '<%s %s: %s:%s pid=%s> ' % (
1181 self.__class__.__name__, self.name,
1182 self.IP(), self.port, self.pid )
1183
1184
1185class OVSController( Controller ):
1186 "Open vSwitch controller"
1187 def __init__( self, name, command='ovs-controller', **kwargs ):
1188 Controller.__init__( self, name, command=command, **kwargs )
1189
1190
1191class NOX( Controller ):
1192 "Controller to run a NOX application."
1193
1194 def __init__( self, name, *noxArgs, **kwargs ):
1195 """Init.
1196 name: name to give controller
1197 noxArgs: arguments (strings) to pass to NOX"""
1198 if not noxArgs:
1199 warn( 'warning: no NOX modules specified; '
1200 'running packetdump only\n' )
1201 noxArgs = [ 'packetdump' ]
1202 elif type( noxArgs ) not in ( list, tuple ):
1203 noxArgs = [ noxArgs ]
1204
1205 if 'NOX_CORE_DIR' not in os.environ:
1206 exit( 'exiting; please set missing NOX_CORE_DIR env var' )
1207 noxCoreDir = os.environ[ 'NOX_CORE_DIR' ]
1208
1209 Controller.__init__( self, name,
1210 command=noxCoreDir + '/nox_core',
1211 cargs='--libdir=/usr/local/lib -v -i ptcp:%s ' +
1212 ' '.join( noxArgs ),
1213 cdir=noxCoreDir,
1214 **kwargs )
1215
1216
1217class RemoteController( Controller ):
1218 "Controller running outside of Mininet's control."
1219
1220 def __init__( self, name, ip='127.0.0.1',
1221 port=6633, **kwargs):
1222 """Init.
1223 name: name to give controller
1224 ip: the IP address where the remote controller is
1225 listening
1226 port: the port where the remote controller is listening"""
1227 Controller.__init__( self, name, ip=ip, port=port, **kwargs )
1228
1229 def start( self ):
1230 "Overridden to do nothing."
1231 return
1232
1233 def stop( self ):
1234 "Overridden to do nothing."
1235 return
1236
1237 def checkListening( self ):
1238 "Warn if remote controller is not accessible"
1239 listening = self.cmd( "echo A | telnet -e A %s %d" %
1240 ( self.ip, self.port ) )
1241 if 'Unable' in listening:
1242 warn( "Unable to contact the remote controller"
1243 " at %s:%d\n" % ( self.ip, self.port ) )