carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame^] | 1 | """ |
| 2 | |
| 3 | Mininet: A simple networking testbed for OpenFlow/SDN! |
| 4 | |
| 5 | author: Bob Lantz (rlantz@cs.stanford.edu) |
| 6 | author: Brandon Heller (brandonh@stanford.edu) |
| 7 | |
| 8 | Mininet creates scalable OpenFlow test networks by using |
| 9 | process-based virtualization and network namespaces. |
| 10 | |
| 11 | Simulated hosts are created as processes in separate network |
| 12 | namespaces. This allows a complete OpenFlow network to be simulated on |
| 13 | top of a single Linux kernel. |
| 14 | |
| 15 | Each host has: |
| 16 | |
| 17 | A virtual console (pipes to a shell) |
| 18 | A virtual interfaces (half of a veth pair) |
| 19 | A parent shell (and possibly some child processes) in a namespace |
| 20 | |
| 21 | Hosts have a network interface which is configured via ifconfig/ip |
| 22 | link/etc. |
| 23 | |
| 24 | This version supports both the kernel and user space datapaths |
| 25 | from the OpenFlow reference implementation (openflowswitch.org) |
| 26 | as well as OpenVSwitch (openvswitch.org.) |
| 27 | |
| 28 | In kernel datapath mode, the controller and switches are simply |
| 29 | processes in the root namespace. |
| 30 | |
| 31 | Kernel OpenFlow datapaths are instantiated using dpctl(8), and are |
| 32 | attached to the one side of a veth pair; the other side resides in the |
| 33 | host namespace. In this mode, switch processes can simply connect to the |
| 34 | controller via the loopback interface. |
| 35 | |
| 36 | In user datapath mode, the controller and switches can be full-service |
| 37 | nodes that live in their own network namespaces and have management |
| 38 | interfaces and IP addresses on a control network (e.g. 192.168.123.1, |
| 39 | currently routed although it could be bridged.) |
| 40 | |
| 41 | In addition to a management interface, user mode switches also have |
| 42 | several switch interfaces, halves of veth pairs whose other halves |
| 43 | reside in the host nodes that the switches are connected to. |
| 44 | |
| 45 | Consistent, straightforward naming is important in order to easily |
| 46 | identify hosts, switches and controllers, both from the CLI and |
| 47 | from program code. Interfaces are named to make it easy to identify |
| 48 | which interfaces belong to which node. |
| 49 | |
| 50 | The basic naming scheme is as follows: |
| 51 | |
| 52 | Host nodes are named h1-hN |
| 53 | Switch nodes are named s1-sN |
| 54 | Controller nodes are named c0-cN |
| 55 | Interfaces are named {nodename}-eth0 .. {nodename}-ethN |
| 56 | |
| 57 | Note: If the network topology is created using mininet.topo, then |
| 58 | node numbers are unique among hosts and switches (e.g. we have |
| 59 | h1..hN and SN..SN+M) and also correspond to their default IP addresses |
| 60 | of 10.x.y.z/8 where x.y.z is the base-256 representation of N for |
| 61 | hN. This mapping allows easy determination of a node's IP |
| 62 | address from its name, e.g. h1 -> 10.0.0.1, h257 -> 10.0.1.1. |
| 63 | |
| 64 | Note also that 10.0.0.1 can often be written as 10.1 for short, e.g. |
| 65 | "ping 10.1" is equivalent to "ping 10.0.0.1". |
| 66 | |
| 67 | Currently we wrap the entire network in a 'mininet' object, which |
| 68 | constructs a simulated network based on a network topology created |
| 69 | using a topology object (e.g. LinearTopo) from mininet.topo or |
| 70 | mininet.topolib, and a Controller which the switches will connect |
| 71 | to. Several configuration options are provided for functions such as |
| 72 | automatically setting MAC addresses, populating the ARP table, or |
| 73 | even running a set of terminals to allow direct interaction with nodes. |
| 74 | |
| 75 | After the network is created, it can be started using start(), and a |
| 76 | variety of useful tasks maybe performed, including basic connectivity |
| 77 | and bandwidth tests and running the mininet CLI. |
| 78 | |
| 79 | Once the network is up and running, test code can easily get access |
| 80 | to host and switch objects which can then be used for arbitrary |
| 81 | experiments, typically involving running a series of commands on the |
| 82 | hosts. |
| 83 | |
| 84 | After all desired tests or activities have been completed, the stop() |
| 85 | method may be called to shut down the network. |
| 86 | |
| 87 | """ |
| 88 | |
| 89 | import os |
| 90 | import re |
| 91 | import select |
| 92 | import signal |
| 93 | from time import sleep |
| 94 | |
| 95 | from mininet.cli import CLI |
| 96 | from mininet.log import info, error, debug, output |
| 97 | from mininet.node import Host, OVSKernelSwitch, Controller |
| 98 | from mininet.link import Link, Intf |
| 99 | from mininet.util import quietRun, fixLimits, numCores, ensureRoot |
| 100 | from mininet.util import macColonHex, ipStr, ipParse, netParse, ipAdd, nextCCNnet |
| 101 | from mininet.term import cleanUpScreens, makeTerms |
| 102 | |
| 103 | # Mininet version: should be consistent with README and LICENSE |
| 104 | VERSION = "2.0.0" |
| 105 | |
| 106 | class Mininet( object ): |
| 107 | "Network emulation with hosts spawned in network namespaces." |
| 108 | |
| 109 | def __init__( self, topo=None, switch=OVSKernelSwitch, host=Host, |
| 110 | controller=Controller, link=Link, intf=Intf, |
| 111 | build=True, xterms=False, cleanup=False, ipBase='10.0.0.0/8', |
| 112 | inNamespace=False, |
| 113 | autoSetMacs=False, autoStaticArp=False, autoPinCpus=False, |
| 114 | listenPort=None ): |
| 115 | """Create Mininet object. |
| 116 | topo: Topo (topology) object or None |
| 117 | switch: default Switch class |
| 118 | host: default Host class/constructor |
| 119 | controller: default Controller class/constructor |
| 120 | link: default Link class/constructor |
| 121 | intf: default Intf class/constructor |
| 122 | ipBase: base IP address for hosts, |
| 123 | build: build now from topo? |
| 124 | xterms: if build now, spawn xterms? |
| 125 | cleanup: if build now, cleanup before creating? |
| 126 | inNamespace: spawn switches and controller in net namespaces? |
| 127 | autoSetMacs: set MAC addrs automatically like IP addresses? |
| 128 | autoStaticArp: set all-pairs static MAC addrs? |
| 129 | autoPinCpus: pin hosts to (real) cores (requires CPULimitedHost)? |
| 130 | listenPort: base listening port to open; will be incremented for |
| 131 | each additional switch in the net if inNamespace=False""" |
| 132 | self.topo = topo |
| 133 | self.switch = switch |
| 134 | self.host = host |
| 135 | self.controller = controller |
| 136 | self.link = link |
| 137 | self.intf = intf |
| 138 | self.ipBase = ipBase |
| 139 | self.ipBaseNum, self.prefixLen = netParse( self.ipBase ) |
| 140 | self.nextIP = 1 # start for address allocation |
| 141 | self.ccnNetBase = '1.0.0.0' |
| 142 | self.inNamespace = inNamespace |
| 143 | self.xterms = xterms |
| 144 | self.cleanup = cleanup |
| 145 | self.autoSetMacs = autoSetMacs |
| 146 | self.autoStaticArp = autoStaticArp |
| 147 | self.autoPinCpus = autoPinCpus |
| 148 | self.numCores = numCores() |
| 149 | self.nextCore = 0 # next core for pinning hosts to CPUs |
| 150 | self.listenPort = listenPort |
| 151 | |
| 152 | self.hosts = [] |
| 153 | self.switches = [] |
| 154 | self.controllers = [] |
| 155 | |
| 156 | self.nameToNode = {} # name to Node (Host/Switch) objects |
| 157 | |
| 158 | self.terms = [] # list of spawned xterm processes |
| 159 | |
| 160 | Mininet.init() # Initialize Mininet if necessary |
| 161 | |
| 162 | self.built = False |
| 163 | if topo and build: |
| 164 | self.build() |
| 165 | |
| 166 | def isCCNhost(self, node): |
| 167 | if 'fib' in node.params: |
| 168 | return True |
| 169 | else: |
| 170 | return False |
| 171 | |
| 172 | def addHost( self, name, cls=None, **params ): |
| 173 | """Add host. |
| 174 | name: name of host to add |
| 175 | cls: custom host class/constructor (optional) |
| 176 | params: parameters for host |
| 177 | returns: added host""" |
| 178 | # Default IP and MAC addresses |
| 179 | |
| 180 | defaults = { 'ip': ipAdd( self.nextIP, |
| 181 | ipBaseNum=self.ipBaseNum, |
| 182 | prefixLen=self.prefixLen ) + |
| 183 | '/%s' % self.prefixLen } |
| 184 | if self.autoSetMacs: |
| 185 | defaults[ 'mac'] = macColonHex( self.nextIP ) |
| 186 | if self.autoPinCpus: |
| 187 | defaults[ 'cores' ] = self.nextCore |
| 188 | self.nextCore = ( self.nextCore + 1 ) % self.numCores |
| 189 | self.nextIP += 1 |
| 190 | defaults.update( params ) |
| 191 | if not cls: |
| 192 | cls = self.host |
| 193 | h = cls( name, **defaults ) |
| 194 | self.hosts.append( h ) |
| 195 | self.nameToNode[ name ] = h |
| 196 | return h |
| 197 | |
| 198 | def addSwitch( self, name, cls=None, **params ): |
| 199 | """Add switch. |
| 200 | name: name of switch to add |
| 201 | cls: custom switch class/constructor (optional) |
| 202 | returns: added switch |
| 203 | side effect: increments listenPort ivar .""" |
| 204 | defaults = { 'listenPort': self.listenPort, |
| 205 | 'inNamespace': self.inNamespace } |
| 206 | defaults.update( params ) |
| 207 | if not cls: |
| 208 | cls = self.switch |
| 209 | sw = cls( name, **defaults ) |
| 210 | if not self.inNamespace and self.listenPort: |
| 211 | self.listenPort += 1 |
| 212 | self.switches.append( sw ) |
| 213 | self.nameToNode[ name ] = sw |
| 214 | return sw |
| 215 | |
| 216 | def addController( self, name='c0', controller=None, **params ): |
| 217 | """Add controller. |
| 218 | controller: Controller class""" |
| 219 | if not controller: |
| 220 | controller = self.controller |
| 221 | controller_new = controller( name, **params ) |
| 222 | if controller_new: # allow controller-less setups |
| 223 | self.controllers.append( controller_new ) |
| 224 | self.nameToNode[ name ] = controller_new |
| 225 | return controller_new |
| 226 | |
| 227 | # BL: is this better than just using nameToNode[] ? |
| 228 | # Should it have a better name? |
| 229 | def getNodeByName( self, *args ): |
| 230 | "Return node(s) with given name(s)" |
| 231 | if len( args ) == 1: |
| 232 | return self.nameToNode[ args[ 0 ] ] |
| 233 | return [ self.nameToNode[ n ] for n in args ] |
| 234 | |
| 235 | def get( self, *args ): |
| 236 | "Convenience alias for getNodeByName" |
| 237 | return self.getNodeByName( *args ) |
| 238 | |
| 239 | def addLink( self, node1, node2, port1=None, port2=None, |
| 240 | cls=None, **params ): |
| 241 | """"Add a link from node1 to node2 |
| 242 | node1: source node |
| 243 | node2: dest node |
| 244 | port1: source port |
| 245 | port2: dest port |
| 246 | returns: link object""" |
| 247 | defaults = { 'port1': port1, |
| 248 | 'port2': port2, |
| 249 | 'intf': self.intf } |
| 250 | defaults.update( params ) |
| 251 | if not cls: |
| 252 | cls = self.link |
| 253 | return cls( node1, node2, **defaults ) |
| 254 | |
| 255 | def configHosts( self ): |
| 256 | "Configure a set of hosts." |
| 257 | for host in self.hosts: |
| 258 | info( host.name + ' ' ) |
| 259 | intf = host.defaultIntf() |
| 260 | if self.isCCNhost(host): |
| 261 | host.configCCN() |
| 262 | host.configDefault(ip=None,mac=None) |
| 263 | elif intf: |
| 264 | host.configDefault( defaultRoute=intf ) |
| 265 | else: |
| 266 | # Don't configure nonexistent intf |
| 267 | host.configDefault( ip=None, mac=None ) |
| 268 | # You're low priority, dude! |
| 269 | # BL: do we want to do this here or not? |
| 270 | # May not make sense if we have CPU lmiting... |
| 271 | # quietRun( 'renice +18 -p ' + repr( host.pid ) ) |
| 272 | # This may not be the right place to do this, but |
| 273 | # it needs to be done somewhere. |
| 274 | host.cmd( 'ifconfig lo up' ) |
| 275 | info( '\n' ) |
| 276 | |
| 277 | def buildFromTopo( self, topo=None ): |
| 278 | """Build mininet from a topology object |
| 279 | At the end of this function, everything should be connected |
| 280 | and up.""" |
| 281 | |
| 282 | # Possibly we should clean up here and/or validate |
| 283 | # the topo |
| 284 | if self.cleanup: |
| 285 | pass |
| 286 | |
| 287 | info( '*** Creating network\n' ) |
| 288 | |
| 289 | if not self.controllers: |
| 290 | # Add a default controller |
| 291 | info( '*** Adding controller\n' ) |
| 292 | classes = self.controller |
| 293 | if type( classes ) is not list: |
| 294 | classes = [ classes ] |
| 295 | for i, cls in enumerate( classes ): |
| 296 | self.addController( 'c%d' % i, cls ) |
| 297 | |
| 298 | info( '*** Adding hosts:\n' ) |
| 299 | for hostName in topo.hosts(): |
| 300 | self.addHost( hostName, **topo.nodeInfo( hostName ) ) |
| 301 | info( hostName + ' ' ) |
| 302 | |
| 303 | info( '\n*** Adding switches:\n' ) |
| 304 | for switchName in topo.switches(): |
| 305 | self.addSwitch( switchName, **topo.nodeInfo( switchName) ) |
| 306 | info( switchName + ' ' ) |
| 307 | |
| 308 | info( '\n*** Adding links:\n' ) |
| 309 | for srcName, dstName in topo.links(sort=True): |
| 310 | src, dst = self.nameToNode[ srcName ], self.nameToNode[ dstName ] |
| 311 | params = topo.linkInfo( srcName, dstName ) |
| 312 | srcPort, dstPort = topo.port( srcName, dstName ) |
| 313 | self.addLink( src, dst, srcPort, dstPort, **params ) |
| 314 | if self.isCCNhost(src): |
| 315 | src.setIP(ipStr(ipParse(self.ccnNetBase) + 1) + '/30', intf=src.name + '-eth' + str(srcPort)) |
| 316 | dst.setIP(ipStr(ipParse(self.ccnNetBase) + 2) + '/30', intf=dst.name + '-eth' + str(dstPort)) |
| 317 | self.ccnNetBase=nextCCNnet(self.ccnNetBase) |
| 318 | |
| 319 | info( '(%s, %s) ' % ( src.name, dst.name ) ) |
| 320 | |
| 321 | info( '\n' ) |
| 322 | |
| 323 | |
| 324 | def configureControlNetwork( self ): |
| 325 | "Control net config hook: override in subclass" |
| 326 | raise Exception( 'configureControlNetwork: ' |
| 327 | 'should be overriden in subclass', self ) |
| 328 | |
| 329 | def build( self ): |
| 330 | "Build mininet." |
| 331 | if self.topo: |
| 332 | self.buildFromTopo( self.topo ) |
| 333 | if ( self.inNamespace ): |
| 334 | self.configureControlNetwork() |
| 335 | info( '*** Configuring hosts\n' ) |
| 336 | self.configHosts() |
| 337 | if self.xterms: |
| 338 | self.startTerms() |
| 339 | if self.autoStaticArp: |
| 340 | self.staticArp() |
| 341 | self.built = True |
| 342 | |
| 343 | def startTerms( self ): |
| 344 | "Start a terminal for each node." |
| 345 | info( "*** Running terms on %s\n" % os.environ[ 'DISPLAY' ] ) |
| 346 | cleanUpScreens() |
| 347 | self.terms += makeTerms( self.controllers, 'controller' ) |
| 348 | self.terms += makeTerms( self.switches, 'switch' ) |
| 349 | self.terms += makeTerms( self.hosts, 'host' ) |
| 350 | |
| 351 | def stopXterms( self ): |
| 352 | "Kill each xterm." |
| 353 | for term in self.terms: |
| 354 | os.kill( term.pid, signal.SIGKILL ) |
| 355 | cleanUpScreens() |
| 356 | |
| 357 | def staticArp( self ): |
| 358 | "Add all-pairs ARP entries to remove the need to handle broadcast." |
| 359 | for src in self.hosts: |
| 360 | for dst in self.hosts: |
| 361 | if src != dst: |
| 362 | src.setARP( ip=dst.IP(), mac=dst.MAC() ) |
| 363 | |
| 364 | def start( self ): |
| 365 | "Start controller and switches." |
| 366 | if not self.built: |
| 367 | self.build() |
| 368 | info( '*** Starting controller\n' ) |
| 369 | for controller in self.controllers: |
| 370 | controller.start() |
| 371 | info( '*** Starting %s switches\n' % len( self.switches ) ) |
| 372 | for switch in self.switches: |
| 373 | info( switch.name + ' ') |
| 374 | switch.start( self.controllers ) |
| 375 | info( '\n' ) |
| 376 | |
| 377 | def stop( self ): |
| 378 | "Stop the controller(s), switches and hosts" |
| 379 | if self.terms: |
| 380 | info( '*** Stopping %i terms\n' % len( self.terms ) ) |
| 381 | self.stopXterms() |
| 382 | info( '*** Stopping %i hosts\n' % len( self.hosts ) ) |
| 383 | for host in self.hosts: |
| 384 | info( host.name + ' ' ) |
| 385 | host.terminate() |
| 386 | info( '\n' ) |
| 387 | info( '*** Stopping %i switches\n' % len( self.switches ) ) |
| 388 | for switch in self.switches: |
| 389 | info( switch.name + ' ' ) |
| 390 | switch.stop() |
| 391 | info( '\n' ) |
| 392 | info( '*** Stopping %i controllers\n' % len( self.controllers ) ) |
| 393 | for controller in self.controllers: |
| 394 | info( controller.name + ' ' ) |
| 395 | controller.stop() |
| 396 | info( '\n*** Done\n' ) |
| 397 | |
| 398 | def run( self, test, *args, **kwargs ): |
| 399 | "Perform a complete start/test/stop cycle." |
| 400 | self.start() |
| 401 | info( '*** Running test\n' ) |
| 402 | result = test( *args, **kwargs ) |
| 403 | self.stop() |
| 404 | return result |
| 405 | |
| 406 | def monitor( self, hosts=None, timeoutms=-1 ): |
| 407 | """Monitor a set of hosts (or all hosts by default), |
| 408 | and return their output, a line at a time. |
| 409 | hosts: (optional) set of hosts to monitor |
| 410 | timeoutms: (optional) timeout value in ms |
| 411 | returns: iterator which returns host, line""" |
| 412 | if hosts is None: |
| 413 | hosts = self.hosts |
| 414 | poller = select.poll() |
| 415 | Node = hosts[ 0 ] # so we can call class method fdToNode |
| 416 | for host in hosts: |
| 417 | poller.register( host.stdout ) |
| 418 | while True: |
| 419 | ready = poller.poll( timeoutms ) |
| 420 | for fd, event in ready: |
| 421 | host = Node.fdToNode( fd ) |
| 422 | if event & select.POLLIN: |
| 423 | line = host.readline() |
| 424 | if line is not None: |
| 425 | yield host, line |
| 426 | # Return if non-blocking |
| 427 | if not ready and timeoutms >= 0: |
| 428 | yield None, None |
| 429 | |
| 430 | # XXX These test methods should be moved out of this class. |
| 431 | # Probably we should create a tests.py for them |
| 432 | |
| 433 | @staticmethod |
| 434 | def _parsePing( pingOutput ): |
| 435 | "Parse ping output and return packets sent, received." |
| 436 | # Check for downed link |
| 437 | if 'connect: Network is unreachable' in pingOutput: |
| 438 | return (1, 0) |
| 439 | r = r'(\d+) packets transmitted, (\d+) received' |
| 440 | m = re.search( r, pingOutput ) |
| 441 | if m is None: |
| 442 | error( '*** Error: could not parse ping output: %s\n' % |
| 443 | pingOutput ) |
| 444 | return (1, 0) |
| 445 | sent, received = int( m.group( 1 ) ), int( m.group( 2 ) ) |
| 446 | return sent, received |
| 447 | |
| 448 | def ping( self, hosts=None, timeout=None ): |
| 449 | """Ping between all specified hosts. |
| 450 | hosts: list of hosts |
| 451 | timeout: time to wait for a response, as string |
| 452 | returns: ploss packet loss percentage""" |
| 453 | # should we check if running? |
| 454 | packets = 0 |
| 455 | lost = 0 |
| 456 | ploss = None |
| 457 | if not hosts: |
| 458 | hosts = self.hosts |
| 459 | output( '*** Ping: testing ping reachability\n' ) |
| 460 | for node in hosts: |
| 461 | output( '%s -> ' % node.name ) |
| 462 | for dest in hosts: |
| 463 | if node != dest: |
| 464 | opts = '' |
| 465 | if timeout: |
| 466 | opts = '-W %s' % timeout |
| 467 | result = node.cmd( 'ping -c1 %s %s' % (opts, dest.IP()) ) |
| 468 | sent, received = self._parsePing( result ) |
| 469 | packets += sent |
| 470 | if received > sent: |
| 471 | error( '*** Error: received too many packets' ) |
| 472 | error( '%s' % result ) |
| 473 | node.cmdPrint( 'route' ) |
| 474 | exit( 1 ) |
| 475 | lost += sent - received |
| 476 | output( ( '%s ' % dest.name ) if received else 'X ' ) |
| 477 | output( '\n' ) |
| 478 | ploss = 100 * lost / packets |
| 479 | output( "*** Results: %i%% dropped (%d/%d lost)\n" % |
| 480 | ( ploss, lost, packets ) ) |
| 481 | return ploss |
| 482 | |
| 483 | @staticmethod |
| 484 | def _parsePingFull( pingOutput ): |
| 485 | "Parse ping output and return all data." |
| 486 | # Check for downed link |
| 487 | if 'connect: Network is unreachable' in pingOutput: |
| 488 | return (1, 0) |
| 489 | r = r'(\d+) packets transmitted, (\d+) received' |
| 490 | m = re.search( r, pingOutput ) |
| 491 | if m is None: |
| 492 | error( '*** Error: could not parse ping output: %s\n' % |
| 493 | pingOutput ) |
| 494 | return (1, 0, 0, 0, 0, 0) |
| 495 | sent, received = int( m.group( 1 ) ), int( m.group( 2 ) ) |
| 496 | r = r'rtt min/avg/max/mdev = ' |
| 497 | r += r'(\d+\.\d+)/(\d+\.\d+)/(\d+\.\d+)/(\d+\.\d+) ms' |
| 498 | m = re.search( r, pingOutput ) |
| 499 | rttmin = float( m.group( 1 ) ) |
| 500 | rttavg = float( m.group( 2 ) ) |
| 501 | rttmax = float( m.group( 3 ) ) |
| 502 | rttdev = float( m.group( 4 ) ) |
| 503 | return sent, received, rttmin, rttavg, rttmax, rttdev |
| 504 | |
| 505 | def pingFull( self, hosts=None, timeout=None ): |
| 506 | """Ping between all specified hosts and return all data. |
| 507 | hosts: list of hosts |
| 508 | timeout: time to wait for a response, as string |
| 509 | returns: all ping data; see function body.""" |
| 510 | # should we check if running? |
| 511 | # Each value is a tuple: (src, dsd, [all ping outputs]) |
| 512 | all_outputs = [] |
| 513 | if not hosts: |
| 514 | hosts = self.hosts |
| 515 | output( '*** Ping: testing ping reachability\n' ) |
| 516 | for node in hosts: |
| 517 | output( '%s -> ' % node.name ) |
| 518 | for dest in hosts: |
| 519 | if node != dest: |
| 520 | opts = '' |
| 521 | if timeout: |
| 522 | opts = '-W %s' % timeout |
| 523 | result = node.cmd( 'ping -c1 %s %s' % (opts, dest.IP()) ) |
| 524 | outputs = self._parsePingFull( result ) |
| 525 | sent, received, rttmin, rttavg, rttmax, rttdev = outputs |
| 526 | all_outputs.append( (node, dest, outputs) ) |
| 527 | output( ( '%s ' % dest.name ) if received else 'X ' ) |
| 528 | output( '\n' ) |
| 529 | output( "*** Results: \n" ) |
| 530 | for outputs in all_outputs: |
| 531 | src, dest, ping_outputs = outputs |
| 532 | sent, received, rttmin, rttavg, rttmax, rttdev = ping_outputs |
| 533 | output( " %s->%s: %s/%s, " % (src, dest, sent, received ) ) |
| 534 | output( "rtt min/avg/max/mdev %0.3f/%0.3f/%0.3f/%0.3f ms\n" % |
| 535 | (rttmin, rttavg, rttmax, rttdev) ) |
| 536 | return all_outputs |
| 537 | |
| 538 | def pingAll( self ): |
| 539 | """Ping between all hosts. |
| 540 | returns: ploss packet loss percentage""" |
| 541 | return self.ping() |
| 542 | |
| 543 | def pingPair( self ): |
| 544 | """Ping between first two hosts, useful for testing. |
| 545 | returns: ploss packet loss percentage""" |
| 546 | hosts = [ self.hosts[ 0 ], self.hosts[ 1 ] ] |
| 547 | return self.ping( hosts=hosts ) |
| 548 | |
| 549 | def pingAllFull( self ): |
| 550 | """Ping between all hosts. |
| 551 | returns: ploss packet loss percentage""" |
| 552 | return self.pingFull() |
| 553 | |
| 554 | def pingPairFull( self ): |
| 555 | """Ping between first two hosts, useful for testing. |
| 556 | returns: ploss packet loss percentage""" |
| 557 | hosts = [ self.hosts[ 0 ], self.hosts[ 1 ] ] |
| 558 | return self.pingFull( hosts=hosts ) |
| 559 | |
| 560 | @staticmethod |
| 561 | def _parseIperf( iperfOutput ): |
| 562 | """Parse iperf output and return bandwidth. |
| 563 | iperfOutput: string |
| 564 | returns: result string""" |
| 565 | r = r'([\d\.]+ \w+/sec)' |
| 566 | m = re.findall( r, iperfOutput ) |
| 567 | if m: |
| 568 | return m[-1] |
| 569 | else: |
| 570 | # was: raise Exception(...) |
| 571 | error( 'could not parse iperf output: ' + iperfOutput ) |
| 572 | return '' |
| 573 | |
| 574 | # XXX This should be cleaned up |
| 575 | |
| 576 | def iperf( self, hosts=None, l4Type='TCP', udpBw='10M' ): |
| 577 | """Run iperf between two hosts. |
| 578 | hosts: list of hosts; if None, uses opposite hosts |
| 579 | l4Type: string, one of [ TCP, UDP ] |
| 580 | returns: results two-element array of server and client speeds""" |
| 581 | if not quietRun( 'which telnet' ): |
| 582 | error( 'Cannot find telnet in $PATH - required for iperf test' ) |
| 583 | return |
| 584 | if not hosts: |
| 585 | hosts = [ self.hosts[ 0 ], self.hosts[ -1 ] ] |
| 586 | else: |
| 587 | assert len( hosts ) == 2 |
| 588 | client, server = hosts |
| 589 | output( '*** Iperf: testing ' + l4Type + ' bandwidth between ' ) |
| 590 | output( "%s and %s\n" % ( client.name, server.name ) ) |
| 591 | server.cmd( 'killall -9 iperf' ) |
| 592 | iperfArgs = 'iperf ' |
| 593 | bwArgs = '' |
| 594 | if l4Type == 'UDP': |
| 595 | iperfArgs += '-u ' |
| 596 | bwArgs = '-b ' + udpBw + ' ' |
| 597 | elif l4Type != 'TCP': |
| 598 | raise Exception( 'Unexpected l4 type: %s' % l4Type ) |
| 599 | server.sendCmd( iperfArgs + '-s', printPid=True ) |
| 600 | servout = '' |
| 601 | while server.lastPid is None: |
| 602 | servout += server.monitor() |
| 603 | if l4Type == 'TCP': |
| 604 | while 'Connected' not in client.cmd( |
| 605 | 'sh -c "echo A | telnet -e A %s 5001"' % server.IP()): |
| 606 | output('waiting for iperf to start up...') |
| 607 | sleep(.5) |
| 608 | cliout = client.cmd( iperfArgs + '-t 5 -c ' + server.IP() + ' ' + |
| 609 | bwArgs ) |
| 610 | debug( 'Client output: %s\n' % cliout ) |
| 611 | server.sendInt() |
| 612 | servout += server.waitOutput() |
| 613 | debug( 'Server output: %s\n' % servout ) |
| 614 | result = [ self._parseIperf( servout ), self._parseIperf( cliout ) ] |
| 615 | if l4Type == 'UDP': |
| 616 | result.insert( 0, udpBw ) |
| 617 | output( '*** Results: %s\n' % result ) |
| 618 | return result |
| 619 | |
| 620 | def runCpuLimitTest( self, cpu, duration=5 ): |
| 621 | """run CPU limit test with 'while true' processes. |
| 622 | cpu: desired CPU fraction of each host |
| 623 | duration: test duration in seconds |
| 624 | returns a single list of measured CPU fractions as floats. |
| 625 | """ |
| 626 | pct = cpu * 100 |
| 627 | info('*** Testing CPU %.0f%% bandwidth limit\n' % pct) |
| 628 | hosts = self.hosts |
| 629 | for h in hosts: |
| 630 | h.cmd( 'while true; do a=1; done &' ) |
| 631 | pids = [h.cmd( 'echo $!' ).strip() for h in hosts] |
| 632 | pids_str = ",".join(["%s" % pid for pid in pids]) |
| 633 | cmd = 'ps -p %s -o pid,%%cpu,args' % pids_str |
| 634 | # It's a shame that this is what pylint prefers |
| 635 | outputs = [] |
| 636 | for _ in range( duration ): |
| 637 | sleep( 1 ) |
| 638 | outputs.append( quietRun( cmd ).strip() ) |
| 639 | for h in hosts: |
| 640 | h.cmd( 'kill %1' ) |
| 641 | cpu_fractions = [] |
| 642 | for test_output in outputs: |
| 643 | # Split by line. Ignore first line, which looks like this: |
| 644 | # PID %CPU COMMAND\n |
| 645 | for line in test_output.split('\n')[1:]: |
| 646 | r = r'\d+\s*(\d+\.\d+)' |
| 647 | m = re.search( r, line ) |
| 648 | if m is None: |
| 649 | error( '*** Error: could not extract CPU fraction: %s\n' % |
| 650 | line ) |
| 651 | return None |
| 652 | cpu_fractions.append( float( m.group( 1 ) ) ) |
| 653 | output( '*** Results: %s\n' % cpu_fractions ) |
| 654 | return cpu_fractions |
| 655 | |
| 656 | # BL: I think this can be rewritten now that we have |
| 657 | # a real link class. |
| 658 | def configLinkStatus( self, src, dst, status ): |
| 659 | """Change status of src <-> dst links. |
| 660 | src: node name |
| 661 | dst: node name |
| 662 | status: string {up, down}""" |
| 663 | if src not in self.nameToNode: |
| 664 | error( 'src not in network: %s\n' % src ) |
| 665 | elif dst not in self.nameToNode: |
| 666 | error( 'dst not in network: %s\n' % dst ) |
| 667 | else: |
| 668 | if type( src ) is str: |
| 669 | src = self.nameToNode[ src ] |
| 670 | if type( dst ) is str: |
| 671 | dst = self.nameToNode[ dst ] |
| 672 | connections = src.connectionsTo( dst ) |
| 673 | if len( connections ) == 0: |
| 674 | error( 'src and dst not connected: %s %s\n' % ( src, dst) ) |
| 675 | for srcIntf, dstIntf in connections: |
| 676 | result = srcIntf.ifconfig( status ) |
| 677 | if result: |
| 678 | error( 'link src status change failed: %s\n' % result ) |
| 679 | result = dstIntf.ifconfig( status ) |
| 680 | if result: |
| 681 | error( 'link dst status change failed: %s\n' % result ) |
| 682 | |
| 683 | def interact( self ): |
| 684 | "Start network and run our simple CLI." |
| 685 | self.start() |
| 686 | result = CLI( self ) |
| 687 | self.stop() |
| 688 | return result |
| 689 | |
| 690 | inited = False |
| 691 | |
| 692 | @classmethod |
| 693 | def init( cls ): |
| 694 | "Initialize Mininet" |
| 695 | if cls.inited: |
| 696 | return |
| 697 | ensureRoot() |
| 698 | fixLimits() |
| 699 | cls.inited = True |
| 700 | |
| 701 | |
| 702 | class MininetWithControlNet( Mininet ): |
| 703 | |
| 704 | """Control network support: |
| 705 | |
| 706 | Create an explicit control network. Currently this is only |
| 707 | used/usable with the user datapath. |
| 708 | |
| 709 | Notes: |
| 710 | |
| 711 | 1. If the controller and switches are in the same (e.g. root) |
| 712 | namespace, they can just use the loopback connection. |
| 713 | |
| 714 | 2. If we can get unix domain sockets to work, we can use them |
| 715 | instead of an explicit control network. |
| 716 | |
| 717 | 3. Instead of routing, we could bridge or use 'in-band' control. |
| 718 | |
| 719 | 4. Even if we dispense with this in general, it could still be |
| 720 | useful for people who wish to simulate a separate control |
| 721 | network (since real networks may need one!) |
| 722 | |
| 723 | 5. Basically nobody ever used this code, so it has been moved |
| 724 | into its own class. |
| 725 | |
| 726 | 6. Ultimately we may wish to extend this to allow us to create a |
| 727 | control network which every node's control interface is |
| 728 | attached to.""" |
| 729 | |
| 730 | def configureControlNetwork( self ): |
| 731 | "Configure control network." |
| 732 | self.configureRoutedControlNetwork() |
| 733 | |
| 734 | # We still need to figure out the right way to pass |
| 735 | # in the control network location. |
| 736 | |
| 737 | def configureRoutedControlNetwork( self, ip='192.168.123.1', |
| 738 | prefixLen=16 ): |
| 739 | """Configure a routed control network on controller and switches. |
| 740 | For use with the user datapath only right now.""" |
| 741 | controller = self.controllers[ 0 ] |
| 742 | info( controller.name + ' <->' ) |
| 743 | cip = ip |
| 744 | snum = ipParse( ip ) |
| 745 | for switch in self.switches: |
| 746 | info( ' ' + switch.name ) |
| 747 | link = self.link( switch, controller, port1=0 ) |
| 748 | sintf, cintf = link.intf1, link.intf2 |
| 749 | switch.controlIntf = sintf |
| 750 | snum += 1 |
| 751 | while snum & 0xff in [ 0, 255 ]: |
| 752 | snum += 1 |
| 753 | sip = ipStr( snum ) |
| 754 | cintf.setIP( cip, prefixLen ) |
| 755 | sintf.setIP( sip, prefixLen ) |
| 756 | controller.setHostRoute( sip, cintf ) |
| 757 | switch.setHostRoute( cip, sintf ) |
| 758 | info( '\n' ) |
| 759 | info( '*** Testing control network\n' ) |
| 760 | while not cintf.isUp(): |
| 761 | info( '*** Waiting for', cintf, 'to come up\n' ) |
| 762 | sleep( 1 ) |
| 763 | for switch in self.switches: |
| 764 | while not sintf.isUp(): |
| 765 | info( '*** Waiting for', sintf, 'to come up\n' ) |
| 766 | sleep( 1 ) |
| 767 | if self.ping( hosts=[ switch, controller ] ) != 0: |
| 768 | error( '*** Error: control network test failed\n' ) |
| 769 | exit( 1 ) |
| 770 | info( '\n' ) |