Statistics
| Branch: | Tag: | Revision:

mininet / mininet / node.py @ 723d068c

History | View | Annotate | Download (13.8 KB)

1
#!/usr/bin/env python
2
'''Node objects for Mininet.'''
3

    
4
from subprocess import Popen, PIPE, STDOUT
5
import os
6
import signal
7
import sys
8
import select
9

    
10
flush = sys.stdout.flush
11

    
12
from mininet.logging_mod import lg
13
from mininet.util import quietRun, macColonHex, ipStr
14

    
15

    
16
class Node(object):
17
    '''A virtual network node is simply a shell in a network namespace.
18
       We communicate with it using pipes.'''
19
    inToNode = {}
20
    outToNode = {}
21

    
22
    def __init__(self, name, inNamespace = True):
23
        self.name = name
24
        closeFds = False # speed vs. memory use
25
        # xpg_echo is needed so we can echo our sentinel in sendCmd
26
        cmd = ['/bin/bash', '-O', 'xpg_echo']
27
        self.inNamespace = inNamespace
28
        if self.inNamespace:
29
            cmd = ['netns'] + cmd
30
        self.shell = Popen(cmd, stdin = PIPE, stdout = PIPE, stderr = STDOUT,
31
            close_fds = closeFds)
32
        self.stdin = self.shell.stdin
33
        self.stdout = self.shell.stdout
34
        self.pollOut = select.poll()
35
        self.pollOut.register(self.stdout)
36
        # Maintain mapping between file descriptors and nodes
37
        # This could be useful for monitoring multiple nodes
38
        # using select.poll()
39
        self.outToNode[self.stdout.fileno()] = self
40
        self.inToNode[self.stdin.fileno()] = self
41
        self.pid = self.shell.pid
42
        self.intfCount = 0
43
        self.intfs = [] # list of interface names, as strings
44
        self.ips = {} # dict of interfaces to ip addresses as strings
45
        self.connection = {}
46
        self.waiting = False
47
        self.execed = False
48
        self.ports = {} # dict of ints to interface strings
49
                        # replace with Port object, eventually
50

    
51
    def fdToNode(self, f):
52
        '''Insert docstring.
53

54
        @param f unknown
55
        @return bool unknown
56
        '''
57
        node = self.outToNode.get(f)
58
        return node or self.inToNode.get(f)
59

    
60
    def cleanup(self):
61
        '''Help python collect its garbage.'''
62
        self.shell = None
63

    
64
    # Subshell I/O, commands and control
65
    def read(self, fileno_max):
66
        '''Insert docstring.
67

68
        @param fileno_max unknown
69
        '''
70
        return os.read(self.stdout.fileno(), fileno_max)
71

    
72
    def write(self, data):
73
        '''Write data to node.
74

75
        @param data string
76
        '''
77
        os.write(self.stdin.fileno(), data)
78

    
79
    def terminate(self):
80
        '''Send kill signal to Node and cleanup after it.'''
81
        os.kill(self.pid, signal.SIGKILL)
82
        self.cleanup()
83

    
84
    def stop(self):
85
        '''Stop node.'''
86
        self.terminate()
87

    
88
    def waitReadable(self):
89
        '''Poll on node.'''
90
        self.pollOut.poll()
91

    
92
    def sendCmd(self, cmd):
93
        '''Send a command, followed by a command to echo a sentinel,
94
           and return without waiting for the command to complete.'''
95
        assert not self.waiting
96
        if cmd[-1] == '&':
97
            separator = '&'
98
            cmd = cmd[:-1]
99
        else:
100
            separator = ';'
101
        if isinstance(cmd, list):
102
            cmd = ' '.join(cmd)
103
        self.write(cmd + separator + ' echo -n "\\0177" \n')
104
        self.waiting = True
105

    
106
    def monitor(self):
107
        '''Monitor the output of a command, returning (done, data).'''
108
        assert self.waiting
109
        self.waitReadable()
110
        data = self.read(1024)
111
        if len(data) > 0 and data[-1] == chr(0177):
112
            self.waiting = False
113
            return True, data[:-1]
