mininet / mininet / node.py @ 7485b035
History | View | Annotate | Download (59.2 KB)
1 |
"""
|
---|---|
2 |
Node objects for Mininet.
|
3 |
|
4 |
Nodes provide a simple abstraction for interacting with hosts, switches
|
5 |
and controllers. Local nodes are simply one or more processes on the local
|
6 |
machine.
|
7 |
|
8 |
Node: superclass for all (primarily local) network nodes.
|
9 |
|
10 |
Host: 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. By default,
|
15 |
hosts share the root file system, but they may also specify private
|
16 |
directories.
|
17 |
|
18 |
CPULimitedHost: a virtual host whose CPU bandwidth is limited by
|
19 |
RT or CFS bandwidth limiting.
|
20 |
|
21 |
Switch: superclass for switch nodes.
|
22 |
|
23 |
UserSwitch: a switch using the user-space switch from the OpenFlow
|
24 |
reference implementation.
|
25 |
|
26 |
KernelSwitch: a switch using the kernel switch from the OpenFlow reference
|
27 |
implementation.
|
28 |
|
29 |
OVSSwitch: a switch using the OpenVSwitch OpenFlow-compatible switch
|
30 |
implementation (openvswitch.org).
|
31 |
|
32 |
Controller: superclass for OpenFlow controllers. The default controller
|
33 |
is controller(8) from the reference implementation.
|
34 |
|
35 |
NOXController: a controller node using NOX (noxrepo.org).
|
36 |
|
37 |
RemoteController: a remote controller node, which may use any
|
38 |
arbitrary OpenFlow-compatible controller, and which is not
|
39 |
created or managed by mininet.
|
40 |
|
41 |
Future enhancements:
|
42 |
|
43 |
- Possibly make Node, Switch and Controller more abstract so that
|
44 |
they can be used for both local and remote nodes
|
45 |
|
46 |
- Create proxy objects for remote nodes (Mininet: Cluster Edition)
|
47 |
"""
|
48 |
|
49 |
import os |
50 |
import pty |
51 |
import re |
52 |
import signal |
53 |
import select |
54 |
from subprocess import Popen, PIPE |
55 |
from time import sleep |
56 |
|
57 |
from mininet.log import info, error, warn, debug |
58 |
from mininet.util import ( quietRun, errRun, errFail, moveIntf, isShellBuiltin, |
59 |
numCores, retry, mountCgroups ) |
60 |
from mininet.moduledeps import moduleDeps, pathCheck, OVS_KMOD, OF_KMOD, TUN |
61 |
from mininet.link import Link, Intf, TCIntf, OVSIntf |
62 |
from re import findall |
63 |
from distutils.version import StrictVersion |
64 |
|
65 |
class Node( object ): |
66 |
"""A virtual network node is simply a shell in a network namespace.
|
67 |
We communicate with it using pipes."""
|
68 |
|
69 |
portBase = 0 # Nodes always start with eth0/port0, even in OF 1.0 |
70 |
|
71 |
def __init__( self, name, inNamespace=True, **params ): |
72 |
"""name: name of node
|
73 |
inNamespace: in network namespace?
|
74 |
privateDirs: list of private directory strings or tuples
|
75 |
params: Node parameters (see config() for details)"""
|
76 |
|
77 |
# Make sure class actually works
|
78 |
self.checkSetup()
|
79 |
|
80 |
self.name = params.get( 'name', name ) |
81 |
self.privateDirs = params.get( 'privateDirs', [] ) |
82 |
self.inNamespace = params.get( 'inNamespace', inNamespace ) |
83 |
|
84 |
# Stash configuration parameters for future reference
|
85 |
self.params = params
|
86 |
|
87 |
self.intfs = {} # dict of port numbers to interfaces |
88 |
self.ports = {} # dict of interfaces to port numbers |
89 |
# replace with Port objects, eventually ?
|
90 |
self.nameToIntf = {} # dict of interface names to Intfs |
91 |
|
92 |
# Make pylint happy
|
93 |
( self.shell, self.execed, self.pid, self.stdin, self.stdout, |
94 |
self.lastPid, self.lastCmd, self.pollOut ) = ( |
95 |
None, None, None, None, None, None, None, None ) |
96 |
self.waiting = False |
97 |
self.readbuf = '' |
98 |
|
99 |
# Start command interpreter shell
|
100 |
self.startShell()
|
101 |
self.mountPrivateDirs()
|
102 |
|
103 |
# File descriptor to node mapping support
|
104 |
# Class variables and methods
|
105 |
|
106 |
inToNode = {} # mapping of input fds to nodes
|
107 |
outToNode = {} # mapping of output fds to nodes
|
108 |
|
109 |
@classmethod
|
110 |
def fdToNode( cls, fd ): |
111 |
"""Return node corresponding to given file descriptor.
|
112 |
fd: file descriptor
|
113 |
returns: node"""
|
114 |
node = cls.outToNode.get( fd ) |
115 |
return node or cls.inToNode.get( fd ) |
116 |
|
117 |
# Command support via shell process in namespace
|
118 |
def startShell( self, mnopts=None ): |
119 |
"Start a shell process for running commands"
|
120 |
if self.shell: |
121 |
error( "%s: shell is already running\n" % self.name ) |
122 |
return
|
123 |
# mnexec: (c)lose descriptors, (d)etach from tty,
|
124 |
# (p)rint pid, and run in (n)amespace
|
125 |
opts = '-cd' if mnopts is None else mnopts |
126 |
if self.inNamespace: |
127 |
opts += 'n'
|
128 |
# bash -m: enable job control, i: force interactive
|
129 |
# -s: pass $* to shell, and make process easy to find in ps
|
130 |
# prompt is set to sentinel chr( 127 )
|
131 |
cmd = [ 'mnexec', opts, 'env', 'PS1=' + chr( 127 ), |
132 |
'bash', '--norc', '-mis', 'mininet:' + self.name ] |
133 |
# Spawn a shell subprocess in a pseudo-tty, to disable buffering
|
134 |
# in the subprocess and insulate it from signals (e.g. SIGINT)
|
135 |
# received by the parent
|
136 |
master, slave = pty.openpty() |
137 |
self.shell = self._popen( cmd, stdin=slave, stdout=slave, stderr=slave, |
138 |
close_fds=False )
|
139 |
self.stdin = os.fdopen( master, 'rw' ) |
140 |
self.stdout = self.stdin |
141 |
self.pid = self.shell.pid |
142 |
self.pollOut = select.poll()
|
143 |
self.pollOut.register( self.stdout ) |
144 |
# Maintain mapping between file descriptors and nodes
|
145 |
# This is useful for monitoring multiple nodes
|
146 |
# using select.poll()
|
147 |
self.outToNode[ self.stdout.fileno() ] = self |
148 |
self.inToNode[ self.stdin.fileno() ] = self |
149 |
self.execed = False |
150 |
self.lastCmd = None |
151 |
self.lastPid = None |
152 |
self.readbuf = '' |
153 |
# Wait for prompt
|
154 |
while True: |
155 |
data = self.read( 1024 ) |
156 |
if data[ -1 ] == chr( 127 ): |
157 |
break
|
158 |
self.pollOut.poll()
|
159 |
self.waiting = False |
160 |
self.cmd( 'stty -echo' ) |
161 |
self.cmd( 'set +m' ) |
162 |
|
163 |
def mountPrivateDirs( self ): |
164 |
"mount private directories"
|
165 |
for directory in self.privateDirs: |
166 |
if isinstance( directory, tuple ): |
167 |
# mount given private directory
|
168 |
privateDir = directory[ 1 ] % self.__dict__ |
169 |
mountPoint = directory[ 0 ]
|
170 |
self.cmd( 'mkdir -p %s' % privateDir ) |
171 |
self.cmd( 'mkdir -p %s' % mountPoint ) |
172 |
self.cmd( 'mount --bind %s %s' % |
173 |
( privateDir, mountPoint ) ) |
174 |
else:
|
175 |
# mount temporary filesystem on directory
|
176 |
self.cmd( 'mkdir -p %s' % directory ) |
177 |
self.cmd( 'mount -n -t tmpfs tmpfs %s' % directory ) |
178 |
|
179 |
def unmountPrivateDirs( self ): |
180 |
"mount private directories"
|
181 |
for directory in self.privateDirs: |
182 |
if isinstance( directory, tuple ): |
183 |
self.cmd( 'umount ', directory[ 0 ] ) |
184 |
else:
|
185 |
self.cmd( 'umount ', directory ) |
186 |
|
187 |
def _popen( self, cmd, **params ): |
188 |
"""Internal method: spawn and return a process
|
189 |
cmd: command to run (list)
|
190 |
params: parameters to Popen()"""
|
191 |
# Leave this is as an instance method for now
|
192 |
assert self |
193 |
return Popen( cmd, **params )
|
194 |
|
195 |
def cleanup( self ): |
196 |
"Help python collect its garbage."
|
197 |
# Intfs may end up in root NS
|
198 |
for intfName in self.intfNames(): |
199 |
if self.name in intfName: |
200 |
quietRun( 'ip link del ' + intfName )
|
201 |
self.shell = None |
202 |
|
203 |
# Subshell I/O, commands and control
|
204 |
|
205 |
def read( self, maxbytes=1024 ): |
206 |
"""Buffered read from node, non-blocking.
|
207 |
maxbytes: maximum number of bytes to return"""
|
208 |
count = len( self.readbuf ) |
209 |
if count < maxbytes:
|
210 |
data = os.read( self.stdout.fileno(), maxbytes - count )
|
211 |
self.readbuf += data
|
212 |
if maxbytes >= len( self.readbuf ): |
213 |
result = self.readbuf
|
214 |
self.readbuf = '' |
215 |
else:
|
216 |
result = self.readbuf[ :maxbytes ]
|
217 |
self.readbuf = self.readbuf[ maxbytes: ] |
218 |
return result
|
219 |
|
220 |
def readline( self ): |
221 |
"""Buffered readline from node, non-blocking.
|
222 |
returns: line (minus newline) or None"""
|
223 |
self.readbuf += self.read( 1024 ) |
224 |
if '\n' not in self.readbuf: |
225 |
return None |
226 |
pos = self.readbuf.find( '\n' ) |
227 |
line = self.readbuf[ 0: pos ] |
228 |
self.readbuf = self.readbuf[ pos + 1: ] |
229 |
return line
|
230 |
|
231 |
def write( self, data ): |
232 |
"""Write data to node.
|
233 |
data: string"""
|
234 |
os.write( self.stdin.fileno(), data )
|
235 |
|
236 |
def terminate( self ): |
237 |
"Send kill signal to Node and clean up after it."
|
238 |
self.unmountPrivateDirs()
|
239 |
if self.shell: |
240 |
if self.shell.poll() is None: |
241 |
os.killpg( self.shell.pid, signal.SIGHUP )
|
242 |
self.cleanup()
|
243 |
|
244 |
def stop( self, deleteIntfs=False ): |
245 |
"""Stop node.
|
246 |
deleteIntfs: delete interfaces? (False)"""
|
247 |
if deleteIntfs:
|
248 |
self.deleteIntfs()
|
249 |
self.terminate()
|
250 |
|
251 |
def waitReadable( self, timeoutms=None ): |
252 |
"""Wait until node's output is readable.
|
253 |
timeoutms: timeout in ms or None to wait indefinitely."""
|
254 |
if len( self.readbuf ) == 0: |
255 |
self.pollOut.poll( timeoutms )
|
256 |
|
257 |
def sendCmd( self, *args, **kwargs ): |
258 |
"""Send a command, followed by a command to echo a sentinel,
|
259 |
and return without waiting for the command to complete.
|
260 |
args: command and arguments, or string
|
261 |
printPid: print command's PID?"""
|
262 |
assert self.shell and not self.waiting |
263 |
printPid = kwargs.get( 'printPid', True ) |
264 |
# Allow sendCmd( [ list ] )
|
265 |
if len( args ) == 1 and isinstance( args[ 0 ], list ): |
266 |
cmd = args[ 0 ]
|
267 |
# Allow sendCmd( cmd, arg1, arg2... )
|
268 |
elif len( args ) > 0: |
269 |
cmd = args |
270 |
# Convert to string
|
271 |
if not isinstance( cmd, str ): |
272 |
cmd = ' '.join( [ str( c ) for c in cmd ] ) |
273 |
if not re.search( r'\w', cmd ): |
274 |
# Replace empty commands with something harmless
|
275 |
cmd = 'echo -n'
|
276 |
self.lastCmd = cmd
|
277 |
# if a builtin command is backgrounded, it still yields a PID
|
278 |
if len( cmd ) > 0 and cmd[ -1 ] == '&': |
279 |
# print ^A{pid}\n so monitor() can set lastPid
|
280 |
cmd += ' printf "\\001%d\\012" $! '
|
281 |
elif printPid and not isShellBuiltin( cmd ): |
282 |
cmd = 'mnexec -p ' + cmd
|
283 |
self.write( cmd + '\n' ) |
284 |
self.lastPid = None |
285 |
self.waiting = True |
286 |
|
287 |
def sendInt( self, intr=chr( 3 ) ): |
288 |
"Interrupt running command."
|
289 |
debug( 'sendInt: writing chr(%d)\n' % ord( intr ) ) |
290 |
self.write( intr )
|
291 |
|
292 |
def monitor( self, timeoutms=None, findPid=True ): |
293 |
"""Monitor and return the output of a command.
|
294 |
Set self.waiting to False if command has completed.
|
295 |
timeoutms: timeout in ms or None to wait indefinitely
|
296 |
findPid: look for PID from mnexec -p"""
|
297 |
self.waitReadable( timeoutms )
|
298 |
data = self.read( 1024 ) |
299 |
pidre = r'\[\d+\] \d+\r\n'
|
300 |
# Look for PID
|
301 |
marker = chr( 1 ) + r'\d+\r\n' |
302 |
if findPid and chr( 1 ) in data: |
303 |
# suppress the job and PID of a backgrounded command
|
304 |
if re.findall( pidre, data ):
|
305 |
data = re.sub( pidre, '', data )
|
306 |
# Marker can be read in chunks; continue until all of it is read
|
307 |
while not re.findall( marker, data ): |
308 |
data += self.read( 1024 ) |
309 |
markers = re.findall( marker, data ) |
310 |
if markers:
|
311 |
self.lastPid = int( markers[ 0 ][ 1: ] ) |
312 |
data = re.sub( marker, '', data )
|
313 |
# Look for sentinel/EOF
|
314 |
if len( data ) > 0 and data[ -1 ] == chr( 127 ): |
315 |
self.waiting = False |
316 |
data = data[ :-1 ]
|
317 |
elif chr( 127 ) in data: |
318 |
self.waiting = False |
319 |
data = data.replace( chr( 127 ), '' ) |
320 |
return data
|
321 |
|
322 |
def waitOutput( self, verbose=False, findPid=True ): |
323 |
"""Wait for a command to complete.
|
324 |
Completion is signaled by a sentinel character, ASCII(127)
|
325 |
appearing in the output stream. Wait for the sentinel and return
|
326 |
the output, including trailing newline.
|
327 |
verbose: print output interactively"""
|
328 |
log = info if verbose else debug |
329 |
output = ''
|
330 |
while self.waiting: |
331 |
data = self.monitor( findPid=findPid )
|
332 |
output += data |
333 |
log( data ) |
334 |
return output
|
335 |
|
336 |
def cmd( self, *args, **kwargs ): |
337 |
"""Send a command, wait for output, and return it.
|
338 |
cmd: string"""
|
339 |
verbose = kwargs.get( 'verbose', False ) |
340 |
log = info if verbose else debug |
341 |
log( '*** %s : %s\n' % ( self.name, args ) ) |
342 |
if self.shell: |
343 |
self.sendCmd( *args, **kwargs )
|
344 |
return self.waitOutput( verbose ) |
345 |
else:
|
346 |
warn( '(%s exited - ignoring cmd%s)\n' % ( self, args ) ) |
347 |
|
348 |
def cmdPrint( self, *args): |
349 |
"""Call cmd and printing its output
|
350 |
cmd: string"""
|
351 |
return self.cmd( *args, **{ 'verbose': True } ) |
352 |
|
353 |
def popen( self, *args, **kwargs ): |
354 |
"""Return a Popen() object in our namespace
|
355 |
args: Popen() args, single list, or string
|
356 |
kwargs: Popen() keyword args"""
|
357 |
defaults = { 'stdout': PIPE, 'stderr': PIPE, |
358 |
'mncmd':
|
359 |
[ 'mnexec', '-da', str( self.pid ) ] } |
360 |
defaults.update( kwargs ) |
361 |
if len( args ) == 1: |
362 |
if isinstance( args[ 0 ], list ): |
363 |
# popen([cmd, arg1, arg2...])
|
364 |
cmd = args[ 0 ]
|
365 |
elif isinstance( args[ 0 ], basestring ): |
366 |
# popen("cmd arg1 arg2...")
|
367 |
cmd = args[ 0 ].split()
|
368 |
else:
|
369 |
raise Exception( 'popen() requires a string or list' ) |
370 |
elif len( args ) > 0: |
371 |
# popen( cmd, arg1, arg2... )
|
372 |
cmd = list( args )
|
373 |
# Attach to our namespace using mnexec -a
|
374 |
cmd = defaults.pop( 'mncmd' ) + cmd
|
375 |
# Shell requires a string, not a list!
|
376 |
if defaults.get( 'shell', False ): |
377 |
cmd = ' '.join( cmd )
|
378 |
popen = self._popen( cmd, **defaults )
|
379 |
return popen
|
380 |
|
381 |
def pexec( self, *args, **kwargs ): |
382 |
"""Execute a command using popen
|
383 |
returns: out, err, exitcode"""
|
384 |
popen = self.popen( *args, stdin=PIPE, stdout=PIPE, stderr=PIPE,
|
385 |
**kwargs ) |
386 |
# Warning: this can fail with large numbers of fds!
|
387 |
out, err = popen.communicate() |
388 |
exitcode = popen.wait() |
389 |
return out, err, exitcode
|
390 |
|
391 |
# Interface management, configuration, and routing
|
392 |
|
393 |
# BL notes: This might be a bit redundant or over-complicated.
|
394 |
# However, it does allow a bit of specialization, including
|
395 |
# changing the canonical interface names. It's also tricky since
|
396 |
# the real interfaces are created as veth pairs, so we can't
|
397 |
# make a single interface at a time.
|
398 |
|
399 |
def newPort( self ): |
400 |
"Return the next port number to allocate."
|
401 |
if len( self.ports ) > 0: |
402 |
return max( self.ports.values() ) + 1 |
403 |
return self.portBase |
404 |
|
405 |
def addIntf( self, intf, port=None, moveIntfFn=moveIntf ): |
406 |
"""Add an interface.
|
407 |
intf: interface
|
408 |
port: port number (optional, typically OpenFlow port number)
|
409 |
moveIntfFn: function to move interface (optional)"""
|
410 |
if port is None: |
411 |
port = self.newPort()
|
412 |
self.intfs[ port ] = intf
|
413 |
self.ports[ intf ] = port
|
414 |
self.nameToIntf[ intf.name ] = intf
|
415 |
debug( '\n' )
|
416 |
debug( 'added intf %s (%d) to node %s\n' % (
|
417 |
intf, port, self.name ) )
|
418 |
if self.inNamespace: |
419 |
debug( 'moving', intf, 'into namespace for', self.name, '\n' ) |
420 |
moveIntfFn( intf.name, self )
|
421 |
|
422 |
def defaultIntf( self ): |
423 |
"Return interface for lowest port"
|
424 |
ports = self.intfs.keys()
|
425 |
if ports:
|
426 |
return self.intfs[ min( ports ) ] |
427 |
else:
|
428 |
warn( '*** defaultIntf: warning:', self.name, |
429 |
'has no interfaces\n' )
|
430 |
|
431 |
def intf( self, intf=None ): |
432 |
"""Return our interface object with given string name,
|
433 |
default intf if name is falsy (None, empty string, etc).
|
434 |
or the input intf arg.
|
435 |
|
436 |
Having this fcn return its arg for Intf objects makes it
|
437 |
easier to construct functions with flexible input args for
|
438 |
interfaces (those that accept both string names and Intf objects).
|
439 |
"""
|
440 |
if not intf: |
441 |
return self.defaultIntf() |
442 |
elif isinstance( intf, basestring): |
443 |
return self.nameToIntf[ intf ] |
444 |
else:
|
445 |
return intf
|
446 |
|
447 |
def connectionsTo( self, node): |
448 |
"Return [ intf1, intf2... ] for all intfs that connect self to node."
|
449 |
# We could optimize this if it is important
|
450 |
connections = [] |
451 |
for intf in self.intfList(): |
452 |
link = intf.link |
453 |
if link:
|
454 |
node1, node2 = link.intf1.node, link.intf2.node |
455 |
if node1 == self and node2 == node: |
456 |
connections += [ ( intf, link.intf2 ) ] |
457 |
elif node1 == node and node2 == self: |
458 |
connections += [ ( intf, link.intf1 ) ] |
459 |
return connections
|
460 |
|
461 |
def deleteIntfs( self, checkName=True ): |
462 |
"""Delete all of our interfaces.
|
463 |
checkName: only delete interfaces that contain our name"""
|
464 |
# In theory the interfaces should go away after we shut down.
|
465 |
# However, this takes time, so we're better off removing them
|
466 |
# explicitly so that we won't get errors if we run before they
|
467 |
# have been removed by the kernel. Unfortunately this is very slow,
|
468 |
# at least with Linux kernels before 2.6.33
|
469 |
for intf in self.intfs.values(): |
470 |
# Protect against deleting hardware interfaces
|
471 |
if ( self.name in intf.name ) or ( not checkName ): |
472 |
intf.delete() |
473 |
info( '.' )
|
474 |
|
475 |
# Routing support
|
476 |
|
477 |
def setARP( self, ip, mac ): |
478 |
"""Add an ARP entry.
|
479 |
ip: IP address as string
|
480 |
mac: MAC address as string"""
|
481 |
result = self.cmd( 'arp', '-s', ip, mac ) |
482 |
return result
|
483 |
|
484 |
def setHostRoute( self, ip, intf ): |
485 |
"""Add route to host.
|
486 |
ip: IP address as dotted decimal
|
487 |
intf: string, interface name"""
|
488 |
return self.cmd( 'route add -host', ip, 'dev', intf ) |
489 |
|
490 |
def setDefaultRoute( self, intf=None ): |
491 |
"""Set the default route to go through intf.
|
492 |
intf: Intf or {dev <intfname> via <gw-ip> ...}"""
|
493 |
# Note setParam won't call us if intf is none
|
494 |
if isinstance( intf, basestring ) and ' ' in intf: |
495 |
params = intf |
496 |
else:
|
497 |
params = 'dev %s' % intf
|
498 |
# Do this in one line in case we're messing with the root namespace
|
499 |
self.cmd( 'ip route del default; ip route add default', params ) |
500 |
|
501 |
# Convenience and configuration methods
|
502 |
|
503 |
def setMAC( self, mac, intf=None ): |
504 |
"""Set the MAC address for an interface.
|
505 |
intf: intf or intf name
|
506 |
mac: MAC address as string"""
|
507 |
return self.intf( intf ).setMAC( mac ) |
508 |
|
509 |
def setIP( self, ip, prefixLen=8, intf=None ): |
510 |
"""Set the IP address for an interface.
|
511 |
intf: intf or intf name
|
512 |
ip: IP address as a string
|
513 |
prefixLen: prefix length, e.g. 8 for /8 or 16M addrs"""
|
514 |
# This should probably be rethought
|
515 |
if '/' not in ip: |
516 |
ip = '%s/%s' % ( ip, prefixLen )
|
517 |
return self.intf( intf ).setIP( ip ) |
518 |
|
519 |
def IP( self, intf=None ): |
520 |
"Return IP address of a node or specific interface."
|
521 |
return self.intf( intf ).IP() |
522 |
|
523 |
def MAC( self, intf=None ): |
524 |
"Return MAC address of a node or specific interface."
|
525 |
return self.intf( intf ).MAC() |
526 |
|
527 |
def intfIsUp( self, intf=None ): |
528 |
"Check if an interface is up."
|
529 |
return self.intf( intf ).isUp() |
530 |
|
531 |
# The reason why we configure things in this way is so
|
532 |
# That the parameters can be listed and documented in
|
533 |
# the config method.
|
534 |
# Dealing with subclasses and superclasses is slightly
|
535 |
# annoying, but at least the information is there!
|
536 |
|
537 |
def setParam( self, results, method, **param ): |
538 |
"""Internal method: configure a *single* parameter
|
539 |
results: dict of results to update
|
540 |
method: config method name
|
541 |
param: arg=value (ignore if value=None)
|
542 |
value may also be list or dict"""
|
543 |
name, value = param.items()[ 0 ]
|
544 |
if value is None: |
545 |
return
|
546 |
f = getattr( self, method, None ) |
547 |
if not f: |
548 |
return
|
549 |
if isinstance( value, list ): |
550 |
result = f( *value ) |
551 |
elif isinstance( value, dict ): |
552 |
result = f( **value ) |
553 |
else:
|
554 |
result = f( value ) |
555 |
results[ name ] = result |
556 |
return result
|
557 |
|
558 |
def config( self, mac=None, ip=None, |
559 |
defaultRoute=None, lo='up', **_params ): |
560 |
"""Configure Node according to (optional) parameters:
|
561 |
mac: MAC address for default interface
|
562 |
ip: IP address for default interface
|
563 |
ifconfig: arbitrary interface configuration
|
564 |
Subclasses should override this method and call
|
565 |
the parent class's config(**params)"""
|
566 |
# If we were overriding this method, we would call
|
567 |
# the superclass config method here as follows:
|
568 |
# r = Parent.config( **_params )
|
569 |
r = {} |
570 |
self.setParam( r, 'setMAC', mac=mac ) |
571 |
self.setParam( r, 'setIP', ip=ip ) |
572 |
self.setParam( r, 'setDefaultRoute', defaultRoute=defaultRoute ) |
573 |
# This should be examined
|
574 |
self.cmd( 'ifconfig lo ' + lo ) |
575 |
return r
|
576 |
|
577 |
def configDefault( self, **moreParams ): |
578 |
"Configure with default parameters"
|
579 |
self.params.update( moreParams )
|
580 |
self.config( **self.params ) |
581 |
|
582 |
# This is here for backward compatibility
|
583 |
def linkTo( self, node, link=Link ): |
584 |
"""(Deprecated) Link to another node
|
585 |
replace with Link( node1, node2)"""
|
586 |
return link( self, node ) |
587 |
|
588 |
# Other methods
|
589 |
|
590 |
def intfList( self ): |
591 |
"List of our interfaces sorted by port number"
|
592 |
return [ self.intfs[ p ] for p in sorted( self.intfs.iterkeys() ) ] |
593 |
|
594 |
def intfNames( self ): |
595 |
"The names of our interfaces sorted by port number"
|
596 |
return [ str( i ) for i in self.intfList() ] |
597 |
|
598 |
def __repr__( self ): |
599 |
"More informative string representation"
|
600 |
intfs = ( ','.join( [ '%s:%s' % ( i.name, i.IP() ) |
601 |
for i in self.intfList() ] ) ) |
602 |
return '<%s %s: %s pid=%s> ' % ( |
603 |
self.__class__.__name__, self.name, intfs, self.pid ) |
604 |
|
605 |
def __str__( self ): |
606 |
"Abbreviated string representation"
|
607 |
return self.name |
608 |
|
609 |
# Automatic class setup support
|
610 |
|
611 |
isSetup = False
|
612 |
|
613 |
@classmethod
|
614 |
def checkSetup( cls ): |
615 |
"Make sure our class and superclasses are set up"
|
616 |
while cls and not getattr( cls, 'isSetup', True ): |
617 |
cls.setup() |
618 |
cls.isSetup = True
|
619 |
# Make pylint happy
|
620 |
cls = getattr( type( cls ), '__base__', None ) |
621 |
|
622 |
@classmethod
|
623 |
def setup( cls ): |
624 |
"Make sure our class dependencies are available"
|
625 |
pathCheck( 'mnexec', 'ifconfig', moduleName='Mininet') |
626 |
|
627 |
class Host( Node ): |
628 |
"A host is simply a Node"
|
629 |
pass
|
630 |
|
631 |
class CPULimitedHost( Host ): |
632 |
|
633 |
"CPU limited host"
|
634 |
|
635 |
def __init__( self, name, sched='cfs', **kwargs ): |
636 |
Host.__init__( self, name, **kwargs )
|
637 |
# Initialize class if necessary
|
638 |
if not CPULimitedHost.inited: |
639 |
CPULimitedHost.init() |
640 |
# Create a cgroup and move shell into it
|
641 |
self.cgroup = 'cpu,cpuacct,cpuset:/' + self.name |
642 |
errFail( 'cgcreate -g ' + self.cgroup ) |
643 |
# We don't add ourselves to a cpuset because you must
|
644 |
# specify the cpu and memory placement first
|
645 |
errFail( 'cgclassify -g cpu,cpuacct:/%s %s' % ( self.name, self.pid ) ) |
646 |
# BL: Setting the correct period/quota is tricky, particularly
|
647 |
# for RT. RT allows very small quotas, but the overhead
|
648 |
# seems to be high. CFS has a mininimum quota of 1 ms, but
|
649 |
# still does better with larger period values.
|
650 |
self.period_us = kwargs.get( 'period_us', 100000 ) |
651 |
self.sched = sched
|
652 |
if sched == 'rt': |
653 |
self.checkRtGroupSched()
|
654 |
self.rtprio = 20 |
655 |
|
656 |
def cgroupSet( self, param, value, resource='cpu' ): |
657 |
"Set a cgroup parameter and return its value"
|
658 |
cmd = 'cgset -r %s.%s=%s /%s' % (
|
659 |
resource, param, value, self.name )
|
660 |
quietRun( cmd ) |
661 |
nvalue = int( self.cgroupGet( param, resource ) ) |
662 |
if nvalue != value:
|
663 |
error( '*** error: cgroupSet: %s set to %s instead of %s\n'
|
664 |
% ( param, nvalue, value ) ) |
665 |
return nvalue
|
666 |
|
667 |
def cgroupGet( self, param, resource='cpu' ): |
668 |
"Return value of cgroup parameter"
|
669 |
cmd = 'cgget -r %s.%s /%s' % (
|
670 |
resource, param, self.name )
|
671 |
return int( quietRun( cmd ).split()[ -1 ] ) |
672 |
|
673 |
def cgroupDel( self ): |
674 |
"Clean up our cgroup"
|
675 |
# info( '*** deleting cgroup', self.cgroup, '\n' )
|
676 |
_out, _err, exitcode = errRun( 'cgdelete -r ' + self.cgroup ) |
677 |
return exitcode == 0 # success condition |
678 |
|
679 |
def popen( self, *args, **kwargs ): |
680 |
"""Return a Popen() object in node's namespace
|
681 |
args: Popen() args, single list, or string
|
682 |
kwargs: Popen() keyword args"""
|
683 |
# Tell mnexec to execute command in our cgroup
|
684 |
mncmd = [ 'mnexec', '-g', self.name, |
685 |
'-da', str( self.pid ) ] |
686 |
# if our cgroup is not given any cpu time,
|
687 |
# we cannot assign the RR Scheduler.
|
688 |
if self.sched == 'rt': |
689 |
if int( self.cgroupGet( 'rt_runtime_us', 'cpu' ) ) <= 0: |
690 |
mncmd += [ '-r', str( self.rtprio ) ] |
691 |
else:
|
692 |
debug( '*** error: not enough cpu time available for %s.' %
|
693 |
self.name, 'Using cfs scheduler for subprocess\n' ) |
694 |
return Host.popen( self, *args, mncmd=mncmd, **kwargs ) |
695 |
|
696 |
def cleanup( self ): |
697 |
"Clean up Node, then clean up our cgroup"
|
698 |
super( CPULimitedHost, self ).cleanup() |
699 |
retry( retries=3, delaySecs=1, fn=self.cgroupDel ) |
700 |
|
701 |
_rtGroupSched = False # internal class var: Is CONFIG_RT_GROUP_SCHED set? |
702 |
|
703 |
@classmethod
|
704 |
def checkRtGroupSched( cls ): |
705 |
"Check (Ubuntu,Debian) kernel config for CONFIG_RT_GROUP_SCHED for RT"
|
706 |
if not cls._rtGroupSched: |
707 |
release = quietRun( 'uname -r' ).strip('\r\n') |
708 |
output = quietRun( 'grep CONFIG_RT_GROUP_SCHED /boot/config-%s' %
|
709 |
release ) |
710 |
if output == '# CONFIG_RT_GROUP_SCHED is not set\n': |
711 |
error( '\n*** error: please enable RT_GROUP_SCHED '
|
712 |
'in your kernel\n' )
|
713 |
exit( 1 ) |
714 |
cls._rtGroupSched = True
|
715 |
|
716 |
def chrt( self ): |
717 |
"Set RT scheduling priority"
|
718 |
quietRun( 'chrt -p %s %s' % ( self.rtprio, self.pid ) ) |
719 |
result = quietRun( 'chrt -p %s' % self.pid ) |
720 |
firstline = result.split( '\n' )[ 0 ] |
721 |
lastword = firstline.split( ' ' )[ -1 ] |
722 |
if lastword != 'SCHED_RR': |
723 |
error( '*** error: could not assign SCHED_RR to %s\n' % self.name ) |
724 |
return lastword
|
725 |
|
726 |
def rtInfo( self, f ): |
727 |
"Internal method: return parameters for RT bandwidth"
|
728 |
pstr, qstr = 'rt_period_us', 'rt_runtime_us' |
729 |
# RT uses wall clock time for period and quota
|
730 |
quota = int( self.period_us * f ) |
731 |
return pstr, qstr, self.period_us, quota |
732 |
|
733 |
def cfsInfo( self, f ): |
734 |
"Internal method: return parameters for CFS bandwidth"
|
735 |
pstr, qstr = 'cfs_period_us', 'cfs_quota_us' |
736 |
# CFS uses wall clock time for period and CPU time for quota.
|
737 |
quota = int( self.period_us * f * numCores() ) |
738 |
period = self.period_us
|
739 |
if f > 0 and quota < 1000: |
740 |
debug( '(cfsInfo: increasing default period) ' )
|
741 |
quota = 1000
|
742 |
period = int( quota / f / numCores() )
|
743 |
# Reset to unlimited on negative quota
|
744 |
if quota < 0: |
745 |
quota = -1
|
746 |
return pstr, qstr, period, quota
|
747 |
|
748 |
# BL comment:
|
749 |
# This may not be the right API,
|
750 |
# since it doesn't specify CPU bandwidth in "absolute"
|
751 |
# units the way link bandwidth is specified.
|
752 |
# We should use MIPS or SPECINT or something instead.
|
753 |
# Alternatively, we should change from system fraction
|
754 |
# to CPU seconds per second, essentially assuming that
|
755 |
# all CPUs are the same.
|
756 |
|
757 |
def setCPUFrac( self, f, sched=None ): |
758 |
"""Set overall CPU fraction for this host
|
759 |
f: CPU bandwidth limit (positive fraction, or -1 for cfs unlimited)
|
760 |
sched: 'rt' or 'cfs'
|
761 |
Note 'cfs' requires CONFIG_CFS_BANDWIDTH,
|
762 |
and 'rt' requires CONFIG_RT_GROUP_SCHED"""
|
763 |
if not sched: |
764 |
sched = self.sched
|
765 |
if sched == 'rt': |
766 |
if not f or f < 0: |
767 |
raise Exception( 'Please set a positive CPU fraction' |
768 |
' for sched=rt\n' )
|
769 |
pstr, qstr, period, quota = self.rtInfo( f )
|
770 |
elif sched == 'cfs': |
771 |
pstr, qstr, period, quota = self.cfsInfo( f )
|
772 |
else:
|
773 |
return
|
774 |
# Set cgroup's period and quota
|
775 |
setPeriod = self.cgroupSet( pstr, period )
|
776 |
setQuota = self.cgroupSet( qstr, quota )
|
777 |
if sched == 'rt': |
778 |
# Set RT priority if necessary
|
779 |
sched = self.chrt()
|
780 |
info( '(%s %d/%dus) ' % ( sched, setQuota, setPeriod ) )
|
781 |
|
782 |
def setCPUs( self, cores, mems=0 ): |
783 |
"Specify (real) cores that our cgroup can run on"
|
784 |
if not cores: |
785 |
return
|
786 |
if isinstance( cores, list ): |
787 |
cores = ','.join( [ str( c ) for c in cores ] ) |
788 |
self.cgroupSet( resource='cpuset', param='cpus', |
789 |
value=cores ) |
790 |
# Memory placement is probably not relevant, but we
|
791 |
# must specify it anyway
|
792 |
self.cgroupSet( resource='cpuset', param='mems', |
793 |
value=mems) |
794 |
# We have to do this here after we've specified
|
795 |
# cpus and mems
|
796 |
errFail( 'cgclassify -g cpuset:/%s %s' % (
|
797 |
self.name, self.pid ) ) |
798 |
|
799 |
def config( self, cpu=-1, cores=None, **params ): |
800 |
"""cpu: desired overall system CPU fraction
|
801 |
cores: (real) core(s) this host can run on
|
802 |
params: parameters for Node.config()"""
|
803 |
r = Node.config( self, **params )
|
804 |
# Was considering cpu={'cpu': cpu , 'sched': sched}, but
|
805 |
# that seems redundant
|
806 |
self.setParam( r, 'setCPUFrac', cpu=cpu ) |
807 |
self.setParam( r, 'setCPUs', cores=cores ) |
808 |
return r
|
809 |
|
810 |
inited = False
|
811 |
|
812 |
@classmethod
|
813 |
def init( cls ): |
814 |
"Initialization for CPULimitedHost class"
|
815 |
mountCgroups() |
816 |
cls.inited = True
|
817 |
|
818 |
|
819 |
# Some important things to note:
|
820 |
#
|
821 |
# The "IP" address which setIP() assigns to the switch is not
|
822 |
# an "IP address for the switch" in the sense of IP routing.
|
823 |
# Rather, it is the IP address for the control interface,
|
824 |
# on the control network, and it is only relevant to the
|
825 |
# controller. If you are running in the root namespace
|
826 |
# (which is the only way to run OVS at the moment), the
|
827 |
# control interface is the loopback interface, and you
|
828 |
# normally never want to change its IP address!
|
829 |
#
|
830 |
# In general, you NEVER want to attempt to use Linux's
|
831 |
# network stack (i.e. ifconfig) to "assign" an IP address or
|
832 |
# MAC address to a switch data port. Instead, you "assign"
|
833 |
# the IP and MAC addresses in the controller by specifying
|
834 |
# packets that you want to receive or send. The "MAC" address
|
835 |
# reported by ifconfig for a switch data port is essentially
|
836 |
# meaningless. It is important to understand this if you
|
837 |
# want to create a functional router using OpenFlow.
|
838 |
|
839 |
class Switch( Node ): |
840 |
"""A Switch is a Node that is running (or has execed?)
|
841 |
an OpenFlow switch."""
|
842 |
|
843 |
portBase = 1 # Switches start with port 1 in OpenFlow |
844 |
dpidLen = 16 # digits in dpid passed to switch |
845 |
|
846 |
def __init__( self, name, dpid=None, opts='', listenPort=None, **params): |
847 |
"""dpid: dpid hex string (or None to derive from name, e.g. s1 -> 1)
|
848 |
opts: additional switch options
|
849 |
listenPort: port to listen on for dpctl connections"""
|
850 |
Node.__init__( self, name, **params )
|
851 |
self.dpid = self.defaultDpid( dpid ) |
852 |
self.opts = opts
|
853 |
self.listenPort = listenPort
|
854 |
if not self.inNamespace: |
855 |
self.controlIntf = Intf( 'lo', self, port=0 ) |
856 |
|
857 |
def defaultDpid( self, dpid=None ): |
858 |
"Return correctly formatted dpid from dpid or switch name (s1 -> 1)"
|
859 |
if dpid:
|
860 |
# Remove any colons and make sure it's a good hex number
|
861 |
dpid = dpid.translate( None, ':' ) |
862 |
assert len( dpid ) <= self.dpidLen and int( dpid, 16 ) >= 0 |
863 |
else:
|
864 |
# Use hex of the first number in the switch name
|
865 |
nums = re.findall( r'\d+', self.name ) |
866 |
if nums:
|
867 |
dpid = hex( int( nums[ 0 ] ) )[ 2: ] |
868 |
else:
|
869 |
raise Exception( 'Unable to derive default datapath ID - ' |
870 |
'please either specify a dpid or use a '
|
871 |
'canonical switch name such as s23.' )
|
872 |
return '0' * ( self.dpidLen - len( dpid ) ) + dpid |
873 |
|
874 |
def defaultIntf( self ): |
875 |
"Return control interface"
|
876 |
if self.controlIntf: |
877 |
return self.controlIntf |
878 |
else:
|
879 |
return Node.defaultIntf( self ) |
880 |
|
881 |
def sendCmd( self, *cmd, **kwargs ): |
882 |
"""Send command to Node.
|
883 |
cmd: string"""
|
884 |
kwargs.setdefault( 'printPid', False ) |
885 |
if not self.execed: |
886 |
return Node.sendCmd( self, *cmd, **kwargs ) |
887 |
else:
|
888 |
error( '*** Error: %s has execed and cannot accept commands' %
|
889 |
self.name )
|
890 |
|
891 |
def connected( self ): |
892 |
"Is the switch connected to a controller? (override this method)"
|
893 |
# Assume that we are connected by default to whatever we need to
|
894 |
# be connected to. This should be overridden by any OpenFlow
|
895 |
# switch, but not by a standalone bridge.
|
896 |
debug( 'Assuming', repr( self ), 'is connected to a controller\n' ) |
897 |
return True |
898 |
|
899 |
def __repr__( self ): |
900 |
"More informative string representation"
|
901 |
intfs = ( ','.join( [ '%s:%s' % ( i.name, i.IP() ) |
902 |
for i in self.intfList() ] ) ) |
903 |
return '<%s %s: %s pid=%s> ' % ( |
904 |
self.__class__.__name__, self.name, intfs, self.pid ) |
905 |
|
906 |
|
907 |
class UserSwitch( Switch ): |
908 |
"User-space switch."
|
909 |
|
910 |
dpidLen = 12
|
911 |
|
912 |
def __init__( self, name, dpopts='--no-slicing', **kwargs ): |
913 |
"""Init.
|
914 |
name: name for the switch
|
915 |
dpopts: additional arguments to ofdatapath (--no-slicing)"""
|
916 |
Switch.__init__( self, name, **kwargs )
|
917 |
pathCheck( 'ofdatapath', 'ofprotocol', |
918 |
moduleName='the OpenFlow reference user switch' +
|
919 |
'(openflow.org)' )
|
920 |
if self.listenPort: |
921 |
self.opts += ' --listen=ptcp:%i ' % self.listenPort |
922 |
else:
|
923 |
self.opts += ' --listen=punix:/tmp/%s.listen' % self.name |
924 |
self.dpopts = dpopts
|
925 |
|
926 |
@classmethod
|
927 |
def setup( cls ): |
928 |
"Ensure any dependencies are loaded; if not, try to load them."
|
929 |
if not os.path.exists( '/dev/net/tun' ): |
930 |
moduleDeps( add=TUN ) |
931 |
|
932 |
def dpctl( self, *args ): |
933 |
"Run dpctl command"
|
934 |
listenAddr = None
|
935 |
if not self.listenPort: |
936 |
listenAddr = 'unix:/tmp/%s.listen' % self.name |
937 |
else:
|
938 |
listenAddr = 'tcp:127.0.0.1:%i' % self.listenPort |
939 |
return self.cmd( 'dpctl ' + ' '.join( args ) + |
940 |
' ' + listenAddr )
|
941 |
|
942 |
def connected( self ): |
943 |
"Is the switch connected to a controller?"
|
944 |
status = self.dpctl( 'status' ) |
945 |
return ( 'remote.is-connected=true' in status and |
946 |
'local.is-connected=true' in status ) |
947 |
|
948 |
@staticmethod
|
949 |
def TCReapply( intf ): |
950 |
"""Unfortunately user switch and Mininet are fighting
|
951 |
over tc queuing disciplines. To resolve the conflict,
|
952 |
we re-create the user switch's configuration, but as a
|
953 |
leaf of the TCIntf-created configuration."""
|
954 |
if isinstance( intf, TCIntf ): |
955 |
ifspeed = 10000000000 # 10 Gbps |
956 |
minspeed = ifspeed * 0.001
|
957 |
|
958 |
res = intf.config( **intf.params ) |
959 |
|
960 |
if res is None: # link may not have TC parameters |
961 |
return
|
962 |
|
963 |
# Re-add qdisc, root, and default classes user switch created, but
|
964 |
# with new parent, as setup by Mininet's TCIntf
|
965 |
parent = res['parent']
|
966 |
intf.tc( "%s qdisc add dev %s " + parent +
|
967 |
" handle 1: htb default 0xfffe" )
|
968 |
intf.tc( "%s class add dev %s classid 1:0xffff parent 1: htb rate "
|
969 |
+ str(ifspeed) )
|
970 |
intf.tc( "%s class add dev %s classid 1:0xfffe parent 1:0xffff " +
|
971 |
"htb rate " + str(minspeed) + " ceil " + str(ifspeed) ) |
972 |
|
973 |
def start( self, controllers ): |
974 |
"""Start OpenFlow reference user datapath.
|
975 |
Log to /tmp/sN-{ofd,ofp}.log.
|
976 |
controllers: list of controller objects"""
|
977 |
# Add controllers
|
978 |
clist = ','.join( [ 'tcp:%s:%d' % ( c.IP(), c.port ) |
979 |
for c in controllers ] ) |
980 |
ofdlog = '/tmp/' + self.name + '-ofd.log' |
981 |
ofplog = '/tmp/' + self.name + '-ofp.log' |
982 |
intfs = [ str( i ) for i in self.intfList() if not i.IP() ] |
983 |
self.cmd( 'ofdatapath -i ' + ','.join( intfs ) + |
984 |
' punix:/tmp/' + self.name + ' -d %s ' % self.dpid + |
985 |
self.dpopts +
|
986 |
' 1> ' + ofdlog + ' 2> ' + ofdlog + ' &' ) |
987 |
self.cmd( 'ofprotocol unix:/tmp/' + self.name + |
988 |
' ' + clist +
|
989 |
' --fail=closed ' + self.opts + |
990 |
' 1> ' + ofplog + ' 2>' + ofplog + ' &' ) |
991 |
if "no-slicing" not in self.dpopts: |
992 |
# Only TCReapply if slicing is enable
|
993 |
sleep(1) # Allow ofdatapath to start before re-arranging qdisc's |
994 |
for intf in self.intfList(): |
995 |
if not intf.IP(): |
996 |
self.TCReapply( intf )
|
997 |
|
998 |
def stop( self, deleteIntfs=True ): |
999 |
"""Stop OpenFlow reference user datapath.
|
1000 |
deleteIntfs: delete interfaces? (True)"""
|
1001 |
self.cmd( 'kill %ofdatapath' ) |
1002 |
self.cmd( 'kill %ofprotocol' ) |
1003 |
super( UserSwitch, self ).stop( deleteIntfs ) |
1004 |
|
1005 |
class OVSLegacyKernelSwitch( Switch ): |
1006 |
"""Open VSwitch legacy kernel-space switch using ovs-openflowd.
|
1007 |
Currently only works in the root namespace."""
|
1008 |
|
1009 |
def __init__( self, name, dp=None, **kwargs ): |
1010 |
"""Init.
|
1011 |
name: name for switch
|
1012 |
dp: netlink id (0, 1, 2, ...)
|
1013 |
defaultMAC: default MAC as unsigned int; random value if None"""
|
1014 |
Switch.__init__( self, name, **kwargs )
|
1015 |
self.dp = dp if dp else self.name |
1016 |
self.intf = self.dp |
1017 |
if self.inNamespace: |
1018 |
error( "OVSKernelSwitch currently only works"
|
1019 |
" in the root namespace.\n" )
|
1020 |
exit( 1 ) |
1021 |
|
1022 |
@classmethod
|
1023 |
def setup( cls ): |
1024 |
"Ensure any dependencies are loaded; if not, try to load them."
|
1025 |
pathCheck( 'ovs-dpctl', 'ovs-openflowd', |
1026 |
moduleName='Open vSwitch (openvswitch.org)')
|
1027 |
moduleDeps( subtract=OF_KMOD, add=OVS_KMOD ) |
1028 |
|
1029 |
def start( self, controllers ): |
1030 |
"Start up kernel datapath."
|
1031 |
ofplog = '/tmp/' + self.name + '-ofp.log' |
1032 |
# Delete local datapath if it exists;
|
1033 |
# then create a new one monitoring the given interfaces
|
1034 |
self.cmd( 'ovs-dpctl del-dp ' + self.dp ) |
1035 |
self.cmd( 'ovs-dpctl add-dp ' + self.dp ) |
1036 |
intfs = [ str( i ) for i in self.intfList() if not i.IP() ] |
1037 |
self.cmd( 'ovs-dpctl', 'add-if', self.dp, ' '.join( intfs ) ) |
1038 |
# Run protocol daemon
|
1039 |
clist = ','.join( [ 'tcp:%s:%d' % ( c.IP(), c.port ) |
1040 |
for c in controllers ] ) |
1041 |
self.cmd( 'ovs-openflowd ' + self.dp + |
1042 |
' ' + clist +
|
1043 |
' --fail=secure ' + self.opts + |
1044 |
' --datapath-id=' + self.dpid + |
1045 |
' 1>' + ofplog + ' 2>' + ofplog + '&' ) |
1046 |
self.execed = False |
1047 |
|
1048 |
def stop( self, deleteIntfs=True ): |
1049 |
"""Terminate kernel datapath."
|
1050 |
deleteIntfs: delete interfaces? (True)"""
|
1051 |
quietRun( 'ovs-dpctl del-dp ' + self.dp ) |
1052 |
self.cmd( 'kill %ovs-openflowd' ) |
1053 |
super( OVSLegacyKernelSwitch, self ).stop( deleteIntfs ) |
1054 |
|
1055 |
|
1056 |
class OVSSwitch( Switch ): |
1057 |
"Open vSwitch switch. Depends on ovs-vsctl."
|
1058 |
|
1059 |
def __init__( self, name, failMode='secure', datapath='kernel', |
1060 |
inband=False, protocols=None, |
1061 |
reconnectms=1000, stp=False, **params ): |
1062 |
"""name: name for switch
|
1063 |
failMode: controller loss behavior (secure|open)
|
1064 |
datapath: userspace or kernel mode (kernel|user)
|
1065 |
inband: use in-band control (False)
|
1066 |
protocols: use specific OpenFlow version(s) (e.g. OpenFlow13)
|
1067 |
Unspecified (or old OVS version) uses OVS default
|
1068 |
reconnectms: max reconnect timeout in ms (0/None for default)
|
1069 |
stp: enable STP (False, requires failMode=standalone)"""
|
1070 |
Switch.__init__( self, name, **params )
|
1071 |
self.failMode = failMode
|
1072 |
self.datapath = datapath
|
1073 |
self.inband = inband
|
1074 |
self.protocols = protocols
|
1075 |
self.reconnectms = reconnectms
|
1076 |
self.stp = stp
|
1077 |
self._uuids = [] # controller UUIDs |
1078 |
|
1079 |
@classmethod
|
1080 |
def setup( cls ): |
1081 |
"Make sure Open vSwitch is installed and working"
|
1082 |
pathCheck( 'ovs-vsctl',
|
1083 |
moduleName='Open vSwitch (openvswitch.org)')
|
1084 |
# This should no longer be needed, and it breaks
|
1085 |
# with OVS 1.7 which has renamed the kernel module:
|
1086 |
# moduleDeps( subtract=OF_KMOD, add=OVS_KMOD )
|
1087 |
out, err, exitcode = errRun( 'ovs-vsctl -t 1 show' )
|
1088 |
if exitcode:
|
1089 |
error( out + err + |
1090 |
'ovs-vsctl exited with code %d\n' % exitcode +
|
1091 |
'*** Error connecting to ovs-db with ovs-vsctl\n'
|
1092 |
'Make sure that Open vSwitch is installed, '
|
1093 |
'that ovsdb-server is running, and that\n'
|
1094 |
'"ovs-vsctl show" works correctly.\n'
|
1095 |
'You may wish to try '
|
1096 |
'"service openvswitch-switch start".\n' )
|
1097 |
exit( 1 ) |
1098 |
version = quietRun( 'ovs-vsctl --version' )
|
1099 |
cls.OVSVersion = findall( r'\d+\.\d+', version )[ 0 ] |
1100 |
|
1101 |
@classmethod
|
1102 |
def isOldOVS( cls ): |
1103 |
"Is OVS ersion < 1.10?"
|
1104 |
return ( StrictVersion( cls.OVSVersion ) <
|
1105 |
StrictVersion( '1.10' ) )
|
1106 |
|
1107 |
@classmethod
|
1108 |
def batchShutdown( cls, switches ): |
1109 |
"Shut down a list of OVS switches"
|
1110 |
delcmd = 'del-br %s'
|
1111 |
if not cls.isOldOVS(): |
1112 |
delcmd = '--if-exists ' + delcmd
|
1113 |
# First, delete them all from ovsdb
|
1114 |
quietRun( 'ovs-vsctl ' +
|
1115 |
' -- '.join( delcmd % s for s in switches ) ) |
1116 |
# Next, shut down all of the processes
|
1117 |
pids = ' '.join( str( switch.pid ) for switch in switches ) |
1118 |
quietRun( 'kill -HUP ' + pids )
|
1119 |
for switch in switches: |
1120 |
switch.shell = None
|
1121 |
return True |
1122 |
|
1123 |
def dpctl( self, *args ): |
1124 |
"Run ovs-ofctl command"
|
1125 |
return self.cmd( 'ovs-ofctl', args[ 0 ], self, *args[ 1: ] ) |
1126 |
|
1127 |
def vsctl( self, *args, **kwargs ): |
1128 |
"Run ovs-vsctl command"
|
1129 |
return self.cmd( 'ovs-vsctl', *args, **kwargs ) |
1130 |
|
1131 |
@staticmethod
|
1132 |
def TCReapply( intf ): |
1133 |
"""Unfortunately OVS and Mininet are fighting
|
1134 |
over tc queuing disciplines. As a quick hack/
|
1135 |
workaround, we clear OVS's and reapply our own."""
|
1136 |
if isinstance( intf, TCIntf ): |
1137 |
intf.config( **intf.params ) |
1138 |
|
1139 |
def attach( self, intf ): |
1140 |
"Connect a data port"
|
1141 |
self.vsctl( 'add-port', self, intf ) |
1142 |
self.cmd( 'ifconfig', intf, 'up' ) |
1143 |
self.TCReapply( intf )
|
1144 |
|
1145 |
def detach( self, intf ): |
1146 |
"Disconnect a data port"
|
1147 |
self.vsctl( 'del-port', self, intf ) |
1148 |
|
1149 |
def controllerUUIDs( self, update=False ): |
1150 |
"""Return ovsdb UUIDs for our controllers
|
1151 |
update: update cached value"""
|
1152 |
if not self._uuids or update: |
1153 |
controllers = self.cmd( 'ovs-vsctl -- get Bridge', self, |
1154 |
'Controller' ).strip()
|
1155 |
if controllers.startswith( '[' ) and controllers.endswith( ']' ): |
1156 |
controllers = controllers[ 1 : -1 ] |
1157 |
if controllers:
|
1158 |
self._uuids = [ c.strip()
|
1159 |
for c in controllers.split( ',' ) ] |
1160 |
return self._uuids |
1161 |
|
1162 |
def connected( self ): |
1163 |
"Are we connected to at least one of our controllers?"
|
1164 |
for uuid in self.controllerUUIDs(): |
1165 |
if 'true' in self.vsctl( '-- get Controller', |
1166 |
uuid, 'is_connected' ):
|
1167 |
return True |
1168 |
return self.failMode == 'standalone' |
1169 |
|
1170 |
def intfOpts( self, intf ): |
1171 |
"Return OVS interface options for intf"
|
1172 |
opts = ''
|
1173 |
if not self.isOldOVS(): |
1174 |
# ofport_request is not supported on old OVS
|
1175 |
opts += ' ofport_request=%s' % self.ports[ intf ] |
1176 |
# Patch ports don't work well with old OVS
|
1177 |
if isinstance( intf, OVSIntf ): |
1178 |
intf1, intf2 = intf.link.intf1, intf.link.intf2 |
1179 |
peer = intf1 if intf1 != intf else intf2 |
1180 |
opts += ' type=patch options:peer=%s' % peer
|
1181 |
return '' if not opts else ' -- set Interface %s' % intf + opts |
1182 |
|
1183 |
def bridgeOpts( self ): |
1184 |
"Return OVS bridge options"
|
1185 |
opts = ( ' other_config:datapath-id=%s' % self.dpid + |
1186 |
' fail_mode=%s' % self.failMode ) |
1187 |
if not self.inband: |
1188 |
opts += ' other-config:disable-in-band=true'
|
1189 |
if self.datapath == 'user': |
1190 |
opts += ' datapath_type=netdev' % self |
1191 |
if self.protocols and not self.isOldOVS(): |
1192 |
opts += ' protocols=%s' % ( self, self.protocols ) |
1193 |
if self.stp and self.failMode == 'standalone': |
1194 |
opts += ' stp_enable=true' % self |
1195 |
return opts
|
1196 |
|
1197 |
def start( self, controllers ): |
1198 |
"Start up a new OVS OpenFlow switch using ovs-vsctl"
|
1199 |
if self.inNamespace: |
1200 |
raise Exception( |
1201 |
'OVS kernel switch does not work in a namespace' )
|
1202 |
int( self.dpid, 16 ) # DPID must be a hex string |
1203 |
# Command to add interfaces
|
1204 |
intfs = ''.join( ' -- add-port %s %s' % ( self, intf ) + |
1205 |
self.intfOpts( intf )
|
1206 |
for intf in self.intfList() |
1207 |
if self.ports[ intf ] and not intf.IP() ) |
1208 |
# Command to create controller entries
|
1209 |
clist = [ ( self.name + c.name, '%s:%s:%d' % |
1210 |
( c.protocol, c.IP(), c.port ) ) |
1211 |
for c in controllers ] |
1212 |
if self.listenPort: |
1213 |
clist.append( ( self.name + '-listen', |
1214 |
'ptcp:%s' % self.listenPort ) ) |
1215 |
ccmd = '-- --id=@%s create Controller target=\\"%s\\"'
|
1216 |
if self.reconnectms: |
1217 |
ccmd += ' max_backoff=%d' % self.reconnectms |
1218 |
cargs = ' '.join( ccmd % ( name, target )
|
1219 |
for name, target in clist ) |
1220 |
# Controller ID list
|
1221 |
cids = ','.join( '@%s' % name for name, _target in clist ) |
1222 |
# Try to delete any existing bridges with the same name
|
1223 |
if not self.isOldOVS(): |
1224 |
cargs += ' -- --if-exists del-br %s' % self |
1225 |
# One ovs-vsctl command to rule them all!
|
1226 |
self.vsctl( cargs +
|
1227 |
' -- add-br %s' % self + |
1228 |
' -- set bridge %s controller=[%s]' % ( self, cids ) + |
1229 |
self.bridgeOpts() +
|
1230 |
intfs ) |
1231 |
# XXX BROKEN - need to fix this!!
|
1232 |
# If necessary, restore TC config overwritten by OVS
|
1233 |
# for intf in self.intfList():
|
1234 |
# self.TCReapply( intf )
|
1235 |
|
1236 |
def stop( self, deleteIntfs=True ): |
1237 |
"""Terminate OVS switch.
|
1238 |
deleteIntfs: delete interfaces? (True)"""
|
1239 |
self.cmd( 'ovs-vsctl del-br', self ) |
1240 |
if self.datapath == 'user': |
1241 |
self.cmd( 'ip link del', self ) |
1242 |
super( OVSSwitch, self ).stop( deleteIntfs ) |
1243 |
|
1244 |
|
1245 |
OVSKernelSwitch = OVSSwitch |
1246 |
|
1247 |
|
1248 |
class OVSBridge( OVSSwitch ): |
1249 |
"OVSBridge is an OVSSwitch in standalone/bridge mode"
|
1250 |
|
1251 |
def __init__( self, args, **kwargs ): |
1252 |
kwargs.update( failMode='standalone' )
|
1253 |
OVSSwitch.__init__( self, args, **kwargs )
|
1254 |
|
1255 |
def start( self, controllers ): |
1256 |
OVSSwitch.start( self, controllers=[] )
|
1257 |
|
1258 |
def connected( self ): |
1259 |
"Are we forwarding yet?"
|
1260 |
if self.stp: |
1261 |
status = self.dpctl( 'show' ) |
1262 |
return 'STP_FORWARD' in status and not 'STP_LEARN' in status |
1263 |
else:
|
1264 |
return True |
1265 |
|
1266 |
|
1267 |
class OVSBatch( OVSSwitch ): |
1268 |
"Experiment: batch startup of OVS switches"
|
1269 |
|
1270 |
# This should be ~ int( quietRun( 'getconf ARG_MAX' ) ),
|
1271 |
# but the real limit seems to be much lower
|
1272 |
argmax = 128000
|
1273 |
|
1274 |
def __init__( self, *args, **kwargs ): |
1275 |
self.commands = []
|
1276 |
self.started = False |
1277 |
super( OVSBatch, self ).__init__( *args, **kwargs ) |
1278 |
|
1279 |
@classmethod
|
1280 |
def batchStartup( cls, switches ): |
1281 |
"Batch startup for OVS"
|
1282 |
info( '...' )
|
1283 |
cmds = 'ovs-vsctl'
|
1284 |
for switch in switches: |
1285 |
if cls.isOldOVS():
|
1286 |
quietRun( 'ovs-vsctl del-br %s' % switch )
|
1287 |
for cmd in switch.commands: |
1288 |
cmd = cmd.strip() |
1289 |
# Don't exceed ARG_MAX
|
1290 |
if len( cmds ) + len( cmd ) >= cls.argmax: |
1291 |
errRun( cmds, shell=True )
|
1292 |
cmds = 'ovs-vsctl'
|
1293 |
cmds += ' ' + cmd
|
1294 |
switch.started = True
|
1295 |
if cmds:
|
1296 |
errRun( cmds, shell=True )
|
1297 |
return True |
1298 |
|
1299 |
def vsctl( self, *args, **kwargs ): |
1300 |
"Append ovs-vsctl command to list for later execution"
|
1301 |
if self.started: |
1302 |
return super( OVSBatch, self).vsctl( *args, **kwargs ) |
1303 |
cmd = ' '.join( str( arg ).strip() for arg in args ) |
1304 |
self.commands.append( cmd )
|
1305 |
|
1306 |
def start( self, *args, **kwargs ): |
1307 |
super( OVSBatch, self ).start( *args, **kwargs ) |
1308 |
self.started = True |
1309 |
|
1310 |
def stop( self, *args, **kwargs ): |
1311 |
super( OVSBatch, self ).stop( *args, **kwargs ) |
1312 |
self.started = False |
1313 |
|
1314 |
def cleanup( self): |
1315 |
"Don't bother to clean up"
|
1316 |
return
|
1317 |
|
1318 |
|
1319 |
class IVSSwitch( Switch ): |
1320 |
"Indigo Virtual Switch"
|
1321 |
|
1322 |
def __init__( self, name, verbose=False, **kwargs ): |
1323 |
Switch.__init__( self, name, **kwargs )
|
1324 |
self.verbose = verbose
|
1325 |
|
1326 |
@classmethod
|
1327 |
def setup( cls ): |
1328 |
"Make sure IVS is installed"
|
1329 |
pathCheck( 'ivs-ctl', 'ivs', |
1330 |
moduleName="Indigo Virtual Switch (projectfloodlight.org)" )
|
1331 |
out, err, exitcode = errRun( 'ivs-ctl show' )
|
1332 |
if exitcode:
|
1333 |
error( out + err + |
1334 |
'ivs-ctl exited with code %d\n' % exitcode +
|
1335 |
'*** The openvswitch kernel module might '
|
1336 |
'not be loaded. Try modprobe openvswitch.\n' )
|
1337 |
exit( 1 ) |
1338 |
|
1339 |
@classmethod
|
1340 |
def batchShutdown( cls, switches ): |
1341 |
"Kill each IVS switch, to be waited on later in stop()"
|
1342 |
for switch in switches: |
1343 |
switch.cmd( 'kill %ivs' )
|
1344 |
|
1345 |
def start( self, controllers ): |
1346 |
"Start up a new IVS switch"
|
1347 |
args = ['ivs']
|
1348 |
args.extend( ['--name', self.name] ) |
1349 |
args.extend( ['--dpid', self.dpid] ) |
1350 |
if self.verbose: |
1351 |
args.extend( ['--verbose'] )
|
1352 |
for intf in self.intfs.values(): |
1353 |
if not intf.IP(): |
1354 |
args.extend( ['-i', intf.name] )
|
1355 |
for c in controllers: |
1356 |
args.extend( ['-c', '%s:%d' % (c.IP(), c.port)] ) |
1357 |
if self.listenPort: |
1358 |
args.extend( ['--listen', '127.0.0.1:%i' % self.listenPort] ) |
1359 |
args.append( self.opts )
|
1360 |
|
1361 |
logfile = '/tmp/ivs.%s.log' % self.name |
1362 |
|
1363 |
self.cmd( ' '.join(args) + ' >' + logfile + ' 2>&1 </dev/null &' ) |
1364 |
|
1365 |
def stop( self, deleteIntfs=True ): |
1366 |
"""Terminate IVS switch.
|
1367 |
deleteIntfs: delete interfaces? (True)"""
|
1368 |
self.cmd( 'kill %ivs' ) |
1369 |
self.cmd( 'wait' ) |
1370 |
super( IVSSwitch, self ).stop( deleteIntfs ) |
1371 |
|
1372 |
def attach( self, intf ): |
1373 |
"Connect a data port"
|
1374 |
self.cmd( 'ivs-ctl', 'add-port', '--datapath', self.name, intf ) |
1375 |
|
1376 |
def detach( self, intf ): |
1377 |
"Disconnect a data port"
|
1378 |
self.cmd( 'ivs-ctl', 'del-port', '--datapath', self.name, intf ) |
1379 |
|
1380 |
def dpctl( self, *args ): |
1381 |
"Run dpctl command"
|
1382 |
if not self.listenPort: |
1383 |
return "can't run dpctl without passive listening port" |
1384 |
return self.cmd( 'ovs-ofctl ' + ' '.join( args ) + |
1385 |
' tcp:127.0.0.1:%i' % self.listenPort ) |
1386 |
|
1387 |
|
1388 |
class Controller( Node ): |
1389 |
"""A Controller is a Node that is running (or has execed?) an
|
1390 |
OpenFlow controller."""
|
1391 |
|
1392 |
def __init__( self, name, inNamespace=False, command='controller', |
1393 |
cargs='-v ptcp:%d', cdir=None, ip="127.0.0.1", |
1394 |
port=6633, protocol='tcp', **params ): |
1395 |
self.command = command
|
1396 |
self.cargs = cargs
|
1397 |
self.cdir = cdir
|
1398 |
self.ip = ip
|
1399 |
self.port = port
|
1400 |
self.protocol = protocol
|
1401 |
Node.__init__( self, name, inNamespace=inNamespace,
|
1402 |
ip=ip, **params ) |
1403 |
self.checkListening()
|
1404 |
|
1405 |
def checkListening( self ): |
1406 |
"Make sure no controllers are running on our port"
|
1407 |
# Verify that Telnet is installed first:
|
1408 |
out, _err, returnCode = errRun( "which telnet" )
|
1409 |
if 'telnet' not in out or returnCode != 0: |
1410 |
raise Exception( "Error running telnet to check for listening " |
1411 |
"controllers; please check that it is "
|
1412 |
"installed." )
|
1413 |
listening = self.cmd( "echo A | telnet -e A %s %d" % |
1414 |
( self.ip, self.port ) ) |
1415 |
if 'Connected' in listening: |
1416 |
servers = self.cmd( 'netstat -natp' ).split( '\n' ) |
1417 |
pstr = ':%d ' % self.port |
1418 |
clist = servers[ 0:1 ] + [ s for s in servers if pstr in s ] |
1419 |
raise Exception( "Please shut down the controller which is" |
1420 |
" running on port %d:\n" % self.port + |
1421 |
'\n'.join( clist ) )
|
1422 |
|
1423 |
def start( self ): |
1424 |
"""Start <controller> <args> on controller.
|
1425 |
Log to /tmp/cN.log"""
|
1426 |
pathCheck( self.command )
|
1427 |
cout = '/tmp/' + self.name + '.log' |
1428 |
if self.cdir is not None: |
1429 |
self.cmd( 'cd ' + self.cdir ) |
1430 |
self.cmd( self.command + ' ' + self.cargs % self.port + |
1431 |
' 1>' + cout + ' 2>' + cout + ' &' ) |
1432 |
self.execed = False |
1433 |
|
1434 |
def stop( self, *args, **kwargs ): |
1435 |
"Stop controller."
|
1436 |
self.cmd( 'kill %' + self.command ) |
1437 |
self.cmd( 'wait %' + self.command ) |
1438 |
kwargs.update( deleteIntfs=False )
|
1439 |
super( Controller, self ).stop( *args, **kwargs ) |
1440 |
|
1441 |
def IP( self, intf=None ): |
1442 |
"Return IP address of the Controller"
|
1443 |
if self.intfs: |
1444 |
ip = Node.IP( self, intf )
|
1445 |
else:
|
1446 |
ip = self.ip
|
1447 |
return ip
|
1448 |
|
1449 |
def __repr__( self ): |
1450 |
"More informative string representation"
|
1451 |
return '<%s %s: %s:%s pid=%s> ' % ( |
1452 |
self.__class__.__name__, self.name, |
1453 |
self.IP(), self.port, self.pid ) |
1454 |
|
1455 |
@classmethod
|
1456 |
def isAvailable( cls ): |
1457 |
"Is controller available?"
|
1458 |
return quietRun( 'which controller' ) |
1459 |
|
1460 |
|
1461 |
class OVSController( Controller ): |
1462 |
"Open vSwitch controller"
|
1463 |
def __init__( self, name, command='ovs-controller', **kwargs ): |
1464 |
if quietRun( 'which test-controller' ): |
1465 |
command = 'test-controller'
|
1466 |
Controller.__init__( self, name, command=command, **kwargs )
|
1467 |
|
1468 |
@classmethod
|
1469 |
def isAvailable( cls ): |
1470 |
return ( quietRun( 'which ovs-controller' ) or |
1471 |
quietRun( 'which test-controller' ) )
|
1472 |
|
1473 |
class NOX( Controller ): |
1474 |
"Controller to run a NOX application."
|
1475 |
|
1476 |
def __init__( self, name, *noxArgs, **kwargs ): |
1477 |
"""Init.
|
1478 |
name: name to give controller
|
1479 |
noxArgs: arguments (strings) to pass to NOX"""
|
1480 |
if not noxArgs: |
1481 |
warn( 'warning: no NOX modules specified; '
|
1482 |
'running packetdump only\n' )
|
1483 |
noxArgs = [ 'packetdump' ]
|
1484 |
elif type( noxArgs ) not in ( list, tuple ): |
1485 |
noxArgs = [ noxArgs ] |
1486 |
|
1487 |
if 'NOX_CORE_DIR' not in os.environ: |
1488 |
exit( 'exiting; please set missing NOX_CORE_DIR env var' ) |
1489 |
noxCoreDir = os.environ[ 'NOX_CORE_DIR' ]
|
1490 |
|
1491 |
Controller.__init__( self, name,
|
1492 |
command=noxCoreDir + '/nox_core',
|
1493 |
cargs='--libdir=/usr/local/lib -v -i ptcp:%s ' +
|
1494 |
' '.join( noxArgs ),
|
1495 |
cdir=noxCoreDir, |
1496 |
**kwargs ) |
1497 |
|
1498 |
class RYU( Controller ): |
1499 |
"Controller to run Ryu application"
|
1500 |
def __init__( self, name, *ryuArgs, **kwargs ): |
1501 |
"""Init.
|
1502 |
name: name to give controller.
|
1503 |
ryuArgs: arguments and modules to pass to Ryu"""
|
1504 |
homeDir = quietRun( 'printenv HOME' ).strip( '\r\n' ) |
1505 |
ryuCoreDir = '%s/ryu/ryu/app/' % homeDir
|
1506 |
if not ryuArgs: |
1507 |
warn( 'warning: no Ryu modules specified; '
|
1508 |
'running simple_switch only\n' )
|
1509 |
ryuArgs = [ ryuCoreDir + 'simple_switch.py' ]
|
1510 |
elif type( ryuArgs ) not in ( list, tuple ): |
1511 |
ryuArgs = [ ryuArgs ] |
1512 |
|
1513 |
Controller.__init__( self, name,
|
1514 |
command='ryu-manager',
|
1515 |
cargs='--ofp-tcp-listen-port %s ' +
|
1516 |
' '.join( ryuArgs ),
|
1517 |
cdir=ryuCoreDir, |
1518 |
**kwargs ) |
1519 |
|
1520 |
class RemoteController( Controller ): |
1521 |
"Controller running outside of Mininet's control."
|
1522 |
|
1523 |
def __init__( self, name, ip='127.0.0.1', |
1524 |
port=6633, **kwargs):
|
1525 |
"""Init.
|
1526 |
name: name to give controller
|
1527 |
ip: the IP address where the remote controller is
|
1528 |
listening
|
1529 |
port: the port where the remote controller is listening"""
|
1530 |
Controller.__init__( self, name, ip=ip, port=port, **kwargs )
|
1531 |
|
1532 |
def start( self ): |
1533 |
"Overridden to do nothing."
|
1534 |
return
|
1535 |
|
1536 |
def stop( self ): |
1537 |
"Overridden to do nothing."
|
1538 |
return
|
1539 |
|
1540 |
def checkListening( self ): |
1541 |
"Warn if remote controller is not accessible"
|
1542 |
listening = self.cmd( "echo A | telnet -e A %s %d" % |
1543 |
( self.ip, self.port ) ) |
1544 |
if 'Connected' not in listening: |
1545 |
warn( "Unable to contact the remote controller"
|
1546 |
" at %s:%d\n" % ( self.ip, self.port ) ) |
1547 |
|
1548 |
|
1549 |
DefaultControllers = ( Controller, OVSController ) |
1550 |
|
1551 |
def findController( controllers=DefaultControllers ): |
1552 |
"Return first available controller from list, if any"
|
1553 |
for controller in controllers: |
1554 |
if controller.isAvailable():
|
1555 |
return controller
|
1556 |
|
1557 |
def DefaultController( name, controllers=DefaultControllers, **kwargs ): |
1558 |
"Find a controller that is available and instantiate it"
|
1559 |
controller = findController( controllers ) |
1560 |
if not controller: |
1561 |
raise Exception( 'Could not find a default OpenFlow controller' ) |
1562 |
return controller( name, **kwargs )
|