source: fedd/fedd_client.py @ ea0a821

axis_examplecompt_changesinfo-opsversion-1.30version-2.00version-3.01version-3.02
Last change on this file since ea0a821 was 7a8d667, checked in by Ted Faber <faber@…>, 16 years ago

reconfigure sshd_config explicitly

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