Statistics
| Branch: | Tag: | Revision:

mininet / mininet / net.py @ 54037995

History | View | Annotate | Download (22.5 KB)

1
#!/usr/bin/python
2
"""Mininet: A simple networking testbed for OpenFlow!
3

4
@author Bob Lantz (rlantz@cs.stanford.edu)
5
@author Brandon Heller (brandonh@stanford.edu)
6

7
Mininet creates scalable OpenFlow test networks by using
8
process-based virtualization and network namespaces. 
9

10
Simulated hosts are created as processes in separate network
11
namespaces. This allows a complete OpenFlow network to be simulated on
12
top of a single Linux kernel.
13

14
Each host has:
15
   A virtual console (pipes to a shell)
16
   A virtual interfaces (half of a veth pair)
17
   A parent shell (and possibly some child processes) in a namespace
18
   
19
Hosts have a network interface which is configured via ifconfig/ip
20
link/etc.
21

22
This version supports both the kernel and user space datapaths
23
from the OpenFlow reference implementation.
24

25
In kernel datapath mode, the controller and switches are simply
26
processes in the root namespace.
27

28
Kernel OpenFlow datapaths are instantiated using dpctl(8), and are
29
attached to the one side of a veth pair; the other side resides in the
30
host namespace. In this mode, switch processes can simply connect to the
31
controller via the loopback interface.
32

33
In user datapath mode, the controller and switches are full-service
34
nodes that live in their own network namespaces and have management
35
interfaces and IP addresses on a control network (e.g. 10.0.123.1,
36
currently routed although it could be bridged.)
37

38
In addition to a management interface, user mode switches also have
39
several switch interfaces, halves of veth pairs whose other halves
40
reside in the host nodes that the switches are connected to.
41

42
Naming:
43
   Host nodes are named h1-hN
44
   Switch nodes are named s0-sN
45
   Interfaces are named {nodename}-eth0 .. {nodename}-ethN,
46

47
"""
48
import os
49
import re
50
import signal
51
from subprocess import call
52
import sys
53
from time import sleep
54

    
55
from mininet.logging_mod import lg
56
from mininet.node import KernelSwitch
57
from mininet.util import quietRun, fixLimits
58
from mininet.util import make_veth_pair, move_intf, retry, MOVEINTF_DELAY
59
from mininet.xterm import cleanUpScreens, makeXterms
60

    
61
DATAPATHS = ['kernel'] #['user', 'kernel']
62

    
63

    
64
def init():
65
    "Initialize Mininet."
66
    if os.getuid() != 0:
67
        # Note: this script must be run as root
68
        # Perhaps we should do so automatically!
69
        print "*** Mininet must run as root."
70
        exit(1)
71
    # If which produces no output, then netns is not in the path.
72
    # May want to loosen this to handle netns in the current dir.
73
    if not quietRun(['which', 'netns']):
74
        raise Exception("Could not find netns; see INSTALL")
75
    fixLimits()
76

    
77

    
78
class Mininet(object):
79
    '''Network emulation with hosts spawned in network namespaces.'''
80

    
81
    def __init__(self, topo, switch, host, controller, cparams,
82
                 build = True, xterms = False, cleanup = False,
83
                 in_namespace = False,
84
                 auto_set_macs = False, auto_static_arp = False):
85
        '''Create Mininet object.
86

87
        @param topo Topo object
88
        @param switch Switch class
89
        @param host Host class
90
        @param controller Controller class
91
        @param cparams ControllerParams object
92
        @param now build now?
93
        @param xterms if build now, spawn xterms?
94
        @param cleanup if build now, cleanup before creating?
95
        @param in_namespace spawn switches and hosts in their own namespace?
96
        @param auto_set_macs set MAC addrs to DPIDs?
97
        @param auto_static_arp set all-pairs static MAC addrs?
98
        '''
99
        self.topo = topo
100
        self.switch = switch
101
        self.host = host
102
        self.controller = controller
103
        self.cparams = cparams
104
        self.nodes = {} # dpid to Node{Host, Switch} objects
105
        self.controllers = {} # controller name to Controller objects
106
        self.dps = 0 # number of created kernel datapaths
107
        self.in_namespace = in_namespace
