Statistics
| Branch: | Tag: | Revision:

mininet / mininet / net.py @ 8f20b95d

History | View | Annotate | Download (17.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
from subprocess import call
51
import sys
52
from time import sleep
53

    
54
from mininet.logging_mod import lg
55
from mininet.util import quietRun, fixLimits
56
from mininet.util import make_veth_pair, move_intf, retry, MOVEINTF_DELAY
57

    
58
DATAPATHS = ['kernel'] #['user', 'kernel']
59

    
60

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

    
74

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

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

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

    
104
        self.kernel = True #temporary!
105

    
106
        if build:
107
            self.build(xterms, cleanup)
108

    
109
    def _add_host(self, dpid):
110
        '''Add host.
111

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

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

    
132
    def _add_link(self, src, dst):
133
        '''Add link.
134

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

    
158
    def _add_controller(self, controller):
159
        '''Add controller.
160

161
        @param controller Controller class
162
        '''
163
        controller = self.controller('c0', kernel = self.kernel)
164
        self.controllers['c0'] = controller
165

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

    
187
    def _configureControlNetwork(self):
188
        '''Configure control network.'''
189
        self._configureRoutedControlNetwork()
190

    
191
    def _configureRoutedControlNetwork(self):
192
        '''Configure a routed control network on controller and switches.
193

194
        For use with the user datapath only right now.
195

196
        @todo(brandonh) Test this code and verify that user-space works!
197
        '''
198
        # params were: controller, switches, ips
199

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

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

    
247
    def build(self, xterms, cleanup):
248
        '''Build mininet.
249

250
        At the end of this function, everything should be connected and up.
251

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

    
280
        if not kernel:
281
            lg.info('*** Configuring control network\n')
282
            self._configureControlNetwork()
283

    
284
        lg.info('*** Configuring hosts\n')
285
        self._config_hosts()
286

    
287
        if xterms:
288
            pass # build xterms
289

    
290
    def start(self):
291
        '''Start controller and switches\n'''
292
        lg.info('*** Starting controller\n')
293
        self.controllers['c0'].start()
294
        #for controller in self.controllers:
295
        #    controller.start()
296
        lg.info('*** Starting %s switches\n' % len(self.topo.switches()))
297
        for switch_dpid in self.topo.switches():
298
            switch = self.nodes[switch_dpid]
299
            #lg.info('switch = %s' % switch)
300
            lg.info('0x%x ' % switch_dpid)
301
            switch.start(self.controllers['c0'])
302
        lg.info('\n')
303

    
304
    def stop(self):
305
        '''Stop the controller(s), switches and hosts\n'''
306
        lg.info('*** Stopping %i hosts\n' % len(self.topo.hosts()))
307
        for host_dpid in self.topo.hosts():
308
            host = self.nodes[host_dpid]
309
            lg.info('%s ' % host.name)
310
            host.terminate()
311
        lg.info('\n')
312
        lg.info('*** Stopping %i switches\n' % len(self.topo.switches()))
313
        for switch_dpid in self.topo.switches():
314
            switch = self.nodes[switch_dpid]
315
            lg.info('%s' % switch.name)
316
            switch.stop()
317
        lg.info('\n')
318
        lg.info('*** Stopping controller\n')
319
        #for controller in self.controllers.iteriterms():
320
        self.controllers['c0'].stop()
321
        lg.info('*** Test complete\n')
322

    
323
    def run(self, test, **params):
324
        '''Perform a complete start/test/stop cycle.'''
325
        self.start()
326
        lg.info('*** Running test\n')
327
        result = test(self, **params)
328
        self.stop()
329
        return result
330

    
331
    @staticmethod
332
    def _parse_ping(pingOutput):
333
        '''Parse ping output and return packets sent, received.'''
334
        r = r'(\d+) packets transmitted, (\d+) received'
335
        m = re.search( r, pingOutput )
336
        if m == None:
337
            lg.error('*** Error: could not parse ping output: %s\n' %
338
                     pingOutput)
339
            exit(1)
340
        sent, received = int(m.group(1)), int(m.group(2))
341
        return sent, received
342

    
343
    def ping_test(self, hosts = None, verbose = False):
344
        '''Ping between all specified hosts.
345

346
        @param hosts list of host DPIDs
347
        @param verbose verbose printing
348
        @return ploss packet loss percentage
349
        '''
350
        #self.start()
351
        # check if running - only then, start?
352
        packets = 0
353
        lost = 0
354
        ploss = None
355
        if not hosts:
356
            hosts = self.topo.hosts()
357
        for node_dpid in hosts:
358
            node = self.nodes[node_dpid]
359
            if verbose:
360
                lg.info('%s -> ' % node.name)
361
            for dest_dpid in hosts:
362
                dest = self.nodes[dest_dpid]
363
                if node != dest:
364
                    result = node.cmd('ping -c1 ' + dest.IP())
365
                    sent, received = self._parse_ping(result)
366
                    packets += sent
367
                    if received > sent:
368
                        lg.error('*** Error: received too many packets')
369
                        lg.error('%s' % result)
370
                        node.cmdPrint('route')
371
                        exit( 1 )
372
                    lost += sent - received
373
                    if verbose:
374
                        lg.info(('%s ' % dest.name) if received else 'X ')
375
            if verbose:
376
                lg.info('\n')
377
            ploss = 100 * lost / packets
378
        if verbose:
379
            lg.info('%d%% packet loss (%d/%d lost)\n' % (ploss, lost, packets))
380
            #flush()
381
        #self.stop()
382
        return ploss
383

    
384
    def interact(self):
385
        '''Start network and run our simple CLI.'''
386
        self.run(MininetCLI)
387

    
388

    
389
class MininetCLI(object):
390
    '''Simple command-line interface to talk to nodes.'''
391
    cmds = ['?', 'help', 'nodes', 'net', 'sh', 'ping_all', 'exit', \
392
            'ping_pair'] #'iperf'
393

    
394
    def __init__(self, mininet):
395
        self.mn = mininet
396
        self.nodemap = {} # map names to Node objects
397
        for node in self.mn.nodes.values():
398
            self.nodemap[node.name] = node
399
        self.nodemap['c0'] = self.mn.controllers['c0']
400
        self.nodelist = self.nodemap.values()
401
        self.run()
402

    
403
    # Commands
404
    def help(self, args):
405
        '''Semi-useful help for CLI.'''
406
        help_str = 'Available commands are:' + str(self.cmds) + '\n' + \
407
                   'You may also send a command to a node using:\n' + \
408
                   '  <node> command {args}\n' + \
409
                   'For example:\n' + \
410
                   '  mininet> h0 ifconfig\n' + \
411
                   '\n' + \
412
                   'The interpreter automatically substitutes IP ' + \
413
                   'addresses\n' + \
414
                   'for node names, so commands like\n' + \
415
                   '  mininet> h0 ping -c1 h1\n' + \
416
                   'should work.\n' + \
417
                   '\n\n' + \
418
                   'Interactive commands are not really supported yet,\n' + \
419
                   'so please limit commands to ones that do not\n' + \
420
                   'require user interaction and will terminate\n' + \
421
                   'after a reasonable amount of time.\n'
422
        print(help_str)
423

    
424
    def nodes(self, args):
425
        '''List all nodes.'''
426
        lg.info('available nodes are: \n%s\n',
427
                ' '.join([node.name for node in sorted(self.nodelist)]))
428

    
429
    def net(self, args):
430
        '''List network connection.'''
431
        for switch_dpid in self.mn.topo.switches():
432
            switch = self.mn.nodes[switch_dpid]
433
            lg.info('%s <->', switch.name)
434
            for intf in switch.intfs:
435
                node, remoteIntf = switch.connection[intf]
436
                lg.info(' %s' % node.name)
437
            lg.info('\n')
438

    
439
    def sh(self, args):
440
        '''Run an external shell command'''
441
        call( [ 'sh', '-c' ] + args )
442

    
443
    def ping_all(self, args):
444
        '''Ping between all hosts.'''
445
        self.mn.ping_test(verbose = True)
446

    
447
    def ping_pair(self, args):
448
        '''Ping between first two hosts, useful for testing.'''
449
        hosts_unsorted = sorted(self.mn.topo.hosts())
450
        hosts = [hosts_unsorted[0], hosts_unsorted[1]]
451
        self.mn.ping_test(hosts = hosts, verbose = True)
452

    
453
    def run(self):
454
        '''Read and execute commands.'''
455
        lg.warn('*** Starting CLI:\n')
456
        while True:
457
            lg.warn('mininet> ')
458
            input = sys.stdin.readline()
459
            if input == '':
460
                break
461
            if input[-1] == '\n':
462
                input = input[:-1]
463
            cmd = input.split(' ')
464
            first = cmd[0]
465
            rest = cmd[1:]
466
            if first in self.cmds and hasattr(self, first):
467
                getattr(self, first)(rest)
468
            elif first in self.nodemap and rest != []:
469
                node = self.nodemap[first]
470
                # Substitute IP addresses for node names in command
471
                rest = [self.nodemap[arg].IP() if arg in self.nodemap else arg
472
                        for arg in rest]
473
                rest = ' '.join(rest)
474
                # Interactive commands don't work yet, and
475
                # there are still issues with control-c
476
                lg.warn('*** %s: running %s\n' % (node.name, rest))
477
                node.sendCmd(rest)
478
                while True:
479
                    try:
480
                        done, data = node.monitor()
481
                        lg.info('%s\n' % data)
482
                        if done:
483
                            break
484
                    except KeyboardInterrupt:
485
                        node.sendInt()
486
            elif first == '':
487
                pass
488
            elif first in ['exit', 'quit']:
489
                break
490
            elif first == '?':
491
                self.help( rest )
492
            else:
493
                lg.error('CLI: unknown node or command: < %s >\n' % first)
494
            #lg.info('*** CLI: command complete\n')
495
        return 'exited by user command'