source: etherws/trunk/etherws.py @ 191

Revision 191, 25.4 KB checked in by atzm, 12 years ago (diff)
  • implement ctl command "addport"
  • Property svn:keywords set to Id
Line 
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4#                          Ethernet over WebSocket
5#
6# depends on:
7#   - python-2.7.2
8#   - python-pytun-0.2
9#   - websocket-client-0.7.0
10#   - tornado-2.3
11#
12# ===========================================================================
13# Copyright (c) 2012, Atzm WATANABE <atzm@atzm.org>
14# All rights reserved.
15#
16# Redistribution and use in source and binary forms, with or without
17# modification, are permitted provided that the following conditions are met:
18#
19# 1. Redistributions of source code must retain the above copyright notice,
20#    this list of conditions and the following disclaimer.
21# 2. Redistributions in binary form must reproduce the above copyright
22#    notice, this list of conditions and the following disclaimer in the
23#    documentation and/or other materials provided with the distribution.
24#
25# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
26# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
27# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
28# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
29# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
30# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
31# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
32# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
33# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
34# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
35# POSSIBILITY OF SUCH DAMAGE.
36# ===========================================================================
37#
38# $Id$
39
40import os
41import sys
42import ssl
43import time
44import json
45import fcntl
46import base64
47import urllib2
48import hashlib
49import getpass
50import argparse
51import traceback
52
53import tornado
54import websocket
55
56from tornado.web import Application, RequestHandler
57from tornado.websocket import WebSocketHandler
58from tornado.httpserver import HTTPServer
59from tornado.ioloop import IOLoop
60
61from pytun import TunTapDevice, IFF_TAP, IFF_NO_PI
62
63
64class DebugMixIn(object):
65    def dprintf(self, msg, func=lambda: ()):
66        if self._debug:
67            prefix = '[%s] %s - ' % (time.asctime(), self.__class__.__name__)
68            sys.stderr.write(prefix + (msg % func()))
69
70
71class EthernetFrame(object):
72    def __init__(self, data):
73        self.data = data
74
75    @property
76    def dst_multicast(self):
77        return ord(self.data[0]) & 1
78
79    @property
80    def src_multicast(self):
81        return ord(self.data[6]) & 1
82
83    @property
84    def dst_mac(self):
85        return self.data[:6]
86
87    @property
88    def src_mac(self):
89        return self.data[6:12]
90
91    @property
92    def tagged(self):
93        return ord(self.data[12]) == 0x81 and ord(self.data[13]) == 0
94
95    @property
96    def vid(self):
97        if self.tagged:
98            return ((ord(self.data[14]) << 8) | ord(self.data[15])) & 0x0fff
99        return 0
100
101
102class FDB(DebugMixIn):
103    def __init__(self, ageout, debug=False):
104        self._ageout = ageout
105        self._debug = debug
106        self._dict = {}
107
108    def lookup(self, frame):
109        mac = frame.dst_mac
110        vid = frame.vid
111
112        group = self._dict.get(vid)
113        if not group:
114            return None
115
116        entry = group.get(mac)
117        if not entry:
118            return None
119
120        if time.time() - entry['time'] > self._ageout:
121            port = self._dict[vid][mac]['port']
122            del self._dict[vid][mac]
123            if not self._dict[vid]:
124                del self._dict[vid]
125            self.dprintf('aged out: port:%d; vid:%d; mac:%s\n',
126                         lambda: (port.number, vid, mac.encode('hex')))
127            return None
128
129        return entry['port']
130
131    def learn(self, port, frame):
132        mac = frame.src_mac
133        vid = frame.vid
134
135        if vid not in self._dict:
136            self._dict[vid] = {}
137
138        self._dict[vid][mac] = {'time': time.time(), 'port': port}
139        self.dprintf('learned: port:%d; vid:%d; mac:%s\n',
140                     lambda: (port.number, vid, mac.encode('hex')))
141
142    def delete(self, port):
143        for vid in self._dict.keys():
144            for mac in self._dict[vid].keys():
145                if self._dict[vid][mac]['port'].number == port.number:
146                    del self._dict[vid][mac]
147                    self.dprintf('deleted: port:%d; vid:%d; mac:%s\n',
148                                 lambda: (port.number, vid, mac.encode('hex')))
149            if not self._dict[vid]:
150                del self._dict[vid]
151
152
153class SwitchPort(object):
154    def __init__(self, number, interface):
155        self.number = number
156        self.interface = interface
157        self.tx = 0
158        self.rx = 0
159        self.shut = False
160
161    @staticmethod
162    def cmp_by_number(x, y):
163        return cmp(x.number, y.number)
164
165
166class SwitchingHub(DebugMixIn):
167    def __init__(self, fdb, debug=False):
168        self._fdb = fdb
169        self._debug = debug
170        self._table = {}
171        self._next = 1
172
173    @property
174    def portlist(self):
175        return sorted(self._table.itervalues(), cmp=SwitchPort.cmp_by_number)
176
177    def get_port(self, portnum):
178        return self._table[portnum]
179
180    def register_port(self, interface):
181        try:
182            self._set_privattr('portnum', interface, self._next)  # XXX
183            self._table[self._next] = SwitchPort(self._next, interface)
184            return self._next
185        finally:
186            self._next += 1
187
188    def unregister_port(self, interface):
189        portnum = self._get_privattr('portnum', interface)
190        self._del_privattr('portnum', interface)
191        self._fdb.delete(self._table[portnum])
192        del self._table[portnum]
193
194    def send(self, dst_interfaces, frame):
195        portnums = (self._get_privattr('portnum', i) for i in dst_interfaces)
196        ports = (self._table[n] for n in portnums)
197        ports = (p for p in ports if not p.shut)
198        ports = sorted(ports, cmp=SwitchPort.cmp_by_number)
199
200        for p in ports:
201            p.interface.write_message(frame.data, True)
202            p.tx += 1
203
204        if ports:
205            self.dprintf('sent: port:%s; vid:%d; %s -> %s\n',
206                         lambda: (','.join(str(p.number) for p in ports),
207                                  frame.vid,
208                                  frame.src_mac.encode('hex'),
209                                  frame.dst_mac.encode('hex')))
210
211    def receive(self, src_interface, frame):
212        port = self._table[self._get_privattr('portnum', src_interface)]
213
214        if not port.shut:
215            port.rx += 1
216            self._forward(port, frame)
217
218    def _forward(self, src_port, frame):
219        try:
220            if not frame.src_multicast:
221                self._fdb.learn(src_port, frame)
222
223            if not frame.dst_multicast:
224                dst_port = self._fdb.lookup(frame)
225
226                if dst_port:
227                    self.send([dst_port.interface], frame)
228                    return
229
230            ports = set(self.portlist) - set([src_port])
231            self.send((p.interface for p in ports), frame)
232
233        except:  # ex. received invalid frame
234            traceback.print_exc()
235
236    def _privattr(self, name):
237        return '_%s_%s_%s' % (self.__class__.__name__, id(self), name)
238
239    def _set_privattr(self, name, obj, value):
240        return setattr(obj, self._privattr(name), value)
241
242    def _get_privattr(self, name, obj, defaults=None):
243        return getattr(obj, self._privattr(name), defaults)
244
245    def _del_privattr(self, name, obj):
246        return delattr(obj, self._privattr(name))
247
248
249class Htpasswd(object):
250    def __init__(self, path):
251        self._path = path
252        self._stat = None
253        self._data = {}
254
255    def auth(self, name, passwd):
256        passwd = base64.b64encode(hashlib.sha1(passwd).digest())
257        return self._data.get(name) == passwd
258
259    def load(self):
260        old_stat = self._stat
261
262        with open(self._path) as fp:
263            fileno = fp.fileno()
264            fcntl.flock(fileno, fcntl.LOCK_SH | fcntl.LOCK_NB)
265            self._stat = os.fstat(fileno)
266
267            unchanged = old_stat and \
268                        old_stat.st_ino == self._stat.st_ino and \
269                        old_stat.st_dev == self._stat.st_dev and \
270                        old_stat.st_mtime == self._stat.st_mtime
271
272            if not unchanged:
273                self._data = self._parse(fp)
274
275        return self
276
277    def _parse(self, fp):
278        data = {}
279        for line in fp:
280            line = line.strip()
281            if 0 <= line.find(':'):
282                name, passwd = line.split(':', 1)
283                if passwd.startswith('{SHA}'):
284                    data[name] = passwd[5:]
285        return data
286
287
288class BasicAuthMixIn(object):
289    def _execute(self, transforms, *args, **kwargs):
290        def do_execute():
291            sp = super(BasicAuthMixIn, self)
292            return sp._execute(transforms, *args, **kwargs)
293
294        def auth_required():
295            stream = getattr(self, 'stream', self.request.connection.stream)
296            stream.write(tornado.escape.utf8(
297                'HTTP/1.1 401 Authorization Required\r\n'
298                'WWW-Authenticate: Basic realm=etherws\r\n\r\n'
299            ))
300            stream.close()
301
302        try:
303            if not self._htpasswd:
304                return do_execute()
305
306            creds = self.request.headers.get('Authorization')
307
308            if not creds or not creds.startswith('Basic '):
309                return auth_required()
310
311            name, passwd = base64.b64decode(creds[6:]).split(':', 1)
312
313            if self._htpasswd.load().auth(name, passwd):
314                return do_execute()
315        except:
316            traceback.print_exc()
317
318        return auth_required()
319
320
321class EtherWebSocketHandler(DebugMixIn, BasicAuthMixIn, WebSocketHandler):
322    IFTYPE = 'server'
323
324    def __init__(self, app, req, switch, htpasswd=None, debug=False):
325        super(EtherWebSocketHandler, self).__init__(app, req)
326        self._switch = switch
327        self._htpasswd = htpasswd
328        self._debug = debug
329
330    def get_target(self):
331        return self.request.remote_ip
332
333    def open(self):
334        try:
335            return self._switch.register_port(self)
336        finally:
337            self.dprintf('connected: %s\n', lambda: self.request.remote_ip)
338
339    def on_message(self, message):
340        self._switch.receive(self, EthernetFrame(message))
341
342    def on_close(self):
343        self._switch.unregister_port(self)
344        self.dprintf('disconnected: %s\n', lambda: self.request.remote_ip)
345
346
347class TapHandler(DebugMixIn):
348    IFTYPE = 'tap'
349    READ_SIZE = 65535
350
351    def __init__(self, ioloop, switch, dev, debug=False):
352        self._ioloop = ioloop
353        self._switch = switch
354        self._dev = dev
355        self._debug = debug
356        self._tap = None
357
358    def get_target(self):
359        if self.closed:
360            return self._dev
361        return self._tap.name
362
363    @property
364    def closed(self):
365        return not self._tap
366
367    def open(self):
368        if not self.closed:
369            raise ValueError('already opened')
370        self._tap = TunTapDevice(self._dev, IFF_TAP | IFF_NO_PI)
371        self._tap.up()
372        self._ioloop.add_handler(self.fileno(), self, self._ioloop.READ)
373        return self._switch.register_port(self)
374
375    def close(self):
376        if self.closed:
377            raise ValueError('I/O operation on closed tap')
378        self._switch.unregister_port(self)
379        self._ioloop.remove_handler(self.fileno())
380        self._tap.close()
381        self._tap = None
382
383    def fileno(self):
384        if self.closed:
385            raise ValueError('I/O operation on closed tap')
386        return self._tap.fileno()
387
388    def write_message(self, message, binary=False):
389        if self.closed:
390            raise ValueError('I/O operation on closed tap')
391        self._tap.write(message)
392
393    def __call__(self, fd, events):
394        try:
395            self._switch.receive(self, EthernetFrame(self._read()))
396            return
397        except:
398            traceback.print_exc()
399        self.close()
400
401    def _read(self):
402        if self.closed:
403            raise ValueError('I/O operation on closed tap')
404        buf = []
405        while True:
406            buf.append(self._tap.read(self.READ_SIZE))
407            if len(buf[-1]) < self.READ_SIZE:
408                break
409        return ''.join(buf)
410
411
412class EtherWebSocketClient(DebugMixIn):
413    IFTYPE = 'client'
414
415    def __init__(self, ioloop, switch, url, ssl_=None, cred=None, debug=False):
416        self._ioloop = ioloop
417        self._switch = switch
418        self._url = url
419        self._ssl = ssl_
420        self._debug = debug
421        self._sock = None
422        self._options = {}
423
424        if isinstance(cred, dict) and cred['user'] and cred['passwd']:
425            token = base64.b64encode('%s:%s' % (cred['user'], cred['passwd']))
426            auth = ['Authorization: Basic %s' % token]
427            self._options['header'] = auth
428
429    def get_target(self):
430        return self._url
431
432    @property
433    def closed(self):
434        return not self._sock
435
436    def open(self):
437        sslwrap = websocket._SSLSocketWrapper
438
439        if not self.closed:
440            raise websocket.WebSocketException('already opened')
441
442        if self._ssl:
443            websocket._SSLSocketWrapper = self._ssl
444
445        try:
446            self._sock = websocket.WebSocket()
447            self._sock.connect(self._url, **self._options)
448            self._ioloop.add_handler(self.fileno(), self, self._ioloop.READ)
449            return self._switch.register_port(self)
450        finally:
451            websocket._SSLSocketWrapper = sslwrap
452            self.dprintf('connected: %s\n', lambda: self._url)
453
454    def close(self):
455        if self.closed:
456            raise websocket.WebSocketException('already closed')
457        self._switch.unregister_port(self)
458        self._ioloop.remove_handler(self.fileno())
459        self._sock.close()
460        self._sock = None
461        self.dprintf('disconnected: %s\n', lambda: self._url)
462
463    def fileno(self):
464        if self.closed:
465            raise websocket.WebSocketException('closed socket')
466        return self._sock.io_sock.fileno()
467
468    def write_message(self, message, binary=False):
469        if self.closed:
470            raise websocket.WebSocketException('closed socket')
471        if binary:
472            flag = websocket.ABNF.OPCODE_BINARY
473        else:
474            flag = websocket.ABNF.OPCODE_TEXT
475        self._sock.send(message, flag)
476
477    def __call__(self, fd, events):
478        try:
479            data = self._sock.recv()
480            if data is not None:
481                self._switch.receive(self, EthernetFrame(data))
482                return
483        except:
484            traceback.print_exc()
485        self.close()
486
487
488class EtherWebSocketControlHandler(DebugMixIn, BasicAuthMixIn, RequestHandler):
489    NAMESPACE = 'etherws.control'
490    IFTYPES = {
491        TapHandler.IFTYPE:           TapHandler,
492        EtherWebSocketClient.IFTYPE: EtherWebSocketClient,
493    }
494
495    def __init__(self, app, req, ioloop, switch, htpasswd=None, debug=False):
496        super(EtherWebSocketControlHandler, self).__init__(app, req)
497        self._ioloop = ioloop
498        self._switch = switch
499        self._htpasswd = htpasswd
500        self._debug = debug
501
502    def post(self):
503        id_ = None
504
505        try:
506            req = json.loads(self.request.body)
507            method = req['method']
508            params = req['params']
509            id_ = req.get('id')
510
511            if not method.startswith(self.NAMESPACE + '.'):
512                raise ValueError('invalid method: %s' % method)
513
514            if not isinstance(params, list):
515                raise ValueError('invalid params: %s' % params)
516
517            handler = 'handle_' + method[len(self.NAMESPACE) + 1:]
518            result = getattr(self, handler)(params)
519            self.finish({'result': result, 'error': None, 'id': id_})
520
521        except Exception as e:
522            traceback.print_exc()
523            self.finish({'result': None, 'error': {'msg': str(e)}, 'id': id_})
524
525    def handle_listPort(self, params):
526        list_ = [self._portstat(p) for p in self._switch.portlist]
527        return {'portlist': list_}
528
529    def handle_addPort(self, params):
530        list_ = []
531        for p in params:
532            type_ = p['type']
533            target = p['target']
534            options = getattr(self, '_optparse_' + type_)(p.get('options', {}))
535            klass = self.IFTYPES[type_]
536            interface = klass(self._ioloop, self._switch, target, **options)
537            portnum = interface.open()
538            list_.append(self._portstat(self._switch.get_port(portnum)))
539        return {'portlist': list_}
540
541    def handle_delPort(self, params):
542        list_ = []
543        for p in params:
544            port = self._switch.get_port(int(p['port']))
545            list_.append(self._portstat(port))
546            port.interface.close()
547        return {'portlist': list_}
548
549    def handle_shutPort(self, params):
550        list_ = []
551        for p in params:
552            port = self._switch.get_port(int(p['port']))
553            port.shut = bool(p['shut'])
554            list_.append(self._portstat(port))
555        return {'portlist': list_}
556
557    def _optparse_tap(self, opt):
558        return {'debug': self._debug}
559
560    def _optparse_client(self, opt):
561        args = {'cert_reqs': ssl.CERT_REQUIRED, 'ca_certs': opt.get('cacerts')}
562        if opt.get('insecure'):
563            args = {}
564        ssl_ = lambda sock: ssl.wrap_socket(sock, **args)
565        cred = {'user': opt.get('user'), 'passwd': opt.get('passwd')}
566        return {'ssl_': ssl_, 'cred': cred, 'debug': self._debug}
567
568    @staticmethod
569    def _portstat(port):
570        return {
571            'port':   port.number,
572            'type':   port.interface.IFTYPE,
573            'target': port.interface.get_target(),
574            'tx':     port.tx,
575            'rx':     port.rx,
576            'shut':   port.shut,
577        }
578
579
580def start_sw(args):
581    def daemonize(nochdir=False, noclose=False):
582        if os.fork() > 0:
583            sys.exit(0)
584
585        os.setsid()
586
587        if os.fork() > 0:
588            sys.exit(0)
589
590        if not nochdir:
591            os.chdir('/')
592
593        if not noclose:
594            os.umask(0)
595            sys.stdin.close()
596            sys.stdout.close()
597            sys.stderr.close()
598            os.close(0)
599            os.close(1)
600            os.close(2)
601            sys.stdin = open(os.devnull)
602            sys.stdout = open(os.devnull, 'a')
603            sys.stderr = open(os.devnull, 'a')
604
605    def checkabspath(ns, path):
606        val = getattr(ns, path, '')
607        if not val.startswith('/'):
608            raise ValueError('invalid %: %s' % (path, val))
609
610    def getsslopt(ns, key, cert):
611        kval = getattr(ns, key, None)
612        cval = getattr(ns, cert, None)
613        if kval and cval:
614            return {'keyfile': kval, 'certfile': cval}
615        elif kval or cval:
616            raise ValueError('both %s and %s are required' % (key, cert))
617        return None
618
619    def setrealpath(ns, *keys):
620        for k in keys:
621            v = getattr(ns, k, None)
622            if v is not None:
623                v = os.path.realpath(v)
624                open(v).close()  # check readable
625                setattr(ns, k, v)
626
627    def setport(ns, port, isssl):
628        val = getattr(ns, port, None)
629        if val is None:
630            if isssl:
631                return setattr(ns, port, 443)
632            return setattr(ns, port, 80)
633        if not (0 <= val <= 65535):
634            raise ValueError('invalid %s: %s' % (port, val))
635
636    def sethtpasswd(ns, htpasswd):
637        val = getattr(ns, htpasswd, None)
638        if val:
639            return setattr(ns, htpasswd, Htpasswd(val))
640
641    #if args.debug:
642    #    websocket.enableTrace(True)
643
644    if args.ageout <= 0:
645        raise ValueError('invalid ageout: %s' % args.ageout)
646
647    setrealpath(args, 'htpasswd', 'sslkey', 'sslcert')
648    setrealpath(args, 'ctlhtpasswd', 'ctlsslkey', 'ctlsslcert')
649
650    checkabspath(args, 'path')
651    checkabspath(args, 'ctlpath')
652
653    sslopt = getsslopt(args, 'sslkey', 'sslcert')
654    ctlsslopt = getsslopt(args, 'ctlsslkey', 'ctlsslcert')
655
656    setport(args, 'port', sslopt)
657    setport(args, 'ctlport', ctlsslopt)
658
659    sethtpasswd(args, 'htpasswd')
660    sethtpasswd(args, 'ctlhtpasswd')
661
662    ioloop = IOLoop.instance()
663    fdb = FDB(ageout=args.ageout, debug=args.debug)
664    switch = SwitchingHub(fdb, debug=args.debug)
665
666    if args.port == args.ctlport and args.host == args.ctlhost:
667        if args.path == args.ctlpath:
668            raise ValueError('same path/ctlpath on same host')
669        if args.sslkey != args.ctlsslkey:
670            raise ValueError('different sslkey/ctlsslkey on same host')
671        if args.sslcert != args.ctlsslcert:
672            raise ValueError('different sslcert/ctlsslcert on same host')
673
674        app = Application([
675            (args.path, EtherWebSocketHandler, {
676                'switch':   switch,
677                'htpasswd': args.htpasswd,
678                'debug':    args.debug,
679            }),
680            (args.ctlpath, EtherWebSocketControlHandler, {
681                'ioloop':   ioloop,
682                'switch':   switch,
683                'htpasswd': args.ctlhtpasswd,
684                'debug':    args.debug,
685            }),
686        ])
687        server = HTTPServer(app, ssl_options=sslopt)
688        server.listen(args.port, address=args.host)
689
690    else:
691        app = Application([(args.path, EtherWebSocketHandler, {
692            'switch':   switch,
693            'htpasswd': args.htpasswd,
694            'debug':    args.debug,
695        })])
696        server = HTTPServer(app, ssl_options=sslopt)
697        server.listen(args.port, address=args.host)
698
699        ctl = Application([(args.ctlpath, EtherWebSocketControlHandler, {
700            'ioloop':   ioloop,
701            'switch':   switch,
702            'htpasswd': args.ctlhtpasswd,
703            'debug':    args.debug,
704        })])
705        ctlserver = HTTPServer(ctl, ssl_options=ctlsslopt)
706        ctlserver.listen(args.ctlport, address=args.ctlhost)
707
708    if not args.foreground:
709        daemonize()
710
711    ioloop.start()
712
713
714def start_ctl(args):
715    import yaml
716
717    def request(args, method, params):
718        method = '.'.join([EtherWebSocketControlHandler.NAMESPACE, method])
719        data = json.dumps({'method': method, 'params': params})
720        req = urllib2.Request(args.ctlurl)
721        req.add_header('Content-type', 'application/json')
722        if args.ctluser:
723            if not args.ctlpasswd:
724                args.ctlpasswd = getpass.getpass()
725            token = base64.b64encode('%s:%s' % (args.ctluser, args.ctlpasswd))
726            req.add_header('Authorization', 'Basic %s' % token)
727        return json.loads(urllib2.urlopen(req, data).read())
728
729    def handle_ctl_addport(args):
730        params = [{
731            'type':    args.type,
732            'target':  args.target,
733            'options': {
734                'instance': args.insecure,
735                'cacerts':  args.cacerts,
736                'user':     args.user,
737                'passwd':   args.passwd,
738            }
739        }]
740        res = request(args, 'addPort', params)
741        print(yaml.safe_dump(res))
742
743    def handle_ctl_shutport(args):
744        if args.port <= 0:
745            raise ValueError('invalid port: %d' % args.port)
746        res = request(args, 'shutPort', [{'port': args.port, 'shut': args.no}])
747        print(yaml.safe_dump(res))
748
749    def handle_ctl_delport(args):
750        if args.port <= 0:
751            raise ValueError('invalid port: %d' % args.port)
752        res = request(args, 'delPort', [{'port': args.port}])
753        print(yaml.safe_dump(res))
754
755    def handle_ctl_listport(args):
756        res = request(args, 'listPort', [])
757        print(yaml.safe_dump(res))
758
759    locals()['handle_ctl_' + args.control_method](args)
760
761
762def main():
763    parser = argparse.ArgumentParser()
764    subcommand = parser.add_subparsers(dest='subcommand')
765
766    # -- sw command parser
767    parser_s = subcommand.add_parser('sw')
768
769    parser_s.add_argument('--debug', action='store_true', default=False)
770    parser_s.add_argument('--foreground', action='store_true', default=False)
771    parser_s.add_argument('--ageout', type=int, default=300)
772
773    parser_s.add_argument('--path', default='/')
774    parser_s.add_argument('--host', default='')
775    parser_s.add_argument('--port', type=int)
776    parser_s.add_argument('--htpasswd')
777    parser_s.add_argument('--sslkey')
778    parser_s.add_argument('--sslcert')
779
780    parser_s.add_argument('--ctlpath', default='/ctl')
781    parser_s.add_argument('--ctlhost', default='')
782    parser_s.add_argument('--ctlport', type=int)
783    parser_s.add_argument('--ctlhtpasswd')
784    parser_s.add_argument('--ctlsslkey')
785    parser_s.add_argument('--ctlsslcert')
786
787    # -- ctl command parser
788    parser_c = subcommand.add_parser('ctl')
789    parser_c.add_argument('--ctlurl', default='http://localhost/ctl')
790    parser_c.add_argument('--ctluser')
791    parser_c.add_argument('--ctlpasswd')
792
793    control_method = parser_c.add_subparsers(dest='control_method')
794
795    parser_c_ap = control_method.add_parser('addport')
796    parser_c_ap.add_argument(
797        'type', choices=EtherWebSocketControlHandler.IFTYPES.keys())
798    parser_c_ap.add_argument('target')
799    parser_c_ap.add_argument('--insecure', action='store_true', default=False)
800    parser_c_ap.add_argument('--cacerts')
801    parser_c_ap.add_argument('--user')
802    parser_c_ap.add_argument('--passwd')
803
804    parser_c_sp = control_method.add_parser('shutport')
805    parser_c_sp.add_argument('port', type=int)
806    parser_c_sp.add_argument('--no', action='store_false', default=True)
807
808    parser_c_dp = control_method.add_parser('delport')
809    parser_c_dp.add_argument('port', type=int)
810
811    parser_c_lp = control_method.add_parser('listport')
812
813    # -- go
814    args = parser.parse_args()
815    globals()['start_' + args.subcommand](args)
816
817
818if __name__ == '__main__':
819    main()
Note: See TracBrowser for help on using the repository browser.