source: etherws/trunk/etherws.py @ 254

Revision 254, 41.0 KB checked in by atzm, 11 years ago (diff)

add debug message

  • 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):
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):
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 ServerHandler(DebugMixIn, BasicAuthMixIn, WebSocketHandler):
348    IFTYPE = 'server'
349    IFOP_ALLOWED = False
350
351    def __init__(self, app, req, switch, htpasswd, debug):
352        super(ServerHandler, self).__init__(app, req)
353        self._switch = switch
354        self._htpasswd = htpasswd
355        self._debug = debug
356
357    @property
358    def target(self):
359        return ':'.join(str(e) for e in self.request.connection.address)
360
361    def open(self):
362        try:
363            return self._switch.register_port(self)
364        finally:
365            self.dprintf('connected: %s\n', lambda: self.request.remote_ip)
366
367    def on_message(self, message):
368        self._switch.receive(self, EthernetFrame(message))
369
370    def on_close(self):
371        self._switch.unregister_port(self)
372        self.dprintf('disconnected: %s\n', lambda: self.request.remote_ip)
373
374
375class BaseClientHandler(DebugMixIn):
376    IFTYPE = 'baseclient'
377    IFOP_ALLOWED = False
378
379    def __init__(self, ioloop, switch, target, debug, *args, **kwargs):
380        self._ioloop = ioloop
381        self._switch = switch
382        self._target = target
383        self._debug = debug
384        self._args = args
385        self._kwargs = kwargs
386        self._device = None
387
388    @property
389    def address(self):
390        raise NotImplementedError('unsupported')
391
392    @property
393    def netmask(self):
394        raise NotImplementedError('unsupported')
395
396    @property
397    def mtu(self):
398        raise NotImplementedError('unsupported')
399
400    @address.setter
401    def address(self, address):
402        raise NotImplementedError('unsupported')
403
404    @netmask.setter
405    def netmask(self, netmask):
406        raise NotImplementedError('unsupported')
407
408    @mtu.setter
409    def mtu(self, mtu):
410        raise NotImplementedError('unsupported')
411
412    def open(self):
413        raise NotImplementedError('unsupported')
414
415    def write_message(self, message, binary=False):
416        raise NotImplementedError('unsupported')
417
418    def read(self):
419        raise NotImplementedError('unsupported')
420
421    @property
422    def target(self):
423        return self._target
424
425    @property
426    def device(self):
427        return self._device
428
429    @property
430    def closed(self):
431        return not self.device
432
433    def close(self):
434        if self.closed:
435            raise ValueError('I/O operation on closed %s' % self.IFTYPE)
436        self.leave_switch()
437        self.unregister_device()
438        self.dprintf('disconnected: %s\n', lambda: self.target)
439
440    def register_device(self, device):
441        self._device = device
442
443    def unregister_device(self):
444        self._device.close()
445        self._device = None
446
447    def fileno(self):
448        if self.closed:
449            raise ValueError('I/O operation on closed %s' % self.IFTYPE)
450        return self.device.fileno()
451
452    def __call__(self, fd, events):
453        try:
454            data = self.read()
455            if data is not None:
456                self._switch.receive(self, EthernetFrame(data))
457                return
458        except:
459            traceback.print_exc()
460        self.close()
461
462    def join_switch(self):
463        self._ioloop.add_handler(self.fileno(), self, self._ioloop.READ)
464        return self._switch.register_port(self)
465
466    def leave_switch(self):
467        self._switch.unregister_port(self)
468        self._ioloop.remove_handler(self.fileno())
469
470
471class NetdevHandler(BaseClientHandler):
472    IFTYPE = 'netdev'
473    IFOP_ALLOWED = True
474    ETH_P_ALL = 0x0003  # from <linux/if_ether.h>
475
476    @property
477    def address(self):
478        if self.closed:
479            raise ValueError('I/O operation on closed netdev')
480        return ''
481
482    @property
483    def netmask(self):
484        if self.closed:
485            raise ValueError('I/O operation on closed netdev')
486        return ''
487
488    @property
489    def mtu(self):
490        if self.closed:
491            raise ValueError('I/O operation on closed netdev')
492        return ''
493
494    @address.setter
495    def address(self, address):
496        if self.closed:
497            raise ValueError('I/O operation on closed netdev')
498        raise NotImplementedError('unsupported')
499
500    @netmask.setter
501    def netmask(self, netmask):
502        if self.closed:
503            raise ValueError('I/O operation on closed netdev')
504        raise NotImplementedError('unsupported')
505
506    @mtu.setter
507    def mtu(self, mtu):
508        if self.closed:
509            raise ValueError('I/O operation on closed netdev')
510        raise NotImplementedError('unsupported')
511
512    def open(self):
513        if not self.closed:
514            raise ValueError('Already opened')
515        self.register_device(socket.socket(
516            socket.PF_PACKET, socket.SOCK_RAW, socket.htons(self.ETH_P_ALL)))
517        self.device.bind((self.target, self.ETH_P_ALL))
518        self.dprintf('connected: %s\n', lambda: self.target)
519        return self.join_switch()
520
521    def write_message(self, message, binary=False):
522        if self.closed:
523            raise ValueError('I/O operation on closed netdev')
524        self.device.sendall(message)
525
526    def read(self):
527        if self.closed:
528            raise ValueError('I/O operation on closed netdev')
529        buf = []
530        while True:
531            buf.append(self.device.recv(65535))
532            if len(buf[-1]) < 65535:
533                break
534        return ''.join(buf)
535
536
537class TapHandler(BaseClientHandler):
538    IFTYPE = 'tap'
539    IFOP_ALLOWED = True
540
541    @property
542    def address(self):
543        if self.closed:
544            raise ValueError('I/O operation on closed tap')
545        try:
546            return self.device.addr
547        except:
548            return ''
549
550    @property
551    def netmask(self):
552        if self.closed:
553            raise ValueError('I/O operation on closed tap')
554        try:
555            return self.device.netmask
556        except:
557            return ''
558
559    @property
560    def mtu(self):
561        if self.closed:
562            raise ValueError('I/O operation on closed tap')
563        return self.device.mtu
564
565    @address.setter
566    def address(self, address):
567        if self.closed:
568            raise ValueError('I/O operation on closed tap')
569        self.device.addr = address
570
571    @netmask.setter
572    def netmask(self, netmask):
573        if self.closed:
574            raise ValueError('I/O operation on closed tap')
575        self.device.netmask = netmask
576
577    @mtu.setter
578    def mtu(self, mtu):
579        if self.closed:
580            raise ValueError('I/O operation on closed tap')
581        self.device.mtu = mtu
582
583    @property
584    def target(self):
585        if self.closed:
586            return self._target
587        return self.device.name
588
589    def open(self):
590        if not self.closed:
591            raise ValueError('Already opened')
592        self.register_device(TunTapDevice(self.target, IFF_TAP | IFF_NO_PI))
593        self.device.up()
594        self.dprintf('connected: %s\n', lambda: self.target)
595        return self.join_switch()
596
597    def write_message(self, message, binary=False):
598        if self.closed:
599            raise ValueError('I/O operation on closed tap')
600        self.device.write(message)
601
602    def read(self):
603        if self.closed:
604            raise ValueError('I/O operation on closed tap')
605        buf = []
606        while True:
607            buf.append(self.device.read(65535))
608            if len(buf[-1]) < 65535:
609                break
610        return ''.join(buf)
611
612
613class ClientHandler(BaseClientHandler):
614    IFTYPE = 'client'
615    IFOP_ALLOWED = False
616
617    def __init__(self, *args, **kwargs):
618        super(ClientHandler, self).__init__(*args, **kwargs)
619
620        self._ssl = kwargs.get('ssl', False)
621        self._options = {}
622
623        cred = kwargs.get('cred', None)
624
625        if isinstance(cred, dict) and cred['user'] and cred['passwd']:
626            token = base64.b64encode('%s:%s' % (cred['user'], cred['passwd']))
627            auth = ['Authorization: Basic %s' % token]
628            self._options['header'] = auth
629
630    def open(self):
631        sslwrap = websocket._SSLSocketWrapper
632
633        if not self.closed:
634            raise websocket.WebSocketException('Already opened')
635
636        if self._ssl:
637            websocket._SSLSocketWrapper = self._ssl
638
639        # XXX: may be blocked
640        try:
641            self.register_device(websocket.WebSocket())
642            self.device.connect(self.target, **self._options)
643            self.dprintf('connected: %s\n', lambda: self.target)
644            return self.join_switch()
645        finally:
646            websocket._SSLSocketWrapper = sslwrap
647
648    def fileno(self):
649        if self.closed:
650            raise websocket.WebSocketException('Closed socket')
651        return self.device.io_sock.fileno()
652
653    def write_message(self, message, binary=False):
654        if self.closed:
655            raise websocket.WebSocketException('Closed socket')
656        if binary:
657            flag = websocket.ABNF.OPCODE_BINARY
658        else:
659            flag = websocket.ABNF.OPCODE_TEXT
660        self.device.send(message, flag)
661
662    def read(self):
663        if self.closed:
664            raise websocket.WebSocketException('Closed socket')
665        return self.device.recv()
666
667
668class ControlServerHandler(DebugMixIn, BasicAuthMixIn, RequestHandler):
669    NAMESPACE = 'etherws.control'
670    IFTYPES = {
671        NetdevHandler.IFTYPE: NetdevHandler,
672        TapHandler.IFTYPE:    TapHandler,
673        ClientHandler.IFTYPE: ClientHandler,
674    }
675
676    def __init__(self, app, req, ioloop, switch, htpasswd, debug):
677        super(ControlServerHandler, self).__init__(app, req)
678        self._ioloop = ioloop
679        self._switch = switch
680        self._htpasswd = htpasswd
681        self._debug = debug
682
683    def post(self):
684        try:
685            request = json.loads(self.request.body)
686        except Exception as e:
687            return self._jsonrpc_response(error={
688                'code':    0 - 32700,
689                'message': 'Parse error',
690                'data':    '%s: %s' % (e.__class__.__name__, str(e)),
691            })
692
693        try:
694            id_ = request.get('id')
695            params = request.get('params')
696            version = request['jsonrpc']
697            method = request['method']
698            if version != '2.0':
699                raise ValueError('Invalid JSON-RPC version: %s' % version)
700        except Exception as e:
701            return self._jsonrpc_response(id_=id_, error={
702                'code':    0 - 32600,
703                'message': 'Invalid Request',
704                'data':    '%s: %s' % (e.__class__.__name__, str(e)),
705            })
706
707        try:
708            if not method.startswith(self.NAMESPACE + '.'):
709                raise ValueError('Invalid method namespace: %s' % method)
710            handler = 'handle_' + method[len(self.NAMESPACE) + 1:]
711            handler = getattr(self, handler)
712        except Exception as e:
713            return self._jsonrpc_response(id_=id_, error={
714                'code':    0 - 32601,
715                'message': 'Method not found',
716                'data':    '%s: %s' % (e.__class__.__name__, str(e)),
717            })
718
719        try:
720            return self._jsonrpc_response(id_=id_, result=handler(params))
721        except Exception as e:
722            traceback.print_exc()
723            return self._jsonrpc_response(id_=id_, error={
724                'code':    0 - 32602,
725                'message': 'Invalid params',
726                'data':     '%s: %s' % (e.__class__.__name__, str(e)),
727            })
728
729    def handle_listFdb(self, params):
730        list_ = []
731        for vid, mac, entry in self._switch.fdb.each():
732            list_.append({
733                'vid':  vid,
734                'mac':  EthernetFrame.format_mac(mac),
735                'port': entry.port.number,
736                'age':  int(entry.age),
737            })
738        return {'entries': list_}
739
740    def handle_listPort(self, params):
741        return {'entries': [self._portstat(p) for p in self._switch.portlist]}
742
743    def handle_addPort(self, params):
744        type_ = params['type']
745        target = params['target']
746        opt = getattr(self, '_optparse_' + type_)(params.get('options', {}))
747        cls = self.IFTYPES[type_]
748        interface = cls(self._ioloop, self._switch, target, self._debug, **opt)
749        portnum = interface.open()
750        return {'entries': [self._portstat(self._switch.get_port(portnum))]}
751
752    def handle_setPort(self, params):
753        port = self._switch.get_port(int(params['port']))
754        shut = params.get('shut')
755        if shut is not None:
756            port.shut = bool(shut)
757        return {'entries': [self._portstat(port)]}
758
759    def handle_delPort(self, params):
760        port = self._switch.get_port(int(params['port']))
761        port.interface.close()
762        return {'entries': [self._portstat(port)]}
763
764    def handle_setInterface(self, params):
765        portnum = int(params['port'])
766        port = self._switch.get_port(portnum)
767        address = params.get('address')
768        netmask = params.get('netmask')
769        mtu = params.get('mtu')
770        if not port.interface.IFOP_ALLOWED:
771            raise ValueError('Port %d has unsupported interface: %s' %
772                             (portnum, port.interface.IFTYPE))
773        if address is not None:
774            port.interface.address = address
775        if netmask is not None:
776            port.interface.netmask = netmask
777        if mtu is not None:
778            port.interface.mtu = mtu
779        return {'entries': [self._ifstat(port)]}
780
781    def handle_listInterface(self, params):
782        return {'entries': [self._ifstat(p) for p in self._switch.portlist
783                            if p.interface.IFOP_ALLOWED]}
784
785    def _optparse_netdev(self, opt):
786        return {}
787
788    def _optparse_tap(self, opt):
789        return {}
790
791    def _optparse_client(self, opt):
792        args = {'cert_reqs': ssl.CERT_REQUIRED, 'ca_certs': opt.get('cacerts')}
793        if opt.get('insecure'):
794            args = {}
795        ssl_ = lambda sock: ssl.wrap_socket(sock, **args)
796        cred = {'user': opt.get('user'), 'passwd': opt.get('passwd')}
797        return {'ssl': ssl_, 'cred': cred}
798
799    def _jsonrpc_response(self, id_=None, result=None, error=None):
800        res = {'jsonrpc': '2.0', 'id': id_}
801        if result:
802            res['result'] = result
803        if error:
804            res['error'] = error
805        self.finish(res)
806
807    @staticmethod
808    def _portstat(port):
809        return {
810            'port':   port.number,
811            'type':   port.interface.IFTYPE,
812            'target': port.interface.target,
813            'tx':     port.tx,
814            'rx':     port.rx,
815            'shut':   port.shut,
816        }
817
818    @staticmethod
819    def _ifstat(port):
820        return {
821            'port':    port.number,
822            'type':    port.interface.IFTYPE,
823            'target':  port.interface.target,
824            'address': port.interface.address,
825            'netmask': port.interface.netmask,
826            'mtu':     port.interface.mtu,
827        }
828
829
830def _print_error(error):
831    print(%s (%s)' % (error['message'], error['code']))
832    print('    %s' % error['data'])
833
834
835def _start_sw(args):
836    def daemonize(nochdir=False, noclose=False):
837        if os.fork() > 0:
838            sys.exit(0)
839
840        os.setsid()
841
842        if os.fork() > 0:
843            sys.exit(0)
844
845        if not nochdir:
846            os.chdir('/')
847
848        if not noclose:
849            os.umask(0)
850            sys.stdin.close()
851            sys.stdout.close()
852            sys.stderr.close()
853            os.close(0)
854            os.close(1)
855            os.close(2)
856            sys.stdin = open(os.devnull)
857            sys.stdout = open(os.devnull, 'a')
858            sys.stderr = open(os.devnull, 'a')
859
860    def checkabspath(ns, path):
861        val = getattr(ns, path, '')
862        if not val.startswith('/'):
863            raise ValueError('Invalid %: %s' % (path, val))
864
865    def getsslopt(ns, key, cert):
866        kval = getattr(ns, key, None)
867        cval = getattr(ns, cert, None)
868        if kval and cval:
869            return {'keyfile': kval, 'certfile': cval}
870        elif kval or cval:
871            raise ValueError('Both %s and %s are required' % (key, cert))
872        return None
873
874    def setrealpath(ns, *keys):
875        for k in keys:
876            v = getattr(ns, k, None)
877            if v is not None:
878                v = os.path.realpath(v)
879                open(v).close()  # check readable
880                setattr(ns, k, v)
881
882    def setport(ns, port, isssl):
883        val = getattr(ns, port, None)
884        if val is None:
885            if isssl:
886                return setattr(ns, port, 443)
887            return setattr(ns, port, 80)
888        if not (0 <= val <= 65535):
889            raise ValueError('Invalid %s: %s' % (port, val))
890
891    def sethtpasswd(ns, htpasswd):
892        val = getattr(ns, htpasswd, None)
893        if val:
894            return setattr(ns, htpasswd, Htpasswd(val))
895
896    #if args.debug:
897    #    websocket.enableTrace(True)
898
899    if args.ageout <= 0:
900        raise ValueError('Invalid ageout: %s' % args.ageout)
901
902    setrealpath(args, 'htpasswd', 'sslkey', 'sslcert')
903    setrealpath(args, 'ctlhtpasswd', 'ctlsslkey', 'ctlsslcert')
904
905    checkabspath(args, 'path')
906    checkabspath(args, 'ctlpath')
907
908    sslopt = getsslopt(args, 'sslkey', 'sslcert')
909    ctlsslopt = getsslopt(args, 'ctlsslkey', 'ctlsslcert')
910
911    setport(args, 'port', sslopt)
912    setport(args, 'ctlport', ctlsslopt)
913
914    sethtpasswd(args, 'htpasswd')
915    sethtpasswd(args, 'ctlhtpasswd')
916
917    ioloop = IOLoop.instance()
918    fdb = FDB(args.ageout, args.debug)
919    switch = SwitchingHub(fdb, args.debug)
920
921    if args.port == args.ctlport and args.host == args.ctlhost:
922        if args.path == args.ctlpath:
923            raise ValueError('Same path/ctlpath on same host')
924        if args.sslkey != args.ctlsslkey:
925            raise ValueError('Different sslkey/ctlsslkey on same host')
926        if args.sslcert != args.ctlsslcert:
927            raise ValueError('Different sslcert/ctlsslcert on same host')
928
929        app = Application([
930            (args.path, ServerHandler, {
931                'switch':   switch,
932                'htpasswd': args.htpasswd,
933                'debug':    args.debug,
934            }),
935            (args.ctlpath, ControlServerHandler, {
936                'ioloop':   ioloop,
937                'switch':   switch,
938                'htpasswd': args.ctlhtpasswd,
939                'debug':    args.debug,
940            }),
941        ])
942        server = HTTPServer(app, ssl_options=sslopt)
943        server.listen(args.port, address=args.host)
944
945    else:
946        app = Application([(args.path, ServerHandler, {
947            'switch':   switch,
948            'htpasswd': args.htpasswd,
949            'debug':    args.debug,
950        })])
951        server = HTTPServer(app, ssl_options=sslopt)
952        server.listen(args.port, address=args.host)
953
954        ctl = Application([(args.ctlpath, ControlServerHandler, {
955            'ioloop':   ioloop,
956            'switch':   switch,
957            'htpasswd': args.ctlhtpasswd,
958            'debug':    args.debug,
959        })])
960        ctlserver = HTTPServer(ctl, ssl_options=ctlsslopt)
961        ctlserver.listen(args.ctlport, address=args.ctlhost)
962
963    if not args.foreground:
964        daemonize()
965
966    ioloop.start()
967
968
969def _start_ctl(args):
970    def request(args, method, params=None, id_=0):
971        req = urllib2.Request(args.ctlurl)
972        req.add_header('Content-type', 'application/json')
973        if args.ctluser:
974            if not args.ctlpasswd:
975                args.ctlpasswd = getpass.getpass('Control Password: ')
976            token = base64.b64encode('%s:%s' % (args.ctluser, args.ctlpasswd))
977            req.add_header('Authorization', 'Basic %s' % token)
978        method = '.'.join([ControlServerHandler.NAMESPACE, method])
979        data = {'jsonrpc': '2.0', 'method': method, 'id': id_}
980        if params is not None:
981            data['params'] = params
982        return json.loads(urllib2.urlopen(req, json.dumps(data)).read())
983
984    def maxlen(dict_, key, min_):
985        if not dict_:
986            return min_
987        max_ = max(len(str(r[key])) for r in dict_)
988        return min_ if max_ < min_ else max_
989
990    def print_portlist(result):
991        pmax = maxlen(result, 'port', 4)
992        ymax = maxlen(result, 'type', 4)
993        smax = maxlen(result, 'shut', 5)
994        rmax = maxlen(result, 'rx', 2)
995        tmax = maxlen(result, 'tx', 2)
996        fmt = %%%d%%%d%%%d%%%d%%%d%%s' % \
997              (pmax, ymax, smax, rmax, tmax)
998        print(fmt % ('Port', 'Type', 'State', 'RX', 'TX', 'Target'))
999        for r in result:
1000            shut = 'shut' if r['shut'] else 'up'
1001            print(fmt %
1002                  (r['port'], r['type'], shut, r['rx'], r['tx'], r['target']))
1003
1004    def print_iflist(result):
1005        pmax = maxlen(result, 'port', 4)
1006        tmax = maxlen(result, 'type', 4)
1007        amax = maxlen(result, 'address', 7)
1008        nmax = maxlen(result, 'netmask', 7)
1009        mmax = maxlen(result, 'mtu', 3)
1010        fmt = %%%d%%%d%%%d%%%d%%%d%%s' % \
1011              (pmax, tmax, amax, nmax, mmax)
1012        print(fmt % ('Port', 'Type', 'Address', 'Netmask', 'MTU', 'Target'))
1013        for r in result:
1014            print(fmt % (r['port'], r['type'],
1015                         r['address'], r['netmask'], r['mtu'], r['target']))
1016
1017    def handle_ctl_addport(args):
1018        opts = {
1019            'user':     getattr(args, 'user', None),
1020            'passwd':   getattr(args, 'passwd', None),
1021            'cacerts':  getattr(args, 'cacerts', None),
1022            'insecure': getattr(args, 'insecure', None),
1023        }
1024        if args.iftype == ClientHandler.IFTYPE:
1025            if not args.target.startswith('ws://') and \
1026               not args.target.startswith('wss://'):
1027                raise ValueError('Invalid target URL scheme: %s' % args.target)
1028            if not opts['user'] and opts['passwd']:
1029                raise ValueError('Authentication required but username empty')
1030            if opts['user'] and not opts['passwd']:
1031                opts['passwd'] = getpass.getpass('Client Password: ')
1032        result = request(args, 'addPort', {
1033            'type':    args.iftype,
1034            'target':  args.target,
1035            'options': opts,
1036        })
1037        if 'error' in result:
1038            _print_error(result['error'])
1039        else:
1040            print_portlist(result['result']['entries'])
1041
1042    def handle_ctl_setport(args):
1043        if args.port <= 0:
1044            raise ValueError('Invalid port: %d' % args.port)
1045        req = {'port': args.port}
1046        shut = getattr(args, 'shut', None)
1047        if shut is not None:
1048            req['shut'] = bool(shut)
1049        result = request(args, 'setPort', req)
1050        if 'error' in result:
1051            _print_error(result['error'])
1052        else:
1053            print_portlist(result['result']['entries'])
1054
1055    def handle_ctl_delport(args):
1056        if args.port <= 0:
1057            raise ValueError('Invalid port: %d' % args.port)
1058        result = request(args, 'delPort', {'port': args.port})
1059        if 'error' in result:
1060            _print_error(result['error'])
1061        else:
1062            print_portlist(result['result']['entries'])
1063
1064    def handle_ctl_listport(args):
1065        result = request(args, 'listPort')
1066        if 'error' in result:
1067            _print_error(result['error'])
1068        else:
1069            print_portlist(result['result']['entries'])
1070
1071    def handle_ctl_setif(args):
1072        if args.port <= 0:
1073            raise ValueError('Invalid port: %d' % args.port)
1074        req = {'port': args.port}
1075        address = getattr(args, 'address', None)
1076        netmask = getattr(args, 'netmask', None)
1077        mtu = getattr(args, 'mtu', None)
1078        if address is not None:
1079            if address:
1080                socket.inet_aton(address)  # validate
1081            req['address'] = address
1082        if netmask is not None:
1083            if netmask:
1084                socket.inet_aton(netmask)  # validate
1085            req['netmask'] = netmask
1086        if mtu is not None:
1087            if mtu < 576:
1088                raise ValueError('Invalid MTU: %d' % mtu)
1089            req['mtu'] = mtu
1090        result = request(args, 'setInterface', req)
1091        if 'error' in result:
1092            _print_error(result['error'])
1093        else:
1094            print_iflist(result['result']['entries'])
1095
1096    def handle_ctl_listif(args):
1097        result = request(args, 'listInterface')
1098        if 'error' in result:
1099            _print_error(result['error'])
1100        else:
1101            print_iflist(result['result']['entries'])
1102
1103    def handle_ctl_listfdb(args):
1104        result = request(args, 'listFdb')
1105        if 'error' in result:
1106            return _print_error(result['error'])
1107        result = result['result']['entries']
1108        pmax = maxlen(result, 'port', 4)
1109        vmax = maxlen(result, 'vid', 4)
1110        mmax = maxlen(result, 'mac', 3)
1111        amax = maxlen(result, 'age', 3)
1112        fmt = %%%d%%%d%%-%d%%%ds' % (pmax, vmax, mmax, amax)
1113        print(fmt % ('Port', 'VLAN', 'MAC', 'Age'))
1114        for r in result:
1115            print(fmt % (r['port'], r['vid'], r['mac'], r['age']))
1116
1117    locals()['handle_ctl_' + args.control_method](args)
1118
1119
1120def _main():
1121    parser = argparse.ArgumentParser()
1122    subcommand = parser.add_subparsers(dest='subcommand')
1123
1124    # - sw
1125    parser_sw = subcommand.add_parser('sw',
1126                                      help='start virtual switch')
1127
1128    parser_sw.add_argument('--debug', action='store_true', default=False,
1129                           help='run as debug mode')
1130    parser_sw.add_argument('--foreground', action='store_true', default=False,
1131                           help='run as foreground mode')
1132    parser_sw.add_argument('--ageout', type=int, default=300,
1133                           help='FDB ageout time (sec)')
1134
1135    parser_sw.add_argument('--path', default='/',
1136                           help='http(s) path to serve WebSocket')
1137    parser_sw.add_argument('--host', default='',
1138                           help='listen address to serve WebSocket')
1139    parser_sw.add_argument('--port', type=int,
1140                           help='listen port to serve WebSocket')
1141    parser_sw.add_argument('--htpasswd',
1142                           help='path to htpasswd file to auth WebSocket')
1143    parser_sw.add_argument('--sslkey',
1144                           help='path to SSL private key for WebSocket')
1145    parser_sw.add_argument('--sslcert',
1146                           help='path to SSL certificate for WebSocket')
1147
1148    parser_sw.add_argument('--ctlpath', default='/ctl',
1149                           help='http(s) path to serve control API')
1150    parser_sw.add_argument('--ctlhost', default='127.0.0.1',
1151                           help='listen address to serve control API')
1152    parser_sw.add_argument('--ctlport', type=int, default=7867,
1153                           help='listen port to serve control API')
1154    parser_sw.add_argument('--ctlhtpasswd',
1155                           help='path to htpasswd file to auth control API')
1156    parser_sw.add_argument('--ctlsslkey',
1157                           help='path to SSL private key for control API')
1158    parser_sw.add_argument('--ctlsslcert',
1159                           help='path to SSL certificate for control API')
1160
1161    # - ctl
1162    parser_ctl = subcommand.add_parser('ctl',
1163                                       help='control virtual switch')
1164    parser_ctl.add_argument('--ctlurl', default='http://127.0.0.1:7867/ctl',
1165                            help='URL to control API')
1166    parser_ctl.add_argument('--ctluser',
1167                            help='username to auth control API')
1168    parser_ctl.add_argument('--ctlpasswd',
1169                            help='password to auth control API')
1170
1171    control_method = parser_ctl.add_subparsers(dest='control_method')
1172
1173    # -- ctl addport
1174    parser_ctl_addport = control_method.add_parser('addport',
1175                                                   help='create and add port')
1176    iftype = parser_ctl_addport.add_subparsers(dest='iftype')
1177
1178    # --- ctl addport netdev
1179    parser_ctl_addport_netdev = iftype.add_parser(NetdevHandler.IFTYPE,
1180                                                  help='netdev')
1181    parser_ctl_addport_netdev.add_argument('target',
1182                                           help='device name to add interface')
1183
1184    # --- ctl addport tap
1185    parser_ctl_addport_tap = iftype.add_parser(TapHandler.IFTYPE,
1186                                               help='TAP device')
1187    parser_ctl_addport_tap.add_argument('target',
1188                                        help='device name to create interface')
1189
1190    # --- ctl addport client
1191    parser_ctl_addport_client = iftype.add_parser(ClientHandler.IFTYPE,
1192                                                  help='WebSocket client')
1193    parser_ctl_addport_client.add_argument('target',
1194                                           help='URL to connect WebSocket')
1195    parser_ctl_addport_client.add_argument('--user',
1196                                           help='username to auth WebSocket')
1197    parser_ctl_addport_client.add_argument('--passwd',
1198                                           help='password to auth WebSocket')
1199    parser_ctl_addport_client.add_argument('--cacerts',
1200                                           help='path to CA certificate')
1201    parser_ctl_addport_client.add_argument(
1202        '--insecure', action='store_true', default=False,
1203        help='do not verify server certificate')
1204
1205    # -- ctl setport
1206    parser_ctl_setport = control_method.add_parser('setport',
1207                                                   help='set port config')
1208    parser_ctl_setport.add_argument('port', type=int,
1209                                    help='port number to set config')
1210    parser_ctl_setport.add_argument('--shut', type=int, choices=(0, 1),
1211                                    help='set shutdown state')
1212
1213    # -- ctl delport
1214    parser_ctl_delport = control_method.add_parser('delport',
1215                                                   help='delete port')
1216    parser_ctl_delport.add_argument('port', type=int,
1217                                    help='port number to delete')
1218
1219    # -- ctl listport
1220    parser_ctl_listport = control_method.add_parser('listport',
1221                                                    help='show port list')
1222
1223    # -- ctl setif
1224    parser_ctl_setif = control_method.add_parser('setif',
1225                                                 help='set interface config')
1226    parser_ctl_setif.add_argument('port', type=int,
1227                                  help='port number to set config')
1228    parser_ctl_setif.add_argument('--address',
1229                                  help='IPv4 address to set interface')
1230    parser_ctl_setif.add_argument('--netmask',
1231                                  help='IPv4 netmask to set interface')
1232    parser_ctl_setif.add_argument('--mtu', type=int,
1233                                  help='MTU to set interface')
1234
1235    # -- ctl listif
1236    parser_ctl_listif = control_method.add_parser('listif',
1237                                                  help='show interface list')
1238
1239    # -- ctl listfdb
1240    parser_ctl_listfdb = control_method.add_parser('listfdb',
1241                                                   help='show FDB entries')
1242
1243    # -- go
1244    args = parser.parse_args()
1245
1246    try:
1247        globals()['_start_' + args.subcommand](args)
1248    except Exception as e:
1249        _print_error({
1250            'code':    0 - 32603,
1251            'message': 'Internal error',
1252            'data':    '%s: %s' % (e.__class__.__name__, str(e)),
1253        })
1254
1255
1256if __name__ == '__main__':
1257    _main()
Note: See TracBrowser for help on using the repository browser.