Statistics
| Branch: | Tag: | Revision:

mininet / mininet / net.py @ fcf6a16f

History | View | Annotate | Download (21.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
        '''Create Mininet object.
84

85
        @param topo Topo object
86
        @param switch Switch class
87
        @param host Host class
88
        @param controller Controller class
89
        @param cparams ControllerParams object
90
        @param now build now?
91
        @param xterms if build now, spawn xterms?
92
        @param cleanup if build now, cleanup before creating?
93
        @param in_namespace spawn switches and hosts in their own namespace?
94
        '''
95
        self.topo = topo
96
        self.switch = switch
97
        self.host = host
98
        self.controller = controller
99
        self.cparams = cparams
100
        self.nodes = {} # dpid to Node{Host, Switch} objects
101
        self.controllers = {} # controller name to Controller objects
102
        self.dps = 0 # number of created kernel datapaths
103
        self.in_namespace = in_namespace
104
        self.switch_is_kernel = switch_is_kernel
105

    
106
        self.terms = [] # list of spawned xterm processes
107

    
108
        self.kernel = True #temporary!
109

    
110
        if build:
111
            self.build(xterms, cleanup)
112

    
113
    def _add_host(self, dpid):
114
        '''Add host.
115

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

    
124
    def _add_switch(self, dpid):
125
        '''
126
        @param dpid DPID of switch to add
127
        '''
128
        sw = None
129
        if self.switch_is_kernel:
130
            sw = self.switch('s_' + self.topo.name(dpid), 'nl:' + str(self.dps))
131
            self.dps += 1
132
        else:
133
            sw = self.switch('s_' + self.topo.name(dpid))
134
        self.nodes[dpid] = sw
135

    
136
    def _add_link(self, src, dst):
137
        '''Add link.
138

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

    
162
    def _add_controller(self, controller):
163
        '''Add controller.
164

165
        @param controller Controller class
166
        '''
167
        controller = self.controller('c0', not self.kernel)
168
        if controller: # allow controller-less setups
169
            self.controllers['c0'] = controller
170

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

    
192
    def _configureControlNetwork(self):
193
        '''Configure control network.'''
194
        self._configureRoutedControlNetwork()
195

    
196
    def _configureRoutedControlNetwork(self):
197
        '''Configure a routed control network on controller and switches.
198

199
        For use with the user datapath only right now.
200

201
        @todo(brandonh) Test this code and verify that user-space works!
202
        '''
203
        # params were: controller, switches, ips
204

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

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

    
252
    def build(self, xterms, cleanup):
253
        '''Build mininet.
254

255
        At the end of this function, everything should be connected and up.
256

257
        @param xterms spawn xterms on build?
258
        @param cleanup cleanup before creating?
259
        '''
260
        if cleanup:
261
            pass # cleanup
262
        # validate topo?
263
        kernel = self.kernel
264
        if kernel:
265
            lg.info('*** Using kernel datapath\n')
266
        else:
267
            lg.info('*** Using user datapath\n')
268
        lg.info('*** Adding controller\n')
269
        self._add_controller(self.controller)
270
        lg.info('*** Creating network\n')
271
        lg.info('*** Adding hosts:\n')
272
        for host in sorted(self.topo.hosts()):
273
            self._add_host(host)
274
            lg.info('0x%x ' % host)
275
        lg.info('\n*** Adding switches:\n')
276
        for switch in sorted(self.topo.switches()):
277
            self._add_switch(switch)
278
            lg.info('0x%x ' % switch)
279
        lg.info('\n*** Adding edges: ')
280
        for src, dst in sorted(self.topo.edges()):
281
            self._add_link(src, dst)
282
            lg.info('(0x%x, 0x%x) ' % (src, dst))
283
        lg.info('\n')
284

    
285
        if not kernel:
286
            lg.info('*** Configuring control network\n')
287
            self._configureControlNetwork()
288

    
289
        lg.info('*** Configuring hosts\n')
290
        self._config_hosts()
291

    
292
        if xterms:
293
            self.start_xterms()
294

    
295
    def switch_nodes(self):
296
        '''Return switch nodes.'''
297
        return [self.nodes[dpid] for dpid in self.topo.switches()]
298

    
299
    def host_nodes(self):
300
        '''Return host nodes.'''
301
        return [self.nodes[dpid] for dpid in self.topo.hosts()]
302

    
303
    def start_xterms(self):
304
        '''Start an xterm for each node in the topo.'''
305
        lg.info("*** Running xterms on %s\n" % os.environ['DISPLAY'])
306
        cleanUpScreens()
307
        self.terms += makeXterms(self.controllers.values(), 'controller')
308
        self.terms += makeXterms(self.switch_nodes(), 'switch')
309
        self.terms += makeXterms(self.host_nodes(), 'host')
310

    
311
    def stop_xterms(self):
312
        '''Kill each xterm.'''
313
        # Kill xterms
314
        for term in self.terms:
315
            os.kill(term.pid, signal.SIGKILL)
316
        cleanUpScreens()
317

    
318
    def start(self):
319
        '''Start controller and switches\n'''
320
        lg.info('*** Starting controller\n')
321
        for cname, cnode in self.controllers.iteritems():
322
            cnode.start()
323
        lg.info('*** Starting %s switches\n' % len(self.topo.switches()))
324
        for switch_dpid in self.topo.switches():
325
            switch = self.nodes[switch_dpid]
326
            #lg.info('switch = %s' % switch)
327
            lg.info('0x%x ' % switch_dpid)
328
            switch.start(self.controllers)
329
        lg.info('\n')
330

    
331
    def stop(self):
332
        '''Stop the controller(s), switches and hosts\n'''
333
        if self.terms:
334
            lg.info('*** Stopping %i terms\n' % len(self.terms))
335
            self.stop_xterms()
336
        lg.info('*** Stopping %i hosts\n' % len(self.topo.hosts()))
337
        for host_dpid in self.topo.hosts():
338
            host = self.nodes[host_dpid]
339
            lg.info('%s ' % host.name)
340
            host.terminate()
341
        lg.info('\n')
342
        lg.info('*** Stopping %i switches\n' % len(self.topo.switches()))
343
        for switch_dpid in self.topo.switches():
344
            switch = self.nodes[switch_dpid]
345
            lg.info('%s' % switch.name)
346
            switch.stop()
347
        lg.info('\n')
348
        lg.info('*** Stopping controller\n')
349
        for cname, cnode in self.controllers.iteritems():
350
            cnode.stop()
351
        lg.info('*** Test complete\n')
352

    
353
    def run(self, test, **params):
354
        '''Perform a complete start/test/stop cycle.'''
355
        self.start()
356
        lg.info('*** Running test\n')
357
        result = getattr(self, test)(**params)
358
        self.stop()
359
        return result
360

    
361
    @staticmethod
362
    def _parse_ping(pingOutput):
363
        '''Parse ping output and return packets sent, received.'''
364
        r = r'(\d+) packets transmitted, (\d+) received'
365
        m = re.search( r, pingOutput )
366
        if m == None:
367
            lg.error('*** Error: could not parse ping output: %s\n' %
368
                     pingOutput)
369
            exit(1)
370
        sent, received = int(m.group(1)), int(m.group(2))
371
        return sent, received
372

    
373
    def ping(self, hosts = None):
374
        '''Ping between all specified hosts.
375

376
        @param hosts list of host DPIDs
377
        @return ploss packet loss percentage
378
        '''
379
        #self.start()
380
        # check if running - only then, start?
381
        packets = 0
382
        lost = 0
383
        ploss = None
384
        if not hosts:
385
            hosts = self.topo.hosts()
386
        lg.info('*** Ping: testing ping reachability\n')
387
        for node_dpid in hosts:
388
            node = self.nodes[node_dpid]
389
            lg.info('%s -> ' % node.name)
390
            for dest_dpid in hosts:
391
                dest = self.nodes[dest_dpid]
392
                if node != dest:
393
                    result = node.cmd('ping -c1 ' + dest.IP())
394
                    sent, received = self._parse_ping(result)
395
                    packets += sent
396
                    if received > sent:
397
                        lg.error('*** Error: received too many packets')
398
                        lg.error('%s' % result)
399
                        node.cmdPrint('route')
400
                        exit( 1 )
401
                    lost += sent - received
402
                    lg.info(('%s ' % dest.name) if received else 'X ')
403
            lg.info('\n')
404
            ploss = 100 * lost / packets
405
        lg.info("*** Results: %i%% dropped (%d/%d lost)\n" %
406
                (ploss, lost, packets))
407
        return ploss
408

    
409
    def ping_all(self):
410
        '''Ping between all hosts.
411

412
        @return ploss packet loss percentage
413
        '''
414
        return self.ping()
415

    
416
    def ping_pair(self):
417
        '''Ping between first two hosts, useful for testing.
418

419
        @return ploss packet loss percentage
420
        '''
421
        hosts_sorted = sorted(self.topo.hosts())
422
        hosts = [hosts_sorted[0], hosts_sorted[1]]
423
        return self.ping(hosts = hosts)
424

    
425
    @staticmethod
426
    def _parseIperf(iperfOutput):
427
        '''Parse iperf output and return bandwidth.
428

429
        @param iperfOutput string
430
        @return result string
431
        '''
432
        r = r'([\d\.]+ \w+/sec)'
433
        m = re.search(r, iperfOutput)
434
        if m:
435
            return m.group(1)
436
        else:
437
            raise Exception('could not parse iperf output')
438

    
439
    def iperf(self, hosts = None, l4_type = 'TCP', udp_bw = '10M',
440
              verbose = False):
441
        '''Run iperf between two hosts.
442

443
        @param hosts list of host DPIDs; if None, uses opposite hosts
444
        @param l4_type string, one of [TCP, UDP]
445
        @param verbose verbose printing
446
        @return results two-element array of server and client speeds
447
        '''
448
        if not hosts:
449
            hosts_sorted = sorted(self.topo.hosts())
450
            hosts = [hosts_sorted[0], hosts_sorted[-1]]
451
        else:
452
            assert len(hosts) == 2
453
        host0 = self.nodes[hosts[0]]
454
        host1 = self.nodes[hosts[1]]
455
        lg.info('*** Iperf: testing ' + l4_type + ' bandwidth between ')
456
        lg.info("%s and %s\n" % (host0.name, host1.name))
457
        host0.cmd('killall -9 iperf')
458
        iperf_args = 'iperf '
459
        bw_args = ''
460
        if l4_type == 'UDP':
461
            iperf_args += '-u '
462
            bw_args = '-b ' + udp_bw + ' '
463
        elif l4_type != 'TCP':
464
            raise Exception('Unexpected l4 type: %s' % l4_type)
465
        server = host0.cmd(iperf_args + '-s &')
466
        if verbose:
467
            lg.info('%s\n' % server)
468
        client = host1.cmd(iperf_args + '-t 5 -c ' + host0.IP() + ' ' + bw_args)
469
        if verbose:
470
            lg.info('%s\n' % client)
471
        server = host0.cmd('killall -9 iperf')
472
        if verbose:
473
            lg.info('%s\n' % server)
474
        result = [self._parseIperf(server), self._parseIperf(client)]
475
        if l4_type == 'UDP':
476
            result.insert(0, udp_bw)
477
        lg.info('*** Results: %s\n' % result)
478
        return result
479

    
480
    def iperf_udp(self, udp_bw = '10M'):
481
        '''Run iperf UDP test.'''
482
        return self.iperf(l4_type = 'UDP', udp_bw = udp_bw)
483

    
484
    def interact(self):
485
        '''Start network and run our simple CLI.'''
486
        self.start()
487
        result = MininetCLI(self)
488
        self.stop()
489
        return result
490

    
491

    
492
class MininetCLI(object):
493
    '''Simple command-line interface to talk to nodes.'''
494
    cmds = ['?', 'help', 'nodes', 'net', 'sh', 'ping_all', 'exit', \
495
            'ping_pair', 'iperf', 'iperf_udp']
496

    
497
    def __init__(self, mininet):
498
        self.mn = mininet
499
        self.nodemap = {} # map names to Node objects
500
        for node in self.mn.nodes.values():
501
            self.nodemap[node.name] = node
502
        for cname, cnode in self.mn.controllers.iteritems():
503
            self.nodemap[cname] = cnode
504
        self.nodelist = self.nodemap.values()
505
        self.run()
506

    
507
    # Commands
508
    def help(self, args):
509
        '''Semi-useful help for CLI.'''
510
        help_str = 'Available commands are:' + str(self.cmds) + '\n' + \
511
                   'You may also send a command to a node using:\n' + \
512
                   '  <node> command {args}\n' + \
513
                   'For example:\n' + \
514
                   '  mininet> h0 ifconfig\n' + \
515
                   '\n' + \
516
                   'The interpreter automatically substitutes IP ' + \
517
                   'addresses\n' + \
518
                   'for node names, so commands like\n' + \
519
                   '  mininet> h0 ping -c1 h1\n' + \
520
                   'should work.\n' + \
521
                   '\n\n' + \
522
                   'Interactive commands are not really supported yet,\n' + \
523
                   'so please limit commands to ones that do not\n' + \
524
                   'require user interaction and will terminate\n' + \
525
                   'after a reasonable amount of time.\n'
526
        print(help_str)
527

    
528
    def nodes(self, args):
529
        '''List all nodes.'''
530
        lg.info('available nodes are: \n%s\n',
531
                ' '.join([node.name for node in sorted(self.nodelist)]))
532

    
533
    def net(self, args):
534
        '''List network connection.'''
535
        for switch_dpid in self.mn.topo.switches():
536
            switch = self.mn.nodes[switch_dpid]
537
            lg.info('%s <->', switch.name)
538
            for intf in switch.intfs:
539
                node, remoteIntf = switch.connection[intf]
540
                lg.info(' %s' % node.name)
541
            lg.info('\n')
542

    
543
    def sh(self, args):
544
        '''Run an external shell command'''
545
        call(['sh', '-c'] + args)
546

    
547
    def ping_all(self, args):
548
        '''Ping between all hosts.'''
549
        self.mn.ping_all()
550

    
551
    def ping_pair(self, args):
552
        '''Ping between first two hosts, useful for testing.'''
553
        self.mn.ping_pair()
554

    
555
    def iperf(self, args):
556
        '''Simple iperf TCP test between two hosts.'''
557
        self.mn.iperf()
558

    
559
    def iperf_udp(self, args):
560
        '''Simple iperf UDP test between two hosts.'''
561
        udp_bw = args[0] if len(args) else '10M'
562
        self.mn.iperf_udp(udp_bw)
563

    
564
    def run(self):
565
        '''Read and execute commands.'''
566
        lg.warn('*** Starting CLI:\n')
567
        while True:
568
            lg.warn('mininet> ')
569
            input = sys.stdin.readline()
570
            if input == '':
571
                break
572
            if input[-1] == '\n':
573
                input = input[:-1]
574
            cmd = input.split(' ')
575
            first = cmd[0]
576
            rest = cmd[1:]
577
            if first in self.cmds and hasattr(self, first):
578
                getattr(self, first)(rest)
579
            elif first in self.nodemap and rest != []:
580
                node = self.nodemap[first]
581
                # Substitute IP addresses for node names in command
582
                rest = [self.nodemap[arg].IP() if arg in self.nodemap else arg
583
                        for arg in rest]
584
                rest = ' '.join(rest)
585
                # Interactive commands don't work yet, and
586
                # there are still issues with control-c
587
                lg.warn('*** %s: running %s\n' % (node.name, rest))
588
                node.sendCmd(rest)
589
                while True:
590
                    try:
591
                        done, data = node.monitor()
592
                        lg.info('%s\n' % data)
593
                        if done:
594
                            break
595
                    except KeyboardInterrupt:
596
                        node.sendInt()
597
            elif first == '':
598
                pass
599
            elif first in ['exit', 'quit']:
600
                break
601
            elif first == '?':
602
                self.help( rest )
603
            else:
604
                lg.error('CLI: unknown node or command: < %s >\n' % first)
605
            #lg.info('*** CLI: command complete\n')
606
        return 'exited by user command'