source: fedd/fedd_client.py @ 6f02be5

axis_examplecompt_changesinfo-opsversion-3.01version-3.02
Last change on this file since 6f02be5 was d743d60, checked in by Ted Faber <faber@…>, 15 years ago

Totally refactor fedd_client.py into component scripts. The previous layout
have become a twisty hell of misdirected OOP and learning python run amok.
This version is actually pretty readable and will be much easier to build on.

  • Property mode set to 100755
File size: 19.7 KB
Line 
1#!/usr/local/bin/python
2
3import sys
4import os
5import pwd
6import tempfile
7import subprocess
8import re
9import xml.parsers.expat
10import time
11
12from federation import fedid, service_error
13from federation.util import fedd_ssl_context, pack_id, unpack_id
14from federation.remote_service import service_caller
15from federation import topdl
16
17from optparse import OptionParser, OptionValueError
18
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   
45class node_desc:
46    def __init__(self, image, hardware, count=1):
47        if getattr(image, "__iter__", None) == None:
48            if image == None: self.image = [ ]
49            else: self.image = [ image ]
50        else:
51            self.image = image
52
53        if getattr(hardware, "__iter__", None) == None: 
54            if hardware == None: self.hardware = [ ]
55            else: self.hardware = [ hardware ]
56        else:
57            self.hardware = hardware
58        if count != None: self.count = int(count)
59        else: self.count = 1
60
61class fedd_client_opts(OptionParser):
62    """Encapsulate option processing in this class, rather than in main"""
63    def __init__(self):
64        OptionParser.__init__(self, usage="%prog [opts] (--help for details)",
65                version="0.1")
66
67        self.add_option("--cert", action="store", dest="cert",
68                type="string", help="my certificate file")
69        self.add_option( "--debug", action="count", dest="debug", 
70                default=0, help="Set debug.  Repeat for more information")
71        self.add_option("--serialize_only", action="store_true", 
72                dest="serialize_only", default=False,
73                help="Print the SOAP request that would be sent and exit")
74        self.add_option("--trusted", action="store", dest="trusted",
75                type="string", help="Trusted certificates (required)")
76        self.add_option("--url", action="store", dest="url",
77                type="string",default="https://localhost:23235", 
78                help="URL to connect to (default %default)")
79        self.add_option("--transport", action="store", type="choice",
80                choices=("xmlrpc", "soap"), default="soap",
81                help="Transport for request (xmlrpc|soap) (Default: %default)")
82        self.add_option("--trace", action="store_const", dest="tracefile", 
83                const=sys.stderr, help="Print SOAP exchange to stderr")
84
85class fedd_access_opts(fedd_client_opts):
86    def __init__(self):
87        fedd_client_opts.__init__(self)
88        self.add_option("--experiment_cert", dest="out_certfile",
89                type="string", help="output certificate file")
90        self.add_option("--experiment_name", dest="exp_name",
91                type="string", help="Suggested experiment name")
92        self.add_option("--label", action="store", dest="label",
93                type="string", help="Label for output")
94        # Access has been busted for a while.
95        #if add_node_callback:
96        #    self.add_option("--node", action="callback", type="string",
97        #            callback=add_node_callback, callback_args=(node_descs,),
98        #            help="Node description: image:hardware[:count]")
99        self.add_option("--testbed", action="store", dest="testbed",
100                type="string",
101                help="Testbed identifier (URI) to contact (required)")
102        self.add_option("--file", dest="file", 
103                help="experiment description file")
104        self.add_option("--project", action="store", dest="project", 
105                type="string",
106                help="Project to export from master")
107        self.add_option("--master", dest="master",
108                help="Master testbed in the federation (pseudo project export)")
109        self.add_option("--service", dest="service", action="append",
110                type="string", default=[],
111                help="Service description name:exporters:importers:attrs")
112
113class fedd_terminate_opts(fedd_client_opts):
114    def __init__(self):
115        fedd_client_opts.__init__(self)
116        self.add_option("--force", dest="force",
117                action="store_true", default=False,
118                help="Force termination if experiment is in strange state")
119        self.add_option("--logfile", dest="logfile", default=None,
120                help="File to write log to")
121        self.add_option("--print_log", dest="print_log", default=False,
122                action="store_true",
123                help="Print deallocation log to standard output")
124        self.add_option("--experiment_cert", dest="exp_certfile",
125                type="string", help="experiment certificate file")
126        self.add_option("--experiment_name", dest="exp_name",
127                type="string", help="human readable experiment name")
128        self.add_option("--data", dest="data", default=[],
129                action="append", type="choice",
130                choices=("id", "experimentdescription", "federant", "vtopo", 
131                    "vis", "log", "status"),
132                help="data to extract")
133
134class fedd_start_opts(fedd_client_opts):
135    def __init__(self):
136        fedd_client_opts.__init__(self)
137        self.add_option("--file", dest="file", 
138                help="experiment description file")
139
140
141def exit_with_fault(dict, out=sys.stderr):
142    """ Print an error message and exit.
143
144    The dictionary contains the FeddFaultBody elements."""
145    codestr = ""
146
147    if dict.has_key('errstr'):
148        codestr = "Error: %s" % dict['errstr']
149
150    if dict.has_key('code'):
151        if len(codestr) > 0 : 
152            codestr += " (%d)" % dict['code']
153        else:
154            codestr = "Error Code: %d" % dict['code']
155
156    print>>out, codestr
157    print>>out, "Description: %s" % dict['desc']
158    sys.exit(dict.get('code', 20))
159
160# Base class for the various client operations.  It includes some commonly used
161# functions and classes the most important of which are the exception classes
162# and the service calling classes.
163class fedd_rpc:
164
165    class RPCException:
166        """
167        An error during the RPC exchange.  It unifies errors from both SOAP and
168        XMLPRC calls.
169        """
170        def __init__(self, fb):
171            self.desc = fb.get('desc', None)
172            self.code = fb.get('code', -1)
173            self.errstr = fb.get('errstr', None)
174
175    class caller(service_caller):
176        """
177        The caller is used by fedd_rpc.do_rpc to make the rpc call.  The extra
178        stashed information is used to parse responses a little.
179        """
180        def __init__(self, pre):
181            self.ResponseBody="%sResponseBody" % pre
182            self.method = pre
183            service_caller.__init__(self, self.method)
184
185    def __init__(self): 
186        """
187        Specialize the class for the pre method
188        """
189        self.caller = fedd_rpc.caller
190        self.RPCException = fedd_rpc.RPCException
191
192
193    def add_node_desc(self, option, opt_str, value, parser, node_descs):
194        def none_if_zero(x):
195            if len(x) > 0: return x
196            else: return None
197
198        params = map(none_if_zero, value.split(":"));
199       
200        if len(params) < 4 and len(params) > 1:
201            node_descs.append(node_desc(*params))
202        else:
203            raise OptionValueError("Bad node description: %s" % value)
204
205    def get_user_info(self):
206        pw = pwd.getpwuid(os.getuid());
207        try_cert=None
208        user = None
209
210        if pw != None:
211            user = pw[0]
212            try_cert = "%s/.ssl/emulab.pem" % pw[5];
213            if not os.access(try_cert, os.R_OK):
214                try_cert = None
215        return (user, try_cert)
216
217    def do_rpc(self, req_dict, url, transport, cert, trusted, tracefile=None,
218            serialize_only=False, caller=None):
219        """
220        The work of sending and parsing the RPC as either XMLRPC or SOAP
221        """
222
223        if caller is None: 
224            raise RuntimeError("Must provide caller to do_rpc")
225
226        context = None
227        while context == None:
228            try:
229                context = fedd_ssl_context(cert, trusted)
230            except Exception, e:
231                # Yes, doing this on message type is not ideal.  The string
232                # comes from OpenSSL, so check there is this stops working.
233                if str(e) == "bad decrypt": 
234                    print >>sys.stderr, "Bad Passphrase given."
235                else: raise
236
237        if transport == "soap":
238            if serialize_only:
239                print caller.serialize_soap(req_dict) 
240                return { }
241            else:
242                try:
243                    resp = caller.call_soap_service(url, req_dict, 
244                            context=context, tracefile=tracefile)
245                except service_error, e:
246                    raise self.RPCException( {\
247                            'code': e.code, 
248                            'desc': e.desc, 
249                            'errstr': e.code_string()\
250                        })
251        elif transport == "xmlrpc":
252            if serialize_only:
253                ser = dumps((req_dict,))
254                print ser
255                return { }
256            else:
257                try:
258                    resp = caller.call_xmlrpc_service(url, req_dict, 
259                            context=context, tracefile=tracefile)
260                except service_error, e:
261                    raise self.RPCException( {\
262                            'code': e.code, 
263                            'desc': e.desc, 
264                            'errstr': e.code_string()\
265                        })
266
267        else:
268            raise RuntimeError("Unknown RPC transport: %s" % transport)
269
270        if resp.has_key(caller.ResponseBody):
271            return resp[caller.ResponseBody]
272        else:
273            raise RuntimeError("No body in response??")
274
275class terminate_segment(fedd_rpc):
276    def __init__(self): 
277        """
278        Termination request
279        """
280
281        fedd_rpc.__init__(self)
282
283    def __call__(self):
284        """
285        The control flow.  Compose the request and print the response.
286        """
287        # Process the options using the customized option parser defined above
288        parser = fedd_terminate_opts()
289
290        (opts, args) = parser.parse_args()
291
292        (user, cert) = self.get_user_info()
293        if opts.trusted:
294            if ( not os.access(opts.trusted, os.R_OK) ) :
295                sys.exit("Cannot read trusted certificates (%s)" % opts.trusted)
296
297        if opts.debug > 0: opts.tracefile=sys.stderr
298
299        if opts.cert != None: cert = opts.cert
300
301        if cert == None:
302            sys.exit("No certificate given (--cert) or found")
303
304        if os.access(cert, os.R_OK):
305            fid = fedid(file=cert)
306        else:
307            sys.exit("Cannot read certificate (%s)" % cert)
308
309        if opts.exp_name and opts.exp_certfile:
310            sys.exit("Only one of --experiment_cert and " +\
311                    "--experiment_name permitted")
312
313        if opts.print_log and opts.logfile:
314            sys.exit("Only one of --logfile and --print_log is permitted")
315        elif opts.print_log:
316            out = sys.stdout
317        elif opts.logfile:
318            try:
319                out = open(opts.logfile, "w")
320            except IOError,e:
321                sys.exit("Cannot open logfile: %s" %e)
322        else:
323            out = None
324
325        exp_id = None
326
327        if opts.exp_certfile:
328            exp_id = { 'fedid': fedid(file=opts.exp_certfile) }
329
330        if opts.exp_name:
331            exp_id = { 'localname' : opts.exp_name }
332
333        if not exp_id:
334            sys.exit("Must give one of --experiment_cert and " +\
335                    "--experiment_name");
336
337        req = { 'allocID': exp_id, 'force': opts.force }
338
339        try:
340            resp_dict = self.do_rpc(req,
341                    opts.url, opts.transport, cert, opts.trusted, 
342                    serialize_only=opts.serialize_only,
343                    tracefile=opts.tracefile,
344                    caller=self.caller('TerminateSegment'))
345        except self.RPCException, e:
346            exit_with_fault(\
347                    {'desc': e.desc, 'errstr': e.errstr, 'code': e.code})
348        except RuntimeError, e:
349            print e
350            sys.exit("Error processing RPC: %s" % e)
351
352        if out:
353            log = resp_dict.get('deallocationLog', None)
354            if log:
355                print >>out, log
356                out.close()
357            else:
358                out.close()
359                sys.exit("No log returned")
360
361class access(fedd_rpc):
362    def __init__(self):
363        fedd_rpc.__init__(self)
364
365    def print_response_as_testbed(self, resp, label, out=sys.stdout):
366        """Print the response as input to the splitter script"""
367
368        e = resp.get('emulab', None)
369        if e:
370            p = e['project']
371            fields = { 
372                    "Boss": e['boss'],
373                    "OpsNode": e['ops'],
374                    "Domain": e['domain'],
375                    "FileServer": e['fileServer'],
376                    "EventServer": e['eventServer'],
377                    "Project": unpack_id(p['name'])
378                    }
379            if (label != None): print >> out, "[%s]" % label
380
381            for l, v in fields.iteritems():
382                print >>out, "%s: %s" % (l, v)
383
384            for u in p['user']:
385                print >>out, "User: %s" % unpack_id(u['userID'])
386
387            for a in e['fedAttr']:
388                print >>out, "%s: %s" % (a['attribute'], a['value'])
389
390    def __call__(self):
391        node_descs = []
392        proj = None
393
394        # Process the options using the customized option parser defined above
395        parser = fedd_access_opts()
396
397        (opts, args) = parser.parse_args()
398
399        if opts.testbed == None:
400            parser.error("--testbed is required")
401
402        if opts.trusted:
403            if ( not os.access(opts.trusted, os.R_OK) ) :
404                sys.exit("Cannot read trusted certificates (%s)" % opts.trusted)
405
406        if opts.debug > 0: opts.tracefile=sys.stderr
407
408        (user, cert) = self.get_user_info()
409
410        if opts.cert != None: cert = opts.cert
411
412        if cert == None:
413            sys.exit("No certificate given (--cert) or found")
414
415        if os.access(cert, os.R_OK):
416            fid = fedid(file=cert)
417            if opts.use_fedid == True:
418                user = fid
419        else:
420            sys.exit("Cannot read certificate (%s)" % cert)
421
422        msg = {
423                'allocID': pack_id('test alloc'),
424                'destinationTestbed': pack_id(opts.testbed),
425                }
426
427        if len(node_descs) > 0:
428            msg['resources'] = { 
429                    'node': [ 
430                        { 
431                            'image':  n.image ,
432                            'hardware':  n.hardware,
433                            'count': n.count,
434                        } for n in node_descs],
435                    }
436
437        if opts.debug > 1: print >>sys.stderr, msg
438
439        try:
440            resp_dict = self.do_rpc(msg, 
441                    opts.url, opts.transport, cert, opts.trusted, 
442                    serialize_only=opts.serialize_only,
443                    tracefile=opts.tracefile,
444                    caller=self.caller('RequestAccess'))
445        except self.RPCException, e:
446            exit_with_fault(\
447                    {'desc': e.desc, 'errstr': e.errstr, 'code': e.code})
448        except RuntimeError, e:
449            sys.exit("Error processing RPC: %s" % e.message)
450
451        if opts.debug > 1: print >>sys.stderr, resp_dict
452        if not opts.serialize_only:
453            self.print_response_as_testbed(resp_dict, opts.label)
454
455class start_segment(fedd_rpc):
456    def __init__(self): 
457        fedd_rpc.__init__(self)
458    def __call__(self):
459        # Process the options using the customized option parser defined above
460        parser = fedd_start_opts()
461
462        (opts, args) = parser.parse_args()
463
464        if opts.trusted:
465            if ( not os.access(opts.trusted, os.R_OK) ) :
466                sys.exit("Cannot read trusted certificates (%s)" % opts.trusted)
467
468        if opts.debug > 0: opts.tracefile=sys.stderr
469
470        (user, cert) = self.get_user_info()
471
472        if opts.cert != None: cert = opts.cert
473
474        if cert == None:
475            sys.exit("No certificate given (--cert) or found")
476
477        if os.access(cert, os.R_OK):
478            fid = fedid(file=cert)
479        else:
480            sys.exit("Cannot read certificate (%s)" % cert)
481
482        if opts.file:
483            try:
484                top = topdl.topology_from_xml(filename=opts.file, 
485                        top='experiment')
486            except IOError:
487                sys.exit("Cannot read description file (%s)" %opts.file)
488        else:
489            sys.exit("Must specify an experiment description (--file)")
490
491        msg = {
492                'segmentdescription': { 'topdldescription': top.to_dict() },
493                'allocID': pack_id(fid),
494                'master': False,
495                }
496
497        if opts.debug > 1: print >>sys.stderr, msg
498
499        try:
500            resp_dict = self.do_rpc(msg, 
501                    opts.url, opts.transport, cert, opts.trusted, 
502                    serialize_only=opts.serialize_only,
503                    tracefile=opts.tracefile,
504                    caller=self.caller('StartSegment'))
505        except self.RPCException, e:
506            exit_with_fault(\
507                    {'desc': e.desc, 'errstr': e.errstr, 'code': e.code})
508        except RuntimeError, e:
509            sys.exit("Error processing RPC: %s" % e)
510
511        if opts.debug > 1: print >>sys.stderr, resp_dict
512        if not opts.serialize_only:
513            print resp_dict
514
515class ftopo:
516    def __call__(self):
517        sys.exit("Use fedd_ftopo.py")
518
519class exp_data:
520    def __call__(self):
521        sys.exit("Use fedd_info.py")
522
523class vtopo:
524    def __call__(self):
525        sys.exit("Use fedd_info.py --data=vtopo")
526
527class vis:
528    def __call__(self):
529        sys.exit("Use fedd_info.py --data=vis")
530
531class status:
532    def __call__(self):
533        sys.exit("Use fedd_info.py --data=status")
534
535class multi_exp_data:
536    def __call__(self):
537        sys.exit("Use fedd_multiinfo")
538
539class multi_status:
540    def __call__(self):
541        sys.exit("Use fedd_multistatus")
542
543class image:
544    def __call__(self):
545        sys.exit("Use fedd_image.py")
546
547class topdl_image:
548    def __call__(self):
549        sys.exit("Use fedd_image.py")
550
551class ns_image:
552    def __call__(self):
553        sys.exit("Use fedd_image.py")
554
555class terminate:
556    def __call__(self):
557        sys.exit("Use fedd_terminate.py")
558
559class new:
560    def __call__(self):
561        sys.exit("Use fedd_new.py")
562
563class create:
564    def __call__(self):
565        sys.exit("Use fedd_create.py")
566
567class split:
568    def __call__(self):
569        sys.exit("Discontinued function")
570
571class spew_log:
572    def __call__(self):
573        sys.exit("Use fedd_spewlog.py")
574
575class ns_topdl:
576    def __call__(self):
577        sys.exit("Use fedd_ns2topdl.py")
578
579cmds = {\
580        'new': new(),\
581        'create': create(),\
582        'split': split(),\
583        'access': access(),\
584        'ftopo': ftopo(),\
585        'vtopo': vtopo(),\
586        'vis': vis(),\
587        'info': exp_data(),\
588        'multiinfo': multi_exp_data(),\
589        'multistatus': multi_status(),\
590        'image': image(),\
591        'ns_image': ns_image(),\
592        'status': status(),\
593        'terminate': terminate(),\
594        'spewlog': spew_log(),\
595        'topdl_image': topdl_image(),\
596        'start_segment': start_segment(),\
597        'terminate_segment': terminate_segment(),\
598        'ns_topdl': ns_topdl(),\
599    }
600if len(sys.argv) > 1:
601    operation = cmds.get(sys.argv[1], None)
602else:
603    sys.exit("first argument must be one of " + ",".join(cmds.keys()))
604
605if operation:
606    del sys.argv[1]
607    operation()
608else:
609    if sys.argv[1] == '--help':
610        sys.exit(\
611'''Only context sensitive help is available.  For one of the commands:
612
613%s
614
615type
616  %s command --help
617
618to get help, e.g., %s create --help
619''' % (", ".join(cmds.keys()), sys.argv[0], sys.argv[0]))
620    else:
621        sys.exit("Bad command: %s.  Valid ones are: %s" % \
622                (sys.argv[1], ", ".join(cmds.keys())))
623
Note: See TracBrowser for help on using the repository browser.