108
        self.xterms = xterms
109
        self.cleanup = cleanup
110
        self.auto_set_macs = auto_set_macs
111
        self.auto_static_arp = auto_static_arp
112

    
113
        self.terms = [] # list of spawned xterm processes
114

    
115
        self.kernel = True #temporary!
116

    
117
        if build:
118
            self.build()
119

    
120
    def _add_host(self, dpid):
121
        '''Add host.
122

123
        @param dpid DPID of host to add
124
        '''
125
        host = self.host('h_' + self.topo.name(dpid))
126
        # for now, assume one interface per host.
127
        host.intfs.append('h_' + self.topo.name(dpid) + '-eth0')
128
        self.nodes[dpid] = host
129
        #lg.info('%s ' % host.name)
130

    
131
    def _add_switch(self, dpid):
132
        '''Add switch.
133

134
        @param dpid DPID of switch to add
135
        '''
136
        sw = None
137
        sw_dpid = None
138
        if self.auto_set_macs:
139
            sw_dpid = dpid
140
        if self.switch is KernelSwitch:
141
            sw = self.switch('s_' + self.topo.name(dpid), dp = self.dps,
142
                             dpid = sw_dpid)
143
            self.dps += 1
144
        else:
145
            sw = self.switch('s_' + self.topo.name(dpid))
146
        self.nodes[dpid] = sw
147

    
148
    def _add_link(self, src, dst):
149
        '''Add link.
150

151
        @param src source DPID
152
        @param dst destination DPID
153
        '''
154
        src_port, dst_port = self.topo.port(src, dst)
155
        src_node = self.nodes[src]
156
        dst_node = self.nodes[dst]
157
        src_intf = src_node.intfName(src_port)
158
        dst_intf = dst_node.intfName(dst_port)
159
        make_veth_pair(src_intf, dst_intf)
160
        src_node.intfs.append(src_intf)
161
        dst_node.intfs.append(dst_intf)
162
        #lg.info('\n')
163
        #lg.info('added intf %s to src node %x\n' % (src_intf, src))
164
        #lg.info('added intf %s to dst node %x\n' % (dst_intf, dst))
165
        if src_node.inNamespace:
166
            #lg.info('moving src w/inNamespace set\n')
167
            retry(3, MOVEINTF_DELAY, move_intf, src_intf, src_node)
168
        if dst_node.inNamespace:
169
            #lg.info('moving dst w/inNamespace set\n')
170
            retry(3, MOVEINTF_DELAY, move_intf, dst_intf, dst_node)
171
        src_node.connection[src_intf] = (dst_node, dst_intf)
172
        dst_node.connection[dst_intf] = (src_node, src_intf)
173

    
174
    def _add_controller(self, controller):
175
        '''Add controller.
176

177
        @param controller Controller class
178
        '''
179
        controller = self.controller('c0', not self.kernel)
180
        if controller: # allow controller-less setups
181
            self.controllers['c0'] = controller
182

    
183
    # Control network support:
184
    #
185
    # Create an explicit control network. Currently this is only
186
    # used by the user datapath configuration.
187
    #
188
    # Notes:
189
    #
190
    # 1. If the controller and switches are in the same (e.g. root)
191
    #    namespace, they can just use the loopback connection.
192
    #    We may wish to do this for the user datapath as well as the
193
    #    kernel datapath.
194
    #
195
    # 2. If we can get unix domain sockets to work, we can use them
196
    #    instead of an explicit control network.
197
    #
198
    # 3. Instead of routing, we could bridge or use 'in-band' control.
199
    #
200
    # 4. Even if we dispense with this in general, it could still be
201
    #    useful for people who wish to simulate a separate control
202
    #    network (since real networks may need one!)
203

    
204
    def _configureControlNetwork(self):
205
        '''Configure control network.'''
206
        self._configureRoutedControlNetwork()
207

    
208
    def _configureRoutedControlNetwork(self):
209
        '''Configure a routed control network on controller and switches.
210

211
        For use with the user datapath only right now.
212

213
        @todo(brandonh) Test this code and verify that user-space works!
214
        '''
215
        # params were: controller, switches, ips
216

    
217
        controller = self.controllers['c0']
218
        lg.info('%s <-> ' % controller.name)
