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
RevLine 
[133]1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
[186]4#                          Ethernet over WebSocket
[133]5#
6# depends on:
7#   - python-2.7.2
8#   - python-pytun-0.2
[136]9#   - websocket-client-0.7.0
[183]10#   - tornado-2.3
[133]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
[156]42import ssl
[160]43import time
[183]44import json
[175]45import fcntl
[150]46import base64
[212]47import socket
[190]48import urllib2
[150]49import hashlib
[151]50import getpass
[133]51import argparse
[165]52import traceback
[133]53
[185]54import tornado
[133]55import websocket
56
[183]57from tornado.web import Application, RequestHandler
[182]58from tornado.websocket import WebSocketHandler
[183]59from tornado.httpserver import HTTPServer
60from tornado.ioloop import IOLoop
61
[166]62from pytun import TunTapDevice, IFF_TAP, IFF_NO_PI
[133]63
[166]64
[160]65class DebugMixIn(object):
[166]66    def dprintf(self, msg, func=lambda: ()):
[160]67        if self._debug:
68            prefix = '[%s] %s - ' % (time.asctime(), self.__class__.__name__)
[164]69            sys.stderr.write(prefix + (msg % func()))
[160]70
71
[164]72class EthernetFrame(object):
73    def __init__(self, data):
74        self.data = data
75
[176]76    @property
77    def dst_multicast(self):
78        return ord(self.data[0]) & 1
[164]79
80    @property
[176]81    def src_multicast(self):
82        return ord(self.data[6]) & 1
83
84    @property
[164]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
[183]100        return 0
[164]101
[198]102    @staticmethod
103    def format_mac(mac, sep=':'):
104        return sep.join(b.encode('hex') for b in mac)
[164]105
[198]106
[166]107class FDB(DebugMixIn):
[196]108    class Entry(object):
[195]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
[253]122    def __init__(self, ageout, debug):
[164]123        self._ageout = ageout
124        self._debug = debug
[195]125        self._table = {}
[164]126
[195]127    def _set_entry(self, vid, mac, port):
128        if vid not in self._table:
129            self._table[vid] = {}
[196]130        self._table[vid][mac] = self.Entry(port, self._ageout)
[164]131
[195]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]
[164]138
[207]139    def _get_entry(self, vid, mac):
[195]140        try:
141            entry = self._table[vid][mac]
142        except KeyError:
[164]143            return None
144
[195]145        if not entry.agedout:
146            return entry
[164]147
[195]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')))
[164]151
[208]152    def each(self):
[207]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)
[195]158
159    def lookup(self, frame):
160        mac = frame.dst_mac
161        vid = frame.vid
[207]162        entry = self._get_entry(vid, mac)
[195]163        return getattr(entry, 'port', None)
164
[166]165    def learn(self, port, frame):
166        mac = frame.src_mac
167        vid = frame.vid
[195]168        self._set_entry(vid, mac, port)
[183]169        self.dprintf('learned: port:%d; vid:%d; mac:%s\n',
170                     lambda: (port.number, vid, mac.encode('hex')))
[166]171
[164]172    def delete(self, port):
[208]173        for vid, mac, entry in self.each():
[207]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')))
[164]178
179
[195]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
[183]188
[195]189        @staticmethod
190        def cmp_by_number(x, y):
191            return cmp(x.number, y.number)
[183]192
[253]193    def __init__(self, fdb, debug):
[197]194        self.fdb = fdb
[133]195        self._debug = debug
[183]196        self._table = {}
197        self._next = 1
[133]198
[183]199    @property
200    def portlist(self):
[195]201        return sorted(self._table.itervalues(), cmp=self.Port.cmp_by_number)
[133]202
[183]203    def get_port(self, portnum):
204        return self._table[portnum]
205
206    def register_port(self, interface):
[186]207        try:
[187]208            self._set_privattr('portnum', interface, self._next)  # XXX
[195]209            self._table[self._next] = self.Port(self._next, interface)
[186]210            return self._next
211        finally:
212            self._next += 1
[183]213
214    def unregister_port(self, interface):
[187]215        portnum = self._get_privattr('portnum', interface)
216        self._del_privattr('portnum', interface)
[197]217        self.fdb.delete(self._table[portnum])
[187]218        del self._table[portnum]
[183]219
220    def send(self, dst_interfaces, frame):
[187]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)
[195]224        ports = sorted(ports, cmp=self.Port.cmp_by_number)
[183]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):
[187]238        port = self._table[self._get_privattr('portnum', src_interface)]
[183]239
240        if not port.shut:
241            port.rx += 1
242            self._forward(port, frame)
243
244    def _forward(self, src_port, frame):
[166]245        try:
[176]246            if not frame.src_multicast:
[197]247                self.fdb.learn(src_port, frame)
[133]248
[176]249            if not frame.dst_multicast:
[197]250                dst_port = self.fdb.lookup(frame)
[164]251
[166]252                if dst_port:
[183]253                    self.send([dst_port.interface], frame)
[166]254                    return
[133]255
[187]256            ports = set(self.portlist) - set([src_port])
[183]257            self.send((p.interface for p in ports), frame)
[162]258
[166]259        except:  # ex. received invalid frame
260            traceback.print_exc()
[133]261
[187]262    def _privattr(self, name):
263        return '_%s_%s_%s' % (self.__class__.__name__, id(self), name)
[164]264
[187]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
[179]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
[182]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():
[185]321            stream = getattr(self, 'stream', self.request.connection.stream)
322            stream.write(tornado.escape.utf8(
[182]323                'HTTP/1.1 401 Authorization Required\r\n'
324                'WWW-Authenticate: Basic realm=etherws\r\n\r\n'
325            ))
[185]326            stream.close()
[182]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
[253]347class ServerHandler(DebugMixIn, BasicAuthMixIn, WebSocketHandler):
[191]348    IFTYPE = 'server'
[253]349    IFOP_ALLOWED = False
[191]350
[253]351    def __init__(self, app, req, switch, htpasswd, debug):
352        super(ServerHandler, self).__init__(app, req)
[186]353        self._switch = switch
354        self._htpasswd = htpasswd
355        self._debug = debug
356
[203]357    @property
358    def target(self):
359        return ':'.join(str(e) for e in self.request.connection.address)
[186]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
[253]375class BaseClientHandler(DebugMixIn):
376    IFTYPE = 'baseclient'
377    IFOP_ALLOWED = False
[251]378
[253]379    def __init__(self, ioloop, switch, target, debug, *args, **kwargs):
[251]380        self._ioloop = ioloop
381        self._switch = switch
[253]382        self._target = target
[251]383        self._debug = debug
[253]384        self._args = args
385        self._kwargs = kwargs
386        self._device = None
[251]387
388    @property
[253]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
[251]422    def target(self):
[253]423        return self._target
[251]424
425    @property
[253]426    def device(self):
427        return self._device
428
429    @property
[251]430    def closed(self):
[253]431        return not self.device
[251]432
[253]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
[252]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
[251]512    def open(self):
513        if not self.closed:
514            raise ValueError('Already opened')
[253]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))
[254]518        self.dprintf('connected: %s\n', lambda: self.target)
[253]519        return self.join_switch()
[251]520
521    def write_message(self, message, binary=False):
522        if self.closed:
523            raise ValueError('I/O operation on closed netdev')
[253]524        self.device.sendall(message)
[251]525
[253]526    def read(self):
[251]527        if self.closed:
528            raise ValueError('I/O operation on closed netdev')
529        buf = []
530        while True:
[253]531            buf.append(self.device.recv(65535))
532            if len(buf[-1]) < 65535:
[251]533                break
534        return ''.join(buf)
535
536
[253]537class TapHandler(BaseClientHandler):
[191]538    IFTYPE = 'tap'
[253]539    IFOP_ALLOWED = True
[166]540
[203]541    @property
[212]542    def address(self):
543        if self.closed:
544            raise ValueError('I/O operation on closed tap')
545        try:
[253]546            return self.device.addr
[212]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:
[253]555            return self.device.netmask
[212]556        except:
557            return ''
558
559    @property
560    def mtu(self):
561        if self.closed:
562            raise ValueError('I/O operation on closed tap')
[253]563        return self.device.mtu
[212]564
565    @address.setter
566    def address(self, address):
567        if self.closed:
568            raise ValueError('I/O operation on closed tap')
[253]569        self.device.addr = address
[212]570
571    @netmask.setter
572    def netmask(self, netmask):
573        if self.closed:
574            raise ValueError('I/O operation on closed tap')
[253]575        self.device.netmask = netmask
[212]576
577    @mtu.setter
578    def mtu(self, mtu):
579        if self.closed:
580            raise ValueError('I/O operation on closed tap')
[253]581        self.device.mtu = mtu
[212]582
[253]583    @property
584    def target(self):
585        if self.closed:
586            return self._target
587        return self.device.name
588
[166]589    def open(self):
590        if not self.closed:
[202]591            raise ValueError('Already opened')
[253]592        self.register_device(TunTapDevice(self.target, IFF_TAP | IFF_NO_PI))
593        self.device.up()
[254]594        self.dprintf('connected: %s\n', lambda: self.target)
[253]595        return self.join_switch()
[166]596
597    def write_message(self, message, binary=False):
598        if self.closed:
599            raise ValueError('I/O operation on closed tap')
[253]600        self.device.write(message)
[166]601
[253]602    def read(self):
[166]603        if self.closed:
604            raise ValueError('I/O operation on closed tap')
[162]605        buf = []
606        while True:
[253]607            buf.append(self.device.read(65535))
608            if len(buf[-1]) < 65535:
[162]609                break
[166]610        return ''.join(buf)
[162]611
612
[253]613class ClientHandler(BaseClientHandler):
[191]614    IFTYPE = 'client'
[253]615    IFOP_ALLOWED = False
[191]616
[253]617    def __init__(self, *args, **kwargs):
618        super(ClientHandler, self).__init__(*args, **kwargs)
619
620        self._ssl = kwargs.get('ssl', False)
[151]621        self._options = {}
622
[253]623        cred = kwargs.get('cred', None)
624
[174]625        if isinstance(cred, dict) and cred['user'] and cred['passwd']:
626            token = base64.b64encode('%s:%s' % (cred['user'], cred['passwd']))
[151]627            auth = ['Authorization: Basic %s' % token]
628            self._options['header'] = auth
629
630    def open(self):
[181]631        sslwrap = websocket._SSLSocketWrapper
632
[160]633        if not self.closed:
[202]634            raise websocket.WebSocketException('Already opened')
[151]635
[181]636        if self._ssl:
637            websocket._SSLSocketWrapper = self._ssl
638
[251]639        # XXX: may be blocked
[181]640        try:
[253]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()
[181]645        finally:
646            websocket._SSLSocketWrapper = sslwrap
647
[165]648    def fileno(self):
649        if self.closed:
[202]650            raise websocket.WebSocketException('Closed socket')
[253]651        return self.device.io_sock.fileno()
[165]652
[151]653    def write_message(self, message, binary=False):
[160]654        if self.closed:
[202]655            raise websocket.WebSocketException('Closed socket')
[151]656        if binary:
657            flag = websocket.ABNF.OPCODE_BINARY
[160]658        else:
659            flag = websocket.ABNF.OPCODE_TEXT
[253]660        self.device.send(message, flag)
[151]661
[253]662    def read(self):
663        if self.closed:
664            raise websocket.WebSocketException('Closed socket')
665        return self.device.recv()
[151]666
667
[253]668class ControlServerHandler(DebugMixIn, BasicAuthMixIn, RequestHandler):
[183]669    NAMESPACE = 'etherws.control'
[191]670    IFTYPES = {
[253]671        NetdevHandler.IFTYPE: NetdevHandler,
672        TapHandler.IFTYPE:    TapHandler,
673        ClientHandler.IFTYPE: ClientHandler,
[186]674    }
[183]675
[253]676    def __init__(self, app, req, ioloop, switch, htpasswd, debug):
677        super(ControlServerHandler, self).__init__(app, req)
[183]678        self._ioloop = ioloop
679        self._switch = switch
680        self._htpasswd = htpasswd
681        self._debug = debug
682
683    def post(self):
[202]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            })
[183]692
693        try:
[202]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            })
[183]706
[202]707        try:
[183]708            if not method.startswith(self.NAMESPACE + '.'):
[202]709                raise ValueError('Invalid method namespace: %s' % method)
[183]710            handler = 'handle_' + method[len(self.NAMESPACE) + 1:]
[202]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            })
[183]718
[202]719        try:
720            return self._jsonrpc_response(id_=id_, result=handler(params))
[183]721        except Exception as e:
722            traceback.print_exc()
[202]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            })
[183]728
[198]729    def handle_listFdb(self, params):
730        list_ = []
[208]731        for vid, mac, entry in self._switch.fdb.each():
[207]732            list_.append({
733                'vid':  vid,
734                'mac':  EthernetFrame.format_mac(mac),
735                'port': entry.port.number,
736                'age':  int(entry.age),
737            })
[199]738        return {'entries': list_}
[198]739
[183]740    def handle_listPort(self, params):
[202]741        return {'entries': [self._portstat(p) for p in self._switch.portlist]}
[183]742
743    def handle_addPort(self, params):
[202]744        type_ = params['type']
745        target = params['target']
[253]746        opt = getattr(self, '_optparse_' + type_)(params.get('options', {}))
[202]747        cls = self.IFTYPES[type_]
[253]748        interface = cls(self._ioloop, self._switch, target, self._debug, **opt)
[202]749        portnum = interface.open()
750        return {'entries': [self._portstat(self._switch.get_port(portnum))]}
[183]751
[211]752    def handle_setPort(self, params):
[202]753        port = self._switch.get_port(int(params['port']))
[211]754        shut = params.get('shut')
755        if shut is not None:
756            port.shut = bool(shut)
[202]757        return {'entries': [self._portstat(port)]}
[183]758
[211]759    def handle_delPort(self, params):
[202]760        port = self._switch.get_port(int(params['port']))
[211]761        port.interface.close()
[202]762        return {'entries': [self._portstat(port)]}
[183]763
[212]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')
[253]770        if not port.interface.IFOP_ALLOWED:
[212]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
[253]783                            if p.interface.IFOP_ALLOWED]}
[212]784
[251]785    def _optparse_netdev(self, opt):
[253]786        return {}
[251]787
[186]788    def _optparse_tap(self, opt):
[253]789        return {}
[183]790
[186]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')}
[253]797        return {'ssl': ssl_, 'cred': cred}
[183]798
[202]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
[183]807    @staticmethod
[186]808    def _portstat(port):
809        return {
810            'port':   port.number,
[191]811            'type':   port.interface.IFTYPE,
[203]812            'target': port.interface.target,
[186]813            'tx':     port.tx,
814            'rx':     port.rx,
815            'shut':   port.shut,
816        }
[183]817
[212]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        }
[183]828
[212]829
[206]830def _print_error(error):
[205]831    print(%s (%s)' % (error['message'], error['code']))
832    print('    %s' % error['data'])
833
834
[206]835def _start_sw(args):
[186]836    def daemonize(nochdir=False, noclose=False):
837        if os.fork() > 0:
838            sys.exit(0)
[134]839
[186]840        os.setsid()
[134]841
[186]842        if os.fork() > 0:
843            sys.exit(0)
[134]844
[186]845        if not nochdir:
846            os.chdir('/')
[134]847
[186]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')
[134]859
[186]860    def checkabspath(ns, path):
[184]861        val = getattr(ns, path, '')
862        if not val.startswith('/'):
[202]863            raise ValueError('Invalid %: %s' % (path, val))
[184]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:
[202]871            raise ValueError('Both %s and %s are required' % (key, cert))
[184]872        return None
873
[186]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
[184]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):
[202]889            raise ValueError('Invalid %s: %s' % (port, val))
[184]890
891    def sethtpasswd(ns, htpasswd):
892        val = getattr(ns, htpasswd, None)
893        if val:
894            return setattr(ns, htpasswd, Htpasswd(val))
895
[183]896    #if args.debug:
897    #    websocket.enableTrace(True)
898
899    if args.ageout <= 0:
[202]900        raise ValueError('Invalid ageout: %s' % args.ageout)
[183]901
[186]902    setrealpath(args, 'htpasswd', 'sslkey', 'sslcert')
903    setrealpath(args, 'ctlhtpasswd', 'ctlsslkey', 'ctlsslcert')
[183]904
[186]905    checkabspath(args, 'path')
906    checkabspath(args, 'ctlpath')
[183]907
[184]908    sslopt = getsslopt(args, 'sslkey', 'sslcert')
909    ctlsslopt = getsslopt(args, 'ctlsslkey', 'ctlsslcert')
[143]910
[184]911    setport(args, 'port', sslopt)
912    setport(args, 'ctlport', ctlsslopt)
[143]913
[184]914    sethtpasswd(args, 'htpasswd')
915    sethtpasswd(args, 'ctlhtpasswd')
[167]916
[183]917    ioloop = IOLoop.instance()
[253]918    fdb = FDB(args.ageout, args.debug)
919    switch = SwitchingHub(fdb, args.debug)
[167]920
[184]921    if args.port == args.ctlport and args.host == args.ctlhost:
922        if args.path == args.ctlpath:
[202]923            raise ValueError('Same path/ctlpath on same host')
[184]924        if args.sslkey != args.ctlsslkey:
[202]925            raise ValueError('Different sslkey/ctlsslkey on same host')
[184]926        if args.sslcert != args.ctlsslcert:
[202]927            raise ValueError('Different sslcert/ctlsslcert on same host')
[133]928
[184]929        app = Application([
[253]930            (args.path, ServerHandler, {
[184]931                'switch':   switch,
932                'htpasswd': args.htpasswd,
933                'debug':    args.debug,
934            }),
[253]935            (args.ctlpath, ControlServerHandler, {
[184]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)
[151]944
[184]945    else:
[253]946        app = Application([(args.path, ServerHandler, {
[184]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
[253]954        ctl = Application([(args.ctlpath, ControlServerHandler, {
[184]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
[151]963    if not args.foreground:
964        daemonize()
965
[138]966    ioloop.start()
[133]967
968
[206]969def _start_ctl(args):
[202]970    def request(args, method, params=None, id_=0):
[190]971        req = urllib2.Request(args.ctlurl)
972        req.add_header('Content-type', 'application/json')
973        if args.ctluser:
974            if not args.ctlpasswd:
[209]975                args.ctlpasswd = getpass.getpass('Control Password: ')
[190]976            token = base64.b64encode('%s:%s' % (args.ctluser, args.ctlpasswd))
977            req.add_header('Authorization', 'Basic %s' % token)
[253]978        method = '.'.join([ControlServerHandler.NAMESPACE, method])
[202]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())
[190]983
[199]984    def maxlen(dict_, key, min_):
[201]985        if not dict_:
986            return min_
[199]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
[212]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
[190]1017    def handle_ctl_addport(args):
[205]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        }
[253]1024        if args.iftype == ClientHandler.IFTYPE:
[205]1025            if not args.target.startswith('ws://') and \
1026               not args.target.startswith('wss://'):
1027                raise ValueError('Invalid target URL scheme: %s' % args.target)
[210]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: ')
[202]1032        result = request(args, 'addPort', {
[204]1033            'type':    args.iftype,
[191]1034            'target':  args.target,
[205]1035            'options': opts,
[202]1036        })
1037        if 'error' in result:
[206]1038            _print_error(result['error'])
[199]1039        else:
1040            print_portlist(result['result']['entries'])
[190]1041
[211]1042    def handle_ctl_setport(args):
[190]1043        if args.port <= 0:
[202]1044            raise ValueError('Invalid port: %d' % args.port)
[211]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)
[202]1050        if 'error' in result:
[206]1051            _print_error(result['error'])
[199]1052        else:
1053            print_portlist(result['result']['entries'])
[190]1054
1055    def handle_ctl_delport(args):
1056        if args.port <= 0:
[202]1057            raise ValueError('Invalid port: %d' % args.port)
1058        result = request(args, 'delPort', {'port': args.port})
1059        if 'error' in result:
[206]1060            _print_error(result['error'])
[199]1061        else:
1062            print_portlist(result['result']['entries'])
[190]1063
1064    def handle_ctl_listport(args):
[202]1065        result = request(args, 'listPort')
1066        if 'error' in result:
[206]1067            _print_error(result['error'])
[199]1068        else:
1069            print_portlist(result['result']['entries'])
[190]1070
[212]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
[198]1103    def handle_ctl_listfdb(args):
[202]1104        result = request(args, 'listFdb')
1105        if 'error' in result:
[206]1106            return _print_error(result['error'])
[199]1107        result = result['result']['entries']
[201]1108        pmax = maxlen(result, 'port', 4)
[199]1109        vmax = maxlen(result, 'vid', 4)
1110        mmax = maxlen(result, 'mac', 3)
1111        amax = maxlen(result, 'age', 3)
[201]1112        fmt = %%%d%%%d%%-%d%%%ds' % (pmax, vmax, mmax, amax)
1113        print(fmt % ('Port', 'VLAN', 'MAC', 'Age'))
[199]1114        for r in result:
[201]1115            print(fmt % (r['port'], r['vid'], r['mac'], r['age']))
[198]1116
[199]1117    locals()['handle_ctl_' + args.control_method](args)
[190]1118
1119
[206]1120def _main():
[186]1121    parser = argparse.ArgumentParser()
[190]1122    subcommand = parser.add_subparsers(dest='subcommand')
[186]1123
[204]1124    # - sw
[215]1125    parser_sw = subcommand.add_parser('sw',
1126                                      help='start virtual switch')
[190]1127
[215]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)')
[186]1134
[215]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')
[186]1147
[215]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')
[186]1160
[204]1161    # - ctl
[215]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')
[190]1170
[204]1171    control_method = parser_ctl.add_subparsers(dest='control_method')
[190]1172
[204]1173    # -- ctl addport
[215]1174    parser_ctl_addport = control_method.add_parser('addport',
1175                                                   help='create and add port')
[204]1176    iftype = parser_ctl_addport.add_subparsers(dest='iftype')
[190]1177
[251]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
[204]1184    # --- ctl addport tap
[215]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')
[190]1189
[204]1190    # --- ctl addport client
[253]1191    parser_ctl_addport_client = iftype.add_parser(ClientHandler.IFTYPE,
[215]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')
[204]1201    parser_ctl_addport_client.add_argument(
[215]1202        '--insecure', action='store_true', default=False,
1203        help='do not verify server certificate')
[190]1204
[211]1205    # -- ctl setport
[215]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')
[190]1212
[204]1213    # -- ctl delport
[215]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')
[198]1218
[204]1219    # -- ctl listport
[215]1220    parser_ctl_listport = control_method.add_parser('listport',
1221                                                    help='show port list')
[204]1222
[212]1223    # -- ctl setif
[215]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')
[212]1234
1235    # -- ctl listif
[215]1236    parser_ctl_listif = control_method.add_parser('listif',
1237                                                  help='show interface list')
[212]1238
[204]1239    # -- ctl listfdb
[215]1240    parser_ctl_listfdb = control_method.add_parser('listfdb',
1241                                                   help='show FDB entries')
[204]1242
[190]1243    # -- go
[186]1244    args = parser.parse_args()
1245
[205]1246    try:
[206]1247        globals()['_start_' + args.subcommand](args)
[205]1248    except Exception as e:
[206]1249        _print_error({
[205]1250            'code':    0 - 32603,
1251            'message': 'Internal error',
1252            'data':    '%s: %s' % (e.__class__.__name__, str(e)),
1253        })
[186]1254
[205]1255
[133]1256if __name__ == '__main__':
[206]1257    _main()
Note: See TracBrowser for help on using the repository browser.