source: etherws/trunk/etherws.py @ 252

Revision 252, 41.3 KB checked in by atzm, 11 years ago (diff)

add listif support for netdev

  • Property svn:keywords set to Id
Line 
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4#                          Ethernet over WebSocket
5#
6# depends on:
7#   - python-2.7.2
8#   - python-pytun-0.2
9#   - websocket-client-0.7.0
10#   - tornado-2.3
11#
12# ===========================================================================
13# Copyright (c) 2012, Atzm WATANABE <atzm@atzm.org>
14# All rights reserved.
15#
16# Redistribution and use in source and binary forms, with or without
17# modification, are permitted provided that the following conditions are met:
18#
19# 1. Redistributions of source code must retain the above copyright notice,
20#    this list of conditions and the following disclaimer.
21# 2. Redistributions in binary form must reproduce the above copyright
22#    notice, this list of conditions and the following disclaimer in the
23#    documentation and/or other materials provided with the distribution.
24#
25# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
26# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
27# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
28# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
29# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
30# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
31# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
32# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
33# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
34# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
35# POSSIBILITY OF SUCH DAMAGE.
36# ===========================================================================
37#
38# $Id$
39
40import os
41import sys
42import ssl
43import time
44import json
45import fcntl
46import base64
47import socket
48import urllib2
49import hashlib
50import getpass
51import argparse
52import traceback
53
54import tornado
55import websocket
56
57from tornado.web import Application, RequestHandler
58from tornado.websocket import WebSocketHandler
59from tornado.httpserver import HTTPServer
60from tornado.ioloop import IOLoop
61
62from pytun import TunTapDevice, IFF_TAP, IFF_NO_PI
63
64
65class DebugMixIn(object):
66    def dprintf(self, msg, func=lambda: ()):
67        if self._debug:
68            prefix = '[%s] %s - ' % (time.asctime(), self.__class__.__name__)
69            sys.stderr.write(prefix + (msg % func()))
70
71
72class EthernetFrame(object):
73    def __init__(self, data):
74        self.data = data
75
76    @property
77    def dst_multicast(self):
78        return ord(self.data[0]) & 1
79
80    @property
81    def src_multicast(self):
82        return ord(self.data[6]) & 1
83
84    @property
85    def dst_mac(self):
86        return self.data[:6]
87
88    @property
89    def src_mac(self):
90        return self.data[6:12]
91
92    @property
93    def tagged(self):
94        return ord(self.data[12]) == 0x81 and ord(self.data[13]) == 0
95
96    @property
97    def vid(self):
98        if self.tagged:
99            return ((ord(self.data[14]) << 8) | ord(self.data[15])) & 0x0fff
100        return 0
101
102    @staticmethod
103    def format_mac(mac, sep=':'):
104        return sep.join(b.encode('hex') for b in mac)
105
106
107class FDB(DebugMixIn):
108    class Entry(object):
109        def __init__(self, port, ageout):
110            self.port = port
111            self._time = time.time()
112            self._ageout = ageout
113
114        @property
115        def age(self):
116            return time.time() - self._time
117
118        @property
119        def agedout(self):
120            return self.age > self._ageout
121
122    def __init__(self, ageout, debug=False):
123        self._ageout = ageout
124        self._debug = debug
125        self._table = {}
126
127    def _set_entry(self, vid, mac, port):
128        if vid not in self._table:
129            self._table[vid] = {}
130        self._table[vid][mac] = self.Entry(port, self._ageout)
131
132    def _del_entry(self, vid, mac):
133        if vid in self._table:
134            if mac in self._table[vid]:
135                del self._table[vid][mac]
136            if not self._table[vid]:
137                del self._table[vid]
138
139    def _get_entry(self, vid, mac):
140        try:
141            entry = self._table[vid][mac]
142        except KeyError:
143            return None
144
145        if not entry.agedout:
146            return entry
147
148        self._del_entry(vid, mac)
149        self.dprintf('aged out: port:%d; vid:%d; mac:%s\n',
150                     lambda: (entry.port.number, vid, mac.encode('hex')))
151
152    def each(self):
153        for vid in sorted(self._table.iterkeys()):
154            for mac in sorted(self._table[vid].iterkeys()):
155                entry = self._get_entry(vid, mac)
156                if entry:
157                    yield (vid, mac, entry)
158
159    def lookup(self, frame):
160        mac = frame.dst_mac
161        vid = frame.vid
162        entry = self._get_entry(vid, mac)
163        return getattr(entry, 'port', None)
164
165    def learn(self, port, frame):
166        mac = frame.src_mac
167        vid = frame.vid
168        self._set_entry(vid, mac, port)
169        self.dprintf('learned: port:%d; vid:%d; mac:%s\n',
170                     lambda: (port.number, vid, mac.encode('hex')))
171
172    def delete(self, port):
173        for vid, mac, entry in self.each():
174            if entry.port.number == port.number:
175                self._del_entry(vid, mac)
176                self.dprintf('deleted: port:%d; vid:%d; mac:%s\n',
177                             lambda: (port.number, vid, mac.encode('hex')))
178
179
180class SwitchingHub(DebugMixIn):
181    class Port(object):
182        def __init__(self, number, interface):
183            self.number = number
184            self.interface = interface
185            self.tx = 0
186            self.rx = 0
187            self.shut = False
188
189        @staticmethod
190        def cmp_by_number(x, y):
191            return cmp(x.number, y.number)
192
193    def __init__(self, fdb, debug=False):
194        self.fdb = fdb
195        self._debug = debug
196        self._table = {}
197        self._next = 1
198
199    @property
200    def portlist(self):
201        return sorted(self._table.itervalues(), cmp=self.Port.cmp_by_number)
202
203    def get_port(self, portnum):
204        return self._table[portnum]
205
206    def register_port(self, interface):
207        try:
208            self._set_privattr('portnum', interface, self._next)  # XXX
209            self._table[self._next] = self.Port(self._next, interface)
210            return self._next
211        finally:
212            self._next += 1
213
214    def unregister_port(self, interface):
215        portnum = self._get_privattr('portnum', interface)
216        self._del_privattr('portnum', interface)
217        self.fdb.delete(self._table[portnum])
218        del self._table[portnum]
219
220    def send(self, dst_interfaces, frame):
221        portnums = (self._get_privattr('portnum', i) for i in dst_interfaces)
222        ports = (self._table[n] for n in portnums)
223        ports = (p for p in ports if not p.shut)
224        ports = sorted(ports, cmp=self.Port.cmp_by_number)
225
226        for p in ports:
227            p.interface.write_message(frame.data, True)
228            p.tx += 1
229
230        if ports:
231            self.dprintf('sent: port:%s; vid:%d; %s -> %s\n',
232                         lambda: (','.join(str(p.number) for p in ports),
233                                  frame.vid,
234                                  frame.src_mac.encode('hex'),
235                                  frame.dst_mac.encode('hex')))
236
237    def receive(self, src_interface, frame):
238        port = self._table[self._get_privattr('portnum', src_interface)]
239
240        if not port.shut:
241            port.rx += 1
242            self._forward(port, frame)
243
244    def _forward(self, src_port, frame):
245        try:
246            if not frame.src_multicast:
247                self.fdb.learn(src_port, frame)
248
249            if not frame.dst_multicast:
250                dst_port = self.fdb.lookup(frame)
251
252                if dst_port:
253                    self.send([dst_port.interface], frame)
254                    return
255
256            ports = set(self.portlist) - set([src_port])
257            self.send((p.interface for p in ports), frame)
258
259        except:  # ex. received invalid frame
260            traceback.print_exc()
261
262    def _privattr(self, name):
263        return '_%s_%s_%s' % (self.__class__.__name__, id(self), name)
264
265    def _set_privattr(self, name, obj, value):
266        return setattr(obj, self._privattr(name), value)
267
268    def _get_privattr(self, name, obj, defaults=None):
269        return getattr(obj, self._privattr(name), defaults)
270
271    def _del_privattr(self, name, obj):
272        return delattr(obj, self._privattr(name))
273
274
275class Htpasswd(object):
276    def __init__(self, path):
277        self._path = path
278        self._stat = None
279        self._data = {}
280
281    def auth(self, name, passwd):
282        passwd = base64.b64encode(hashlib.sha1(passwd).digest())
283        return self._data.get(name) == passwd
284
285    def load(self):
286        old_stat = self._stat
287
288        with open(self._path) as fp:
289            fileno = fp.fileno()
290            fcntl.flock(fileno, fcntl.LOCK_SH | fcntl.LOCK_NB)
291            self._stat = os.fstat(fileno)
292
293            unchanged = old_stat and \
294                        old_stat.st_ino == self._stat.st_ino and \
295                        old_stat.st_dev == self._stat.st_dev and \
296                        old_stat.st_mtime == self._stat.st_mtime
297
298            if not unchanged:
299                self._data = self._parse(fp)
300
301        return self
302
303    def _parse(self, fp):
304        data = {}
305        for line in fp:
306            line = line.strip()
307            if 0 <= line.find(':'):
308                name, passwd = line.split(':', 1)
309                if passwd.startswith('{SHA}'):
310                    data[name] = passwd[5:]
311        return data
312
313
314class BasicAuthMixIn(object):
315    def _execute(self, transforms, *args, **kwargs):
316        def do_execute():
317            sp = super(BasicAuthMixIn, self)
318            return sp._execute(transforms, *args, **kwargs)
319
320        def auth_required():
321            stream = getattr(self, 'stream', self.request.connection.stream)
322            stream.write(tornado.escape.utf8(
323                'HTTP/1.1 401 Authorization Required\r\n'
324                'WWW-Authenticate: Basic realm=etherws\r\n\r\n'
325            ))
326            stream.close()
327
328        try:
329            if not self._htpasswd:
330                return do_execute()
331
332            creds = self.request.headers.get('Authorization')
333
334            if not creds or not creds.startswith('Basic '):
335                return auth_required()
336
337            name, passwd = base64.b64decode(creds[6:]).split(':', 1)
338
339            if self._htpasswd.load().auth(name, passwd):
340                return do_execute()
341        except:
342            traceback.print_exc()
343
344        return auth_required()
345
346
347class EtherWebSocketHandler(DebugMixIn, BasicAuthMixIn, WebSocketHandler):
348    IFTYPE = 'server'
349
350    def __init__(self, app, req, switch, htpasswd=None, debug=False):
351        super(EtherWebSocketHandler, self).__init__(app, req)
352        self._switch = switch
353        self._htpasswd = htpasswd
354        self._debug = debug
355
356    @property
357    def target(self):
358        return ':'.join(str(e) for e in self.request.connection.address)
359
360    def open(self):
361        try:
362            return self._switch.register_port(self)
363        finally:
364            self.dprintf('connected: %s\n', lambda: self.request.remote_ip)
365
366    def on_message(self, message):
367        self._switch.receive(self, EthernetFrame(message))
368
369    def on_close(self):
370        self._switch.unregister_port(self)
371        self.dprintf('disconnected: %s\n', lambda: self.request.remote_ip)
372
373
374class NetdevHandler(DebugMixIn):
375    IFTYPE = 'netdev'
376    READ_SIZE = 65535
377    ETH_P_ALL = 0x0003  # from <linux/if_ether.h>
378
379    def __init__(self, ioloop, switch, dev, debug=False):
380        self._ioloop = ioloop
381        self._switch = switch
382        self._dev = dev
383        self._debug = debug
384        self._sock = None
385
386    @property
387    def target(self):
388        return self._dev
389
390    @property
391    def closed(self):
392        return not self._sock
393
394    @property
395    def address(self):
396        if self.closed:
397            raise ValueError('I/O operation on closed netdev')
398        return ''
399
400    @property
401    def netmask(self):
402        if self.closed:
403            raise ValueError('I/O operation on closed netdev')
404        return ''
405
406    @property
407    def mtu(self):
408        if self.closed:
409            raise ValueError('I/O operation on closed netdev')
410        return ''
411
412    @address.setter
413    def address(self, address):
414        if self.closed:
415            raise ValueError('I/O operation on closed netdev')
416        raise NotImplementedError('unsupported')
417
418    @netmask.setter
419    def netmask(self, netmask):
420        if self.closed:
421            raise ValueError('I/O operation on closed netdev')
422        raise NotImplementedError('unsupported')
423
424    @mtu.setter
425    def mtu(self, mtu):
426        if self.closed:
427            raise ValueError('I/O operation on closed netdev')
428        raise NotImplementedError('unsupported')
429
430    def open(self):
431        if not self.closed:
432            raise ValueError('Already opened')
433        self._sock = socket.socket(
434            socket.PF_PACKET, socket.SOCK_RAW, socket.htons(self.ETH_P_ALL))
435        self._sock.bind((self._dev, self.ETH_P_ALL))
436        self._ioloop.add_handler(self.fileno(), self, self._ioloop.READ)
437        return self._switch.register_port(self)
438
439    def close(self):
440        if self.closed:
441            raise ValueError('I/O operation on closed netdev')
442        self._switch.unregister_port(self)
443        self._ioloop.remove_handler(self.fileno())
444        self._sock.close()
445        self._sock = None
446
447    def fileno(self):
448        if self.closed:
449            raise ValueError('I/O operation on closed netdev')
450        return self._sock.fileno()
451
452    def write_message(self, message, binary=False):
453        if self.closed:
454            raise ValueError('I/O operation on closed netdev')
455        self._sock.sendall(message)
456
457    def __call__(self, fd, events):
458        try:
459            self._switch.receive(self, EthernetFrame(self._read()))
460            return
461        except:
462            traceback.print_exc()
463        self.close()
464
465    def _read(self):
466        if self.closed:
467            raise ValueError('I/O operation on closed netdev')
468        buf = []
469        while True:
470            buf.append(self._sock.recv(self.READ_SIZE))
471            if len(buf[-1]) < self.READ_SIZE:
472                break
473        return ''.join(buf)
474
475
476class TapHandler(DebugMixIn):
477    IFTYPE = 'tap'
478    READ_SIZE = 65535
479
480    def __init__(self, ioloop, switch, dev, debug=False):
481        self._ioloop = ioloop
482        self._switch = switch
483        self._dev = dev
484        self._debug = debug
485        self._tap = None
486
487    @property
488    def target(self):
489        if self.closed:
490            return self._dev
491        return self._tap.name
492
493    @property
494    def closed(self):
495        return not self._tap
496
497    @property
498    def address(self):
499        if self.closed:
500            raise ValueError('I/O operation on closed tap')
501        try:
502            return self._tap.addr
503        except:
504            return ''
505
506    @property
507    def netmask(self):
508        if self.closed:
509            raise ValueError('I/O operation on closed tap')
510        try:
511            return self._tap.netmask
512        except:
513            return ''
514
515    @property
516    def mtu(self):
517        if self.closed:
518            raise ValueError('I/O operation on closed tap')
519        return self._tap.mtu
520
521    @address.setter
522    def address(self, address):
523        if self.closed:
524            raise ValueError('I/O operation on closed tap')
525        self._tap.addr = address
526
527    @netmask.setter
528    def netmask(self, netmask):
529        if self.closed:
530            raise ValueError('I/O operation on closed tap')
531        self._tap.netmask = netmask
532
533    @mtu.setter
534    def mtu(self, mtu):
535        if self.closed:
536            raise ValueError('I/O operation on closed tap')
537        self._tap.mtu = mtu
538
539    def open(self):
540        if not self.closed:
541            raise ValueError('Already opened')
542        self._tap = TunTapDevice(self._dev, IFF_TAP | IFF_NO_PI)
543        self._tap.up()
544        self._ioloop.add_handler(self.fileno(), self, self._ioloop.READ)
545        return self._switch.register_port(self)
546
547    def close(self):
548        if self.closed:
549            raise ValueError('I/O operation on closed tap')
550        self._switch.unregister_port(self)
551        self._ioloop.remove_handler(self.fileno())
552        self._tap.close()
553        self._tap = None
554
555    def fileno(self):
556        if self.closed:
557            raise ValueError('I/O operation on closed tap')
558        return self._tap.fileno()
559
560    def write_message(self, message, binary=False):
561        if self.closed:
562            raise ValueError('I/O operation on closed tap')
563        self._tap.write(message)
564
565    def __call__(self, fd, events):
566        try:
567            self._switch.receive(self, EthernetFrame(self._read()))
568            return
569        except:
570            traceback.print_exc()
571        self.close()
572
573    def _read(self):
574        if self.closed:
575            raise ValueError('I/O operation on closed tap')
576        buf = []
577        while True:
578            buf.append(self._tap.read(self.READ_SIZE))
579            if len(buf[-1]) < self.READ_SIZE:
580                break
581        return ''.join(buf)
582
583
584class EtherWebSocketClient(DebugMixIn):
585    IFTYPE = 'client'
586
587    def __init__(self, ioloop, switch, url, ssl_=None, cred=None, debug=False):
588        self._ioloop = ioloop
589        self._switch = switch
590        self._url = url
591        self._ssl = ssl_
592        self._debug = debug
593        self._sock = None
594        self._options = {}
595
596        if isinstance(cred, dict) and cred['user'] and cred['passwd']:
597            token = base64.b64encode('%s:%s' % (cred['user'], cred['passwd']))
598            auth = ['Authorization: Basic %s' % token]
599            self._options['header'] = auth
600
601    @property
602    def target(self):
603        return self._url
604
605    @property
606    def closed(self):
607        return not self._sock
608
609    def open(self):
610        sslwrap = websocket._SSLSocketWrapper
611
612        if not self.closed:
613            raise websocket.WebSocketException('Already opened')
614
615        if self._ssl:
616            websocket._SSLSocketWrapper = self._ssl
617
618        # XXX: may be blocked
619        try:
620            self._sock = websocket.WebSocket()
621            self._sock.connect(self._url, **self._options)
622            self._ioloop.add_handler(self.fileno(), self, self._ioloop.READ)
623            self.dprintf('connected: %s\n', lambda: self._url)
624            return self._switch.register_port(self)
625        finally:
626            websocket._SSLSocketWrapper = sslwrap
627
628    def close(self):
629        if self.closed:
630            raise websocket.WebSocketException('Already closed')
631        self._switch.unregister_port(self)
632        self._ioloop.remove_handler(self.fileno())
633        self._sock.close()
634        self._sock = None
635        self.dprintf('disconnected: %s\n', lambda: self._url)
636
637    def fileno(self):
638        if self.closed:
639            raise websocket.WebSocketException('Closed socket')
640        return self._sock.io_sock.fileno()
641
642    def write_message(self, message, binary=False):
643        if self.closed:
644            raise websocket.WebSocketException('Closed socket')
645        if binary:
646            flag = websocket.ABNF.OPCODE_BINARY
647        else:
648            flag = websocket.ABNF.OPCODE_TEXT
649        self._sock.send(message, flag)
650
651    def __call__(self, fd, events):
652        try:
653            data = self._sock.recv()
654            if data is not None:
655                self._switch.receive(self, EthernetFrame(data))
656                return
657        except:
658            traceback.print_exc()
659        self.close()
660
661
662class EtherWebSocketControlHandler(DebugMixIn, BasicAuthMixIn, RequestHandler):
663    NAMESPACE = 'etherws.control'
664    IFTYPES = {
665        NetdevHandler.IFTYPE:        NetdevHandler,
666        TapHandler.IFTYPE:           TapHandler,
667        EtherWebSocketClient.IFTYPE: EtherWebSocketClient,
668    }
669
670    def __init__(self, app, req, ioloop, switch, htpasswd=None, debug=False):
671        super(EtherWebSocketControlHandler, self).__init__(app, req)
672        self._ioloop = ioloop
673        self._switch = switch
674        self._htpasswd = htpasswd
675        self._debug = debug
676
677    def post(self):
678        try:
679            request = json.loads(self.request.body)
680        except Exception as e:
681            return self._jsonrpc_response(error={
682                'code':    0 - 32700,
683                'message': 'Parse error',
684                'data':    '%s: %s' % (e.__class__.__name__, str(e)),
685            })
686
687        try:
688            id_ = request.get('id')
689            params = request.get('params')
690            version = request['jsonrpc']
691            method = request['method']
692            if version != '2.0':
693                raise ValueError('Invalid JSON-RPC version: %s' % version)
694        except Exception as e:
695            return self._jsonrpc_response(id_=id_, error={
696                'code':    0 - 32600,
697                'message': 'Invalid Request',
698                'data':    '%s: %s' % (e.__class__.__name__, str(e)),
699            })
700
701        try:
702            if not method.startswith(self.NAMESPACE + '.'):
703                raise ValueError('Invalid method namespace: %s' % method)
704            handler = 'handle_' + method[len(self.NAMESPACE) + 1:]
705            handler = getattr(self, handler)
706        except Exception as e:
707            return self._jsonrpc_response(id_=id_, error={
708                'code':    0 - 32601,
709                'message': 'Method not found',
710                'data':    '%s: %s' % (e.__class__.__name__, str(e)),
711            })
712
713        try:
714            return self._jsonrpc_response(id_=id_, result=handler(params))
715        except Exception as e:
716            traceback.print_exc()
717            return self._jsonrpc_response(id_=id_, error={
718                'code':    0 - 32602,
719                'message': 'Invalid params',
720                'data':     '%s: %s' % (e.__class__.__name__, str(e)),
721            })
722
723    def handle_listFdb(self, params):
724        list_ = []
725        for vid, mac, entry in self._switch.fdb.each():
726            list_.append({
727                'vid':  vid,
728                'mac':  EthernetFrame.format_mac(mac),
729                'port': entry.port.number,
730                'age':  int(entry.age),
731            })
732        return {'entries': list_}
733
734    def handle_listPort(self, params):
735        return {'entries': [self._portstat(p) for p in self._switch.portlist]}
736
737    def handle_addPort(self, params):
738        type_ = params['type']
739        target = params['target']
740        opts = getattr(self, '_optparse_' + type_)(params.get('options', {}))
741        cls = self.IFTYPES[type_]
742        interface = cls(self._ioloop, self._switch, target, **opts)
743        portnum = interface.open()
744        return {'entries': [self._portstat(self._switch.get_port(portnum))]}
745
746    def handle_setPort(self, params):
747        port = self._switch.get_port(int(params['port']))
748        shut = params.get('shut')
749        if shut is not None:
750            port.shut = bool(shut)
751        return {'entries': [self._portstat(port)]}
752
753    def handle_delPort(self, params):
754        port = self._switch.get_port(int(params['port']))
755        port.interface.close()
756        return {'entries': [self._portstat(port)]}
757
758    def handle_setInterface(self, params):
759        portnum = int(params['port'])
760        port = self._switch.get_port(portnum)
761        address = params.get('address')
762        netmask = params.get('netmask')
763        mtu = params.get('mtu')
764        if isinstance(port.interface, EtherWebSocketClient):
765            raise ValueError('Port %d has unsupported interface: %s' %
766                             (portnum, port.interface.IFTYPE))
767        if address is not None:
768            port.interface.address = address
769        if netmask is not None:
770            port.interface.netmask = netmask
771        if mtu is not None:
772            port.interface.mtu = mtu
773        return {'entries': [self._ifstat(port)]}
774
775    def handle_listInterface(self, params):
776        return {'entries': [self._ifstat(p) for p in self._switch.portlist
777                            if not isinstance(p.interface,
778                                              EtherWebSocketClient)]}
779
780    def _optparse_netdev(self, opt):
781        return {'debug': self._debug}
782
783    def _optparse_tap(self, opt):
784        return {'debug': self._debug}
785
786    def _optparse_client(self, opt):
787        args = {'cert_reqs': ssl.CERT_REQUIRED, 'ca_certs': opt.get('cacerts')}
788        if opt.get('insecure'):
789            args = {}
790        ssl_ = lambda sock: ssl.wrap_socket(sock, **args)
791        cred = {'user': opt.get('user'), 'passwd': opt.get('passwd')}
792        return {'ssl_': ssl_, 'cred': cred, 'debug': self._debug}
793
794    def _jsonrpc_response(self, id_=None, result=None, error=None):
795        res = {'jsonrpc': '2.0', 'id': id_}
796        if result:
797            res['result'] = result
798        if error:
799            res['error'] = error
800        self.finish(res)
801
802    @staticmethod
803    def _portstat(port):
804        return {
805            'port':   port.number,
806            'type':   port.interface.IFTYPE,
807            'target': port.interface.target,
808            'tx':     port.tx,
809            'rx':     port.rx,
810            'shut':   port.shut,
811        }
812
813    @staticmethod
814    def _ifstat(port):
815        return {
816            'port':    port.number,
817            'type':    port.interface.IFTYPE,
818            'target':  port.interface.target,
819            'address': port.interface.address,
820            'netmask': port.interface.netmask,
821            'mtu':     port.interface.mtu,
822        }
823
824
825def _print_error(error):
826    print(%s (%s)' % (error['message'], error['code']))
827    print('    %s' % error['data'])
828
829
830def _start_sw(args):
831    def daemonize(nochdir=False, noclose=False):
832        if os.fork() > 0:
833            sys.exit(0)
834
835        os.setsid()
836
837        if os.fork() > 0:
838            sys.exit(0)
839
840        if not nochdir:
841            os.chdir('/')
842
843        if not noclose:
844            os.umask(0)
845            sys.stdin.close()
846            sys.stdout.close()
847            sys.stderr.close()
848            os.close(0)
849            os.close(1)
850            os.close(2)
851            sys.stdin = open(os.devnull)
852            sys.stdout = open(os.devnull, 'a')
853            sys.stderr = open(os.devnull, 'a')
854
855    def checkabspath(ns, path):
856        val = getattr(ns, path, '')
857        if not val.startswith('/'):
858            raise ValueError('Invalid %: %s' % (path, val))
859
860    def getsslopt(ns, key, cert):
861        kval = getattr(ns, key, None)
862        cval = getattr(ns, cert, None)
863        if kval and cval:
864            return {'keyfile': kval, 'certfile': cval}
865        elif kval or cval:
866            raise ValueError('Both %s and %s are required' % (key, cert))
867        return None
868
869    def setrealpath(ns, *keys):
870        for k in keys:
871            v = getattr(ns, k, None)
872            if v is not None:
873                v = os.path.realpath(v)
874                open(v).close()  # check readable
875                setattr(ns, k, v)
876
877    def setport(ns, port, isssl):
878        val = getattr(ns, port, None)
879        if val is None:
880            if isssl:
881                return setattr(ns, port, 443)
882            return setattr(ns, port, 80)
883        if not (0 <= val <= 65535):
884            raise ValueError('Invalid %s: %s' % (port, val))
885
886    def sethtpasswd(ns, htpasswd):
887        val = getattr(ns, htpasswd, None)
888        if val:
889            return setattr(ns, htpasswd, Htpasswd(val))
890
891    #if args.debug:
892    #    websocket.enableTrace(True)
893
894    if args.ageout <= 0:
895        raise ValueError('Invalid ageout: %s' % args.ageout)
896
897    setrealpath(args, 'htpasswd', 'sslkey', 'sslcert')
898    setrealpath(args, 'ctlhtpasswd', 'ctlsslkey', 'ctlsslcert')
899
900    checkabspath(args, 'path')
901    checkabspath(args, 'ctlpath')
902
903    sslopt = getsslopt(args, 'sslkey', 'sslcert')
904    ctlsslopt = getsslopt(args, 'ctlsslkey', 'ctlsslcert')
905
906    setport(args, 'port', sslopt)
907    setport(args, 'ctlport', ctlsslopt)
908
909    sethtpasswd(args, 'htpasswd')
910    sethtpasswd(args, 'ctlhtpasswd')
911
912    ioloop = IOLoop.instance()
913    fdb = FDB(ageout=args.ageout, debug=args.debug)
914    switch = SwitchingHub(fdb, debug=args.debug)
915
916    if args.port == args.ctlport and args.host == args.ctlhost:
917        if args.path == args.ctlpath:
918            raise ValueError('Same path/ctlpath on same host')
919        if args.sslkey != args.ctlsslkey:
920            raise ValueError('Different sslkey/ctlsslkey on same host')
921        if args.sslcert != args.ctlsslcert:
922            raise ValueError('Different sslcert/ctlsslcert on same host')
923
924        app = Application([
925            (args.path, EtherWebSocketHandler, {
926                'switch':   switch,
927                'htpasswd': args.htpasswd,
928                'debug':    args.debug,
929            }),
930            (args.ctlpath, EtherWebSocketControlHandler, {
931                'ioloop':   ioloop,
932                'switch':   switch,
933                'htpasswd': args.ctlhtpasswd,
934                'debug':    args.debug,
935            }),
936        ])
937        server = HTTPServer(app, ssl_options=sslopt)
938        server.listen(args.port, address=args.host)
939
940    else:
941        app = Application([(args.path, EtherWebSocketHandler, {
942            'switch':   switch,
943            'htpasswd': args.htpasswd,
944            'debug':    args.debug,
945        })])
946        server = HTTPServer(app, ssl_options=sslopt)
947        server.listen(args.port, address=args.host)
948
949        ctl = Application([(args.ctlpath, EtherWebSocketControlHandler, {
950            'ioloop':   ioloop,
951            'switch':   switch,
952            'htpasswd': args.ctlhtpasswd,
953            'debug':    args.debug,
954        })])
955        ctlserver = HTTPServer(ctl, ssl_options=ctlsslopt)
956        ctlserver.listen(args.ctlport, address=args.ctlhost)
957
958    if not args.foreground:
959        daemonize()
960
961    ioloop.start()
962
963
964def _start_ctl(args):
965    def request(args, method, params=None, id_=0):
966        req = urllib2.Request(args.ctlurl)
967        req.add_header('Content-type', 'application/json')
968        if args.ctluser:
969            if not args.ctlpasswd:
970                args.ctlpasswd = getpass.getpass('Control Password: ')
971            token = base64.b64encode('%s:%s' % (args.ctluser, args.ctlpasswd))
972            req.add_header('Authorization', 'Basic %s' % token)
973        method = '.'.join([EtherWebSocketControlHandler.NAMESPACE, method])
974        data = {'jsonrpc': '2.0', 'method': method, 'id': id_}
975        if params is not None:
976            data['params'] = params
977        return json.loads(urllib2.urlopen(req, json.dumps(data)).read())
978
979    def maxlen(dict_, key, min_):
980        if not dict_:
981            return min_
982        max_ = max(len(str(r[key])) for r in dict_)
983        return min_ if max_ < min_ else max_
984
985    def print_portlist(result):
986        pmax = maxlen(result, 'port', 4)
987        ymax = maxlen(result, 'type', 4)
988        smax = maxlen(result, 'shut', 5)
989        rmax = maxlen(result, 'rx', 2)
990        tmax = maxlen(result, 'tx', 2)
991        fmt = %%%d%%%d%%%d%%%d%%%d%%s' % \
992              (pmax, ymax, smax, rmax, tmax)
993        print(fmt % ('Port', 'Type', 'State', 'RX', 'TX', 'Target'))
994        for r in result:
995            shut = 'shut' if r['shut'] else 'up'
996            print(fmt %
997                  (r['port'], r['type'], shut, r['rx'], r['tx'], r['target']))
998
999    def print_iflist(result):
1000        pmax = maxlen(result, 'port', 4)
1001        tmax = maxlen(result, 'type', 4)
1002        amax = maxlen(result, 'address', 7)
1003        nmax = maxlen(result, 'netmask', 7)
1004        mmax = maxlen(result, 'mtu', 3)
1005        fmt = %%%d%%%d%%%d%%%d%%%d%%s' % \
1006              (pmax, tmax, amax, nmax, mmax)
1007        print(fmt % ('Port', 'Type', 'Address', 'Netmask', 'MTU', 'Target'))
1008        for r in result:
1009            print(fmt % (r['port'], r['type'],
1010                         r['address'], r['netmask'], r['mtu'], r['target']))
1011
1012    def handle_ctl_addport(args):
1013        opts = {
1014            'user':     getattr(args, 'user', None),
1015            'passwd':   getattr(args, 'passwd', None),
1016            'cacerts':  getattr(args, 'cacerts', None),
1017            'insecure': getattr(args, 'insecure', None),
1018        }
1019        if args.iftype == EtherWebSocketClient.IFTYPE:
1020            if not args.target.startswith('ws://') and \
1021               not args.target.startswith('wss://'):
1022                raise ValueError('Invalid target URL scheme: %s' % args.target)
1023            if not opts['user'] and opts['passwd']:
1024                raise ValueError('Authentication required but username empty')
1025            if opts['user'] and not opts['passwd']:
1026                opts['passwd'] = getpass.getpass('Client Password: ')
1027        result = request(args, 'addPort', {
1028            'type':    args.iftype,
1029            'target':  args.target,
1030            'options': opts,
1031        })
1032        if 'error' in result:
1033            _print_error(result['error'])
1034        else:
1035            print_portlist(result['result']['entries'])
1036
1037    def handle_ctl_setport(args):
1038        if args.port <= 0:
1039            raise ValueError('Invalid port: %d' % args.port)
1040        req = {'port': args.port}
1041        shut = getattr(args, 'shut', None)
1042        if shut is not None:
1043            req['shut'] = bool(shut)
1044        result = request(args, 'setPort', req)
1045        if 'error' in result:
1046            _print_error(result['error'])
1047        else:
1048            print_portlist(result['result']['entries'])
1049
1050    def handle_ctl_delport(args):
1051        if args.port <= 0:
1052            raise ValueError('Invalid port: %d' % args.port)
1053        result = request(args, 'delPort', {'port': args.port})
1054        if 'error' in result:
1055            _print_error(result['error'])
1056        else:
1057            print_portlist(result['result']['entries'])
1058
1059    def handle_ctl_listport(args):
1060        result = request(args, 'listPort')
1061        if 'error' in result:
1062            _print_error(result['error'])
1063        else:
1064            print_portlist(result['result']['entries'])
1065
1066    def handle_ctl_setif(args):
1067        if args.port <= 0:
1068            raise ValueError('Invalid port: %d' % args.port)
1069        req = {'port': args.port}
1070        address = getattr(args, 'address', None)
1071        netmask = getattr(args, 'netmask', None)
1072        mtu = getattr(args, 'mtu', None)
1073        if address is not None:
1074            if address:
1075                socket.inet_aton(address)  # validate
1076            req['address'] = address
1077        if netmask is not None:
1078            if netmask:
1079                socket.inet_aton(netmask)  # validate
1080            req['netmask'] = netmask
1081        if mtu is not None:
1082            if mtu < 576:
1083                raise ValueError('Invalid MTU: %d' % mtu)
1084            req['mtu'] = mtu
1085        result = request(args, 'setInterface', req)
1086        if 'error' in result:
1087            _print_error(result['error'])
1088        else:
1089            print_iflist(result['result']['entries'])
1090
1091    def handle_ctl_listif(args):
1092        result = request(args, 'listInterface')
1093        if 'error' in result:
1094            _print_error(result['error'])
1095        else:
1096            print_iflist(result['result']['entries'])
1097
1098    def handle_ctl_listfdb(args):
1099        result = request(args, 'listFdb')
1100        if 'error' in result:
1101            return _print_error(result['error'])
1102        result = result['result']['entries']
1103        pmax = maxlen(result, 'port', 4)
1104        vmax = maxlen(result, 'vid', 4)
1105        mmax = maxlen(result, 'mac', 3)
1106        amax = maxlen(result, 'age', 3)
1107        fmt = %%%d%%%d%%-%d%%%ds' % (pmax, vmax, mmax, amax)
1108        print(fmt % ('Port', 'VLAN', 'MAC', 'Age'))
1109        for r in result:
1110            print(fmt % (r['port'], r['vid'], r['mac'], r['age']))
1111
1112    locals()['handle_ctl_' + args.control_method](args)
1113
1114
1115def _main():
1116    parser = argparse.ArgumentParser()
1117    subcommand = parser.add_subparsers(dest='subcommand')
1118
1119    # - sw
1120    parser_sw = subcommand.add_parser('sw',
1121                                      help='start virtual switch')
1122
1123    parser_sw.add_argument('--debug', action='store_true', default=False,
1124                           help='run as debug mode')
1125    parser_sw.add_argument('--foreground', action='store_true', default=False,
1126                           help='run as foreground mode')
1127    parser_sw.add_argument('--ageout', type=int, default=300,
1128                           help='FDB ageout time (sec)')
1129
1130    parser_sw.add_argument('--path', default='/',
1131                           help='http(s) path to serve WebSocket')
1132    parser_sw.add_argument('--host', default='',
1133                           help='listen address to serve WebSocket')
1134    parser_sw.add_argument('--port', type=int,
1135                           help='listen port to serve WebSocket')
1136    parser_sw.add_argument('--htpasswd',
1137                           help='path to htpasswd file to auth WebSocket')
1138    parser_sw.add_argument('--sslkey',
1139                           help='path to SSL private key for WebSocket')
1140    parser_sw.add_argument('--sslcert',
1141                           help='path to SSL certificate for WebSocket')
1142
1143    parser_sw.add_argument('--ctlpath', default='/ctl',
1144                           help='http(s) path to serve control API')
1145    parser_sw.add_argument('--ctlhost', default='127.0.0.1',
1146                           help='listen address to serve control API')
1147    parser_sw.add_argument('--ctlport', type=int, default=7867,
1148                           help='listen port to serve control API')
1149    parser_sw.add_argument('--ctlhtpasswd',
1150                           help='path to htpasswd file to auth control API')
1151    parser_sw.add_argument('--ctlsslkey',
1152                           help='path to SSL private key for control API')
1153    parser_sw.add_argument('--ctlsslcert',
1154                           help='path to SSL certificate for control API')
1155
1156    # - ctl
1157    parser_ctl = subcommand.add_parser('ctl',
1158                                       help='control virtual switch')
1159    parser_ctl.add_argument('--ctlurl', default='http://127.0.0.1:7867/ctl',
1160                            help='URL to control API')
1161    parser_ctl.add_argument('--ctluser',
1162                            help='username to auth control API')
1163    parser_ctl.add_argument('--ctlpasswd',
1164                            help='password to auth control API')
1165
1166    control_method = parser_ctl.add_subparsers(dest='control_method')
1167
1168    # -- ctl addport
1169    parser_ctl_addport = control_method.add_parser('addport',
1170                                                   help='create and add port')
1171    iftype = parser_ctl_addport.add_subparsers(dest='iftype')
1172
1173    # --- ctl addport netdev
1174    parser_ctl_addport_netdev = iftype.add_parser(NetdevHandler.IFTYPE,
1175                                                  help='netdev')
1176    parser_ctl_addport_netdev.add_argument('target',
1177                                           help='device name to add interface')
1178
1179    # --- ctl addport tap
1180    parser_ctl_addport_tap = iftype.add_parser(TapHandler.IFTYPE,
1181                                               help='TAP device')
1182    parser_ctl_addport_tap.add_argument('target',
1183                                        help='device name to create interface')
1184
1185    # --- ctl addport client
1186    parser_ctl_addport_client = iftype.add_parser(EtherWebSocketClient.IFTYPE,
1187                                                  help='WebSocket client')
1188    parser_ctl_addport_client.add_argument('target',
1189                                           help='URL to connect WebSocket')
1190    parser_ctl_addport_client.add_argument('--user',
1191                                           help='username to auth WebSocket')
1192    parser_ctl_addport_client.add_argument('--passwd',
1193                                           help='password to auth WebSocket')
1194    parser_ctl_addport_client.add_argument('--cacerts',
1195                                           help='path to CA certificate')
1196    parser_ctl_addport_client.add_argument(
1197        '--insecure', action='store_true', default=False,
1198        help='do not verify server certificate')
1199
1200    # -- ctl setport
1201    parser_ctl_setport = control_method.add_parser('setport',
1202                                                   help='set port config')
1203    parser_ctl_setport.add_argument('port', type=int,
1204                                    help='port number to set config')
1205    parser_ctl_setport.add_argument('--shut', type=int, choices=(0, 1),
1206                                    help='set shutdown state')
1207
1208    # -- ctl delport
1209    parser_ctl_delport = control_method.add_parser('delport',
1210                                                   help='delete port')
1211    parser_ctl_delport.add_argument('port', type=int,
1212                                    help='port number to delete')
1213
1214    # -- ctl listport
1215    parser_ctl_listport = control_method.add_parser('listport',
1216                                                    help='show port list')
1217
1218    # -- ctl setif
1219    parser_ctl_setif = control_method.add_parser('setif',
1220                                                 help='set interface config')
1221    parser_ctl_setif.add_argument('port', type=int,
1222                                  help='port number to set config')
1223    parser_ctl_setif.add_argument('--address',
1224                                  help='IPv4 address to set interface')
1225    parser_ctl_setif.add_argument('--netmask',
1226                                  help='IPv4 netmask to set interface')
1227    parser_ctl_setif.add_argument('--mtu', type=int,
1228                                  help='MTU to set interface')
1229
1230    # -- ctl listif
1231    parser_ctl_listif = control_method.add_parser('listif',
1232                                                  help='show interface list')
1233
1234    # -- ctl listfdb
1235    parser_ctl_listfdb = control_method.add_parser('listfdb',
1236                                                   help='show FDB entries')
1237
1238    # -- go
1239    args = parser.parse_args()
1240
1241    try:
1242        globals()['_start_' + args.subcommand](args)
1243    except Exception as e:
1244        _print_error({
1245            'code':    0 - 32603,
1246            'message': 'Internal error',
1247            'data':    '%s: %s' % (e.__class__.__name__, str(e)),
1248        })
1249
1250
1251if __name__ == '__main__':
1252    _main()
Note: See TracBrowser for help on using the repository browser.