219
        for switch_dpid in self.topo.switches():
220
            switch = self.nodes[switch_dpid]
221
            lg.info('%s ' % switch.name)
222
            sip = self.topo.ip(switch_dpid)#ips.next()
223
            sintf = switch.intfs[0]
224
            node, cintf = switch.connection[sintf]
225
            if node != controller:
226
                lg.error('*** Error: switch %s not connected to correct'
227
                         'controller' %
228
                         switch.name)
229
                exit(1)
230
            controller.setIP(cintf, self.cparams.ip, '/' +
231
                             self.cparams.subnet_size)
232
            switch.setIP(sintf, sip, '/' + self.cparams.subnet_size)
233
            controller.setHostRoute(sip, cintf)
234
            switch.setHostRoute(self.cparams.ip, sintf)
235
        lg.info('\n')
236
        lg.info('*** Testing control network\n')
237
        while not controller.intfIsUp(controller.intfs[0]):
238
            lg.info('*** Waiting for %s to come up\n', controller.intfs[0])
239
            sleep(1)
240
        for switch_dpid in self.topo.switches():
241
            switch = self.nodes[switch_dpid]
242
            while not switch.intfIsUp(switch.intfs[0]):
243
                lg.info('*** Waiting for %s to come up\n' % switch.intfs[0])
244
                sleep(1)
245
            if self.ping_test(hosts=[switch, controller]) != 0:
246
                lg.error('*** Error: control network test failed\n')
247
                exit(1)
248
        lg.info('\n')
249

    
250
    def _config_hosts( self ):
251
        '''Configure a set of hosts.'''
252
        # params were: hosts, ips
253
        for host_dpid in self.topo.hosts():
254
            host = self.nodes[host_dpid]
255
            hintf = host.intfs[0]
256
            host.setIP(hintf, self.topo.ip(host_dpid),
257
                       '/' + str(self.cparams.subnet_size))
258
            host.setDefaultRoute(hintf)
259
            # You're low priority, dude!
260
            quietRun('renice +18 -p ' + repr(host.pid))
261
            lg.info('%s ', host.name)
262
        lg.info('\n')
263

    
264
    def build(self):
265
        '''Build mininet.
266

267
        At the end of this function, everything should be connected and up.
268
        '''
269
        if self.cleanup:
270
            pass # cleanup
271
        # validate topo?
272
        kernel = self.kernel
273
        if kernel:
274
            lg.info('*** Using kernel datapath\n')
275
        else:
276
            lg.info('*** Using user datapath\n')
277
        lg.info('*** Adding controller\n')
278
        self._add_controller(self.controller)
279
        lg.info('*** Creating network\n')
280
        lg.info('*** Adding hosts:\n')
281
        for host in sorted(self.topo.hosts()):
282
            self._add_host(host)
283
            lg.info('0x%x ' % host)
284
        lg.info('\n*** Adding switches:\n')
285
        for switch in sorted(self.topo.switches()):
286
            self._add_switch(switch)
287
            lg.info('0x%x ' % switch)
288
        lg.info('\n*** Adding edges: ')
289
        for src, dst in sorted(self.topo.edges()):
290
            self._add_link(src, dst)
291
            lg.info('(0x%x, 0x%x) ' % (src, dst))
292
        lg.info('\n')
293

    
294
        if not kernel:
295
            lg.info('*** Configuring control network\n')
296
            self._configureControlNetwork()
297

    
298
        lg.info('*** Configuring hosts\n')
299
        self._config_hosts()
300

    
301
        if self.xterms:
302
            self.start_xterms()
303
        if self.auto_set_macs:
304
            self.set_macs()
305
        if self.auto_static_arp:
306
            self.static_arp()
307

    
308
    def switch_nodes(self):
309
        '''Return switch nodes.'''
310
        return [self.nodes[dpid] for dpid in self.topo.switches()]
311

    
312
    def host_nodes(self):
313
        '''Return host nodes.'''
314
        return [self.nodes[dpid] for dpid in self.topo.hosts()]
315

    
316
    def start_xterms(self):
317
        '''Start an xterm for each node in the topo.'''
318
        lg.info("*** Running xterms on %s\n" % os.environ['DISPLAY'])
