source: fedd/fedd_client.py @ b501f63

version-3.02
Last change on this file since b501f63 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
RevLine 
[6ff0b91]1#!/usr/local/bin/python
2
3import sys
4import os
5import pwd
[63f3746]6import tempfile
7import subprocess
[cc58813]8import re
9import xml.parsers.expat
[281c0ca]10import time
[6ff0b91]11
[5d3f239]12from federation import fedid, service_error
13from federation.util import fedd_ssl_context, pack_id, unpack_id
14from federation.remote_service import service_caller
[df783c1]15from federation import topdl
[6ff0b91]16
17from optparse import OptionParser, OptionValueError
18
[bb3769a]19
[6ff0b91]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):
[16a23a6]27        self.buf = buf
[6ff0b91]28
[16a23a6]29        if type != None: self.type = type
30        else: self.type = access_method.default_type
[6ff0b91]31
[16a23a6]32        if file != None:
33            self.readfile(file)
[6ff0b91]34   
35    def readfile(self, file, type=None):
[16a23a6]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;
[6ff0b91]44   
45class node_desc:
46    def __init__(self, image, hardware, count=1):
[16a23a6]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
[6ff0b91]60
61class fedd_client_opts(OptionParser):
62    """Encapsulate option processing in this class, rather than in main"""
63    def __init__(self):
[16a23a6]64        OptionParser.__init__(self, usage="%prog [opts] (--help for details)",
65                version="0.1")
66
[7d2814a]67        self.add_option("--cert", action="store", dest="cert",
[16a23a6]68                type="string", help="my certificate file")
[7d2814a]69        self.add_option( "--debug", action="count", dest="debug", 
[16a23a6]70                default=0, help="Set debug.  Repeat for more information")
[12658df]71        self.add_option("--serialize_only", action="store_true", 
[16a23a6]72                dest="serialize_only", default=False,
73                help="Print the SOAP request that would be sent and exit")
[7d2814a]74        self.add_option("--trusted", action="store", dest="trusted",
[16a23a6]75                type="string", help="Trusted certificates (required)")
[7d2814a]76        self.add_option("--url", action="store", dest="url",
[16a23a6]77                type="string",default="https://localhost:23235", 
78                help="URL to connect to (default %default)")
[7d2814a]79        self.add_option("--transport", action="store", type="choice",
[16a23a6]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")
[d743d60]84
85class fedd_access_opts(fedd_client_opts):
[a3ad8bd]86    def __init__(self):
87        fedd_client_opts.__init__(self)
[7d2814a]88        self.add_option("--experiment_cert", dest="out_certfile",
[a3ad8bd]89                type="string", help="output certificate file")
[7d2814a]90        self.add_option("--experiment_name", dest="exp_name",
[a3ad8bd]91                type="string", help="Suggested experiment name")
[d743d60]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)")
[7d2814a]102        self.add_option("--file", dest="file", 
[16a23a6]103                help="experiment description file")
[7d2814a]104        self.add_option("--project", action="store", dest="project", 
[16a23a6]105                type="string",
106                help="Project to export from master")
[7d2814a]107        self.add_option("--master", dest="master",
[12658df]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")
[6ff0b91]112
[d743d60]113class fedd_terminate_opts(fedd_client_opts):
[e40c7ee]114    def __init__(self):
[16a23a6]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")
[7d2814a]124        self.add_option("--experiment_cert", dest="exp_certfile",
[d743d60]125                type="string", help="experiment certificate file")
[7d2814a]126        self.add_option("--experiment_name", dest="exp_name",
[16a23a6]127                type="string", help="human readable experiment name")
[d743d60]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")
[63f3746]133
[23dec62]134class fedd_start_opts(fedd_client_opts):
135    def __init__(self):
136        fedd_client_opts.__init__(self)
[7d2814a]137        self.add_option("--file", dest="file", 
[23dec62]138                help="experiment description file")
139
[f00fb7d]140
[0c0b13c]141def exit_with_fault(dict, out=sys.stderr):
142    """ Print an error message and exit.
143
[2d5c8b6]144    The dictionary contains the FeddFaultBody elements."""
[0c0b13c]145    codestr = ""
146
147    if dict.has_key('errstr'):
[16a23a6]148        codestr = "Error: %s" % dict['errstr']
[0c0b13c]149
150    if dict.has_key('code'):
[16a23a6]151        if len(codestr) > 0 : 
152            codestr += " (%d)" % dict['code']
153        else:
154            codestr = "Error Code: %d" % dict['code']
[0c0b13c]155
156    print>>out, codestr
157    print>>out, "Description: %s" % dict['desc']
158    sys.exit(dict.get('code', 20))
[d743d60]159
[7b26c39]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.
[03e0290]163class fedd_rpc:
[7b26c39]164
[03e0290]165    class RPCException:
[7b26c39]166        """
167        An error during the RPC exchange.  It unifies errors from both SOAP and
168        XMLPRC calls.
169        """
[16a23a6]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)
[03e0290]174
[7b26c39]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): 
[16a23a6]186        """
187        Specialize the class for the pre method
188        """
[7b26c39]189        self.caller = fedd_rpc.caller
[16a23a6]190        self.RPCException = fedd_rpc.RPCException
[03e0290]191
192
193    def add_node_desc(self, option, opt_str, value, parser, node_descs):
[16a23a6]194        def none_if_zero(x):
195            if len(x) > 0: return x
196            else: return None
[03e0290]197
[16a23a6]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)
[03e0290]204
[99eb8cf]205    def get_user_info(self):
[16a23a6]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)
[03e0290]216
217    def do_rpc(self, req_dict, url, transport, cert, trusted, tracefile=None,
[7b26c39]218            serialize_only=False, caller=None):
[16a23a6]219        """
220        The work of sending and parsing the RPC as either XMLRPC or SOAP
221        """
222
[7b26c39]223        if caller is None: 
224            raise RuntimeError("Must provide caller to do_rpc")
225
[16a23a6]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:
[12658df]239                print caller.serialize_soap(req_dict) 
240                return { }
[16a23a6]241            else:
242                try:
[7b26c39]243                    resp = caller.call_soap_service(url, req_dict, 
[16a23a6]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
[12658df]255                return { }
[16a23a6]256            else:
257                try:
[7b26c39]258                    resp = caller.call_xmlrpc_service(url, req_dict, 
[16a23a6]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
[7b26c39]270        if resp.has_key(caller.ResponseBody):
271            return resp[caller.ResponseBody]
[16a23a6]272        else:
273            raise RuntimeError("No body in response??")
[058f58e]274
[d743d60]275class terminate_segment(fedd_rpc):
276    def __init__(self): 
[16a23a6]277        """
[d743d60]278        Termination request
[16a23a6]279        """
280
[7b26c39]281        fedd_rpc.__init__(self)
[16a23a6]282
[03e0290]283    def __call__(self):
[16a23a6]284        """
285        The control flow.  Compose the request and print the response.
286        """
287        # Process the options using the customized option parser defined above
[d743d60]288        parser = fedd_terminate_opts()
[03e0290]289
[16a23a6]290        (opts, args) = parser.parse_args()
[03e0290]291
[d743d60]292        (user, cert) = self.get_user_info()
[16a23a6]293        if opts.trusted:
294            if ( not os.access(opts.trusted, os.R_OK) ) :
295                sys.exit("Cannot read trusted certificates (%s)" % opts.trusted)
[6ff0b91]296
[16a23a6]297        if opts.debug > 0: opts.tracefile=sys.stderr
[6ff0b91]298
[16a23a6]299        if opts.cert != None: cert = opts.cert
[6ff0b91]300
[16a23a6]301        if cert == None:
302            sys.exit("No certificate given (--cert) or found")
[6ff0b91]303
[16a23a6]304        if os.access(cert, os.R_OK):
305            fid = fedid(file=cert)
306        else:
307            sys.exit("Cannot read certificate (%s)" % cert)
[03e0290]308
[16a23a6]309        if opts.exp_name and opts.exp_certfile:
310            sys.exit("Only one of --experiment_cert and " +\
[d743d60]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
[e40c7ee]324
[7c9a0a4]325        exp_id = None
[d743d60]326
[16a23a6]327        if opts.exp_certfile:
328            exp_id = { 'fedid': fedid(file=opts.exp_certfile) }
[e40c7ee]329
[16a23a6]330        if opts.exp_name:
331            exp_id = { 'localname' : opts.exp_name }
[e40c7ee]332
[7c9a0a4]333        if not exp_id:
[d743d60]334            sys.exit("Must give one of --experiment_cert and " +\
335                    "--experiment_name");
[7c9a0a4]336
[d743d60]337        req = { 'allocID': exp_id, 'force': opts.force }
[03e0290]338
[16a23a6]339        try:
340            resp_dict = self.do_rpc(req,
341                    opts.url, opts.transport, cert, opts.trusted, 
342                    serialize_only=opts.serialize_only,
[7b26c39]343                    tracefile=opts.tracefile,
[d743d60]344                    caller=self.caller('TerminateSegment'))
[16a23a6]345        except self.RPCException, e:
346            exit_with_fault(\
347                    {'desc': e.desc, 'errstr': e.errstr, 'code': e.code})
348        except RuntimeError, e:
[d743d60]349            print e
[16a23a6]350            sys.exit("Error processing RPC: %s" % e)
[03e0290]351
[d743d60]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")
[d055eb1]360
[d743d60]361class access(fedd_rpc):
[d055eb1]362    def __init__(self):
[d743d60]363        fedd_rpc.__init__(self)
[d055eb1]364
[d743d60]365    def print_response_as_testbed(self, resp, label, out=sys.stdout):
366        """Print the response as input to the splitter script"""
[d055eb1]367
[d743d60]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
[d055eb1]380
[d743d60]381            for l, v in fields.iteritems():
382                print >>out, "%s: %s" % (l, v)
[d055eb1]383
[d743d60]384            for u in p['user']:
385                print >>out, "User: %s" % unpack_id(u['userID'])
[f76d3d7]386
[d743d60]387            for a in e['fedAttr']:
388                print >>out, "%s: %s" % (a['attribute'], a['value'])
[f76d3d7]389
[281c0ca]390    def __call__(self):
[d743d60]391        node_descs = []
392        proj = None
[65f3f29]393
[16a23a6]394        # Process the options using the customized option parser defined above
[d743d60]395        parser = fedd_access_opts()
[16a23a6]396
397        (opts, args) = parser.parse_args()
398
[d743d60]399        if opts.testbed == None:
400            parser.error("--testbed is required")
401
[16a23a6]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
[99eb8cf]408        (user, cert) = self.get_user_info()
[16a23a6]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)
[d743d60]417            if opts.use_fedid == True:
418                user = fid
[16a23a6]419        else:
420            sys.exit("Cannot read certificate (%s)" % cert)
421
[d743d60]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
[16a23a6]438
439        try:
[d743d60]440            resp_dict = self.do_rpc(msg, 
[16a23a6]441                    opts.url, opts.transport, cert, opts.trusted, 
442                    serialize_only=opts.serialize_only,
[7b26c39]443                    tracefile=opts.tracefile,
[d743d60]444                    caller=self.caller('RequestAccess'))
[16a23a6]445        except self.RPCException, e:
446            exit_with_fault(\
447                    {'desc': e.desc, 'errstr': e.errstr, 'code': e.code})
448        except RuntimeError, e:
[d743d60]449            sys.exit("Error processing RPC: %s" % e.message)
[65f3f29]450
[d743d60]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)
[65f3f29]454
[d743d60]455class start_segment(fedd_rpc):
456    def __init__(self): 
457        fedd_rpc.__init__(self)
[65f3f29]458    def __call__(self):
[16a23a6]459        # Process the options using the customized option parser defined above
[d743d60]460        parser = fedd_start_opts()
[16a23a6]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
[99eb8cf]470        (user, cert) = self.get_user_info()
[16a23a6]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
[d743d60]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
[16a23a6]498
499        try:
[d743d60]500            resp_dict = self.do_rpc(msg, 
[16a23a6]501                    opts.url, opts.transport, cert, opts.trusted, 
502                    serialize_only=opts.serialize_only,
[7b26c39]503                    tracefile=opts.tracefile,
[d743d60]504                    caller=self.caller('StartSegment'))
[16a23a6]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
[d743d60]511        if opts.debug > 1: print >>sys.stderr, resp_dict
512        if not opts.serialize_only:
513            print resp_dict
[16a23a6]514
[d743d60]515class ftopo:
516    def __call__(self):
517        sys.exit("Use fedd_ftopo.py")
[16a23a6]518
[d743d60]519class exp_data:
520    def __call__(self):
521        sys.exit("Use fedd_info.py")
[16a23a6]522
[d743d60]523class vtopo:
524    def __call__(self):
525        sys.exit("Use fedd_info.py --data=vtopo")
[16a23a6]526
[d743d60]527class vis:
528    def __call__(self):
529        sys.exit("Use fedd_info.py --data=vis")
[16a23a6]530
[d743d60]531class status:
532    def __call__(self):
533        sys.exit("Use fedd_info.py --data=status")
[16a23a6]534
[d743d60]535class multi_exp_data:
536    def __call__(self):
537        sys.exit("Use fedd_multiinfo")
[63f3746]538
[d743d60]539class multi_status:
540    def __call__(self):
541        sys.exit("Use fedd_multistatus")
[63f3746]542
[d743d60]543class image:
544    def __call__(self):
545        sys.exit("Use fedd_image.py")
[63f3746]546
[d743d60]547class topdl_image:
[63f3746]548    def __call__(self):
[d743d60]549        sys.exit("Use fedd_image.py")
[16a23a6]550
[d743d60]551class ns_image:
552    def __call__(self):
553        sys.exit("Use fedd_image.py")
[16a23a6]554
[d743d60]555class terminate:
556    def __call__(self):
557        sys.exit("Use fedd_terminate.py")
[16a23a6]558
[d743d60]559class new:
560    def __call__(self):
561        sys.exit("Use fedd_new.py")
[16a23a6]562
[d743d60]563class create:
564    def __call__(self):
565        sys.exit("Use fedd_create.py")
[16a23a6]566
[d743d60]567class split:
568    def __call__(self):
569        sys.exit("Discontinued function")
[16a23a6]570
[d743d60]571class spew_log:
572    def __call__(self):
573        sys.exit("Use fedd_spewlog.py")
[16a23a6]574
[d743d60]575class ns_topdl:
[f00fb7d]576    def __call__(self):
[d743d60]577        sys.exit("Use fedd_ns2topdl.py")
[f00fb7d]578
[03e0290]579cmds = {\
[a3ad8bd]580        'new': new(),\
[16a23a6]581        'create': create(),\
582        'split': split(),\
583        'access': access(),\
[d055eb1]584        'ftopo': ftopo(),\
[16a23a6]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(),\
[23dec62]596        'start_segment': start_segment(),\
597        'terminate_segment': terminate_segment(),\
[f00fb7d]598        'ns_topdl': ns_topdl(),\
[03e0290]599    }
[f54e8e4]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()))
[03e0290]604
605if operation:
606    del sys.argv[1]
607    operation()
608else:
[d15522f]609    if sys.argv[1] == '--help':
[16a23a6]610        sys.exit(\
[d15522f]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:
[16a23a6]621        sys.exit("Bad command: %s.  Valid ones are: %s" % \
622                (sys.argv[1], ", ".join(cmds.keys())))
[03e0290]623
Note: See TracBrowser for help on using the repository browser.