source: fedd/fedd_client.py @ 2f45140

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