319
        cleanUpScreens()
320
        self.terms += makeXterms(self.controllers.values(), 'controller')
321
        self.terms += makeXterms(self.switch_nodes(), 'switch')
322
        self.terms += makeXterms(self.host_nodes(), 'host')
323

    
324
    def stop_xterms(self):
325
        '''Kill each xterm.'''
326
        # Kill xterms
327
        for term in self.terms:
328
            os.kill(term.pid, signal.SIGKILL)
329
        cleanUpScreens()
330

    
331
    def set_macs(self):
332
        '''Set MAC addrs to correspond to datapath IDs on hosts.
333

334
        Assume that the host only has one interface.
335
        '''
336
        for dpid in self.topo.hosts():
337
            host_node = self.nodes[dpid]
338
            host_node.setMAC(host_node.intfs[0], dpid)
339

    
340
    def static_arp(self):
341
        '''Add all-pairs ARP entries to remove the need to handle broadcast.'''
342
        for src in self.topo.hosts():
343
            src_node = self.nodes[src]
344
            for dst in self.topo.hosts():
345
                if src != dst:
346
                    src_node.setARP(dst, dst)
347

    
348
    def start(self):
349
        '''Start controller and switches\n'''
350
        lg.info('*** Starting controller\n')
351
        for cname, cnode in self.controllers.iteritems():
352
            cnode.start()
353
        lg.info('*** Starting %s switches\n' % len(self.topo.switches()))
354
        for switch_dpid in self.topo.switches():
355
            switch = self.nodes[switch_dpid]
356
            #lg.info('switch = %s' % switch)
357
            lg.info('0x%x ' % switch_dpid)
358
            switch.start(self.controllers)
359
        lg.info('\n')
360

    
361
    def stop(self):
362
        '''Stop the controller(s), switches and hosts\n'''
363
        if self.terms:
364
            lg.info('*** Stopping %i terms\n' % len(self.terms))
365
            self.stop_xterms()
366
        lg.info('*** Stopping %i hosts\n' % len(self.topo.hosts()))
367
        for host_dpid in self.topo.hosts():
368
            host = self.nodes[host_dpid]
369
            lg.info('%s ' % host.name)
370
            host.terminate()
371
        lg.info('\n')
372
        lg.info('*** Stopping %i switches\n' % len(self.topo.switches()))
373
        for switch_dpid in self.topo.switches():
374
            switch = self.nodes[switch_dpid]
375
            lg.info('%s' % switch.name)
376
            switch.stop()
377
        lg.info('\n')
378
        lg.info('*** Stopping controller\n')
379
        for cname, cnode in self.controllers.iteritems():
380
            cnode.stop()
381
        lg.info('*** Test complete\n')
382

    
383
    def run(self, test, **params):
384
        '''Perform a complete start/test/stop cycle.'''
385
        self.start()
386
        lg.info('*** Running test\n')
387
        result = getattr(self, test)(**params)
388
        self.stop()
389
        return result
390

    
391
    @staticmethod
392
    def _parse_ping(pingOutput):
393
        '''Parse ping output and return packets sent, received.'''
394
        r = r'(\d+) packets transmitted, (\d+) received'
395
        m = re.search( r, pingOutput )
396
        if m == None:
397
            lg.error('*** Error: could not parse ping output: %s\n' %
398
                     pingOutput)
399
            exit(1)
400
        sent, received = int(m.group(1)), int(m.group(2))
401
        return sent, received
402

    
403
    def ping(self, hosts = None):
404
        '''Ping between all specified hosts.
405

406
        @param hosts list of host DPIDs
407
        @return ploss packet loss percentage
408
        '''
409
        #self.start()
410
        # check if running - only then, start?
411
        packets = 0
412
        lost = 0
413
        ploss = None
414
        if not hosts:
415
            hosts = self.topo.hosts()
416
        lg.info('*** Ping: testing ping reachability\n')
417
        for node_dpid in hosts:
418
            node = self.nodes[node_dpid]
419
            lg.info('%s -> ' % node.name)
420
            for dest_dpid in hosts:
421
                dest = self.nodes[dest_dpid]
422
                if node != dest:
423
                    result = node.cmd('ping -c1 ' + dest.IP())
