source: etherws/trunk/etherws.py @ 190

Revision 190, 25.1 KB checked in by atzm, 12 years ago (diff)
  • add ctl command
  • 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    def __init__(self, app, req, switch, htpasswd=None, debug=False):
323        super(EtherWebSocketHandler, self).__init__(app, req)
324        self._switch = switch
325        self._htpasswd = htpasswd
326        self._debug = debug
327
328    @classmethod
329    def get_type(cls):
330        return 'server'
331
332    def get_target(self):
333        return self.request.remote_ip
334
335    def open(self):
336        try:
337            return self._switch.register_port(self)
338        finally:
339            self.dprintf('connected: %s\n', lambda: self.request.remote_ip)
340
341    def on_message(self, message):
342        self._switch.receive(self, EthernetFrame(message))
343
344    def on_close(self):
345        self._switch.unregister_port(self)
346        self.dprintf('disconnected: %s\n', lambda: self.request.remote_ip)
347
348
349class TapHandler(DebugMixIn):
350    READ_SIZE = 65535
351
352    def __init__(self, ioloop, switch, dev, debug=False):
353        self._ioloop = ioloop
354        self._switch = switch
355        self._dev = dev
356        self._debug = debug
357        self._tap = None
358
359    @classmethod
360    def get_type(cls):
361        return 'tap'
362
363    def get_target(self):
364        if self.closed:
365            return self._dev
366        return self._tap.name
367
368    @property
369    def closed(self):
370        return not self._tap
371
372    def open(self):
373        if not self.closed:
374            raise ValueError('already opened')
375        self._tap = TunTapDevice(self._dev, IFF_TAP | IFF_NO_PI)
376        self._tap.up()
377        self._ioloop.add_handler(self.fileno(), self, self._ioloop.READ)
378        return self._switch.register_port(self)
379
380    def close(self):
381        if self.closed:
382            raise ValueError('I/O operation on closed tap')
383        self._switch.unregister_port(self)
384        self._ioloop.remove_handler(self.fileno())
385        self._tap.close()
386        self._tap = None
387
388    def fileno(self):
389        if self.closed:
390            raise ValueError('I/O operation on closed tap')
391        return self._tap.fileno()
392
393    def write_message(self, message, binary=False):
394        if self.closed:
395            raise ValueError('I/O operation on closed tap')
396        self._tap.write(message)
397
398    def __call__(self, fd, events):
399        try:
400            self._switch.receive(self, EthernetFrame(self._read()))
401            return
402        except:
403            traceback.print_exc()
404        self.close()
405
406    def _read(self):
407        if self.closed:
408            raise ValueError('I/O operation on closed tap')
409        buf = []
410        while True:
411            buf.append(self._tap.read(self.READ_SIZE))
412            if len(buf[-1]) < self.READ_SIZE:
413                break
414        return ''.join(buf)
415
416
417class EtherWebSocketClient(DebugMixIn):
418    def __init__(self, ioloop, switch, url, ssl_=None, cred=None, debug=False):
419        self._ioloop = ioloop
420        self._switch = switch
421        self._url = url
422        self._ssl = ssl_
423        self._debug = debug
424        self._sock = None
425        self._options = {}
426
427        if isinstance(cred, dict) and cred['user'] and cred['passwd']:
428            token = base64.b64encode('%s:%s' % (cred['user'], cred['passwd']))
429            auth = ['Authorization: Basic %s' % token]
430            self._options['header'] = auth
431
432    @classmethod
433    def get_type(cls):
434        return 'client'
435
436    def get_target(self):
437        return self._url
438
439    @property
440    def closed(self):
441        return not self._sock
442
443    def open(self):
444        sslwrap = websocket._SSLSocketWrapper
445
446        if not self.closed:
447            raise websocket.WebSocketException('already opened')
448
449        if self._ssl:
450            websocket._SSLSocketWrapper = self._ssl
451
452        try:
453            self._sock = websocket.WebSocket()
454            self._sock.connect(self._url, **self._options)
455            self._ioloop.add_handler(self.fileno(), self, self._ioloop.READ)
456            return self._switch.register_port(self)
457        finally:
458            websocket._SSLSocketWrapper = sslwrap
459            self.dprintf('connected: %s\n', lambda: self._url)
460
461    def close(self):
462        if self.closed:
463            raise websocket.WebSocketException('already closed')
464        self._switch.unregister_port(self)
465        self._ioloop.remove_handler(self.fileno())
466        self._sock.close()
467        self._sock = None
468        self.dprintf('disconnected: %s\n', lambda: self._url)
469
470    def fileno(self):
471        if self.closed:
472            raise websocket.WebSocketException('closed socket')
473        return self._sock.io_sock.fileno()
474
475    def write_message(self, message, binary=False):
476        if self.closed:
477            raise websocket.WebSocketException('closed socket')
478        if binary:
479            flag = websocket.ABNF.OPCODE_BINARY
480        else:
481            flag = websocket.ABNF.OPCODE_TEXT
482        self._sock.send(message, flag)
483
484    def __call__(self, fd, events):
485        try:
486            data = self._sock.recv()
487            if data is not None:
488                self._switch.receive(self, EthernetFrame(data))
489                return
490        except:
491            traceback.print_exc()
492        self.close()
493
494
495class EtherWebSocketControlHandler(DebugMixIn, BasicAuthMixIn, RequestHandler):
496    NAMESPACE = 'etherws.control'
497    INTERFACES = {
498        TapHandler.get_type():           TapHandler,
499        EtherWebSocketClient.get_type(): EtherWebSocketClient,
500    }
501
502    def __init__(self, app, req, ioloop, switch, htpasswd=None, debug=False):
503        super(EtherWebSocketControlHandler, self).__init__(app, req)
504        self._ioloop = ioloop
505        self._switch = switch
506        self._htpasswd = htpasswd
507        self._debug = debug
508
509    def post(self):
510        id_ = None
511
512        try:
513            req = json.loads(self.request.body)
514            method = req['method']
515            params = req['params']
516            id_ = req.get('id')
517
518            if not method.startswith(self.NAMESPACE + '.'):
519                raise ValueError('invalid method: %s' % method)
520
521            if not isinstance(params, list):
522                raise ValueError('invalid params: %s' % params)
523
524            handler = 'handle_' + method[len(self.NAMESPACE) + 1:]
525            result = getattr(self, handler)(params)
526            self.finish({'result': result, 'error': None, 'id': id_})
527
528        except Exception as e:
529            traceback.print_exc()
530            self.finish({'result': None, 'error': {'msg': str(e)}, 'id': id_})
531
532    def handle_listPort(self, params):
533        list_ = [self._portstat(p) for p in self._switch.portlist]
534        return {'portlist': list_}
535
536    def handle_addPort(self, params):
537        list_ = []
538        for p in params:
539            type_ = p['type']
540            target = p['target']
541            options = getattr(self, '_optparse_' + type_)(p.get('options', {}))
542            klass = self.INTERFACES[type_]
543            interface = klass(self._ioloop, self._switch, target, **options)
544            portnum = interface.open()
545            list_.append(self._portstat(self._switch.get_port(portnum)))
546        return {'portlist': list_}
547
548    def handle_delPort(self, params):
549        list_ = []
550        for p in params:
551            port = self._switch.get_port(int(p['port']))
552            list_.append(self._portstat(port))
553            port.interface.close()
554        return {'portlist': list_}
555
556    def handle_shutPort(self, params):
557        list_ = []
558        for p in params:
559            port = self._switch.get_port(int(p['port']))
560            port.shut = bool(p['shut'])
561            list_.append(self._portstat(port))
562        return {'portlist': list_}
563
564    def _optparse_tap(self, opt):
565        return {'debug': self._debug}
566
567    def _optparse_client(self, opt):
568        args = {'cert_reqs': ssl.CERT_REQUIRED, 'ca_certs': opt.get('cacerts')}
569        if opt.get('insecure'):
570            args = {}
571        ssl_ = lambda sock: ssl.wrap_socket(sock, **args)
572        cred = {'user': opt.get('user'), 'passwd': opt.get('passwd')}
573        return {'ssl_': ssl_, 'cred': cred, 'debug': self._debug}
574
575    @staticmethod
576    def _portstat(port):
577        return {
578            'port':   port.number,
579            'type':   port.interface.get_type(),
580            'target': port.interface.get_target(),
581            'tx':     port.tx,
582            'rx':     port.rx,
583            'shut':   port.shut,
584        }
585
586
587def start_sw(args):
588    def daemonize(nochdir=False, noclose=False):
589        if os.fork() > 0:
590            sys.exit(0)
591
592        os.setsid()
593
594        if os.fork() > 0:
595            sys.exit(0)
596
597        if not nochdir:
598            os.chdir('/')
599
600        if not noclose:
601            os.umask(0)
602            sys.stdin.close()
603            sys.stdout.close()
604            sys.stderr.close()
605            os.close(0)
606            os.close(1)
607            os.close(2)
608            sys.stdin = open(os.devnull)
609            sys.stdout = open(os.devnull, 'a')
610            sys.stderr = open(os.devnull, 'a')
611
612    def checkabspath(ns, path):
613        val = getattr(ns, path, '')
614        if not val.startswith('/'):
615            raise ValueError('invalid %: %s' % (path, val))
616
617    def getsslopt(ns, key, cert):
618        kval = getattr(ns, key, None)
619        cval = getattr(ns, cert, None)
620        if kval and cval:
621            return {'keyfile': kval, 'certfile': cval}
622        elif kval or cval:
623            raise ValueError('both %s and %s are required' % (key, cert))
624        return None
625
626    def setrealpath(ns, *keys):
627        for k in keys:
628            v = getattr(ns, k, None)
629            if v is not None:
630                v = os.path.realpath(v)
631                open(v).close()  # check readable
632                setattr(ns, k, v)
633
634    def setport(ns, port, isssl):
635        val = getattr(ns, port, None)
636        if val is None:
637            if isssl:
638                return setattr(ns, port, 443)
639            return setattr(ns, port, 80)
640        if not (0 <= val <= 65535):
641            raise ValueError('invalid %s: %s' % (port, val))
642
643    def sethtpasswd(ns, htpasswd):
644        val = getattr(ns, htpasswd, None)
645        if val:
646            return setattr(ns, htpasswd, Htpasswd(val))
647
648    #if args.debug:
649    #    websocket.enableTrace(True)
650
651    if args.ageout <= 0:
652        raise ValueError('invalid ageout: %s' % args.ageout)
653
654    setrealpath(args, 'htpasswd', 'sslkey', 'sslcert')
655    setrealpath(args, 'ctlhtpasswd', 'ctlsslkey', 'ctlsslcert')
656
657    checkabspath(args, 'path')
658    checkabspath(args, 'ctlpath')
659
660    sslopt = getsslopt(args, 'sslkey', 'sslcert')
661    ctlsslopt = getsslopt(args, 'ctlsslkey', 'ctlsslcert')
662
663    setport(args, 'port', sslopt)
664    setport(args, 'ctlport', ctlsslopt)
665
666    sethtpasswd(args, 'htpasswd')
667    sethtpasswd(args, 'ctlhtpasswd')
668
669    ioloop = IOLoop.instance()
670    fdb = FDB(ageout=args.ageout, debug=args.debug)
671    switch = SwitchingHub(fdb, debug=args.debug)
672
673    if args.port == args.ctlport and args.host == args.ctlhost:
674        if args.path == args.ctlpath:
675            raise ValueError('same path/ctlpath on same host')
676        if args.sslkey != args.ctlsslkey:
677            raise ValueError('different sslkey/ctlsslkey on same host')
678        if args.sslcert != args.ctlsslcert:
679            raise ValueError('different sslcert/ctlsslcert on same host')
680
681        app = Application([
682            (args.path, EtherWebSocketHandler, {
683                'switch':   switch,
684                'htpasswd': args.htpasswd,
685                'debug':    args.debug,
686            }),
687            (args.ctlpath, EtherWebSocketControlHandler, {
688                'ioloop':   ioloop,
689                'switch':   switch,
690                'htpasswd': args.ctlhtpasswd,
691                'debug':    args.debug,
692            }),
693        ])
694        server = HTTPServer(app, ssl_options=sslopt)
695        server.listen(args.port, address=args.host)
696
697    else:
698        app = Application([(args.path, EtherWebSocketHandler, {
699            'switch':   switch,
700            'htpasswd': args.htpasswd,
701            'debug':    args.debug,
702        })])
703        server = HTTPServer(app, ssl_options=sslopt)
704        server.listen(args.port, address=args.host)
705
706        ctl = Application([(args.ctlpath, EtherWebSocketControlHandler, {
707            'ioloop':   ioloop,
708            'switch':   switch,
709            'htpasswd': args.ctlhtpasswd,
710            'debug':    args.debug,
711        })])
712        ctlserver = HTTPServer(ctl, ssl_options=ctlsslopt)
713        ctlserver.listen(args.ctlport, address=args.ctlhost)
714
715    if not args.foreground:
716        daemonize()
717
718    ioloop.start()
719
720
721def start_ctl(args):
722    import yaml
723
724    def request(args, method, params):
725        method = '.'.join([EtherWebSocketControlHandler.NAMESPACE, method])
726        data = json.dumps({'method': method, 'params': params})
727        req = urllib2.Request(args.ctlurl)
728        req.add_header('Content-type', 'application/json')
729        if args.ctluser:
730            if not args.ctlpasswd:
731                args.ctlpasswd = getpass.getpass()
732            token = base64.b64encode('%s:%s' % (args.ctluser, args.ctlpasswd))
733            req.add_header('Authorization', 'Basic %s' % token)
734        return json.loads(urllib2.urlopen(req, data).read())
735
736    def handle_ctl_addport(args):
737        raise NotImplementedError('addport')
738
739    def handle_ctl_shutport(args):
740        if args.port <= 0:
741            raise ValueError('invalid port: %d' % args.port)
742        res = request(args, 'shutPort', [{'port': args.port, 'shut': args.no}])
743        print(yaml.safe_dump(res))
744
745    def handle_ctl_delport(args):
746        if args.port <= 0:
747            raise ValueError('invalid port: %d' % args.port)
748        res = request(args, 'delPort', [{'port': args.port}])
749        print(yaml.safe_dump(res))
750
751    def handle_ctl_listport(args):
752        res = request(args, 'listPort', [])
753        print(yaml.safe_dump(res))
754
755    locals()['handle_ctl_' + args.control_method](args)
756
757
758def main():
759    parser = argparse.ArgumentParser()
760    subcommand = parser.add_subparsers(dest='subcommand')
761
762    # -- sw command parser
763    parser_s = subcommand.add_parser('sw')
764
765    parser_s.add_argument('--debug', action='store_true', default=False)
766    parser_s.add_argument('--foreground', action='store_true', default=False)
767    parser_s.add_argument('--ageout', type=int, default=300)
768
769    parser_s.add_argument('--path', default='/')
770    parser_s.add_argument('--host', default='')
771    parser_s.add_argument('--port', type=int)
772    parser_s.add_argument('--htpasswd')
773    parser_s.add_argument('--sslkey')
774    parser_s.add_argument('--sslcert')
775
776    parser_s.add_argument('--ctlpath', default='/ctl')
777    parser_s.add_argument('--ctlhost', default='')
778    parser_s.add_argument('--ctlport', type=int)
779    parser_s.add_argument('--ctlhtpasswd')
780    parser_s.add_argument('--ctlsslkey')
781    parser_s.add_argument('--ctlsslcert')
782
783    # -- ctl command parser
784    parser_c = subcommand.add_parser('ctl')
785    parser_c.add_argument('--ctlurl', default='http://localhost/ctl')
786    parser_c.add_argument('--ctluser')
787    parser_c.add_argument('--ctlpasswd')
788
789    control_method = parser_c.add_subparsers(dest='control_method')
790
791    parser_c_ap = control_method.add_parser('addport')
792    parser_c_ap.add_argument('target')
793    parser_c_ap.add_argument('--insecure', action='store_true', default=False)
794    parser_c_ap.add_argument('--cacerts')
795    parser_c_ap.add_argument('--user')
796    parser_c_ap.add_argument('--passwd')
797
798    parser_c_sp = control_method.add_parser('shutport')
799    parser_c_sp.add_argument('port', type=int)
800    parser_c_sp.add_argument('--no', action='store_false', default=True)
801
802    parser_c_dp = control_method.add_parser('delport')
803    parser_c_dp.add_argument('port', type=int)
804
805    parser_c_lp = control_method.add_parser('listport')
806
807    # -- go
808    args = parser.parse_args()
809    globals()['start_' + args.subcommand](args)
810
811
812if __name__ == '__main__':
813    main()
Note: See TracBrowser for help on using the repository browser.