source: etherws/trunk/etherws.py @ 288

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