424
                    sent, received = self._parse_ping(result)
425
                    packets += sent
426
                    if received > sent:
427
                        lg.error('*** Error: received too many packets')
428
                        lg.error('%s' % result)
429
                        node.cmdPrint('route')
430
                        exit( 1 )
431
                    lost += sent - received
432
                    lg.info(('%s ' % dest.name) if received else 'X ')
433
            lg.info('\n')
434
            ploss = 100 * lost / packets
435
        lg.info("*** Results: %i%% dropped (%d/%d lost)\n" %
436
                (ploss, lost, packets))
437
        return ploss
438

    
439
    def ping_all(self):
440
        '''Ping between all hosts.
441

442
        @return ploss packet loss percentage
443
        '''
444
        return self.ping()
445

    
446
    def ping_pair(self):
447
        '''Ping between first two hosts, useful for testing.
448

449
        @return ploss packet loss percentage
450
        '''
451
        hosts_sorted = sorted(self.topo.hosts())
452
        hosts = [hosts_sorted[0], hosts_sorted[1]]
453
        return self.ping(hosts = hosts)
454

    
455
    @staticmethod
456
    def _parseIperf(iperfOutput):
457
        '''Parse iperf output and return bandwidth.
458

459
        @param iperfOutput string
460
        @return result string
461
        '''
462
        r = r'([\d\.]+ \w+/sec)'
463
        m = re.search(r, iperfOutput)
464
        if m:
465
            return m.group(1)
466
        else:
467
            raise Exception('could not parse iperf output')
468

    
469
    def iperf(self, hosts = None, l4_type = 'TCP', udp_bw = '10M',
470
              verbose = False):
471
        '''Run iperf between two hosts.
472

473
        @param hosts list of host DPIDs; if None, uses opposite hosts
474
        @param l4_type string, one of [TCP, UDP]
475
        @param verbose verbose printing
476
        @return results two-element array of server and client speeds
477
        '''
478
        if not hosts:
479
            hosts_sorted = sorted(self.topo.hosts())
480
            hosts = [hosts_sorted[0], hosts_sorted[-1]]
481
        else:
482
            assert len(hosts) == 2
483
        host0 = self.nodes[hosts[0]]
484
        host1 = self.nodes[hosts[1]]
485
        lg.info('*** Iperf: testing ' + l4_type + ' bandwidth between ')
486
        lg.info("%s and %s\n" % (host0.name, host1.name))
487
        host0.cmd('killall -9 iperf')
488
        iperf_args = 'iperf '
489
        bw_args = ''
490
        if l4_type == 'UDP':
491
            iperf_args += '-u '
492
            bw_args = '-b ' + udp_bw + ' '
493
        elif l4_type != 'TCP':
494
            raise Exception('Unexpected l4 type: %s' % l4_type)
495
        server = host0.cmd(iperf_args + '-s &')
496
        if verbose:
497
            lg.info('%s\n' % server)
498
        client = host1.cmd(iperf_args + '-t 5 -c ' + host0.IP() + ' ' + bw_args)
499
        if verbose:
500
            lg.info('%s\n' % client)
501
        server = host0.cmd('killall -9 iperf')
502
        if verbose:
503
            lg.info('%s\n' % server)
504
        result = [self._parseIperf(server), self._parseIperf(client)]
505
        if l4_type == 'UDP':
506
            result.insert(0, udp_bw)
507
        lg.info('*** Results: %s\n' % result)
508
        return result
509

    
510
    def iperf_udp(self, udp_bw = '10M'):
511
        '''Run iperf UDP test.'''
512
        return self.iperf(l4_type = 'UDP', udp_bw = udp_bw)
513

    
514
    def interact(self):
515
        '''Start network and run our simple CLI.'''
516
        self.start()
517
        result = MininetCLI(self)
518
        self.stop()
519
        return result
520

    
521

    
522
class MininetCLI(object):
523
    '''Simple command-line interface to talk to nodes.'''
524
    cmds = ['?', 'help', 'nodes', 'net', 'sh', 'ping_all', 'exit', \
525
            'ping_pair', 'iperf', 'iperf_udp']
526

    
527
    def __init__(self, mininet):
528
        self.mn = mininet
