Statistics
| Branch: | Tag: | Revision:

mininet / mininet / net.py @ 376bcba4

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.util import quietRun, fixLimits
57
from mininet.util import make_veth_pair, move_intf, retry, MOVEINTF_DELAY
58
from mininet.xterm import cleanUpScreens, makeXterms
59

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

    
62

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

    
76

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

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

86
        @param topo Topo object
87
        @param switch Switch class
88
        @param host Host class
89
        @param controller Controller class
90
        @param cparams ControllerParams object
91
        @param now build now?
92
        @param xterms if build now, spawn xterms?
93
        @param cleanup if build now, cleanup before creating?
94
        @param in_namespace spawn switches and hosts in their own namespace?
95
        @param switch_is_kernel is the switch kernel-based?
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.switch_is_kernel = switch_is_kernel
109
        self.xterms = xterms
110
        self.cleanup = cleanup
111
        self.auto_set_macs = auto_set_macs
112
        self.auto_static_arp = auto_static_arp
113

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

    
116
        self.kernel = True #temporary!
117

    
118
        if build:
119
            self.build()
120

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

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

    
132
    def _add_switch(self, dpid):
133
        '''
134
        @param dpid DPID of switch to add
135
        '''
136
        sw = None
137
        if self.switch_is_kernel:
138
            sw = self.switch('s_' + self.topo.name(dpid), 'nl:' + str(self.dps))
139
            self.dps += 1
140
        else:
141
            sw = self.switch('s_' + self.topo.name(dpid))
142
        self.nodes[dpid] = sw
143

    
144
    def _add_link(self, src, dst):
145
        '''Add link.
146

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

    
170
    def _add_controller(self, controller):
171
        '''Add controller.
172

173
        @param controller Controller class
174
        '''
175
        controller = self.controller('c0', not self.kernel)
176
        if controller: # allow controller-less setups
177
            self.controllers['c0'] = controller
178

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

    
200
    def _configureControlNetwork(self):
201
        '''Configure control network.'''
202
        self._configureRoutedControlNetwork()
203

    
204
    def _configureRoutedControlNetwork(self):
205
        '''Configure a routed control network on controller and switches.
206

207
        For use with the user datapath only right now.
208

209
        @todo(brandonh) Test this code and verify that user-space works!
210
        '''
211
        # params were: controller, switches, ips
212

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

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

    
260
    def build(self):
261
        '''Build mininet.
262

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

    
290
        if not kernel:
291
            lg.info('*** Configuring control network\n')
292
            self._configureControlNetwork()
293

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

469
        @param hosts list of host DPIDs; if None, uses opposite hosts
470
        @param l4_type string, one of [TCP, UDP]
471
        @param verbose verbose printing
472
        @return results two-element array of server and client speeds
473
        '''
474
        if not hosts:
475
            hosts_sorted = sorted(self.topo.hosts())
476
            hosts = [hosts_sorted[0], hosts_sorted[-1]]
477
        else:
478
            assert len(hosts) == 2
479
        host0 = self.nodes[hosts[0]]
480
        host1 = self.nodes[hosts[1]]
481
        lg.info('*** Iperf: testing ' + l4_type + ' bandwidth between ')
482
        lg.info("%s and %s\n" % (host0.name, host1.name))
483
        host0.cmd('killall -9 iperf')
484
        iperf_args = 'iperf '
485
        bw_args = ''
486
        if l4_type == 'UDP':
487
            iperf_args += '-u '
488
            bw_args = '-b ' + udp_bw + ' '
489
        elif l4_type != 'TCP':
490
            raise Exception('Unexpected l4 type: %s' % l4_type)
491
        server = host0.cmd(iperf_args + '-s &')
492
        if verbose:
493
            lg.info('%s\n' % server)
494
        client = host1.cmd(iperf_args + '-t 5 -c ' + host0.IP() + ' ' + 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']
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
    # Commands
534
    def help(self, args):
535
        '''Semi-useful help for CLI.'''
536
        help_str = 'Available commands are:' + str(self.cmds) + '\n' + \
537
                   'You may also send a command to a node using:\n' + \
538
                   '  <node> command {args}\n' + \
539
                   'For example:\n' + \
540
                   '  mininet> h0 ifconfig\n' + \
541
                   '\n' + \
542
                   'The interpreter automatically substitutes IP ' + \
543
                   'addresses\n' + \
544
                   'for node names, so commands like\n' + \
545
                   '  mininet> h0 ping -c1 h1\n' + \
546
                   'should work.\n' + \
547
                   '\n\n' + \
548
                   'Interactive commands are not really supported yet,\n' + \
549
                   'so please limit commands to ones that do not\n' + \
550
                   'require user interaction and will terminate\n' + \
551
                   'after a reasonable amount of time.\n'
552
        print(help_str)
553

    
554
    def nodes(self, args):
555
        '''List all nodes.'''
556
        lg.info('available nodes are: \n%s\n',
557
                ' '.join([node.name for node in sorted(self.nodelist)]))
558

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

    
569
    def sh(self, args):
570
        '''Run an external shell command'''
571
        call(['sh', '-c'] + args)
572

    
573
    def ping_all(self, args):
574
        '''Ping between all hosts.'''
575
        self.mn.ping_all()
576

    
577
    def ping_pair(self, args):
578
        '''Ping between first two hosts, useful for testing.'''
579
        self.mn.ping_pair()
580

    
581
    def iperf(self, args):
582
        '''Simple iperf TCP test between two hosts.'''
583
        self.mn.iperf()
584

    
585
    def iperf_udp(self, args):
586
        '''Simple iperf UDP test between two hosts.'''
587
        udp_bw = args[0] if len(args) else '10M'
588
        self.mn.iperf_udp(udp_bw)
589

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