source: fedd/fedd_client.py @ abb87eb

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

add placeholder for other experiment descriptions

  • Property mode set to 100755
File size: 20.7 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' : getattr(e, "faultCode", "No fault code"),
320                        'desc' : getattr(e, "faultString", "No fault string") }\
321                        }
322            if resp:
323                if resp.has_key(self.ResponseBody): 
324                    return decapsulate_binaries(resp[self.ResponseBody],
325                            ('fedid',))
326                elif resp.has_key('FeddFaultBody'):
327                    raise self.RPCException(resp['FeddFaultBody'])
328                else: 
329                    raise RuntimeError("No body in response!?")
330            else: 
331                raise RuntimeError("No response?!?")
332        else:
333            raise RuntimeError("Unknown RPC transport: %s" % transport)
334
335# Querying experiment data follows the same control flow regardless of the
336# specific data retrieved.  This class encapsulates that control flow.
337class exp_data(fedd_rpc):
338    def __init__(self, op): 
339        """
340        Specialize the class for the type of data requested (op)
341        """
342
343        fedd_rpc.__init__(self, op)
344        if op =='Vtopo':
345            self.key="vtopo"
346            self.xml='experiment'
347        elif op == 'Vis':
348            self.key="vis"
349            self.xml='vis'
350        elif op == 'Info': pass
351        else:
352            raise TypeError("Bad op: %s" % op)
353
354    def print_xml(self, d, out=sys.stdout):
355        """
356        Print the retrieved data is a simple xml representation of the dict.
357        """
358        str = "<%s>\n" % self.xml
359        for t in ('node', 'lan'):
360            if d.has_key(t): 
361                for x in d[t]:
362                    str += "<%s>" % t
363                    for k in x.keys():
364                        str += "<%s>%s</%s>" % (k, x[k],k)
365                    str += "</%s>\n" % t
366        str+= "</%s>" % self.xml
367        print >>out, str
368
369    def __call__(self):
370        """
371        The control flow.  Compose the request and print the response.
372        """
373        # Process the options using the customized option parser defined above
374        parser = fedd_exp_data_opts()
375
376        (opts, args) = parser.parse_args()
377
378        if opts.trusted != None:
379            if ( not os.access(opts.trusted, os.R_OK) ) :
380                sys.exit("Cannot read trusted certificates (%s)" % opts.trusted)
381        else:
382            parser.error("--trusted is required")
383
384        if opts.debug > 0: opts.tracefile=sys.stderr
385
386        if opts.cert != None: cert = opts.cert
387
388        if cert == None:
389            sys.exit("No certificate given (--cert) or found")
390
391        if os.access(cert, os.R_OK):
392            fid = fedid(file=cert)
393        else:
394            sys.exit("Cannot read certificate (%s)" % cert)
395
396        if opts.exp_name and opts.exp_certfile:
397            sys.exit("Only one of --experiment_cert and " +\
398                    "--experiment_name permitted");
399
400        if opts.exp_certfile:
401            exp_id = { 'fedid': fedid(file=opts.exp_certfile) }
402
403        if opts.exp_name:
404            exp_id = { 'localname' : opts.exp_name }
405
406        req = { 'experiment': exp_id }
407
408        try:
409            resp_dict = self.do_rpc(req,
410                    opts.url, opts.transport, cert, opts.trusted, 
411                    serialize_only=opts.serialize_only,
412                    tracefile=opts.tracefile)
413        except self.RPCException, e:
414            exit_with_fault(\
415                    {'desc': e.desc, 'errstr': e.errstr, 'code': e.code})
416        except RuntimeError, e:
417            print e
418            sys.exit("Error processing RPC: %s" % e)
419
420        if getattr(self, 'key', None):
421            try:
422                if resp_dict.has_key(self.key):
423                    self.print_xml(resp_dict[self.key])
424            except RuntimeError, e:
425                sys.exit("Bad response. %s" % e.message)
426        else:
427            print resp_dict
428
429class terminate(fedd_rpc):
430    def __init__(self): 
431        """
432        Termination request
433        """
434
435        fedd_rpc.__init__(self, "Terminate")
436
437    def __call__(self):
438        """
439        The control flow.  Compose the request and print the response.
440        """
441        # Process the options using the customized option parser defined above
442        parser = fedd_exp_data_opts()
443
444        (opts, args) = parser.parse_args()
445
446        if opts.trusted != None:
447            if ( not os.access(opts.trusted, os.R_OK) ) :
448                sys.exit("Cannot read trusted certificates (%s)" % opts.trusted)
449        else:
450            parser.error("--trusted is required")
451
452        if opts.debug > 0: opts.tracefile=sys.stderr
453
454        if opts.cert != None: cert = opts.cert
455
456        if cert == None:
457            sys.exit("No certificate given (--cert) or found")
458
459        if os.access(cert, os.R_OK):
460            fid = fedid(file=cert)
461        else:
462            sys.exit("Cannot read certificate (%s)" % cert)
463
464        if opts.exp_name and opts.exp_certfile:
465            sys.exit("Only one of --experiment_cert and " +\
466                    "--experiment_name permitted");
467
468        if opts.exp_certfile:
469            exp_id = { 'fedid': fedid(file=opts.exp_certfile) }
470
471        if opts.exp_name:
472            exp_id = { 'localname' : opts.exp_name }
473
474        req = { 'experiment': exp_id }
475
476        try:
477            resp_dict = self.do_rpc(req,
478                    opts.url, opts.transport, cert, opts.trusted, 
479                    serialize_only=opts.serialize_only,
480                    tracefile=opts.tracefile)
481        except self.RPCException, e:
482            exit_with_fault(\
483                    {'desc': e.desc, 'errstr': e.errstr, 'code': e.code})
484        except RuntimeError, e:
485            print e
486            sys.exit("Error processing RPC: %s" % e)
487
488        eid = resp_dict.get('experimentID', None)
489        if eid:
490            for id in eid:
491                for k in id.keys():
492                    if k == 'fedid': print "%s: %s" % (k,fedid(bits=id[k]))
493                    else: print "%s: %s" % (k, id[k])
494
495class create(fedd_rpc):
496    def __init__(self): 
497        fedd_rpc.__init__(self, "Create")
498    def __call__(self):
499        access_keys = []
500        # Process the options using the customized option parser defined above
501        parser = fedd_create_opts(access_keys, self.add_ssh_key,
502                self.add_x509_cert)
503
504        (opts, args) = parser.parse_args()
505
506        if opts.trusted != None:
507            if ( not os.access(opts.trusted, os.R_OK) ) :
508                sys.exit("Cannot read trusted certificates (%s)" % opts.trusted)
509        else:
510            parser.error("--trusted is required")
511
512        if opts.debug > 0: opts.tracefile=sys.stderr
513
514        (user, cert) = self.get_user_info(access_keys)
515
516        if opts.user: user = opts.user
517
518        if opts.cert != None: cert = opts.cert
519
520        if cert == None:
521            sys.exit("No certificate given (--cert) or found")
522
523        if os.access(cert, os.R_OK):
524            fid = fedid(file=cert)
525            if opts.use_fedid == True:
526                user = fid
527        else:
528            sys.exit("Cannot read certificate (%s)" % cert)
529
530        if opts.file:
531            exp_desc = ""
532            try:
533                f = open(opts.file, 'r')
534                for line in f:
535                    exp_desc += line
536                f.close()
537            except IOError:
538                sys.exit("Cannot read description file (%s)" %opts.file)
539        else:
540            sys.exit("Must specify an experiment description (--file)")
541
542        if not opts.master:
543            sys.exit("Must specify a master testbed (--master)")
544
545        out_certfile = opts.out_certfile
546
547        msg = {
548                'experimentdescription': { 'ns2description': exp_desc },
549                'master': opts.master,
550                'user' : [ {\
551                        'userID': pack_id(user), \
552                        'access': [ { a.type: a.buf } for a in access_keys]\
553                        } ]
554                }
555
556        if opts.exp_name:
557            msg['experimentID'] = { 'localname': opts.exp_name }
558
559        if opts.debug > 1: print >>sys.stderr, msg
560
561        try:
562            resp_dict = self.do_rpc(msg, 
563                    opts.url, opts.transport, cert, opts.trusted, 
564                    serialize_only=opts.serialize_only,
565                    tracefile=opts.tracefile)
566        except self.RPCException, e:
567            exit_with_fault(\
568                    {'desc': e.desc, 'errstr': e.errstr, 'code': e.code})
569        except RuntimeError, e:
570            sys.exit("Error processing RPC: %s" % e)
571
572        if opts.debug > 1: print >>sys.stderr, resp_dict
573
574        ea = resp_dict.get('experimentAccess', None)
575        if out_certfile and ea and ea.has_key('X509'):
576            try:
577                f = open(out_certfile, "w")
578                print >>f, ea['X509']
579                f.close()
580            except IOError:
581                sys.exit('Could not write to %s' %  out_certfile)
582        eid = resp_dict.get('experimentID', None)
583        if eid:
584            for id in eid:
585                for k in id.keys():
586                    if k == 'fedid': print "%s: %s" % (k,fedid(bits=id[k]))
587                    else: print "%s: %s" % (k, id[k])
588
589class access(fedd_rpc):
590    def __init__(self):
591        fedd_rpc.__init__(self, "RequestAccess")
592
593    def print_response_as_testbed(self, resp, label, out=sys.stdout):
594        """Print the response as input to the splitter script"""
595
596        e = resp['emulab']
597        p = e['project']
598        fields = { 
599                "Boss": e['boss'],
600                "OpsNode": e['ops'],
601                "Domain": e['domain'],
602                "FileServer": e['fileServer'],
603                "EventServer": e['eventServer'],
604                "Project": unpack_id(p['name'])
605                }
606        if (label != None): print >> out, "[%s]" % label
607
608        for l, v in fields.iteritems():
609            print >>out, "%s: %s" % (l, v)
610
611        for u in p['user']:
612            print >>out, "User: %s" % unpack_id(u['userID'])
613
614        for a in e['fedAttr']:
615            print >>out, "%s: %s" % (a['attribute'], a['value'])
616
617    def __call__(self):
618        access_keys = []
619        node_descs = []
620        proj = None
621
622        # Process the options using the customized option parser defined above
623        parser = fedd_access_opts(access_keys, node_descs, self.add_ssh_key,
624                self.add_x509_cert, self.add_node_desc)
625
626        (opts, args) = parser.parse_args()
627
628        if opts.testbed == None:
629            parser.error("--testbed is required")
630
631        if opts.trusted != None:
632            if ( not os.access(opts.trusted, os.R_OK) ) :
633                sys.exit("Cannot read trusted certificates (%s)" % opts.trusted)
634        else:
635            parser.error("--trusted is required")
636
637        if opts.debug > 0: opts.tracefile=sys.stderr
638
639        (user, cert) = self.get_user_info(access_keys)
640
641        if opts.user: user = opts.user
642
643        if opts.cert != None: cert = opts.cert
644
645        if cert == None:
646            sys.exit("No certificate given (--cert) or found")
647
648        if os.access(cert, os.R_OK):
649            fid = fedid(file=cert)
650            if opts.use_fedid == True:
651                user = fid
652        else:
653            sys.exit("Cannot read certificate (%s)" % cert)
654
655        msg = {
656                'allocID': pack_id('test alloc'),
657                'destinationTestbed': pack_id(opts.testbed),
658                'access' : [ { a.type: a.buf } for a in access_keys ],
659                }
660
661        if len(node_descs) > 0:
662            msg['resources'] = { 
663                    'node': [ 
664                        { 
665                            'image':  n.image ,
666                            'hardware':  n.hardware,
667                            'count': n.count,
668                        } for n in node_descs],
669                    }
670
671        if opts.project != None:
672            if not opts.anonymous and user != None:
673                msg['project'] = {
674                        'name': pack_id(opts.project),
675                        'user': [ { 'userID': pack_id(user) } ],
676                        }
677            else:
678                msg['project'] = { 'name': pack_id(opts.project) }
679        else:
680            if not opts.anonymous and user != None:
681                msg['user'] = [ { 'userID': pack_id(user) } ]
682            else:
683                msg['user'] = [];
684
685        if opts.debug > 1: print >>sys.stderr, msg
686
687        try:
688            resp_dict = self.do_rpc(msg, 
689                    opts.url, opts.transport, cert, opts.trusted, 
690                    serialize_only=opts.serialize_only,
691                    tracefile=opts.tracefile)
692        except self.RPCException, e:
693            exit_with_fault(\
694                    {'desc': e.desc, 'errstr': e.errstr, 'code': e.code})
695        except RuntimeError, e:
696            sys.exit("Error processing RPC: %s" % e.message)
697
698        if opts.debug > 1: print >>sys.stderr, resp_dict
699        self.print_response_as_testbed(resp_dict, opts.label)
700
701cmds = {\
702        'create': create(),\
703        'access': access(),\
704        'vtopo': exp_data('Vtopo'),\
705        'vis': exp_data('Vis'),\
706        'info': exp_data('Info'),\
707        'terminate': terminate(),\
708    }
709
710operation = cmds.get(sys.argv[1], None)
711if operation:
712    del sys.argv[1]
713    operation()
714else:
715    sys.exit("Bad command: %s.  Valid ones are: %s" % \
716            (sys.argv[1], ", ".join(cmds.keys())))
717
Note: See TracBrowser for help on using the repository browser.