source: etherws/trunk/etherws.py @ 192

Revision 192, 25.5 KB checked in by atzm, 12 years ago (diff)
  • adjust ctl outputs
  • 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            msg = '%s: %s' % (e.__class__.__name__, str(e))
524            self.finish({'result': None, 'error': {'message': msg}, 'id': id_})
525
526    def handle_listPort(self, params):
527        list_ = [self._portstat(p) for p in self._switch.portlist]
528        return {'portlist': list_}
529
530    def handle_addPort(self, params):
531        list_ = []
532        for p in params:
533            type_ = p['type']
534            target = p['target']
535            options = getattr(self, '_optparse_' + type_)(p.get('options', {}))
536            klass = self.IFTYPES[type_]
537            interface = klass(self._ioloop, self._switch, target, **options)
538            portnum = interface.open()
539            list_.append(self._portstat(self._switch.get_port(portnum)))
540        return {'portlist': list_}
541
542    def handle_delPort(self, params):
543        list_ = []
544        for p in params:
545            port = self._switch.get_port(int(p['port']))
546            list_.append(self._portstat(port))
547            port.interface.close()
548        return {'portlist': list_}
549
550    def handle_shutPort(self, params):
551        list_ = []
552        for p in params:
553            port = self._switch.get_port(int(p['port']))
554            port.shut = bool(p['shut'])
555            list_.append(self._portstat(port))
556        return {'portlist': list_}
557
558    def _optparse_tap(self, opt):
559        return {'debug': self._debug}
560
561    def _optparse_client(self, opt):
562        args = {'cert_reqs': ssl.CERT_REQUIRED, 'ca_certs': opt.get('cacerts')}
563        if opt.get('insecure'):
564            args = {}
565        ssl_ = lambda sock: ssl.wrap_socket(sock, **args)
566        cred = {'user': opt.get('user'), 'passwd': opt.get('passwd')}
567        return {'ssl_': ssl_, 'cred': cred, 'debug': self._debug}
568
569    @staticmethod
570    def _portstat(port):
571        return {
572            'port':   port.number,
573            'type':   port.interface.IFTYPE,
574            'target': port.interface.get_target(),
575            'tx':     port.tx,
576            'rx':     port.rx,
577            'shut':   port.shut,
578        }
579
580
581def start_sw(args):
582    def daemonize(nochdir=False, noclose=False):
583        if os.fork() > 0:
584            sys.exit(0)
585
586        os.setsid()
587
588        if os.fork() > 0:
589            sys.exit(0)
590
591        if not nochdir:
592            os.chdir('/')
593
594        if not noclose:
595            os.umask(0)
596            sys.stdin.close()
597            sys.stdout.close()
598            sys.stderr.close()
599            os.close(0)
600            os.close(1)
601            os.close(2)
602            sys.stdin = open(os.devnull)
603            sys.stdout = open(os.devnull, 'a')
604            sys.stderr = open(os.devnull, 'a')
605
606    def checkabspath(ns, path):
607        val = getattr(ns, path, '')
608        if not val.startswith('/'):
609            raise ValueError('invalid %: %s' % (path, val))
610
611    def getsslopt(ns, key, cert):
612        kval = getattr(ns, key, None)
613        cval = getattr(ns, cert, None)
614        if kval and cval:
615            return {'keyfile': kval, 'certfile': cval}
616        elif kval or cval:
617            raise ValueError('both %s and %s are required' % (key, cert))
618        return None
619
620    def setrealpath(ns, *keys):
621        for k in keys:
622            v = getattr(ns, k, None)
623            if v is not None:
624                v = os.path.realpath(v)
625                open(v).close()  # check readable
626                setattr(ns, k, v)
627
628    def setport(ns, port, isssl):
629        val = getattr(ns, port, None)
630        if val is None:
631            if isssl:
632                return setattr(ns, port, 443)
633            return setattr(ns, port, 80)
634        if not (0 <= val <= 65535):
635            raise ValueError('invalid %s: %s' % (port, val))
636
637    def sethtpasswd(ns, htpasswd):
638        val = getattr(ns, htpasswd, None)
639        if val:
640            return setattr(ns, htpasswd, Htpasswd(val))
641
642    #if args.debug:
643    #    websocket.enableTrace(True)
644
645    if args.ageout <= 0:
646        raise ValueError('invalid ageout: %s' % args.ageout)
647
648    setrealpath(args, 'htpasswd', 'sslkey', 'sslcert')
649    setrealpath(args, 'ctlhtpasswd', 'ctlsslkey', 'ctlsslcert')
650
651    checkabspath(args, 'path')
652    checkabspath(args, 'ctlpath')
653
654    sslopt = getsslopt(args, 'sslkey', 'sslcert')
655    ctlsslopt = getsslopt(args, 'ctlsslkey', 'ctlsslcert')
656
657    setport(args, 'port', sslopt)
658    setport(args, 'ctlport', ctlsslopt)
659
660    sethtpasswd(args, 'htpasswd')
661    sethtpasswd(args, 'ctlhtpasswd')
662
663    ioloop = IOLoop.instance()
664    fdb = FDB(ageout=args.ageout, debug=args.debug)
665    switch = SwitchingHub(fdb, debug=args.debug)
666
667    if args.port == args.ctlport and args.host == args.ctlhost:
668        if args.path == args.ctlpath:
669            raise ValueError('same path/ctlpath on same host')
670        if args.sslkey != args.ctlsslkey:
671            raise ValueError('different sslkey/ctlsslkey on same host')
672        if args.sslcert != args.ctlsslcert:
673            raise ValueError('different sslcert/ctlsslcert on same host')
674
675        app = Application([
676            (args.path, EtherWebSocketHandler, {
677                'switch':   switch,
678                'htpasswd': args.htpasswd,
679                'debug':    args.debug,
680            }),
681            (args.ctlpath, EtherWebSocketControlHandler, {
682                'ioloop':   ioloop,
683                'switch':   switch,
684                'htpasswd': args.ctlhtpasswd,
685                'debug':    args.debug,
686            }),
687        ])
688        server = HTTPServer(app, ssl_options=sslopt)
689        server.listen(args.port, address=args.host)
690
691    else:
692        app = Application([(args.path, EtherWebSocketHandler, {
693            'switch':   switch,
694            'htpasswd': args.htpasswd,
695            'debug':    args.debug,
696        })])
697        server = HTTPServer(app, ssl_options=sslopt)
698        server.listen(args.port, address=args.host)
699
700        ctl = Application([(args.ctlpath, EtherWebSocketControlHandler, {
701            'ioloop':   ioloop,
702            'switch':   switch,
703            'htpasswd': args.ctlhtpasswd,
704            'debug':    args.debug,
705        })])
706        ctlserver = HTTPServer(ctl, ssl_options=ctlsslopt)
707        ctlserver.listen(args.ctlport, address=args.ctlhost)
708
709    if not args.foreground:
710        daemonize()
711
712    ioloop.start()
713
714
715def start_ctl(args):
716    import yaml
717
718    def request(args, method, params):
719        method = '.'.join([EtherWebSocketControlHandler.NAMESPACE, method])
720        data = json.dumps({'method': method, 'params': params})
721        req = urllib2.Request(args.ctlurl)
722        req.add_header('Content-type', 'application/json')
723        if args.ctluser:
724            if not args.ctlpasswd:
725                args.ctlpasswd = getpass.getpass()
726            token = base64.b64encode('%s:%s' % (args.ctluser, args.ctlpasswd))
727            req.add_header('Authorization', 'Basic %s' % token)
728        return json.loads(urllib2.urlopen(req, data).read())
729
730    def handle_ctl_addport(args):
731        params = [{
732            'type':    args.type,
733            'target':  args.target,
734            'options': {
735                'instance': args.insecure,
736                'cacerts':  args.cacerts,
737                'user':     args.user,
738                'passwd':   args.passwd,
739            }
740        }]
741        return request(args, 'addPort', params)
742
743    def handle_ctl_shutport(args):
744        if args.port <= 0:
745            raise ValueError('invalid port: %d' % args.port)
746        params = [{'port': args.port, 'shut': args.no}]
747        return request(args, 'shutPort', params)
748
749    def handle_ctl_delport(args):
750        if args.port <= 0:
751            raise ValueError('invalid port: %d' % args.port)
752        params = [{'port': args.port}]
753        return request(args, 'delPort', params)
754
755    def handle_ctl_listport(args):
756        return request(args, 'listPort', [])
757
758    res = locals()['handle_ctl_' + args.control_method](args)
759
760    if res['error']:
761        print(res['error']['message'])
762    else:
763        print(yaml.safe_dump(res['result']['portlist']).strip())
764
765
766def main():
767    parser = argparse.ArgumentParser()
768    subcommand = parser.add_subparsers(dest='subcommand')
769
770    # -- sw command parser
771    parser_s = subcommand.add_parser('sw')
772
773    parser_s.add_argument('--debug', action='store_true', default=False)
774    parser_s.add_argument('--foreground', action='store_true', default=False)
775    parser_s.add_argument('--ageout', type=int, default=300)
776
777    parser_s.add_argument('--path', default='/')
778    parser_s.add_argument('--host', default='')
779    parser_s.add_argument('--port', type=int)
780    parser_s.add_argument('--htpasswd')
781    parser_s.add_argument('--sslkey')
782    parser_s.add_argument('--sslcert')
783
784    parser_s.add_argument('--ctlpath', default='/ctl')
785    parser_s.add_argument('--ctlhost', default='')
786    parser_s.add_argument('--ctlport', type=int)
787    parser_s.add_argument('--ctlhtpasswd')
788    parser_s.add_argument('--ctlsslkey')
789    parser_s.add_argument('--ctlsslcert')
790
791    # -- ctl command parser
792    parser_c = subcommand.add_parser('ctl')
793    parser_c.add_argument('--ctlurl', default='http://localhost/ctl')
794    parser_c.add_argument('--ctluser')
795    parser_c.add_argument('--ctlpasswd')
796
797    control_method = parser_c.add_subparsers(dest='control_method')
798
799    parser_c_ap = control_method.add_parser('addport')
800    parser_c_ap.add_argument(
801        'type', choices=EtherWebSocketControlHandler.IFTYPES.keys())
802    parser_c_ap.add_argument('target')
803    parser_c_ap.add_argument('--insecure', action='store_true', default=False)
804    parser_c_ap.add_argument('--cacerts')
805    parser_c_ap.add_argument('--user')
806    parser_c_ap.add_argument('--passwd')
807
808    parser_c_sp = control_method.add_parser('shutport')
809    parser_c_sp.add_argument('port', type=int)
810    parser_c_sp.add_argument('--no', action='store_false', default=True)
811
812    parser_c_dp = control_method.add_parser('delport')
813    parser_c_dp.add_argument('port', type=int)
814
815    parser_c_lp = control_method.add_parser('listport')
816
817    # -- go
818    args = parser.parse_args()
819    globals()['start_' + args.subcommand](args)
820
821
822if __name__ == '__main__':
823    main()
Note: See TracBrowser for help on using the repository browser.