carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 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. |
| 15 | |
| 16 | CPULimitedHost: a virtual host whose CPU bandwidth is limited by |
| 17 | RT or CFS bandwidth limiting. |
| 18 | |
| 19 | Switch: superclass for switch nodes. |
| 20 | |
| 21 | UserSwitch: a switch using the user-space switch from the OpenFlow |
| 22 | reference implementation. |
| 23 | |
| 24 | KernelSwitch: a switch using the kernel switch from the OpenFlow reference |
| 25 | implementation. |
| 26 | |
| 27 | OVSSwitch: a switch using the OpenVSwitch OpenFlow-compatible switch |
| 28 | implementation (openvswitch.org). |
| 29 | |
| 30 | Controller: superclass for OpenFlow controllers. The default controller |
| 31 | is controller(8) from the reference implementation. |
| 32 | |
| 33 | NOXController: a controller node using NOX (noxrepo.org). |
| 34 | |
| 35 | RemoteController: a remote controller node, which may use any |
| 36 | arbitrary OpenFlow-compatible controller, and which is not |
| 37 | created or managed by mininet. |
| 38 | |
| 39 | Future 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 | |
| 47 | import os |
| 48 | import re |
| 49 | import signal |
| 50 | import select |
| 51 | from subprocess import Popen, PIPE, STDOUT |
| 52 | |
| 53 | from mininet.log import info, error, warn, debug |
| 54 | from mininet.util import ( quietRun, errRun, errFail, moveIntf, isShellBuiltin, |
carlosmscabral | e121a7b | 2013-02-18 18:14:53 -0300 | [diff] [blame] | 55 | numCores, retry, mountCgroups, run ) |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 56 | from mininet.moduledeps import moduleDeps, pathCheck, OVS_KMOD, OF_KMOD, TUN |
| 57 | from mininet.link import Link, Intf, TCIntf |
| 58 | |
| 59 | class 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 | |
| 551 | class Host( Node ): |
| 552 | "A host is simply a Node" |
| 553 | pass |
| 554 | |
| 555 | |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 556 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 557 | class 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 ) |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 595 | |
carlosmscabral | e121a7b | 2013-02-18 18:14:53 -0300 | [diff] [blame] | 596 | return int(quietRun( cmd ).split()[ -1 ] ) |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 597 | |
| 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 | |
| 719 | class CCNHost( Host ): |
| 720 | "CCNHost is a Host that always runs the ccnd daemon" |
| 721 | |
| 722 | def __init__( self, name, **kwargs ): |
| 723 | |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 724 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 725 | Host.__init__( self, name, **kwargs ) |
| 726 | if not CCNHost.inited: |
| 727 | CCNHost.init() |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 728 | |
carlosmscabral | b9d85b7 | 2013-07-15 15:24:33 -0300 | [diff] [blame] | 729 | self.cmd("export CCND_DEBUG=6") |
| 730 | self.cmd("export CCND_LOG=./log.{0}".format(self.name)) |
| 731 | # print self.params['cache'] |
| 732 | if self.params['cache'] != None: |
| 733 | self.cmd("export CCND_CAP={0}".format(self.params['cache'])) |
| 734 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 735 | self.cmd("export CCN_LOCAL_SOCKNAME=/tmp/.sock.ccnx.{0}".format(self.name)) |
| 736 | self.cmd("ccndstart") |
| 737 | self.peerList = {} |
| 738 | |
carlosmscabral | b9d85b7 | 2013-07-15 15:24:33 -0300 | [diff] [blame] | 739 | def config( self, fib=None, app=None, cache=None, **params ): |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 740 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 741 | r = Node.config( self, **params ) |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 742 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 743 | self.setParam( r, 'app', fib=fib ) |
| 744 | self.setParam( r, 'fib', app=app) |
carlosmscabral | b9d85b7 | 2013-07-15 15:24:33 -0300 | [diff] [blame] | 745 | self.setParam( r, 'cache', cache=cache ) |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 746 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 747 | return r |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 748 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 749 | def configCCN(self): |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 750 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 751 | self.buildPeerIP() |
| 752 | self.setFIB() |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 753 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 754 | def buildPeerIP(self): |
| 755 | for iface in self.intfList(): |
| 756 | link = iface.link |
| 757 | if link: |
| 758 | node1, node2 = link.intf1.node, link.intf2.node |
| 759 | if node1 == self: |
| 760 | self.peerList[node2.name] = link.intf2.node.IP(link.intf2) |
| 761 | else: |
| 762 | self.peerList[node1.name] = link.intf1.node.IP(link.intf1) |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 763 | |
| 764 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 765 | def setFIB(self): |
| 766 | |
| 767 | for name in self.params['fib']: |
| 768 | if not name: |
| 769 | pass |
| 770 | else: |
| 771 | self.insert_fib(name[0],self.peerList[name[1]]) |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 772 | |
| 773 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 774 | def insert_fib(self, uri, host): |
| 775 | self.cmd('ccndc add {0} tcp {1}'.format(uri,host)) |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 776 | # self.cmd('ccndc add {0} udp {1}'.format(uri,host)) |
| 777 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 778 | def terminate( self ): |
| 779 | "Stop node." |
| 780 | self.cmd('ccndstop') |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 781 | self.cmd('killall -r zebra ospf') |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 782 | Host.terminate(self) |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 783 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 784 | inited = False |
| 785 | |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 786 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 787 | @classmethod |
| 788 | def init( cls ): |
| 789 | "Initialization for CCNHost class" |
| 790 | cls.inited = True |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 791 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 792 | class CPULimitedCCNHost( CPULimitedHost ): |
| 793 | '''CPULimitedCCNHost is a Host that always runs the ccnd daemon and extends CPULimitedHost. |
| 794 | It should be used when one wants to limit the resources of CCN routers and hosts ''' |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 795 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 796 | |
| 797 | def __init__( self, name, sched='cfs', **kwargs ): |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 798 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 799 | CPULimitedHost.__init__( self, name, sched, **kwargs ) |
| 800 | if not CCNHost.inited: |
| 801 | CCNHost.init() |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 802 | |
carlosmscabral | b9d85b7 | 2013-07-15 15:24:33 -0300 | [diff] [blame] | 803 | self.cmd("export CCND_DEBUG=6") |
| 804 | self.cmd("export CCND_LOG=./log.{0}".format(self.name)) |
| 805 | if self.params['cache'] != None: |
| 806 | self.cmd("export CCND_CAP={0}".format(self.params['cache'])) |
| 807 | |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 808 | self.cmd("export CCN_LOCAL_SOCKNAME=/tmp/.sock.ccnx.{0}".format(self.name)) |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 809 | self.cmd("ccndstart") |
| 810 | self.peerList = {} |
| 811 | |
carlosmscabral | b9d85b7 | 2013-07-15 15:24:33 -0300 | [diff] [blame] | 812 | def config( self, fib=None, app=None, cpu=None, cores=None, cache=None, **params): |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 813 | |
| 814 | r = CPULimitedHost.config(self,cpu,cores, **params) |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 815 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 816 | self.setParam( r, 'app', fib=fib ) |
| 817 | self.setParam( r, 'fib', app=app) |
carlosmscabral | b9d85b7 | 2013-07-15 15:24:33 -0300 | [diff] [blame] | 818 | self.setParam( r, 'cache', cache=cache) |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 819 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 820 | return r |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 821 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 822 | def configCCN(self): |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 823 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 824 | self.buildPeerIP() |
| 825 | self.setFIB() |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 826 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 827 | def buildPeerIP(self): |
| 828 | for iface in self.intfList(): |
| 829 | link = iface.link |
| 830 | if link: |
| 831 | node1, node2 = link.intf1.node, link.intf2.node |
| 832 | if node1 == self: |
| 833 | self.peerList[node2.name] = link.intf2.node.IP(link.intf2) |
| 834 | else: |
| 835 | self.peerList[node1.name] = link.intf1.node.IP(link.intf1) |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 836 | |
| 837 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 838 | def setFIB(self): |
| 839 | |
| 840 | for name in self.params['fib']: |
| 841 | if not name: |
| 842 | pass |
| 843 | else: |
| 844 | self.insert_fib(name[0],self.peerList[name[1]]) |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 845 | |
| 846 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 847 | def insert_fib(self, uri, host): |
| 848 | self.cmd('ccndc add {0} tcp {1}'.format(uri,host)) |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 849 | # self.cmd('ccndc add {0} udp {1}'.format(uri,host)) |
| 850 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 851 | def terminate( self ): |
| 852 | "Stop node." |
| 853 | self.cmd('ccndstop') |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 854 | self.cmd('killall -r zebra ospf') |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 855 | Host.terminate(self) |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 856 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 857 | inited = False |
| 858 | |
carlosmscabral | 6d3dd60 | 2013-03-23 11:12:34 -0300 | [diff] [blame] | 859 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 860 | @classmethod |
| 861 | def init( cls ): |
| 862 | "Initialization for CCNHost class" |
| 863 | cls.inited = True |
| 864 | |
| 865 | # Some important things to note: |
| 866 | # |
| 867 | # The "IP" address which setIP() assigns to the switch is not |
| 868 | # an "IP address for the switch" in the sense of IP routing. |
| 869 | # Rather, it is the IP address for the control interface, |
| 870 | # on the control network, and it is only relevant to the |
| 871 | # controller. If you are running in the root namespace |
| 872 | # (which is the only way to run OVS at the moment), the |
| 873 | # control interface is the loopback interface, and you |
| 874 | # normally never want to change its IP address! |
| 875 | # |
| 876 | # In general, you NEVER want to attempt to use Linux's |
| 877 | # network stack (i.e. ifconfig) to "assign" an IP address or |
| 878 | # MAC address to a switch data port. Instead, you "assign" |
| 879 | # the IP and MAC addresses in the controller by specifying |
| 880 | # packets that you want to receive or send. The "MAC" address |
| 881 | # reported by ifconfig for a switch data port is essentially |
| 882 | # meaningless. It is important to understand this if you |
| 883 | # want to create a functional router using OpenFlow. |
| 884 | |
| 885 | class Switch( Node ): |
| 886 | """A Switch is a Node that is running (or has execed?) |
| 887 | an OpenFlow switch.""" |
| 888 | |
| 889 | portBase = 1 # Switches start with port 1 in OpenFlow |
| 890 | dpidLen = 16 # digits in dpid passed to switch |
| 891 | |
| 892 | def __init__( self, name, dpid=None, opts='', listenPort=None, **params): |
| 893 | """dpid: dpid for switch (or None to derive from name, e.g. s1 -> 1) |
| 894 | opts: additional switch options |
| 895 | listenPort: port to listen on for dpctl connections""" |
| 896 | Node.__init__( self, name, **params ) |
| 897 | self.dpid = dpid if dpid else self.defaultDpid() |
| 898 | self.opts = opts |
| 899 | self.listenPort = listenPort |
| 900 | if not self.inNamespace: |
| 901 | self.controlIntf = Intf( 'lo', self, port=0 ) |
| 902 | |
| 903 | def defaultDpid( self ): |
| 904 | "Derive dpid from switch name, s1 -> 1" |
| 905 | try: |
| 906 | dpid = int( re.findall( '\d+', self.name )[ 0 ] ) |
| 907 | dpid = hex( dpid )[ 2: ] |
| 908 | dpid = '0' * ( self.dpidLen - len( dpid ) ) + dpid |
| 909 | return dpid |
| 910 | except IndexError: |
| 911 | raise Exception( 'Unable to derive default datapath ID - ' |
| 912 | 'please either specify a dpid or use a ' |
| 913 | 'canonical switch name such as s23.' ) |
| 914 | |
| 915 | def defaultIntf( self ): |
| 916 | "Return control interface" |
| 917 | if self.controlIntf: |
| 918 | return self.controlIntf |
| 919 | else: |
| 920 | return Node.defaultIntf( self ) |
| 921 | |
| 922 | def sendCmd( self, *cmd, **kwargs ): |
| 923 | """Send command to Node. |
| 924 | cmd: string""" |
| 925 | kwargs.setdefault( 'printPid', False ) |
| 926 | if not self.execed: |
| 927 | return Node.sendCmd( self, *cmd, **kwargs ) |
| 928 | else: |
| 929 | error( '*** Error: %s has execed and cannot accept commands' % |
| 930 | self.name ) |
| 931 | |
| 932 | def __repr__( self ): |
| 933 | "More informative string representation" |
| 934 | intfs = ( ','.join( [ '%s:%s' % ( i.name, i.IP() ) |
| 935 | for i in self.intfList() ] ) ) |
| 936 | return '<%s %s: %s pid=%s> ' % ( |
| 937 | self.__class__.__name__, self.name, intfs, self.pid ) |
| 938 | |
| 939 | class UserSwitch( Switch ): |
| 940 | "User-space switch." |
| 941 | |
| 942 | dpidLen = 12 |
| 943 | |
| 944 | def __init__( self, name, **kwargs ): |
| 945 | """Init. |
| 946 | name: name for the switch""" |
| 947 | Switch.__init__( self, name, **kwargs ) |
| 948 | pathCheck( 'ofdatapath', 'ofprotocol', |
| 949 | moduleName='the OpenFlow reference user switch' + |
| 950 | '(openflow.org)' ) |
| 951 | if self.listenPort: |
| 952 | self.opts += ' --listen=ptcp:%i ' % self.listenPort |
| 953 | |
| 954 | @classmethod |
| 955 | def setup( cls ): |
| 956 | "Ensure any dependencies are loaded; if not, try to load them." |
| 957 | if not os.path.exists( '/dev/net/tun' ): |
| 958 | moduleDeps( add=TUN ) |
| 959 | |
| 960 | def dpctl( self, *args ): |
| 961 | "Run dpctl command" |
| 962 | if not self.listenPort: |
| 963 | return "can't run dpctl without passive listening port" |
| 964 | return self.cmd( 'dpctl ' + ' '.join( args ) + |
| 965 | ' tcp:127.0.0.1:%i' % self.listenPort ) |
| 966 | |
| 967 | def start( self, controllers ): |
| 968 | """Start OpenFlow reference user datapath. |
| 969 | Log to /tmp/sN-{ofd,ofp}.log. |
| 970 | controllers: list of controller objects""" |
| 971 | # Add controllers |
| 972 | clist = ','.join( [ 'tcp:%s:%d' % ( c.IP(), c.port ) |
| 973 | for c in controllers ] ) |
| 974 | ofdlog = '/tmp/' + self.name + '-ofd.log' |
| 975 | ofplog = '/tmp/' + self.name + '-ofp.log' |
| 976 | self.cmd( 'ifconfig lo up' ) |
| 977 | intfs = [ str( i ) for i in self.intfList() if not i.IP() ] |
| 978 | self.cmd( 'ofdatapath -i ' + ','.join( intfs ) + |
| 979 | ' punix:/tmp/' + self.name + ' -d ' + self.dpid + |
| 980 | ' 1> ' + ofdlog + ' 2> ' + ofdlog + ' &' ) |
| 981 | self.cmd( 'ofprotocol unix:/tmp/' + self.name + |
| 982 | ' ' + clist + |
| 983 | ' --fail=closed ' + self.opts + |
| 984 | ' 1> ' + ofplog + ' 2>' + ofplog + ' &' ) |
| 985 | |
| 986 | def stop( self ): |
| 987 | "Stop OpenFlow reference user datapath." |
| 988 | self.cmd( 'kill %ofdatapath' ) |
| 989 | self.cmd( 'kill %ofprotocol' ) |
| 990 | self.deleteIntfs() |
| 991 | |
| 992 | |
| 993 | class OVSLegacyKernelSwitch( Switch ): |
| 994 | """Open VSwitch legacy kernel-space switch using ovs-openflowd. |
| 995 | Currently only works in the root namespace.""" |
| 996 | |
| 997 | def __init__( self, name, dp=None, **kwargs ): |
| 998 | """Init. |
| 999 | name: name for switch |
| 1000 | dp: netlink id (0, 1, 2, ...) |
| 1001 | defaultMAC: default MAC as unsigned int; random value if None""" |
| 1002 | Switch.__init__( self, name, **kwargs ) |
| 1003 | self.dp = dp if dp else self.name |
| 1004 | self.intf = self.dp |
| 1005 | if self.inNamespace: |
| 1006 | error( "OVSKernelSwitch currently only works" |
| 1007 | " in the root namespace.\n" ) |
| 1008 | exit( 1 ) |
| 1009 | |
| 1010 | @classmethod |
| 1011 | def setup( cls ): |
| 1012 | "Ensure any dependencies are loaded; if not, try to load them." |
| 1013 | pathCheck( 'ovs-dpctl', 'ovs-openflowd', |
| 1014 | moduleName='Open vSwitch (openvswitch.org)') |
| 1015 | moduleDeps( subtract=OF_KMOD, add=OVS_KMOD ) |
| 1016 | |
| 1017 | def start( self, controllers ): |
| 1018 | "Start up kernel datapath." |
| 1019 | ofplog = '/tmp/' + self.name + '-ofp.log' |
| 1020 | quietRun( 'ifconfig lo up' ) |
| 1021 | # Delete local datapath if it exists; |
| 1022 | # then create a new one monitoring the given interfaces |
| 1023 | self.cmd( 'ovs-dpctl del-dp ' + self.dp ) |
| 1024 | self.cmd( 'ovs-dpctl add-dp ' + self.dp ) |
| 1025 | intfs = [ str( i ) for i in self.intfList() if not i.IP() ] |
| 1026 | self.cmd( 'ovs-dpctl', 'add-if', self.dp, ' '.join( intfs ) ) |
| 1027 | # Run protocol daemon |
| 1028 | clist = ','.join( [ 'tcp:%s:%d' % ( c.IP(), c.port ) |
| 1029 | for c in controllers ] ) |
| 1030 | self.cmd( 'ovs-openflowd ' + self.dp + |
| 1031 | ' ' + clist + |
| 1032 | ' --fail=secure ' + self.opts + |
| 1033 | ' --datapath-id=' + self.dpid + |
| 1034 | ' 1>' + ofplog + ' 2>' + ofplog + '&' ) |
| 1035 | self.execed = False |
| 1036 | |
| 1037 | def stop( self ): |
| 1038 | "Terminate kernel datapath." |
| 1039 | quietRun( 'ovs-dpctl del-dp ' + self.dp ) |
| 1040 | self.cmd( 'kill %ovs-openflowd' ) |
| 1041 | self.deleteIntfs() |
| 1042 | |
| 1043 | |
| 1044 | class OVSSwitch( Switch ): |
| 1045 | "Open vSwitch switch. Depends on ovs-vsctl." |
| 1046 | |
| 1047 | def __init__( self, name, failMode='secure', **params ): |
| 1048 | """Init. |
| 1049 | name: name for switch |
| 1050 | failMode: controller loss behavior (secure|open)""" |
| 1051 | Switch.__init__( self, name, **params ) |
| 1052 | self.failMode = failMode |
| 1053 | |
| 1054 | @classmethod |
| 1055 | def setup( cls ): |
| 1056 | "Make sure Open vSwitch is installed and working" |
| 1057 | pathCheck( 'ovs-vsctl', |
| 1058 | moduleName='Open vSwitch (openvswitch.org)') |
| 1059 | # This should no longer be needed, and it breaks |
| 1060 | # with OVS 1.7 which has renamed the kernel module: |
| 1061 | # moduleDeps( subtract=OF_KMOD, add=OVS_KMOD ) |
| 1062 | out, err, exitcode = errRun( 'ovs-vsctl -t 1 show' ) |
| 1063 | if exitcode: |
| 1064 | error( out + err + |
| 1065 | 'ovs-vsctl exited with code %d\n' % exitcode + |
| 1066 | '*** Error connecting to ovs-db with ovs-vsctl\n' |
| 1067 | 'Make sure that Open vSwitch is installed, ' |
| 1068 | 'that ovsdb-server is running, and that\n' |
| 1069 | '"ovs-vsctl show" works correctly.\n' |
| 1070 | 'You may wish to try ' |
| 1071 | '"service openvswitch-switch start".\n' ) |
| 1072 | exit( 1 ) |
| 1073 | |
| 1074 | def dpctl( self, *args ): |
| 1075 | "Run ovs-dpctl command" |
| 1076 | return self.cmd( 'ovs-dpctl', args[ 0 ], self, *args[ 1: ] ) |
| 1077 | |
| 1078 | @staticmethod |
| 1079 | def TCReapply( intf ): |
| 1080 | """Unfortunately OVS and Mininet are fighting |
| 1081 | over tc queuing disciplines. As a quick hack/ |
| 1082 | workaround, we clear OVS's and reapply our own.""" |
| 1083 | if type( intf ) is TCIntf: |
| 1084 | intf.config( **intf.params ) |
| 1085 | |
| 1086 | def attach( self, intf ): |
| 1087 | "Connect a data port" |
| 1088 | self.cmd( 'ovs-vsctl add-port', self, intf ) |
| 1089 | self.cmd( 'ifconfig', intf, 'up' ) |
| 1090 | self.TCReapply( intf ) |
| 1091 | |
| 1092 | def detach( self, intf ): |
| 1093 | "Disconnect a data port" |
| 1094 | self.cmd( 'ovs-vsctl del-port', self, intf ) |
| 1095 | |
| 1096 | def start( self, controllers ): |
| 1097 | "Start up a new OVS OpenFlow switch using ovs-vsctl" |
| 1098 | if self.inNamespace: |
| 1099 | raise Exception( |
| 1100 | 'OVS kernel switch does not work in a namespace' ) |
| 1101 | # We should probably call config instead, but this |
| 1102 | # requires some rethinking... |
| 1103 | self.cmd( 'ifconfig lo up' ) |
| 1104 | # Annoyingly, --if-exists option seems not to work |
| 1105 | self.cmd( 'ovs-vsctl del-br', self ) |
| 1106 | self.cmd( 'ovs-vsctl add-br', self ) |
| 1107 | self.cmd( 'ovs-vsctl -- set Bridge', self, |
| 1108 | 'other_config:datapath-id=' + self.dpid ) |
| 1109 | self.cmd( 'ovs-vsctl set-fail-mode', self, self.failMode ) |
| 1110 | for intf in self.intfList(): |
| 1111 | if not intf.IP(): |
| 1112 | self.attach( intf ) |
| 1113 | # Add controllers |
| 1114 | clist = ' '.join( [ 'tcp:%s:%d' % ( c.IP(), c.port ) |
| 1115 | for c in controllers ] ) |
| 1116 | if self.listenPort: |
| 1117 | clist += ' ptcp:%s' % self.listenPort |
| 1118 | self.cmd( 'ovs-vsctl set-controller', self, clist ) |
| 1119 | |
| 1120 | def stop( self ): |
| 1121 | "Terminate OVS switch." |
| 1122 | self.cmd( 'ovs-vsctl del-br', self ) |
| 1123 | self.deleteIntfs() |
| 1124 | |
| 1125 | OVSKernelSwitch = OVSSwitch |
| 1126 | |
| 1127 | |
| 1128 | class Controller( Node ): |
| 1129 | """A Controller is a Node that is running (or has execed?) an |
| 1130 | OpenFlow controller.""" |
| 1131 | |
| 1132 | def __init__( self, name, inNamespace=False, command='controller', |
| 1133 | cargs='-v ptcp:%d', cdir=None, ip="127.0.0.1", |
| 1134 | port=6633, **params ): |
| 1135 | self.command = command |
| 1136 | self.cargs = cargs |
| 1137 | self.cdir = cdir |
| 1138 | self.ip = ip |
| 1139 | self.port = port |
| 1140 | Node.__init__( self, name, inNamespace=inNamespace, |
| 1141 | ip=ip, **params ) |
| 1142 | self.cmd( 'ifconfig lo up' ) # Shouldn't be necessary |
| 1143 | self.checkListening() |
| 1144 | |
| 1145 | def checkListening( self ): |
| 1146 | "Make sure no controllers are running on our port" |
| 1147 | # Verify that Telnet is installed first: |
| 1148 | out, _err, returnCode = errRun( "which telnet" ) |
| 1149 | if 'telnet' not in out or returnCode != 0: |
| 1150 | raise Exception( "Error running telnet to check for listening " |
| 1151 | "controllers; please check that it is " |
| 1152 | "installed." ) |
| 1153 | listening = self.cmd( "echo A | telnet -e A %s %d" % |
| 1154 | ( self.ip, self.port ) ) |
| 1155 | if 'Unable' not in listening: |
| 1156 | servers = self.cmd( 'netstat -atp' ).split( '\n' ) |
| 1157 | pstr = ':%d ' % self.port |
| 1158 | clist = servers[ 0:1 ] + [ s for s in servers if pstr in s ] |
| 1159 | raise Exception( "Please shut down the controller which is" |
| 1160 | " running on port %d:\n" % self.port + |
| 1161 | '\n'.join( clist ) ) |
| 1162 | |
| 1163 | def start( self ): |
| 1164 | """Start <controller> <args> on controller. |
| 1165 | Log to /tmp/cN.log""" |
| 1166 | pathCheck( self.command ) |
| 1167 | cout = '/tmp/' + self.name + '.log' |
| 1168 | if self.cdir is not None: |
| 1169 | self.cmd( 'cd ' + self.cdir ) |
| 1170 | self.cmd( self.command + ' ' + self.cargs % self.port + |
| 1171 | ' 1>' + cout + ' 2>' + cout + '&' ) |
| 1172 | self.execed = False |
| 1173 | |
| 1174 | def stop( self ): |
| 1175 | "Stop controller." |
| 1176 | self.cmd( 'kill %' + self.command ) |
| 1177 | self.terminate() |
| 1178 | |
| 1179 | def IP( self, intf=None ): |
| 1180 | "Return IP address of the Controller" |
| 1181 | if self.intfs: |
| 1182 | ip = Node.IP( self, intf ) |
| 1183 | else: |
| 1184 | ip = self.ip |
| 1185 | return ip |
| 1186 | |
| 1187 | def __repr__( self ): |
| 1188 | "More informative string representation" |
| 1189 | return '<%s %s: %s:%s pid=%s> ' % ( |
| 1190 | self.__class__.__name__, self.name, |
| 1191 | self.IP(), self.port, self.pid ) |
| 1192 | |
| 1193 | |
| 1194 | class OVSController( Controller ): |
| 1195 | "Open vSwitch controller" |
| 1196 | def __init__( self, name, command='ovs-controller', **kwargs ): |
| 1197 | Controller.__init__( self, name, command=command, **kwargs ) |
| 1198 | |
| 1199 | |
| 1200 | class NOX( Controller ): |
| 1201 | "Controller to run a NOX application." |
| 1202 | |
| 1203 | def __init__( self, name, *noxArgs, **kwargs ): |
| 1204 | """Init. |
| 1205 | name: name to give controller |
| 1206 | noxArgs: arguments (strings) to pass to NOX""" |
| 1207 | if not noxArgs: |
| 1208 | warn( 'warning: no NOX modules specified; ' |
| 1209 | 'running packetdump only\n' ) |
| 1210 | noxArgs = [ 'packetdump' ] |
| 1211 | elif type( noxArgs ) not in ( list, tuple ): |
| 1212 | noxArgs = [ noxArgs ] |
| 1213 | |
| 1214 | if 'NOX_CORE_DIR' not in os.environ: |
| 1215 | exit( 'exiting; please set missing NOX_CORE_DIR env var' ) |
| 1216 | noxCoreDir = os.environ[ 'NOX_CORE_DIR' ] |
| 1217 | |
| 1218 | Controller.__init__( self, name, |
| 1219 | command=noxCoreDir + '/nox_core', |
| 1220 | cargs='--libdir=/usr/local/lib -v -i ptcp:%s ' + |
| 1221 | ' '.join( noxArgs ), |
| 1222 | cdir=noxCoreDir, |
| 1223 | **kwargs ) |
| 1224 | |
| 1225 | |
| 1226 | class RemoteController( Controller ): |
| 1227 | "Controller running outside of Mininet's control." |
| 1228 | |
| 1229 | def __init__( self, name, ip='127.0.0.1', |
| 1230 | port=6633, **kwargs): |
| 1231 | """Init. |
| 1232 | name: name to give controller |
| 1233 | ip: the IP address where the remote controller is |
| 1234 | listening |
| 1235 | port: the port where the remote controller is listening""" |
| 1236 | Controller.__init__( self, name, ip=ip, port=port, **kwargs ) |
| 1237 | |
| 1238 | def start( self ): |
| 1239 | "Overridden to do nothing." |
| 1240 | return |
| 1241 | |
| 1242 | def stop( self ): |
| 1243 | "Overridden to do nothing." |
| 1244 | return |
| 1245 | |
| 1246 | def checkListening( self ): |
| 1247 | "Warn if remote controller is not accessible" |
| 1248 | listening = self.cmd( "echo A | telnet -e A %s %d" % |
| 1249 | ( self.ip, self.port ) ) |
| 1250 | if 'Unable' in listening: |
| 1251 | warn( "Unable to contact the remote controller" |
| 1252 | " at %s:%d\n" % ( self.ip, self.port ) ) |