mininet / mininet / net.py @ 2e3258d2
History | View | Annotate | Download (36.3 KB)
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 |
import random |
94 |
|
95 |
from time import sleep |
96 |
from itertools import chain, groupby |
97 |
from math import ceil |
98 |
|
99 |
from mininet.cli import CLI |
100 |
from mininet.log import info, error, debug, output, warn |
101 |
from mininet.node import ( Node, Host, OVSKernelSwitch, DefaultController, |
102 |
Controller ) |
103 |
from mininet.nodelib import NAT |
104 |
from mininet.link import Link, Intf |
105 |
from mininet.util import ( quietRun, fixLimits, numCores, ensureRoot, |
106 |
macColonHex, ipStr, ipParse, netParse, ipAdd, |
107 |
waitListening ) |
108 |
from mininet.term import cleanUpScreens, makeTerms |
109 |
|
110 |
# Mininet version: should be consistent with README and LICENSE
|
111 |
VERSION = "2.2.1"
|
112 |
|
113 |
class Mininet( object ): |
114 |
"Network emulation with hosts spawned in network namespaces."
|
115 |
|
116 |
def __init__( self, topo=None, switch=OVSKernelSwitch, host=Host, |
117 |
controller=DefaultController, link=Link, intf=Intf, |
118 |
build=True, xterms=False, cleanup=False, ipBase='10.0.0.0/8', |
119 |
inNamespace=False,
|
120 |
autoSetMacs=False, autoStaticArp=False, autoPinCpus=False, |
121 |
listenPort=None, waitConnected=False ): |
122 |
"""Create Mininet object.
|
123 |
topo: Topo (topology) object or None
|
124 |
switch: default Switch class
|
125 |
host: default Host class/constructor
|
126 |
controller: default Controller class/constructor
|
127 |
link: default Link class/constructor
|
128 |
intf: default Intf class/constructor
|
129 |
ipBase: base IP address for hosts,
|
130 |
build: build now from topo?
|
131 |
xterms: if build now, spawn xterms?
|
132 |
cleanup: if build now, cleanup before creating?
|
133 |
inNamespace: spawn switches and controller in net namespaces?
|
134 |
autoSetMacs: set MAC addrs automatically like IP addresses?
|
135 |
autoStaticArp: set all-pairs static MAC addrs?
|
136 |
autoPinCpus: pin hosts to (real) cores (requires CPULimitedHost)?
|
137 |
listenPort: base listening port to open; will be incremented for
|
138 |
each additional switch in the net if inNamespace=False"""
|
139 |
self.topo = topo
|
140 |
self.switch = switch
|
141 |
self.host = host
|
142 |
self.controller = controller
|
143 |
self.link = link
|
144 |
self.intf = intf
|
145 |
self.ipBase = ipBase
|
146 |
self.ipBaseNum, self.prefixLen = netParse( self.ipBase ) |
147 |
self.nextIP = 1 # start for address allocation |
148 |
self.inNamespace = inNamespace
|
149 |
self.xterms = xterms
|
150 |
self.cleanup = cleanup
|
151 |
self.autoSetMacs = autoSetMacs
|
152 |
self.autoStaticArp = autoStaticArp
|
153 |
self.autoPinCpus = autoPinCpus
|
154 |
self.numCores = numCores()
|
155 |
self.nextCore = 0 # next core for pinning hosts to CPUs |
156 |
self.listenPort = listenPort
|
157 |
self.waitConn = waitConnected
|
158 |
|
159 |
self.hosts = []
|
160 |
self.switches = []
|
161 |
self.controllers = []
|
162 |
self.links = []
|
163 |
|
164 |
self.nameToNode = {} # name to Node (Host/Switch) objects |
165 |
|
166 |
self.terms = [] # list of spawned xterm processes |
167 |
|
168 |
Mininet.init() # Initialize Mininet if necessary
|
169 |
|
170 |
self.built = False |
171 |
if topo and build: |
172 |
self.build()
|
173 |
|
174 |
def waitConnected( self, timeout=None, delay=.5 ): |
175 |
"""wait for each switch to connect to a controller,
|
176 |
up to 5 seconds
|
177 |
timeout: time to wait, or None to wait indefinitely
|
178 |
delay: seconds to sleep per iteration
|
179 |
returns: True if all switches are connected"""
|
180 |
info( '*** Waiting for switches to connect\n' )
|
181 |
time = 0
|
182 |
remaining = list( self.switches ) |
183 |
while True: |
184 |
for switch in tuple( remaining ): |
185 |
if switch.connected():
|
186 |
info( '%s ' % switch )
|
187 |
remaining.remove( switch ) |
188 |
if not remaining: |
189 |
info( '\n' )
|
190 |
return True |
191 |
if time > timeout and timeout is not None: |
192 |
break
|
193 |
sleep( delay ) |
194 |
time += delay |
195 |
warn( 'Timed out after %d seconds\n' % time )
|
196 |
for switch in remaining: |
197 |
if not switch.connected(): |
198 |
warn( 'Warning: %s is not connected to a controller\n'
|
199 |
% switch.name ) |
200 |
else:
|
201 |
remaining.remove( switch ) |
202 |
return not remaining |
203 |
|
204 |
def addHost( self, name, cls=None, **params ): |
205 |
"""Add host.
|
206 |
name: name of host to add
|
207 |
cls: custom host class/constructor (optional)
|
208 |
params: parameters for host
|
209 |
returns: added host"""
|
210 |
# Default IP and MAC addresses
|
211 |
defaults = { 'ip': ipAdd( self.nextIP, |
212 |
ipBaseNum=self.ipBaseNum,
|
213 |
prefixLen=self.prefixLen ) +
|
214 |
'/%s' % self.prefixLen } |
215 |
if self.autoSetMacs: |
216 |
defaults[ 'mac' ] = macColonHex( self.nextIP ) |
217 |
if self.autoPinCpus: |
218 |
defaults[ 'cores' ] = self.nextCore |
219 |
self.nextCore = ( self.nextCore + 1 ) % self.numCores |
220 |
self.nextIP += 1 |
221 |
defaults.update( params ) |
222 |
if not cls: |
223 |
cls = self.host
|
224 |
h = cls( name, **defaults ) |
225 |
self.hosts.append( h )
|
226 |
self.nameToNode[ name ] = h
|
227 |
return h
|
228 |
|
229 |
def addSwitch( self, name, cls=None, **params ): |
230 |
"""Add switch.
|
231 |
name: name of switch to add
|
232 |
cls: custom switch class/constructor (optional)
|
233 |
returns: added switch
|
234 |
side effect: increments listenPort ivar ."""
|
235 |
defaults = { 'listenPort': self.listenPort, |
236 |
'inNamespace': self.inNamespace } |
237 |
defaults.update( params ) |
238 |
if not cls: |
239 |
cls = self.switch
|
240 |
sw = cls( name, **defaults ) |
241 |
if not self.inNamespace and self.listenPort: |
242 |
self.listenPort += 1 |
243 |
self.switches.append( sw )
|
244 |
self.nameToNode[ name ] = sw
|
245 |
return sw
|
246 |
|
247 |
def addController( self, name='c0', controller=None, **params ): |
248 |
"""Add controller.
|
249 |
controller: Controller class"""
|
250 |
# Get controller class
|
251 |
if not controller: |
252 |
controller = self.controller
|
253 |
# Construct new controller if one is not given
|
254 |
if isinstance( name, Controller ): |
255 |
controller_new = name |
256 |
# Pylint thinks controller is a str()
|
257 |
# pylint: disable=maybe-no-member
|
258 |
name = controller_new.name |
259 |
# pylint: enable=maybe-no-member
|
260 |
else:
|
261 |
controller_new = controller( name, **params ) |
262 |
# Add new controller to net
|
263 |
if controller_new: # allow controller-less setups |
264 |
self.controllers.append( controller_new )
|
265 |
self.nameToNode[ name ] = controller_new
|
266 |
return controller_new
|
267 |
|
268 |
def addNAT( self, name='nat0', connect=True, inNamespace=False, |
269 |
**params): |
270 |
"""Add a NAT to the Mininet network
|
271 |
name: name of NAT node
|
272 |
connect: switch to connect to | True (s1) | None
|
273 |
inNamespace: create in a network namespace
|
274 |
params: other NAT node params, notably:
|
275 |
ip: used as default gateway address"""
|
276 |
nat = self.addHost( name, cls=NAT, inNamespace=inNamespace,
|
277 |
subnet=self.ipBase, **params )
|
278 |
# find first switch and create link
|
279 |
if connect:
|
280 |
if not isinstance( connect, Node ): |
281 |
# Use first switch if not specified
|
282 |
connect = self.switches[ 0 ] |
283 |
# Connect the nat to the switch
|
284 |
self.addLink( nat, connect )
|
285 |
# Set the default route on hosts
|
286 |
natIP = nat.params[ 'ip' ].split('/')[ 0 ] |
287 |
for host in self.hosts: |
288 |
if host.inNamespace:
|
289 |
host.setDefaultRoute( 'via %s' % natIP )
|
290 |
return nat
|
291 |
|
292 |
# BL: We now have four ways to look up nodes
|
293 |
# This may (should?) be cleaned up in the future.
|
294 |
def getNodeByName( self, *args ): |
295 |
"Return node(s) with given name(s)"
|
296 |
if len( args ) == 1: |
297 |
return self.nameToNode[ args[ 0 ] ] |
298 |
return [ self.nameToNode[ n ] for n in args ] |
299 |
|
300 |
def get( self, *args ): |
301 |
"Convenience alias for getNodeByName"
|
302 |
return self.getNodeByName( *args ) |
303 |
|
304 |
# Even more convenient syntax for node lookup and iteration
|
305 |
def __getitem__( self, key ): |
306 |
"""net [ name ] operator: Return node(s) with given name(s)"""
|
307 |
return self.nameToNode[ key ] |
308 |
|
309 |
def __iter__( self ): |
310 |
"return iterator over node names"
|
311 |
for node in chain( self.hosts, self.switches, self.controllers ): |
312 |
yield node.name
|
313 |
|
314 |
def __len__( self ): |
315 |
"returns number of nodes in net"
|
316 |
return ( len( self.hosts ) + len( self.switches ) + |
317 |
len( self.controllers ) ) |
318 |
|
319 |
def __contains__( self, item ): |
320 |
"returns True if net contains named node"
|
321 |
return item in self.nameToNode |
322 |
|
323 |
def keys( self ): |
324 |
"return a list of all node names or net's keys"
|
325 |
return list( self ) |
326 |
|
327 |
def values( self ): |
328 |
"return a list of all nodes or net's values"
|
329 |
return [ self[name] for name in self ] |
330 |
|
331 |
def items( self ): |
332 |
"return (key,value) tuple list for every node in net"
|
333 |
return zip( self.keys(), self.values() ) |
334 |
|
335 |
@staticmethod
|
336 |
def randMac(): |
337 |
"Return a random, non-multicast MAC address"
|
338 |
return macColonHex( random.randint(1, 2**48 - 1) & 0xfeffffffffff | |
339 |
0x020000000000 )
|
340 |
|
341 |
def addLink( self, node1, node2, port1=None, port2=None, |
342 |
cls=None, **params ):
|
343 |
""""Add a link from node1 to node2
|
344 |
node1: source node (or name)
|
345 |
node2: dest node (or name)
|
346 |
port1: source port (optional)
|
347 |
port2: dest port (optional)
|
348 |
cls: link class (optional)
|
349 |
params: additional link params (optional)
|
350 |
returns: link object"""
|
351 |
# Accept node objects or names
|
352 |
node1 = node1 if not isinstance( node1, basestring ) else self[ node1 ] |
353 |
node2 = node2 if not isinstance( node2, basestring ) else self[ node2 ] |
354 |
options = dict( params )
|
355 |
# Port is optional
|
356 |
if port1 is not None: |
357 |
options.setdefault( 'port1', port1 )
|
358 |
if port2 is not None: |
359 |
options.setdefault( 'port2', port2 )
|
360 |
if self.intf is not None: |
361 |
options.setdefault( 'intf', self.intf ) |
362 |
# Set default MAC - this should probably be in Link
|
363 |
options.setdefault( 'addr1', self.randMac() ) |
364 |
options.setdefault( 'addr2', self.randMac() ) |
365 |
cls = self.link if cls is None else cls |
366 |
link = cls( node1, node2, **options ) |
367 |
self.links.append( link )
|
368 |
return link
|
369 |
|
370 |
def configHosts( self ): |
371 |
"Configure a set of hosts."
|
372 |
for host in self.hosts: |
373 |
info( host.name + ' ' )
|
374 |
intf = host.defaultIntf() |
375 |
if intf:
|
376 |
if intf.prefixLen:
|
377 |
host.configDefault(ip=intf.ip+'/'+intf.prefixLen,mac=intf.mac)
|
378 |
else:
|
379 |
if intf.ip :
|
380 |
host.configDefault(ip=intf.ip,mac=intf.mac) |
381 |
else:
|
382 |
host.configDefault() |
383 |
else:
|
384 |
# Don't configure nonexistent intf
|
385 |
host.configDefault( ip=None, mac=None ) |
386 |
# You're low priority, dude!
|
387 |
# BL: do we want to do this here or not?
|
388 |
# May not make sense if we have CPU lmiting...
|
389 |
# quietRun( 'renice +18 -p ' + repr( host.pid ) )
|
390 |
# This may not be the right place to do this, but
|
391 |
# it needs to be done somewhere.
|
392 |
info( '\n' )
|
393 |
|
394 |
def buildFromTopo( self, topo=None ): |
395 |
"""Build mininet from a topology object
|
396 |
At the end of this function, everything should be connected
|
397 |
and up."""
|
398 |
|
399 |
# Possibly we should clean up here and/or validate
|
400 |
# the topo
|
401 |
if self.cleanup: |
402 |
pass
|
403 |
|
404 |
info( '*** Creating network\n' )
|
405 |
|
406 |
if not self.controllers and self.controller: |
407 |
# Add a default controller
|
408 |
info( '*** Adding controller\n' )
|
409 |
classes = self.controller
|
410 |
if not isinstance( classes, list ): |
411 |
classes = [ classes ] |
412 |
for i, cls in enumerate( classes ): |
413 |
# Allow Controller objects because nobody understands partial()
|
414 |
if isinstance( cls, Controller ): |
415 |
self.addController( cls )
|
416 |
else:
|
417 |
self.addController( 'c%d' % i, cls ) |
418 |
|
419 |
info( '*** Adding hosts:\n' )
|
420 |
for hostName in topo.hosts(): |
421 |
self.addHost( hostName, **topo.nodeInfo( hostName ) )
|
422 |
info( hostName + ' ' )
|
423 |
|
424 |
info( '\n*** Adding switches:\n' )
|
425 |
for switchName in topo.switches(): |
426 |
# A bit ugly: add batch parameter if appropriate
|
427 |
params = topo.nodeInfo( switchName) |
428 |
cls = params.get( 'cls', self.switch ) |
429 |
if hasattr( cls, 'batchStartup' ): |
430 |
params.setdefault( 'batch', True ) |
431 |
self.addSwitch( switchName, **params )
|
432 |
info( switchName + ' ' )
|
433 |
|
434 |
info( '\n*** Adding links:\n' )
|
435 |
for srcName, dstName, params in topo.links( |
436 |
sort=True, withInfo=True ): |
437 |
self.addLink( **params )
|
438 |
info( '(%s, %s) ' % ( srcName, dstName ) )
|
439 |
|
440 |
info( '\n' )
|
441 |
|
442 |
def configureControlNetwork( self ): |
443 |
"Control net config hook: override in subclass"
|
444 |
raise Exception( 'configureControlNetwork: ' |
445 |
'should be overriden in subclass', self ) |
446 |
|
447 |
def build( self ): |
448 |
"Build mininet."
|
449 |
if self.topo: |
450 |
self.buildFromTopo( self.topo ) |
451 |
if self.inNamespace: |
452 |
self.configureControlNetwork()
|
453 |
info( '*** Configuring hosts\n' )
|
454 |
self.configHosts()
|
455 |
if self.xterms: |
456 |
self.startTerms()
|
457 |
if self.autoStaticArp: |
458 |
self.staticArp()
|
459 |
self.built = True |
460 |
|
461 |
def startTerms( self ): |
462 |
"Start a terminal for each node."
|
463 |
if 'DISPLAY' not in os.environ: |
464 |
error( "Error starting terms: Cannot connect to display\n" )
|
465 |
return
|
466 |
info( "*** Running terms on %s\n" % os.environ[ 'DISPLAY' ] ) |
467 |
cleanUpScreens() |
468 |
self.terms += makeTerms( self.controllers, 'controller' ) |
469 |
self.terms += makeTerms( self.switches, 'switch' ) |
470 |
self.terms += makeTerms( self.hosts, 'host' ) |
471 |
|
472 |
def stopXterms( self ): |
473 |
"Kill each xterm."
|
474 |
for term in self.terms: |
475 |
os.kill( term.pid, signal.SIGKILL ) |
476 |
cleanUpScreens() |
477 |
|
478 |
def staticArp( self ): |
479 |
"Add all-pairs ARP entries to remove the need to handle broadcast."
|
480 |
for src in self.hosts: |
481 |
for dst in self.hosts: |
482 |
if src != dst:
|
483 |
src.setARP( ip=dst.IP(), mac=dst.MAC() ) |
484 |
|
485 |
def start( self ): |
486 |
"Start controller and switches."
|
487 |
if not self.built: |
488 |
self.build()
|
489 |
info( '*** Starting controller\n' )
|
490 |
for controller in self.controllers: |
491 |
info( controller.name + ' ')
|
492 |
controller.start() |
493 |
info( '\n' )
|
494 |
info( '*** Starting %s switches\n' % len( self.switches ) ) |
495 |
for switch in self.switches: |
496 |
info( switch.name + ' ')
|
497 |
switch.start( self.controllers )
|
498 |
started = {} |
499 |
for swclass, switches in groupby( |
500 |
sorted( self.switches, key=type ), type ): |
501 |
switches = tuple( switches )
|
502 |
if hasattr( swclass, 'batchStartup' ): |
503 |
success = swclass.batchStartup( switches ) |
504 |
started.update( { s: s for s in success } ) |
505 |
info( '\n' )
|
506 |
if self.waitConn: |
507 |
self.waitConnected()
|
508 |
|
509 |
def stop( self ): |
510 |
"Stop the controller(s), switches and hosts"
|
511 |
info( '*** Stopping %i controllers\n' % len( self.controllers ) ) |
512 |
for controller in self.controllers: |
513 |
info( controller.name + ' ' )
|
514 |
controller.stop() |
515 |
info( '\n' )
|
516 |
if self.terms: |
517 |
info( '*** Stopping %i terms\n' % len( self.terms ) ) |
518 |
self.stopXterms()
|
519 |
info( '*** Stopping %i links\n' % len( self.links ) ) |
520 |
for link in self.links: |
521 |
info( '.' )
|
522 |
link.stop() |
523 |
info( '\n' )
|
524 |
info( '*** Stopping %i switches\n' % len( self.switches ) ) |
525 |
stopped = {} |
526 |
for swclass, switches in groupby( |
527 |
sorted( self.switches, key=type ), type ): |
528 |
switches = tuple( switches )
|
529 |
if hasattr( swclass, 'batchShutdown' ): |
530 |
success = swclass.batchShutdown( switches ) |
531 |
stopped.update( { s: s for s in success } ) |
532 |
for switch in self.switches: |
533 |
info( switch.name + ' ' )
|
534 |
if switch not in stopped: |
535 |
switch.stop() |
536 |
switch.terminate() |
537 |
info( '\n' )
|
538 |
info( '*** Stopping %i hosts\n' % len( self.hosts ) ) |
539 |
for host in self.hosts: |
540 |
info( host.name + ' ' )
|
541 |
host.terminate() |
542 |
info( '\n*** Done\n' )
|
543 |
|
544 |
def run( self, test, *args, **kwargs ): |
545 |
"Perform a complete start/test/stop cycle."
|
546 |
self.start()
|
547 |
info( '*** Running test\n' )
|
548 |
result = test( *args, **kwargs ) |
549 |
self.stop()
|
550 |
return result
|
551 |
|
552 |
def monitor( self, hosts=None, timeoutms=-1 ): |
553 |
"""Monitor a set of hosts (or all hosts by default),
|
554 |
and return their output, a line at a time.
|
555 |
hosts: (optional) set of hosts to monitor
|
556 |
timeoutms: (optional) timeout value in ms
|
557 |
returns: iterator which returns host, line"""
|
558 |
if hosts is None: |
559 |
hosts = self.hosts
|
560 |
poller = select.poll() |
561 |
h1 = hosts[ 0 ] # so we can call class method fdToNode |
562 |
for host in hosts: |
563 |
poller.register( host.stdout ) |
564 |
while True: |
565 |
ready = poller.poll( timeoutms ) |
566 |
for fd, event in ready: |
567 |
host = h1.fdToNode( fd ) |
568 |
if event & select.POLLIN:
|
569 |
line = host.readline() |
570 |
if line is not None: |
571 |
yield host, line
|
572 |
# Return if non-blocking
|
573 |
if not ready and timeoutms >= 0: |
574 |
yield None, None |
575 |
|
576 |
# XXX These test methods should be moved out of this class.
|
577 |
# Probably we should create a tests.py for them
|
578 |
|
579 |
@staticmethod
|
580 |
def _parsePing( pingOutput ): |
581 |
"Parse ping output and return packets sent, received."
|
582 |
# Check for downed link
|
583 |
if 'connect: Network is unreachable' in pingOutput: |
584 |
return 1, 0 |
585 |
r = r'(\d+) packets transmitted, (\d+) received'
|
586 |
m = re.search( r, pingOutput ) |
587 |
if m is None: |
588 |
error( '*** Error: could not parse ping output: %s\n' %
|
589 |
pingOutput ) |
590 |
return 1, 0 |
591 |
sent, received = int( m.group( 1 ) ), int( m.group( 2 ) ) |
592 |
return sent, received
|
593 |
|
594 |
def ping( self, hosts=None, timeout=None ): |
595 |
"""Ping between all specified hosts.
|
596 |
hosts: list of hosts
|
597 |
timeout: time to wait for a response, as string
|
598 |
returns: ploss packet loss percentage"""
|
599 |
# should we check if running?
|
600 |
packets = 0
|
601 |
lost = 0
|
602 |
ploss = None
|
603 |
if not hosts: |
604 |
hosts = self.hosts
|
605 |
output( '*** Ping: testing ping reachability\n' )
|
606 |
for node in hosts: |
607 |
output( '%s -> ' % node.name )
|
608 |
for dest in hosts: |
609 |
if node != dest:
|
610 |
opts = ''
|
611 |
if timeout:
|
612 |
opts = '-W %s' % timeout
|
613 |
if dest.intfs:
|
614 |
result = node.cmd( 'ping -c1 %s %s' %
|
615 |
(opts, dest.IP()) ) |
616 |
sent, received = self._parsePing( result )
|
617 |
else:
|
618 |
sent, received = 0, 0 |
619 |
packets += sent |
620 |
if received > sent:
|
621 |
error( '*** Error: received too many packets' )
|
622 |
error( '%s' % result )
|
623 |
node.cmdPrint( 'route' )
|
624 |
exit( 1 ) |
625 |
lost += sent - received |
626 |
output( ( '%s ' % dest.name ) if received else 'X ' ) |
627 |
output( '\n' )
|
628 |
if packets > 0: |
629 |
ploss = 100.0 * lost / packets
|
630 |
received = packets - lost |
631 |
output( "*** Results: %i%% dropped (%d/%d received)\n" %
|
632 |
( ploss, received, packets ) ) |
633 |
else:
|
634 |
ploss = 0
|
635 |
output( "*** Warning: No packets sent\n" )
|
636 |
return ploss
|
637 |
|
638 |
@staticmethod
|
639 |
def _parsePingFull( pingOutput ): |
640 |
"Parse ping output and return all data."
|
641 |
errorTuple = (1, 0, 0, 0, 0, 0) |
642 |
# Check for downed link
|
643 |
r = r'[uU]nreachable'
|
644 |
m = re.search( r, pingOutput ) |
645 |
if m is not None: |
646 |
return errorTuple
|
647 |
r = r'(\d+) packets transmitted, (\d+) received'
|
648 |
m = re.search( r, pingOutput ) |
649 |
if m is None: |
650 |
error( '*** Error: could not parse ping output: %s\n' %
|
651 |
pingOutput ) |
652 |
return errorTuple
|
653 |
sent, received = int( m.group( 1 ) ), int( m.group( 2 ) ) |
654 |
r = r'rtt min/avg/max/mdev = '
|
655 |
r += r'(\d+\.\d+)/(\d+\.\d+)/(\d+\.\d+)/(\d+\.\d+) ms'
|
656 |
m = re.search( r, pingOutput ) |
657 |
if m is None: |
658 |
if received == 0: |
659 |
return errorTuple
|
660 |
error( '*** Error: could not parse ping output: %s\n' %
|
661 |
pingOutput ) |
662 |
return errorTuple
|
663 |
rttmin = float( m.group( 1 ) ) |
664 |
rttavg = float( m.group( 2 ) ) |
665 |
rttmax = float( m.group( 3 ) ) |
666 |
rttdev = float( m.group( 4 ) ) |
667 |
return sent, received, rttmin, rttavg, rttmax, rttdev
|
668 |
|
669 |
def pingFull( self, hosts=None, timeout=None ): |
670 |
"""Ping between all specified hosts and return all data.
|
671 |
hosts: list of hosts
|
672 |
timeout: time to wait for a response, as string
|
673 |
returns: all ping data; see function body."""
|
674 |
# should we check if running?
|
675 |
# Each value is a tuple: (src, dsd, [all ping outputs])
|
676 |
all_outputs = [] |
677 |
if not hosts: |
678 |
hosts = self.hosts
|
679 |
output( '*** Ping: testing ping reachability\n' )
|
680 |
for node in hosts: |
681 |
output( '%s -> ' % node.name )
|
682 |
for dest in hosts: |
683 |
if node != dest:
|
684 |
opts = ''
|
685 |
if timeout:
|
686 |
opts = '-W %s' % timeout
|
687 |
result = node.cmd( 'ping -c1 %s %s' % (opts, dest.IP()) )
|
688 |
outputs = self._parsePingFull( result )
|
689 |
sent, received, rttmin, rttavg, rttmax, rttdev = outputs |
690 |
all_outputs.append( (node, dest, outputs) ) |
691 |
output( ( '%s ' % dest.name ) if received else 'X ' ) |
692 |
output( '\n' )
|
693 |
output( "*** Results: \n" )
|
694 |
for outputs in all_outputs: |
695 |
src, dest, ping_outputs = outputs |
696 |
sent, received, rttmin, rttavg, rttmax, rttdev = ping_outputs |
697 |
output( " %s->%s: %s/%s, " % (src, dest, sent, received ) )
|
698 |
output( "rtt min/avg/max/mdev %0.3f/%0.3f/%0.3f/%0.3f ms\n" %
|
699 |
(rttmin, rttavg, rttmax, rttdev) ) |
700 |
return all_outputs
|
701 |
|
702 |
def pingAll( self, timeout=None ): |
703 |
"""Ping between all hosts.
|
704 |
returns: ploss packet loss percentage"""
|
705 |
return self.ping( timeout=timeout ) |
706 |
|
707 |
def pingPair( self ): |
708 |
"""Ping between first two hosts, useful for testing.
|
709 |
returns: ploss packet loss percentage"""
|
710 |
hosts = [ self.hosts[ 0 ], self.hosts[ 1 ] ] |
711 |
return self.ping( hosts=hosts ) |
712 |
|
713 |
def pingAllFull( self ): |
714 |
"""Ping between all hosts.
|
715 |
returns: ploss packet loss percentage"""
|
716 |
return self.pingFull() |
717 |
|
718 |
def pingPairFull( self ): |
719 |
"""Ping between first two hosts, useful for testing.
|
720 |
returns: ploss packet loss percentage"""
|
721 |
hosts = [ self.hosts[ 0 ], self.hosts[ 1 ] ] |
722 |
return self.pingFull( hosts=hosts ) |
723 |
|
724 |
@staticmethod
|
725 |
def _parseIperf( iperfOutput ): |
726 |
"""Parse iperf output and return bandwidth.
|
727 |
iperfOutput: string
|
728 |
returns: result string"""
|
729 |
r = r'([\d\.]+ \w+/sec)'
|
730 |
m = re.findall( r, iperfOutput ) |
731 |
if m:
|
732 |
return m[-1] |
733 |
else:
|
734 |
# was: raise Exception(...)
|
735 |
error( 'could not parse iperf output: ' + iperfOutput )
|
736 |
return '' |
737 |
|
738 |
# XXX This should be cleaned up
|
739 |
|
740 |
def iperf( self, hosts=None, l4Type='TCP', udpBw='10M', fmt=None, |
741 |
seconds=5, port=5001): |
742 |
"""Run iperf between two hosts.
|
743 |
hosts: list of hosts; if None, uses first and last hosts
|
744 |
l4Type: string, one of [ TCP, UDP ]
|
745 |
udpBw: bandwidth target for UDP test
|
746 |
fmt: iperf format argument if any
|
747 |
seconds: iperf time to transmit
|
748 |
port: iperf port
|
749 |
returns: two-element array of [ server, client ] speeds
|
750 |
note: send() is buffered, so client rate can be much higher than
|
751 |
the actual transmission rate; on an unloaded system, server
|
752 |
rate should be much closer to the actual receive rate"""
|
753 |
hosts = hosts or [ self.hosts[ 0 ], self.hosts[ -1 ] ] |
754 |
assert len( hosts ) == 2 |
755 |
client, server = hosts |
756 |
output( '*** Iperf: testing', l4Type, 'bandwidth between', |
757 |
client, 'and', server, '\n' ) |
758 |
server.cmd( 'killall -9 iperf' )
|
759 |
iperfArgs = 'iperf -p %d ' % port
|
760 |
bwArgs = ''
|
761 |
if l4Type == 'UDP': |
762 |
iperfArgs += '-u '
|
763 |
bwArgs = '-b ' + udpBw + ' ' |
764 |
elif l4Type != 'TCP': |
765 |
raise Exception( 'Unexpected l4 type: %s' % l4Type ) |
766 |
if fmt:
|
767 |
iperfArgs += '-f %s ' % fmt
|
768 |
server.sendCmd( iperfArgs + '-s' )
|
769 |
if l4Type == 'TCP': |
770 |
if not waitListening( client, server.IP(), port ): |
771 |
raise Exception( 'Could not connect to iperf on port %d' |
772 |
% port ) |
773 |
cliout = client.cmd( iperfArgs + '-t %d -c ' % seconds +
|
774 |
server.IP() + ' ' + bwArgs )
|
775 |
debug( 'Client output: %s\n' % cliout )
|
776 |
servout = ''
|
777 |
# We want the last *b/sec from the iperf server output
|
778 |
# for TCP, there are two fo them because of waitListening
|
779 |
count = 2 if l4Type == 'TCP' else 1 |
780 |
while len( re.findall( '/sec', servout ) ) < count: |
781 |
servout += server.monitor( timeoutms=5000 )
|
782 |
server.sendInt() |
783 |
servout += server.waitOutput() |
784 |
debug( 'Server output: %s\n' % servout )
|
785 |
result = [ self._parseIperf( servout ), self._parseIperf( cliout ) ] |
786 |
if l4Type == 'UDP': |
787 |
result.insert( 0, udpBw )
|
788 |
output( '*** Results: %s\n' % result )
|
789 |
return result
|
790 |
|
791 |
def runCpuLimitTest( self, cpu, duration=5 ): |
792 |
"""run CPU limit test with 'while true' processes.
|
793 |
cpu: desired CPU fraction of each host
|
794 |
duration: test duration in seconds (integer)
|
795 |
returns a single list of measured CPU fractions as floats.
|
796 |
"""
|
797 |
cores = int( quietRun( 'nproc' ) ) |
798 |
pct = cpu * 100
|
799 |
info( '*** Testing CPU %.0f%% bandwidth limit\n' % pct )
|
800 |
hosts = self.hosts
|
801 |
cores = int( quietRun( 'nproc' ) ) |
802 |
# number of processes to run a while loop on per host
|
803 |
num_procs = int( ceil( cores * cpu ) )
|
804 |
pids = {} |
805 |
for h in hosts: |
806 |
pids[ h ] = [] |
807 |
for _core in range( num_procs ): |
808 |
h.cmd( 'while true; do a=1; done &' )
|
809 |
pids[ h ].append( h.cmd( 'echo $!' ).strip() )
|
810 |
outputs = {} |
811 |
time = {} |
812 |
# get the initial cpu time for each host
|
813 |
for host in hosts: |
814 |
outputs[ host ] = [] |
815 |
with open( '/sys/fs/cgroup/cpuacct/%s/cpuacct.usage' % |
816 |
host, 'r' ) as f: |
817 |
time[ host ] = float( f.read() )
|
818 |
for _ in range( duration ): |
819 |
sleep( 1 )
|
820 |
for host in hosts: |
821 |
with open( '/sys/fs/cgroup/cpuacct/%s/cpuacct.usage' % |
822 |
host, 'r' ) as f: |
823 |
readTime = float( f.read() )
|
824 |
outputs[ host ].append( ( ( readTime - time[ host ] ) |
825 |
/ 1000000000 ) / cores * 100 ) |
826 |
time[ host ] = readTime |
827 |
for h, pids in pids.items(): |
828 |
for pid in pids: |
829 |
h.cmd( 'kill -9 %s' % pid )
|
830 |
cpu_fractions = [] |
831 |
for _host, outputs in outputs.items(): |
832 |
for pct in outputs: |
833 |
cpu_fractions.append( pct ) |
834 |
output( '*** Results: %s\n' % cpu_fractions )
|
835 |
return cpu_fractions
|
836 |
|
837 |
# BL: I think this can be rewritten now that we have
|
838 |
# a real link class.
|
839 |
def configLinkStatus( self, src, dst, status ): |
840 |
"""Change status of src <-> dst links.
|
841 |
src: node name
|
842 |
dst: node name
|
843 |
status: string {up, down}"""
|
844 |
if src not in self.nameToNode: |
845 |
error( 'src not in network: %s\n' % src )
|
846 |
elif dst not in self.nameToNode: |
847 |
error( 'dst not in network: %s\n' % dst )
|
848 |
else:
|
849 |
if isinstance( src, basestring ): |
850 |
src = self.nameToNode[ src ]
|
851 |
if isinstance( dst, basestring ): |
852 |
dst = self.nameToNode[ dst ]
|
853 |
connections = src.connectionsTo( dst ) |
854 |
if len( connections ) == 0: |
855 |
error( 'src and dst not connected: %s %s\n' % ( src, dst) )
|
856 |
for srcIntf, dstIntf in connections: |
857 |
result = srcIntf.ifconfig( status ) |
858 |
if result:
|
859 |
error( 'link src status change failed: %s\n' % result )
|
860 |
result = dstIntf.ifconfig( status ) |
861 |
if result:
|
862 |
error( 'link dst status change failed: %s\n' % result )
|
863 |
|
864 |
def interact( self ): |
865 |
"Start network and run our simple CLI."
|
866 |
self.start()
|
867 |
result = CLI( self )
|
868 |
self.stop()
|
869 |
return result
|
870 |
|
871 |
inited = False
|
872 |
|
873 |
@classmethod
|
874 |
def init( cls ): |
875 |
"Initialize Mininet"
|
876 |
if cls.inited:
|
877 |
return
|
878 |
ensureRoot() |
879 |
fixLimits() |
880 |
cls.inited = True
|
881 |
|
882 |
|
883 |
class MininetWithControlNet( Mininet ): |
884 |
|
885 |
"""Control network support:
|
886 |
|
887 |
Create an explicit control network. Currently this is only
|
888 |
used/usable with the user datapath.
|
889 |
|
890 |
Notes:
|
891 |
|
892 |
1. If the controller and switches are in the same (e.g. root)
|
893 |
namespace, they can just use the loopback connection.
|
894 |
|
895 |
2. If we can get unix domain sockets to work, we can use them
|
896 |
instead of an explicit control network.
|
897 |
|
898 |
3. Instead of routing, we could bridge or use 'in-band' control.
|
899 |
|
900 |
4. Even if we dispense with this in general, it could still be
|
901 |
useful for people who wish to simulate a separate control
|
902 |
network (since real networks may need one!)
|
903 |
|
904 |
5. Basically nobody ever used this code, so it has been moved
|
905 |
into its own class.
|
906 |
|
907 |
6. Ultimately we may wish to extend this to allow us to create a
|
908 |
control network which every node's control interface is
|
909 |
attached to."""
|
910 |
|
911 |
def configureControlNetwork( self ): |
912 |
"Configure control network."
|
913 |
self.configureRoutedControlNetwork()
|
914 |
|
915 |
# We still need to figure out the right way to pass
|
916 |
# in the control network location.
|
917 |
|
918 |
def configureRoutedControlNetwork( self, ip='192.168.123.1', |
919 |
prefixLen=16 ):
|
920 |
"""Configure a routed control network on controller and switches.
|
921 |
For use with the user datapath only right now."""
|
922 |
controller = self.controllers[ 0 ] |
923 |
info( controller.name + ' <->' )
|
924 |
cip = ip |
925 |
snum = ipParse( ip ) |
926 |
for switch in self.switches: |
927 |
info( ' ' + switch.name )
|
928 |
link = self.link( switch, controller, port1=0 ) |
929 |
sintf, cintf = link.intf1, link.intf2 |
930 |
switch.controlIntf = sintf |
931 |
snum += 1
|
932 |
while snum & 0xff in [ 0, 255 ]: |
933 |
snum += 1
|
934 |
sip = ipStr( snum ) |
935 |
cintf.setIP( cip, prefixLen ) |
936 |
sintf.setIP( sip, prefixLen ) |
937 |
controller.setHostRoute( sip, cintf ) |
938 |
switch.setHostRoute( cip, sintf ) |
939 |
info( '\n' )
|
940 |
info( '*** Testing control network\n' )
|
941 |
while not cintf.isUp(): |
942 |
info( '*** Waiting for', cintf, 'to come up\n' ) |
943 |
sleep( 1 )
|
944 |
for switch in self.switches: |
945 |
while not sintf.isUp(): |
946 |
info( '*** Waiting for', sintf, 'to come up\n' ) |
947 |
sleep( 1 )
|
948 |
if self.ping( hosts=[ switch, controller ] ) != 0: |
949 |
error( '*** Error: control network test failed\n' )
|
950 |
exit( 1 ) |
951 |
info( '\n' )
|