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