114
        else:
115
            return False, data
116

    
117
    def sendInt(self):
118
        '''Send ^C, hopefully interrupting a running subprocess.'''
119
        self.write(chr(3))
120

    
121
    def waitOutput(self):
122
        '''Wait for a command to complete.
123

124
        Completion is signaled by a sentinel character, ASCII(127) appearing in
125
        the output stream.  Wait for the sentinel and return the output,
126
        including trailing newline.
127
        '''
128
        assert self.waiting
129
        output = ''
130
        while True:
131
            self.waitReadable()
132
            data = self.read(1024)
133
            if len(data) > 0  and data[-1] == chr(0177):
134
                output += data[:-1]
135
                break
136
            else:
137
                output += data
138
        self.waiting = False
139
        return output
140

    
141
    def cmd(self, cmd):
142
        '''Send a command, wait for output, and return it.
143

144
        @param cmd string
145
        '''
146
        self.sendCmd(cmd)
147
        return self.waitOutput()
148

    
149
    def cmdPrint(self, cmd):
150
        '''Call cmd and printing its output
151

152
        @param cmd string
153
        '''
154
        #lg.info('*** %s : %s', self.name, cmd)
155
        result = self.cmd(cmd)
156
        #lg.info('%s\n', result)
157
        return result
158

    
159
    # Interface management, configuration, and routing
160
    def intfName(self, n):
161
        '''Construct a canonical interface name node-intf for interface N.'''
162
        return self.name + '-eth' + repr(n)
163

    
164
    def newIntf(self):
165
        '''Reserve and return a new interface name.'''
166
        intfName = self.intfName(self.intfCount)
167
        self.intfCount += 1
168
        self.intfs += [intfName]
169
        return intfName
170

    
171
    def setMAC(self, intf, mac):
172
        '''Set the MAC address for an interface.
173

174
        @param mac MAC address as unsigned int
175
        '''
176
        mac_str = macColonHex(mac)
177
        result = self.cmd(['ifconfig', intf, 'down'])
178
        result += self.cmd(['ifconfig', intf, 'hw', 'ether', mac_str])
179
        result += self.cmd(['ifconfig', intf, 'up'])
180
        return result
181

    
182
    def setARP(self, ip, mac):
183
        '''Add an ARP entry.
184

185
        @param ip IP address as unsigned int
186
        @param mac MAC address as unsigned int
187
        '''
188
        ip_str = ipStr(ip)
189
        mac_str = macColonHex(mac)
190
        result = self.cmd(['arp', '-s', ip_str, mac_str])
191
        return result
192

    
193
    def setIP(self, intf, ip, bits):
194
        '''Set the IP address for an interface.
195

196
        @param intf string, interface name
197
        @param ip IP address as a string
198
        @param bits
199
        '''
200
        result = self.cmd(['ifconfig', intf, ip + bits, 'up'])
201
        self.ips[intf] = ip
202
        return result
203

    
204
    def setHostRoute(self, ip, intf):
205
        '''Add route to host.
206

207
        @param ip IP address as dotted decimal
208
        @param intf string, interface name
209
        '''
210
        return self.cmd('route add -host ' + ip + ' dev ' + intf)
211

    
212
    def setDefaultRoute(self, intf):
213
        '''Set the default route to go through intf.
214

215
        @param intf string, interface name
216
        '''
217
        self.cmd('ip route flush')
218
        return self.cmd('route add default ' + intf)
219

    
220
    def IP(self):
221
        '''Return IP address of first interface'''
222
        if len(self.intfs) > 0:
223
            return self.ips.get(self.intfs[0], None)
224

    
225
    def intfIsUp(self):
226
        '''Check if one of our interfaces is up.'''
227
        return 'UP' in self.cmd('ifconfig ' + self.intfs[0])
228

    
229
    # Other methods
230
    def __str__(self):
231
        result = self.name + ':'
232
        if self.IP():
233
            result += ' IP=' + self.IP()
234
        result += ' intfs=' + ','.join(self.intfs)
235
        result += ' waiting=' + repr(self.waiting)
