source: etherws/trunk/etherws.py @ 265

Revision 265, 41.9 KB checked in by atzm, 11 years ago (diff)

correspond with libraries version bump

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