source: fedd/fedd_client.py @ e48d8eb

Last change on this file since e48d8eb was 6bedbdba, checked in by Ted Faber <faber@…>, 13 years ago

Split topdl and fedid out to different packages. Add differential
installs

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