236
        return result
237

    
238

    
239
class Host(Node):
240
    '''A host is simply a Node.'''
241
    pass
242

    
243

    
244
class Switch(Node):
245
    '''A Switch is a Node that is running (or has execed)
246
       an OpenFlow switch.'''
247

    
248
    def sendCmd(self, cmd):
249
        '''Send command to Node.
250

251
        @param cmd string
252
        '''
253
        if not self.execed:
254
            return Node.sendCmd(self, cmd)
255
        else:
256
            lg.error('*** Error: %s has execed and cannot accept commands' %
257
                     self.name)
258

    
259
    def monitor(self):
260
        '''Monitor node.'''
261
        if not self.execed:
262
            return Node.monitor(self)
263
        else:
264
            return True, ''
265

    
266

    
267
class UserSwitch(Switch):
268
    '''User-space switch.
269

270
    Currently only works in the root namespace.
271
    '''
272

    
273
    def __init__(self, name):
274
        '''Init.
275

276
        @param name
277
        '''
278
        Switch.__init__(self, name, inNamespace = False)
279

    
280
    def start(self, controllers):
281
        '''Start OpenFlow reference user datapath.
282

283
        Log to /tmp/sN-{ofd,ofp}.log.
284

285
        @param controllers dict of controller names to objects
286
        '''
287
        if 'c0' not in controllers:
288
            raise Exception('User datapath start() requires controller c0')
289
        controller = controllers['c0']
290
        ofdlog = '/tmp/' + self.name + '-ofd.log'
291
        ofplog = '/tmp/' + self.name + '-ofp.log'
292
        self.cmd('ifconfig lo up')
293
        intfs = self.intfs
294
        self.cmdPrint('ofdatapath -i ' + ','.join(intfs) + ' punix:/tmp/' +
295
                      self.name + ' 1> ' + ofdlog + ' 2> ' + ofdlog + ' &')
296
        self.cmdPrint('ofprotocol unix:/tmp/' + self.name + ' tcp:' +
297
                      controller.IP() + ' --fail=closed 1> ' + ofplog + ' 2>' +
298
                      ofplog + ' &')
299

    
300
    def stop(self):
301
        '''Stop OpenFlow reference user datapath.'''
302
        self.cmd('kill %ofdatapath')
303
        self.cmd('kill %ofprotocol')
304

    
305

    
306
class KernelSwitch(Switch):
307
    '''Kernel-space switch.
308

309
    Much faster than user-space!
310

311
    Currently only works in the root namespace.
312
    '''
313

    
314
    def __init__(self, name, dp = None, dpid = None):
315
        '''Init.
316

317
        @param name
318
        @param dp netlink id (0, 1, 2, ...)
319
        @param dpid datapath ID as unsigned int; random value if None
320
        '''
321
        Switch.__init__(self, name, inNamespace = False)
322
        self.dp = dp
323
        self.dpid = dpid
324

    
325
    def start(self, controllers):
326
        '''Start up reference kernel datapath.'''
327
        ofplog = '/tmp/' + self.name + '-ofp.log'
328
        quietRun('ifconfig lo up')
329
        # Delete local datapath if it exists;
330
        # then create a new one monitoring the given interfaces
331
        quietRun('dpctl deldp nl:%i' % self.dp)
332
        self.cmdPrint('dpctl adddp nl:%i' % self.dp)
333
        if self.dpid:
334
            intf = 'of%i' % self.dp
335
            mac_str = macColonHex(self.dpid)
336
            self.cmd(['ifconfig', intf, 'hw', 'ether', mac_str])
337

    
338
        if len(self.ports) != max(self.ports.keys()) + 1:
339
            raise Exception('only contiguous, zero-indexed port ranges'
340
                            'supported: %s' % self.ports)
341
        intfs = [self.ports[port] for port in self.ports.keys()]
342
        self.cmdPrint('dpctl addif nl:' + str(self.dp) + ' ' + ' '.join(intfs))
343
        # Run protocol daemon