529
        self.nodemap = {} # map names to Node objects
530
        for node in self.mn.nodes.values():
531
            self.nodemap[node.name] = node
532
        for cname, cnode in self.mn.controllers.iteritems():
533
            self.nodemap[cname] = cnode
534
        self.nodelist = self.nodemap.values()
535
        self.run()
536

    
537
    # Commands
538
    def help(self, args):
539
        '''Semi-useful help for CLI.'''
540
        help_str = 'Available commands are:' + str(self.cmds) + '\n' + \
541
                   'You may also send a command to a node using:\n' + \
542
                   '  <node> command {args}\n' + \
543
                   'For example:\n' + \
544
                   '  mininet> h0 ifconfig\n' + \
545
                   '\n' + \
546
                   'The interpreter automatically substitutes IP ' + \
547
                   'addresses\n' + \
548
                   'for node names, so commands like\n' + \
549
                   '  mininet> h0 ping -c1 h1\n' + \
550
                   'should work.\n' + \
551
                   '\n\n' + \
552
                   'Interactive commands are not really supported yet,\n' + \
553
                   'so please limit commands to ones that do not\n' + \
554
                   'require user interaction and will terminate\n' + \
555
                   'after a reasonable amount of time.\n'
556
        print(help_str)
557

    
558
    def nodes(self, args):
559
        '''List all nodes.'''
560
        lg.info('available nodes are: \n%s\n',
561
                ' '.join([node.name for node in sorted(self.nodelist)]))
562

    
563
    def net(self, args):
564
        '''List network connection.'''
565
        for switch_dpid in self.mn.topo.switches():
566
            switch = self.mn.nodes[switch_dpid]
567
            lg.info('%s <->', switch.name)
568
            for intf in switch.intfs:
569
                node, remoteIntf = switch.connection[intf]
570
                lg.info(' %s' % node.name)
571
            lg.info('\n')
572

    
573
    def sh(self, args):
574
        '''Run an external shell command'''
575
        call(['sh', '-c'] + args)
576

    
577
    def ping_all(self, args):
578
        '''Ping between all hosts.'''
579
        self.mn.ping_all()
580

    
581
    def ping_pair(self, args):
582
        '''Ping between first two hosts, useful for testing.'''
583
        self.mn.ping_pair()
584

    
585
    def iperf(self, args):
586
        '''Simple iperf TCP test between two hosts.'''
587
        self.mn.iperf()
588

    
589
    def iperf_udp(self, args):
590
        '''Simple iperf UDP test between two hosts.'''
591
        udp_bw = args[0] if len(args) else '10M'
592
        self.mn.iperf_udp(udp_bw)
593

    
594
    def run(self):
595
        '''Read and execute commands.'''
596
        lg.warn('*** Starting CLI:\n')
597
        while True:
598
            lg.warn('mininet> ')
599
            input = sys.stdin.readline()
600
            if input == '':
601
                break
602
            if input[-1] == '\n':
603
                input = input[:-1]
604
            cmd = input.split(' ')
605
            first = cmd[0]
606
            rest = cmd[1:]
607
            if first in self.cmds and hasattr(self, first):
608
                getattr(self, first)(rest)
609
            elif first in self.nodemap and rest != []:
610
                node = self.nodemap[first]
611
                # Substitute IP addresses for node names in command
612
                rest = [self.nodemap[arg].IP() if arg in self.nodemap else arg
613
                        for arg in rest]
614
                rest = ' '.join(rest)
615
                # Interactive commands don't work yet, and
616
                # there are still issues with control-c
617
                lg.warn('*** %s: running %s\n' % (node.name, rest))
618
                node.sendCmd(rest)
619
                while True:
620
                    try:
621
                        done, data = node.monitor()
622
                        lg.info('%s\n' % data)
623
                        if done:
624
                            break
625
                    except KeyboardInterrupt:
626
                        node.sendInt()
627
            elif first == '':
628
                pass
629
            elif first in ['exit', 'quit']:
630
                break
631
            elif first == '?':
632
                self.help( rest )
633
            else:
634
                lg.error('CLI: unknown node or command: < %s >\n' % first)
635
            #lg.info('*** CLI: command complete\n')
636
        return 'exited by user command'