source: fedd/abac_client.py @ 874e015

version-2.00
Last change on this file since 874e015 was 181cf9c, checked in by Jay Jacobs <Jay.Jacobs@…>, 15 years ago

API tested. Use abac_test.sh for example usage.

  • Property mode set to 100755
File size: 22.9 KB
RevLine 
[19a3e06]1#!/usr/local/bin/python
2
3import sys
4import os
5import pwd
6import tempfile
7import traceback
8import subprocess
9import re
10import xml.parsers.expat
11import time
12
13from federation import fedid, service_error
14from federation.util import fedd_ssl_context, pack_id, unpack_id
15from federation.abac_remote_service import abac_service_caller
16#from federation import topdl
17
18from optparse import OptionParser, OptionValueError
19
20class IDFormatException(RuntimeError): pass
21
22class access_method:
23    """Encapsulates an access method generically."""
24    (type_ssh, type_x509, type_pgp) = ('sshPubkey', 'X509', 'pgpPubkey')
25    default_type = type_ssh
26    def __init__(self, buf=None, type=None, file=None):
27        self.buf = buf
28
29        if type != None: self.type = type
30        else: self.type = access_method.default_type
31
32        if file != None:
33            self.readfile(file)
34   
35    def readfile(self, file, type=None):
36        f = open(file, "r")
37        self.buf = f.read();
38        f.close()
39        if type == None:
40            if self.type == None:
41                self.type = access_method.default_type
42        else:
43            self.type = type;
44
45
46# Base class that will do a the SOAP/XMLRPC exchange for a request.
47class abac_rpc:
48    class RPCException:
49        def __init__(self, fb):
50            self.desc = fb.get('desc', None)
51            self.code = fb.get('code', -1)
52            self.errstr = fb.get('errstr', None)
53
54    def __init__(self, pre):
55        """
56        Specialize the class for the pre method
57        """
58        self.RequestBody="%sRequestBody" % pre
59        self.ResponseBody="%sResponseBody" % pre
60        self.method = pre
61
62        self.caller = abac_service_caller(self.method)
63        self.RPCException = abac_rpc.RPCException
64
65
66    def add_ssh_key(self, option, opt_str, value, parser, access_keys):
67        try:
68            access_keys.append(access_method(file=value,
69                type=access_method.type_ssh))
70        except IOError, (errno, strerror):
71            raise OptionValueError("Cannot generate sshPubkey from %s: "\
72                    "%s (%d)" % (value,strerror,errno))
73
74    def add_x509_cert(self, option, opt_str, value, parser, access_keys):
75        try:
76            access_keys.append(access_method(file=value,
77                type=access_method.type_x509))
78        except IOError, (errno, strerror):
79            raise OptionValueError("Cannot read x509 cert from %s: %s (%d)" %
80                    (value,strerror,errno))
81
82    def add_node_desc(self, option, opt_str, value, parser, node_descs):
83        def none_if_zero(x):
84            if len(x) > 0: return x
85            else: return None
86
87        params = map(none_if_zero, value.split(":"));
88
89        if len(params) < 4 and len(params) > 1:
90            node_descs.append(node_desc(*params))
91        else:
92            raise OptionValueError("Bad node description: %s" % value)
93
94    def get_user_info(self, access_keys):
95        pw = pwd.getpwuid(os.getuid());
96        try_cert=None
97        user = None
98
99        if pw != None:
100            user = pw[0]
101            try_cert = "%s/.ssl/emulab.pem" % pw[5];
102            if not os.access(try_cert, os.R_OK):
103                try_cert = None
104            if len(access_keys) == 0:
105                for k in ["%s/.ssh/id_rsa.pub", "%s/.ssh/id_dsa.pub",
106                        "%s/.ssh/identity.pub"]:
107                    try_key = k % pw[5];
108                    if os.access(try_key, os.R_OK):
109                        access_keys.append(access_method(file=try_key,
110                            type=access_method.type_ssh))
111                        break
112        return (user, try_cert)
113
114    def do_rpc(self, req_dict, url, transport, cert, trusted, tracefile=None,
115            serialize_only=False):
116        """
117        The work of sending and parsing the RPC as either XMLRPC or SOAP
118        """
119
120        context = None
121        while context == None:
122            try:
123                context = fedd_ssl_context(cert, trusted)
124            except Exception, e:
125                # Yes, doing this on message type is not ideal.  The string
126                # comes from OpenSSL, so check there is this stops working.
127                if str(e) == "bad decrypt":
128                    print >>sys.stderr, "Bad Passphrase given."
129                else: raise
130
131        if transport == "soap":
132            if serialize_only:
133                print self.caller.serialize_soap(req_dict)
134                sys.exit(0)
135            else:
136                try:
137                    resp = self.caller.call_soap_service(url, req_dict,
138                            context=context, tracefile=tracefile)
139                except service_error, e:
140                    raise self.RPCException( {\
141                            'code': e.code,
142                            'desc': e.desc,
143                            'errstr': e.code_string()\
144                        })
145        elif transport == "xmlrpc":
146            if serialize_only:
147                ser = dumps((req_dict,))
148                print ser
149                sys.exit(0)
150            else:
151                try:
152                    resp = self.caller.call_xmlrpc_service(url, req_dict,
153                            context=context, tracefile=tracefile)
154                except service_error, e:
155                    raise self.RPCException( {\
156                            'code': e.code,
157                            'desc': e.desc,
158                            'errstr': e.code_string()\
159                        })
160
161        else:
162            raise RuntimeError("Unknown RPC transport: %s" % transport)
163
164        if resp.has_key(self.ResponseBody):
165            return resp[self.ResponseBody]
166        else:
167            raise  RuntimeError("No body in response??")
168
169class abac_client_opts(OptionParser):
170    """Encapsulate option processing in this class, rather than in main"""
171    def __init__(self):
172        OptionParser.__init__(self, usage="%prog [opts] (--help for details)",
173                version="0.1")
174
175        self.add_option("-c","--cert", action="store", dest="cert",
176                type="string", help="my certificate file")
177        self.add_option("-d", "--debug", action="count", dest="debug", 
178                default=0, help="Set debug.  Repeat for more information")
179        self.add_option("-s", "--serializeOnly", action="store_true", 
180                dest="serialize_only", default=False,
181                help="Print the SOAP request that would be sent and exit")
182        self.add_option("-T","--trusted", action="store", dest="trusted",
183                type="string", help="Trusted certificates (required)")
184        self.add_option("-u", "--url", action="store", dest="url",
185                type="string",default="https://localhost:23235", 
186                help="URL to connect to (default %default)")
187        self.add_option("-F","--useFedid", action="store_true",
188                dest="use_fedid", default=False,
189                help="Use a fedid derived from my certificate as user identity")
190        self.add_option("-x","--transport", action="store", type="choice",
191                choices=("xmlrpc", "soap"), default="soap",
192                help="Transport for request (xmlrpc|soap) (Default: %default)")
193        self.add_option("--trace", action="store_const", dest="tracefile", 
194                const=sys.stderr, help="Print SOAP exchange to stderr")
195
196class abac_create_opts(abac_client_opts):
197    def __init__(self, access_keys, add_key_callback=None, 
198            add_cert_callback=None):
199        abac_client_opts.__init__(self)
200        self.add_option("-f", "--file", dest="file", type="string",
201                help="negotiation context remote path name")
202        if add_key_callback:
203            self.add_option("-k", "--ssh_key", action="callback",
204                    type="string", callback=add_key_callback,
205                    callback_args=(access_keys,),
206                    help="ssh key for access (can be supplied more than once")
207        if add_cert_callback:
208            self.add_option("-K", "--x509Key", action="callback",
209                    type="string", callback=add_cert_callback,
210                    callback_args=(access_keys,),
211                    help="X509 certificate for access " + \
212                        "(can be supplied more than once")
213            self.add_option("-m", "--master", dest="master",
214                    help="Master testbed in the federation")
215            self.add_option("-U", "--username", action="store", dest="user",
216                    type="string", 
217                    help="Use this username instead of the uid")
218
219class abac_access_opts(abac_create_opts):
220    def __init__(self, access_keys, node_descs, add_key_callback=None, 
221            add_cert_callback=None, add_node_callback=None):
222        abac_create_opts.__init__(self, access_keys, add_key_callback,
223                add_cert_callback)
224        self.add_option("-a","--anonymous", action="store_true",
225                dest="anonymous", default=False,
226                help="Do not include a user in the request")
227        self.add_option("-l","--label", action="store", dest="label",
228                type="string", help="Label for output")
[181cf9c]229        self.add_option("-i", "--context_id", action="store", dest="id",
230                type="string",
231                help="Negotiation context identifier (required)")
232        self.add_option("-g", "--goal", action="store", dest="goal",
233                type="string",
234                help="Trust target goal for negotiation (required)")
[19a3e06]235        if add_node_callback:
236            self.add_option("-n", "--node", action="callback", type="string", 
237                    callback=add_node_callback, callback_args=(node_descs,),
238                    help="Node description: image:hardware[:count]")
239        self.add_option("-t", "--testbed", action="store", dest="testbed",
240                type="string",
241                help="Testbed identifier (URI) to contact (required)")
242
243
244class abac_negotiate_opts(abac_create_opts):
245    def __init__(self, access_keys, node_descs, add_key_callback=None, 
246            add_cert_callback=None, add_node_callback=None):
247        abac_create_opts.__init__(self, access_keys, add_key_callback,
248                add_cert_callback)
249        self.add_option("-a","--anonymous", action="store_true",
250                dest="anonymous", default=False,
251                help="Do not include a user in the request")
252        self.add_option("-l","--label", action="store", dest="label",
253                type="string", help="Label for output")
254        if add_node_callback:
255            self.add_option("-n", "--node", action="callback", type="string", 
256                    callback=add_node_callback, callback_args=(node_descs,),
257                    help="Node description: image:hardware[:count]")
258        self.add_option("-t", "--testbed", action="store", dest="testbed",
259                type="string",
260                help="Testbed identifier (URI) to contact (required)")
261
262
263
264
265class exp_data_base(abac_rpc):
266    def __init__(self, op='Info'):
267        """
268        Init the various conversions
269        """
270
271        abac_rpc.__init__(self, op)
272        # List of things one could ask for and what formatting routine is
273        # called.
274        self.params = {
275                'vis': ('vis', self.print_xml('vis')),
276                'vtopo': ('vtopo', self.print_xml('vtopo')),
277                'federant': ('federant', self.print_xml),
278                'id': ('experimentID', self.print_id),
279                'status': ('experimentStatus', self.print_string),
280                'log': ('allocationLog', self.print_string),
281                'access': ('experimentAccess', self.print_string),
282            }
283
284    # Utility functions
285    def print_string(self, d, out=sys.stdout):
286        print >>out, d
287
288    def print_id(self, d, out=sys.stdout):
289        if d:
290            for id in d:
291                for k in id.keys():
292                    print >>out, "%s: %s" % (k, id[k])
293
294    class print_xml:
295        """
296        Print the retrieved data is a simple xml representation of the dict.
297        """
298        def __init__(self, top):
299            self.xml = top
300
301        def __call__(self, d, out=sys.stdout):
302            str = "<%s>\n" % self.xml
303            for t in ('node', 'lan'):
304                if d.has_key(t):
305                    for x in d[t]:
306                        str += "<%s>" % t
307                        for k in x.keys():
308                            str += "<%s>%s</%s>" % (k, x[k],k)
309                        str += "</%s>\n" % t
310            str+= "</%s>" % self.xml
311            print >>out, str
312
313
314
315class context_data(exp_data_base):
316    def __init__(self):
317        exp_data_base.__init__(self, 'MultiInfo')
318
319
320    def __call__(self):
321        """
322        The control flow.  Compose the request and print the response.
323        """
324        # Process the options using the customized option parser defined above
325        parser = abac_multi_exp_data_opts()
326
327        (opts, args) = parser.parse_args()
328
329        if opts.trusted:
330            if ( not os.access(opts.trusted, os.R_OK) ) :
331                sys.exit("Cannot read trusted certificates (%s)" % opts.trusted)
332
333        if opts.debug > 0: opts.tracefile=sys.stderr
334
335        (user, cert) = self.get_user_info([])
336
337        if opts.cert != None: cert = opts.cert
338
339        if cert == None:
340            sys.exit("No certificate given (--cert) or found")
341
342        if opts.file == None:
343            sys.exit("No file given (--file)")
344
345        if os.access(cert, os.R_OK):
346            fid = fedid(file=cert)
347        else:
348            sys.exit("Cannot read certificate (%s)" % cert)
349
350        req = { 'ContextIn': { 'contextFile': opts.file } }
351
352        try:
353            resp_dict = self.do_rpc(req,
354                    opts.url, opts.transport, cert, opts.trusted,
355                    serialize_only=opts.serialize_only,
356                    tracefile=opts.tracefile)
357        except self.RPCException, e:
358            exit_with_fault(\
359                    {'desc': e.desc, 'errstr': e.errstr, 'code': e.code})
360        except RuntimeError, e:
361            sys.exit("Error processing RPC: %s" % e)
362
363        exps = resp_dict.get('info', [])
364        if exps:
365            print '---'
366            for exp in exps:
367                for d in opts.data:
368                    key, output = self.params[d]
369                    try:
370                        if exp.has_key(key):
371                            output(exp[key])
372                    except RuntimeError, e:
373                        sys.exit("Bad response. %s" % e.message)
374                print '---'
375
376
377    def __call__(self):
378        """
379        The control flow.  Compose the request and print the response.
380        """
381        # Process the options using the customized option parser defined above
382        parser = fedd_image_opts()
383
384        (opts, args) = parser.parse_args()
385
386        if opts.trusted:
387            if ( not os.access(opts.trusted, os.R_OK) ) :
388                sys.exit("Cannot read trusted certificates (%s)" % opts.trusted)
389
390        if opts.debug > 0: opts.tracefile=sys.stderr
391
392        (user, cert) = self.get_user_info([])
393
394        if opts.cert != None: cert = opts.cert
395
396        if cert == None:
397            sys.exit("No certificate given (--cert) or found")
398
399        if os.access(cert, os.R_OK):
400            fid = fedid(file=cert)
401        else:
402            sys.exit("Cannot read certificate (%s)" % cert)
403
404        if opts.exp_name and opts.exp_certfile:
405            sys.exit("Only one of --experiment_cert and " +\
406                    "--experiment_name permitted");
407
408        if opts.exp_certfile:
409            exp_id = { 'fedid': fedid(file=opts.exp_certfile) }
410
411        if opts.exp_name:
412            exp_id = { 'localname' : opts.exp_name }
413
414        if opts.format and opts.outfile:
415            fmt = opts.format
416            file = opts.outfile
417        elif not opts.format and opts.outfile:
418            fmt = opts.outfile[-3:]
419            if fmt not in ("png", "jpg", "dot", "svg"):
420                sys.exit("Unexpected file type and no format specified")
421            file = opts.outfile
422        elif opts.format and not opts.outfile:
423            fmt = opts.format
424            file = None
425        else:
426            fmt="dot"
427            file = None
428
429
430        req = { 'experiment': exp_id }
431
432        try:
433            resp_dict = self.do_rpc(req,
434                    opts.url, opts.transport, cert, opts.trusted,
435                    serialize_only=opts.serialize_only,
436                    tracefile=opts.tracefile)
437        except self.RPCException, e:
438            exit_with_fault(\
439                    {'desc': e.desc, 'errstr': e.errstr, 'code': e.code})
440        except RuntimeError, e:
441            sys.exit("Error processing RPC: %s" % e)
442
443
444        if resp_dict.has_key('vtopo'):
445            self.gen_image(resp_dict['vtopo'],
446                    len(resp_dict['vtopo'].get('node', [])),
447                    file, fmt, opts.neato, opts.labels, opts.pixels)
448        else:
449            sys.exit("Bad response. %s" % e.message)
450
451
452class create(abac_rpc):
453    def __init__(self): 
454        abac_rpc.__init__(self, "CreateContext")
455    def __call__(self):
456        access_keys = []
457        # Process the options using the customized option parser defined above
458        parser = abac_create_opts(access_keys, self.add_ssh_key,
459                self.add_x509_cert)
460
461        (opts, args) = parser.parse_args()
462
463        if opts.trusted:
464            if ( not os.access(opts.trusted, os.R_OK) ) :
465                sys.exit("Cannot read trusted certificates (%s)" % opts.trusted)
466
467        if opts.debug > 0: opts.tracefile=sys.stderr
468
469        (user, cert) = self.get_user_info(access_keys)
470
471        if opts.user: user = opts.user
472
473        if opts.cert != None: cert = opts.cert
474
475        if cert == None:
476            sys.exit("No certificate given (--cert) or found")
477
478        if os.access(cert, os.R_OK):
479            fid = fedid(file=cert)
480            if opts.use_fedid == True:
481                user = fid
482        else:
483            sys.exit("Cannot read certificate (%s)" % cert)
484
485#       if opts.file:
486#           exp_desc = ""
487#           try:
488#               f = open(opts.file, 'r')
489#               for line in f:
490#                   exp_desc += line
491#               f.close()
492#           except IOError:
493#               sys.exit("Cannot read description file (%s)" %opts.file)
494#       else:
495#           sys.exit("Must specify an experiment description (--file)")
496#
497#       if not opts.master:
498#           sys.exit("Must specify a master testbed (--master)")
499#
500#       out_certfile = opts.out_certfile
501
[181cf9c]502        print "New context using file: %s" % opts.file
[19a3e06]503#       msg = { 'ContextIn': { 'contextFile': opts.file } }
504        msg = { 'contextFile': opts.file }
505
506        if opts.debug > 1: print >>sys.stderr, msg
507
508        try:
509            resp_dict = self.do_rpc(msg, 
510                    opts.url, opts.transport, cert, opts.trusted, 
511                    serialize_only=opts.serialize_only,
512                    tracefile=opts.tracefile)
513        except self.RPCException, e:
514            exit_with_fault(\
515                    {'desc': e.desc, 'errstr': e.errstr, 'code': e.code})
516        except RuntimeError, e:
517            sys.exit("Error processing RPC: %s" % e)
518
519        if opts.debug > 1: print >>sys.stderr, resp_dict
520
[181cf9c]521#       ea = resp_dict.get('experimentAccess', None)
522        id = resp_dict.get('contextID', None)
523        print "New context loaded w/id = %s" % id
[19a3e06]524
525class negotiate(abac_rpc):
526    def __init__(self): 
527        abac_rpc.__init__(self, "Negotiate")
528    def __call__(self):
529        access_keys = []
530        # Process the options using the customized option parser defined above
531        parser = abac_create_opts(access_keys, self.add_ssh_key,
532                self.add_x509_cert)
533
534        (opts, args) = parser.parse_args()
535
536        if opts.trusted:
537            if ( not os.access(opts.trusted, os.R_OK) ) :
538                sys.exit("Cannot read trusted certificates (%s)" % opts.trusted)
539
540        if not opts.project :
541            parser.error('--project is required')
542
543        if opts.debug > 0: opts.tracefile=sys.stderr
544
545        (user, cert) = self.get_user_info(access_keys)
546
547        if opts.user: user = opts.user
548
549        if opts.cert != None: cert = opts.cert
550
551        if cert == None:
552            sys.exit("No certificate given (--cert) or found")
553
554        if os.access(cert, os.R_OK):
555            fid = fedid(file=cert)
556            if opts.use_fedid == True:
557                user = fid
558        else:
559            sys.exit("Cannot read certificate (%s)" % cert)
560
561        if opts.file:
562            exp_desc = ""
563            try:
564                f = open(opts.file, 'r')
565                for line in f:
566                    exp_desc += line
567                f.close()
568            except IOError:
569                sys.exit("Cannot read description file (%s)" %opts.file)
570        else:
571            sys.exit("Must specify an experiment description (--file)")
572
573        if not opts.master:
574            sys.exit("Must specify a master testbed (--master)")
575
576        out_certfile = opts.out_certfile
577
578        msg = {
579                'experimentdescription': { 'ns2description': exp_desc },
580                'master': opts.master,
581                'exportProject': { 'localname': opts.project },
582                'user' : [ {\
583                        'userID': pack_id(user), \
584                        'access': [ { a.type: a.buf } for a in access_keys]\
585                        } ]
586                }
587
588        if opts.exp_name:
589            msg['experimentID'] = { 'localname': opts.exp_name }
590
591        if opts.debug > 1: print >>sys.stderr, msg
592
593        try:
594            resp_dict = self.do_rpc(msg, 
595                    opts.url, opts.transport, cert, opts.trusted, 
596                    serialize_only=opts.serialize_only,
597                    tracefile=opts.tracefile)
598        except self.RPCException, e:
599            exit_with_fault(\
600                    {'desc': e.desc, 'errstr': e.errstr, 'code': e.code})
601        except RuntimeError, e:
602            sys.exit("Error processing RPC: %s" % e)
603
604        if opts.debug > 1: print >>sys.stderr, resp_dict
605
[181cf9c]606        ea = resp_dict.get('goal')
[19a3e06]607        if out_certfile and ea and ea.has_key('X509'):
608            try:
609                f = open(out_certfile, "w")
610                print >>f, ea['X509']
611                f.close()
612            except IOError:
613                sys.exit('Could not write to %s' %  out_certfile)
614        eid = resp_dict.get('experimentID', None)
615        if eid:
616            for id in eid:
617                for k in id.keys():
618                    print "%s: %s" % (k, id[k])
619        st = resp_dict.get('experimentStatus', None)
620        if st:
621            print "status: %s" % st
622
623class access(abac_rpc):
624    def __init__(self): 
625        abac_rpc.__init__(self, "Access")
626    def __call__(self):
627        access_keys = []
628        # Process the options using the customized option parser defined above
[181cf9c]629        parser = abac_access_opts(access_keys, self.add_ssh_key,
[19a3e06]630                self.add_x509_cert)
631
632        (opts, args) = parser.parse_args()
633
634        if opts.trusted:
635            if ( not os.access(opts.trusted, os.R_OK) ) :
636                sys.exit("Cannot read trusted certificates (%s)" % opts.trusted)
637
638        if opts.debug > 0: opts.tracefile=sys.stderr
639
640        (user, cert) = self.get_user_info(access_keys)
641
[181cf9c]642#       if opts.user: user = opts.user
[19a3e06]643
644        if opts.cert != None: cert = opts.cert
645
646        if cert == None:
647            sys.exit("No certificate given (--cert) or found")
648
649        if os.access(cert, os.R_OK):
650            fid = fedid(file=cert)
651            if opts.use_fedid == True:
652                user = fid
653        else:
654            sys.exit("Cannot read certificate (%s)" % cert)
655
656        if not opts.id:
657            sys.exit("Must specify a negotiator id (--id)")
658
659        if not opts.goal:
[181cf9c]660            sys.exit("Must specify a goal (--goal)")
[19a3e06]661
[181cf9c]662#       out_certfile = opts.out_certfile
[19a3e06]663
664        msg = {
[181cf9c]665                'context': { 'contextID': opts.id },
[19a3e06]666                'goal': opts.goal
667                }
668
669        if opts.debug > 1: print >>sys.stderr, msg
670
671        try:
672            resp_dict = self.do_rpc(msg, 
673                    opts.url, opts.transport, cert, opts.trusted, 
674                    serialize_only=opts.serialize_only,
675                    tracefile=opts.tracefile)
676        except self.RPCException, e:
677            exit_with_fault(\
678                    {'desc': e.desc, 'errstr': e.errstr, 'code': e.code})
679        except RuntimeError, e:
680            sys.exit("Error processing RPC: %s" % e)
681
682        if opts.debug > 1: print >>sys.stderr, resp_dict
683
[181cf9c]684        result = resp_dict.get('result', None)
685        goal = resp_dict.get('goal', None)
686        print "%s: %s" % (goal, result)
[19a3e06]687
688def exit_with_fault(dict, out=sys.stderr):
689    """ Print an error message and exit.
690
691    The dictionary contains the FeddFaultBody elements."""
692    codestr = ""
693
694    if dict.has_key('errstr'):
695        codestr = "Error: %s" % dict['errstr']
696
697    if dict.has_key('code'):
698        if len(codestr) > 0 : 
699            codestr += " (%d)" % dict['code']
700        else:
701            codestr = "Error Code: %d" % dict['code']
702    print>>out, codestr
703    print>>out, "Description: %s" % dict['desc']
704    traceback.print_stack()
705    sys.exit(dict.get('code', 20))
706
707
708cmds = {\
709        'create': create(),\
710        'access': access()\
711#        'negotiate': negotiate(),\
712#        'status': status()\
713    }
714
715operation = cmds.get(sys.argv[1], None)
716if operation:
717    del sys.argv[1]
718    operation()
719else:
720    if sys.argv[1] == '--help':
721        sys.exit(\
722'''Only context sensitive help is available.  For one of the commands:
723
724%s
725
726type
727  %s command --help
728
729to get help, e.g., %s create --help
730''' % (", ".join(cmds.keys()), sys.argv[0], sys.argv[0]))
731    else:
732        sys.exit("Bad command: %s.  Valid ones are: %s" % \
733                (sys.argv[1], ", ".join(cmds.keys())))
734
Note: See TracBrowser for help on using the repository browser.