344
        self.cmdPrint('ofprotocol nl:' + str(self.dp) + ' tcp:' +
345
                      controllers['c0'].IP() + ':' +
346
                      str(controllers['c0'].port) +
347
                      ' --fail=closed 1> ' + ofplog + ' 2>' + ofplog + ' &')
348
        self.execed = False
349

    
350
    def stop(self):
351
        '''Terminate reference kernel datapath.'''
352
        quietRun('dpctl deldp nl:%i' % self.dp)
353
        # In theory the interfaces should go away after we shut down.
354
        # However, this takes time, so we're better off to remove them
355
        # explicitly so that we won't get errors if we run before they
356
        # have been removed by the kernel. Unfortunately this is very slow.
357
        self.cmd('kill %ofprotocol')
358
        for intf in self.intfs:
359
            quietRun('ip link del ' + intf)
360
            lg.info('.')
361

    
362

    
363
class Controller(Node):
364
    '''A Controller is a Node that is running (or has execed) an
365
      OpenFlow controller.'''
366

    
367
    def __init__(self, name, inNamespace = False, controller = 'controller',
368
                 cargs = '-v ptcp:', cdir = None, ip_address="127.0.0.1",
369
                 port = 6633):
370
        self.controller = controller
371
        self.cargs = cargs
372
        self.cdir = cdir
373
        self.ip_address = ip_address
374
        self.port = port
375
        Node.__init__(self, name, inNamespace = inNamespace)
376

    
377
    def start(self):
378
        '''Start <controller> <args> on controller.
379

380
        Log to /tmp/cN.log
381
        '''
382
        cout = '/tmp/' + self.name + '.log'
383
        if self.cdir is not None:
384
            self.cmdPrint('cd ' + self.cdir)
385
        self.cmdPrint(self.controller + ' ' + self.cargs +
386
            ' 1> ' + cout + ' 2> ' + cout + ' &')
387
        self.execed = False
388

    
389
    def stop(self):
390
        '''Stop controller.'''
391
        self.cmd('kill %' + self.controller)
392
        self.terminate()
393

    
394
    def IP(self):
395
        '''Return IP address of the Controller'''
396
        return self.ip_address
397

    
398

    
399
class ControllerParams(object):
400
    '''Container for controller IP parameters.'''
401

    
402
    def __init__(self, ip, subnet_size):
403
        '''Init.
404

405
        @param ip integer, controller IP
406
        @param subnet_size integer, ex 8 for slash-8, covering 17M
407
        '''
408
        self.ip = ip
409
        self.subnet_size = subnet_size
410

    
411

    
412
class NOX(Controller):
413
    '''Controller to run a NOX application.'''
414

    
415
    def __init__(self, name, inNamespace = False, nox_args = None, **kwargs):
416
        '''Init.
417

418
        @param name name to give controller
419
        @param nox_args list of args, or single arg, to pass to NOX
420
        '''
421
        if type(nox_args) != list:
422
            nox_args = [nox_args]
423
        if not nox_args:
424
            nox_args = ['packetdump']
425
        nox_core_dir = os.environ['NOX_CORE_DIR']
426
        if not nox_core_dir:
427
            raise Exception('please set NOX_CORE_DIR env var\n')
428
        Controller.__init__(self, name,
429
            controller = nox_core_dir + '/nox_core',
430
            cargs = '--libdir=/usr/local/lib -v -i ptcp: ' + \
431
                    ' '.join(nox_args),
432
            cdir = nox_core_dir, **kwargs)
433

    
434

    
435
class RemoteController(Controller):
436
    '''Controller running outside of Mininet's control.'''
437

    
438
    def __init__(self, name, inNamespace = False, ip_address = '127.0.0.1',
439
                 port = 6633):
440
        '''Init.
441

442
        @param name name to give controller
443
        @param ip_address the IP address where the remote controller is
444
            listening
445
        @param port the port where the remote controller is listening
446
        '''
447
        Controller.__init__(self, name, ip_address = ip_address, port = port)
448

    
449
    def start(self):
450
        '''Overridden to do nothing.'''
451
        return
452

    
453
    def stop(self):
454
        '''Overridden to do nothing.'''
455
        return