Statistics
| Branch: | Tag: | Revision:

mininet / mininet / net.py @ 723d068c

History | View | Annotate | Download (22.9 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 controller in net namespaces?
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
        if build:
116
            self.build()
117

    
118
    def _add_host(self, dpid):
119
        '''Add host.
120

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

    
129
    def _add_switch(self, dpid):
130
        '''Add switch.
131

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

    
146
    def _add_link(self, src, dst):
147
        '''Add link.
148

149
        @param src source DPID
150
        @param dst destination DPID
151
        '''
152
        src_port, dst_port = self.topo.port(src, dst)
153
        src_node = self.nodes[src]
154
        dst_node = self.nodes[dst]
155
        src_intf = src_node.intfName(src_port)
156
        dst_intf = dst_node.intfName(dst_port)
157
        make_veth_pair(src_intf, dst_intf)
158
        src_node.intfs.append(src_intf)
159
        dst_node.intfs.append(dst_intf)
160
        src_node.ports[src_port] = src_intf
161
        dst_node.ports[dst_port] = 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', self.in_namespace)
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!
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(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
        lg.info('*** Adding controller\n')
273
        self._add_controller(self.controller)
274
        lg.info('*** Creating network\n')
275
        lg.info('*** Adding hosts:\n')
276
        for host in sorted(self.topo.hosts()):
277
            self._add_host(host)
278
            lg.info('0x%x ' % host)
279
        lg.info('\n*** Adding switches:\n')
280
        for switch in sorted(self.topo.switches()):
281
            self._add_switch(switch)
282
            lg.info('0x%x ' % switch)
283
        lg.info('\n*** Adding edges:\n')
284
        for src, dst in sorted(self.topo.edges()):
285
            self._add_link(src, dst)
286
            lg.info('(0x%x, 0x%x) ' % (src, dst))
287
        lg.info('\n')
288

    
289
        if self.in_namespace:
290
            lg.info('*** Configuring control network\n')
291
            self._configureControlNetwork()
292

    
293
        lg.info('*** Configuring hosts\n')
294
        self._config_hosts()
295

    
296
        if self.xterms:
297
            self.start_xterms()
298
        if self.auto_set_macs:
299
            self.set_macs()
300
        if self.auto_static_arp:
301
            self.static_arp()
302

    
303
    def switch_nodes(self):
304
        '''Return switch nodes.'''
305
        return [self.nodes[dpid] for dpid in self.topo.switches()]
306

    
307
    def host_nodes(self):
308
        '''Return host nodes.'''
309
        return [self.nodes[dpid] for dpid in self.topo.hosts()]
310

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

    
319
    def stop_xterms(self):
320
        '''Kill each xterm.'''
321
        # Kill xterms
322
        for term in self.terms:
323
            os.kill(term.pid, signal.SIGKILL)
324
        cleanUpScreens()
325

    
326
    def set_macs(self):
327
        '''Set MAC addrs to correspond to datapath IDs on hosts.
328

329
        Assume that the host only has one interface.
330
        '''
331
        for dpid in self.topo.hosts():
332
            host_node = self.nodes[dpid]
333
            host_node.setMAC(host_node.intfs[0], dpid)
334

    
335
    def static_arp(self):
336
        '''Add all-pairs ARP entries to remove the need to handle broadcast.'''
337
        for src in self.topo.hosts():
338
            src_node = self.nodes[src]
339
            for dst in self.topo.hosts():
340
                if src != dst:
341
                    src_node.setARP(dst, dst)
342

    
343
    def start(self):
344
        '''Start controller and switches\n'''
345
        lg.info('*** Starting controller\n')
346
        for cnode in self.controllers.values():
347
            cnode.start()
348
        lg.info('*** Starting %s switches\n' % len(self.topo.switches()))
349
        for switch_dpid in self.topo.switches():
350
            switch = self.nodes[switch_dpid]
351
            #lg.info('switch = %s' % switch)
352
            lg.info('0x%x ' % switch_dpid)
353
            switch.start(self.controllers)
354
        lg.info('\n')
355

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

    
378
    def run(self, test, **params):
379
        '''Perform a complete start/test/stop cycle.'''
380
        self.start()
381
        lg.info('*** Running test\n')
382
        result = getattr(self, test)(**params)
383
        self.stop()
384
        return result
385

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

    
398
    def ping(self, hosts = None):
399
        '''Ping between all specified hosts.
400

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

    
434
    def ping_all(self):
435
        '''Ping between all hosts.
436

437
        @return ploss packet loss percentage
438
        '''
439
        return self.ping()
440

    
441
    def ping_pair(self):
442
        '''Ping between first two hosts, useful for testing.
443

444
        @return ploss packet loss percentage
445
        '''
446
        hosts_sorted = sorted(self.topo.hosts())
447
        hosts = [hosts_sorted[0], hosts_sorted[1]]
448
        return self.ping(hosts = hosts)
449

    
450
    @staticmethod
451
    def _parseIperf(iperfOutput):
452
        '''Parse iperf output and return bandwidth.
453

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

    
464
    def iperf(self, hosts = None, l4_type = 'TCP', udp_bw = '10M',
465
              verbose = False):
466
        '''Run iperf between two hosts.
467

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

    
506
    def iperf_udp(self, udp_bw = '10M'):
507
        '''Run iperf UDP test.'''
508
        return self.iperf(l4_type = 'UDP', udp_bw = udp_bw)
509

    
510
    def interact(self):
511
        '''Start network and run our simple CLI.'''
512
        self.start()
513
        result = MininetCLI(self)
514
        self.stop()
515
        return result
516

    
517

    
518
class MininetCLI(object):
519
    '''Simple command-line interface to talk to nodes.'''
520
    cmds = ['?', 'help', 'nodes', 'net', 'sh', 'ping_all', 'exit', \
521
            'ping_pair', 'iperf', 'iperf_udp', 'intfs', 'dump']
522

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

    
533
    # Disable pylint "Unused argument: 'arg's'" messages.
534
    # Each CLI function needs the same interface.
535
    # pylint: disable-msg=W0613
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 connections.'''
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 = 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 intfs(self, args):
595
        '''List interfaces.'''
596
        for node in self.mn.nodes.values():
597
            lg.info('%s: %s\n' % (node.name, ' '.join(node.intfs)))
598

    
599
    def dump(self, args):
600
        '''Dump node info.'''
601
        for node in self.mn.nodes.values():
602
            lg.info('%s\n' % node)
603

    
604
    # Re-enable pylint "Unused argument: 'arg's'" messages.
605
    # pylint: enable-msg=W0613
606

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