source: fedd/fedd_client.py @ 03e0290

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

single client

  • Property mode set to 100755
File size: 18.1 KB
RevLine 
[6ff0b91]1#!/usr/local/bin/python
2
3import sys
4import os
5import pwd
6
7from fedd_services import *
8
9from M2Crypto import SSL, X509
[329f61d]10from M2Crypto.m2xmlrpclib import SSL_Transport
[6ff0b91]11import M2Crypto.httpslib
[329f61d]12
13from xmlrpclib import ServerProxy, Error, dumps, loads
[8f91e66]14from ZSI import SoapWriter
[bb3769a]15from ZSI.TC import QName, String, URI, AnyElement, UNBOUNDED, Any
16from ZSI.wstools.Namespaces import SOAP
17from ZSI.fault import FaultType, Detail
[6ff0b91]18
[2d58549]19import xmlrpclib
20
[2106ed1]21from fedd_util import fedid, fedd_ssl_context, pack_soap, unpack_soap, \
[03e0290]22        pack_id, unpack_id, encapsulate_binaries, decapsulate_binaries
[6ff0b91]23
24from optparse import OptionParser, OptionValueError
25
[bb3769a]26import parse_detail
27
[6ff0b91]28# Turn off the matching of hostname to certificate ID
29SSL.Connection.clientPostConnectionCheck = None
30
31class IDFormatException(RuntimeError): pass
32
33class access_method:
34    """Encapsulates an access method generically."""
35    (type_ssh, type_x509, type_pgp) = ('sshPubkey', 'X509', 'pgpPubkey')
36    default_type = type_ssh
37    def __init__(self, buf=None, type=None, file=None):
38        self.buf = buf
39
40        if type != None: self.type = type
41        else: self.type = access_method.default_type
42
43        if file != None:
44            self.readfile(file)
45   
46    def readfile(self, file, type=None):
47        f = open(file, "r")
48        self.buf = f.read();
49        f.close()
50        if type == None:
51            if self.type == None:
52                self.type = access_method.default_type
53        else:
54            self.type = type;
55   
56class node_desc:
57    def __init__(self, image, hardware, count=1):
58        if getattr(image, "__iter__", None) == None:
59            if image == None: self.image = [ ]
60            else: self.image = [ image ]
61        else:
62            self.image = image
63
64        if getattr(hardware, "__iter__", None) == None: 
65            if hardware == None: self.hardware = [ ]
66            else: self.hardware = [ hardware ]
67        else:
68            self.hardware = hardware
69        if count != None: self.count = int(count)
70        else: self.count = 1
71
72class fedd_client_opts(OptionParser):
73    """Encapsulate option processing in this class, rather than in main"""
74    def __init__(self):
75        OptionParser.__init__(self, usage="%prog [opts] (--help for details)",
76                version="0.1")
77
78        self.add_option("-c","--cert", action="store", dest="cert",
79                type="string", help="my certificate file")
80        self.add_option("-d", "--debug", action="count", dest="debug", 
[03e0290]81                default=0, help="Set debug.  Repeat for more information")
[8f91e66]82        self.add_option("-s", "--serializeOnly", action="store_true", 
[03e0290]83                dest="serialize_only", default=False,
[8f91e66]84                help="Print the SOAP request that would be sent and exit")
[6ff0b91]85        self.add_option("-T","--trusted", action="store", dest="trusted",
86                type="string", help="Trusted certificates (required)")
87        self.add_option("-u", "--url", action="store", dest="url",
[03e0290]88                type="string",default="https://localhost:23235", 
[6ff0b91]89                help="URL to connect to (default %default)")
[329f61d]90        self.add_option("-x","--transport", action="store", type="choice",
[03e0290]91                choices=("xmlrpc", "soap"), default="soap",
[329f61d]92                help="Transport for request (xmlrpc|soap) (Default: %default)")
[6ff0b91]93        self.add_option("--trace", action="store_const", dest="tracefile", 
94                const=sys.stderr, help="Print SOAP exchange to stderr")
95
[03e0290]96class fedd_create_opts(fedd_client_opts):
97    def __init__(self, access_keys, add_key_callback=None, 
98            add_cert_callback=None):
99        fedd_client_opts.__init__(self)
100        self.add_option("-e", "--experiment_cert", dest="out_certfile",
101                type="string", help="output certificate file")
102        self.add_option("-F","--useFedid", action="store_true",
103                dest="use_fedid", default=False,
104                help="Use a fedid derived from my certificate as user identity")
105        self.add_option("-f", "--file", dest="file", 
106                help="experiment description file")
107        if add_key_callback:
108            self.add_option("-k", "--sshKey", action="callback", type="string", 
109                    callback=add_key_callback, callback_args=(access_keys,),
110                    help="ssh key for access (can be supplied more than once")
111        if add_cert_callback:
112            self.add_option("-K", "--x509Key", action="callback",
113                    type="string", callback=add_cert_callback,
114                    callback_args=(access_keys,),
115                    help="X509 certificate for access " + \
116                        "(can be supplied more than once")
117        self.add_option("-m", "--master", dest="master",
118                help="Master testbed in the federation")
119        self.add_option("-U", "--username", action="store", dest="user",
120                type="string", help="Use this username instead of the uid")
[6ff0b91]121
[03e0290]122class fedd_access_opts(fedd_create_opts):
123    def __init__(self, access_keys, node_descs, add_key_callback=None, 
124            add_cert_callback=None, add_node_callback=None):
125        fedd_create_opts.__init__(self, access_keys, add_key_callback,
126                add_cert_callback)
127        self.add_option("-a","--anonymous", action="store_true",
128                dest="anonymous", default=False,
129                help="Do not include a user in the request")
130        self.add_option("-l","--label", action="store", dest="label",
131                type="string", help="Label for output")
132        if add_node_callback:
133            self.add_option("-n", "--node", action="callback", type="string", 
134                    callback=add_node_callback, callback_args=(node_descs,),
135                    help="Node description: image:hardware[:count]")
136        self.add_option("-p", "--project", action="store", dest="project", 
137                type="string",
138                help="Use a project request with this project name")
139        self.add_option("-t", "--testbed", action="store", dest="testbed",
140                type="string",
141                help="Testbed identifier (URI) to contact (required)")
[6ff0b91]142
[0c0b13c]143def exit_with_fault(dict, out=sys.stderr):
144    """ Print an error message and exit.
145
[2d5c8b6]146    The dictionary contains the FeddFaultBody elements."""
[0c0b13c]147    codestr = ""
148
149    if dict.has_key('errstr'):
150        codestr = "Error: %s" % dict['errstr']
151
152    if dict.has_key('code'):
153        if len(codestr) > 0 : 
154            codestr += " (%d)" % dict['code']
155        else:
156            codestr = "Error Code: %d" % dict['code']
157
158    print>>out, codestr
159    print>>out, "Description: %s" % dict['desc']
160    sys.exit(dict.get('code', 20))
161
[03e0290]162class fedd_exp_data_opts(fedd_client_opts):
163    def __init__(self):
164        fedd_client_opts.__init__(self)
165        self.add_option("-e", "--experiment_cert", dest="exp_certfile",
166                type="string", help="output certificate file")
167
168# Base class that will do a the SOAP/XMLRPC exchange for a request.
169class fedd_rpc:
170    class RPCException:
171        def __init__(self, fb):
172            self.desc = fb.get('desc', None)
173            self.code = fb.get('code', None)
174            self.errstr = fb.get('errstr', None)
175
176    def __init__(self, pre): 
177        """
178        Specialize the class for the prc method
179        """
180        self.RequestMessage = globals()["%sRequestMessage" % pre]
181        self.ResponseMessage = globals()["%sResponseMessage" % pre]
182        self.RequestBody="%sRequestBody" % pre
183        self.ResponseBody="%sResponseBody" % pre
184        self.method = pre
185        self.RPCException = fedd_rpc.RPCException
186
187
188    def add_ssh_key(self, option, opt_str, value, parser, access_keys):
189        try:
190            access_keys.append(access_method(file=value,
191                type=access_method.type_ssh))
192        except IOError, (errno, strerror):
193            raise OptionValueError("Cannot generate sshPubkey from %s: "\
194                    "%s (%d)" % (value,strerror,errno))
195
196    def add_x509_cert(self, option, opt_str, value, parser, access_keys):
197        try:
198            access_keys.append(access_method(file=value,
199                type=access_method.type_x509))
200        except IOError, (errno, strerror):
201            raise OptionValueError("Cannot read x509 cert from %s: %s (%d)" %
202                    (value,strerror,errno))
203    def add_node_desc(self, option, opt_str, value, parser, node_descs):
204        def none_if_zero(x):
205            if len(x) > 0: return x
206            else: return None
207
208        params = map(none_if_zero, value.split(":"));
209       
210        if len(params) < 4 and len(params) > 1:
211            node_descs.append(node_desc(*params))
212        else:
213            raise OptionValueError("Bad node description: %s" % value)
214
215    def get_user_info(self, access_keys):
216        pw = pwd.getpwuid(os.getuid());
217        try_cert=None
218        user = None
219
220        if pw != None:
221            user = pw[0]
222            try_cert = "%s/.ssl/emulab.pem" % pw[5];
223            if not os.access(try_cert, os.R_OK):
224                try_cert = None
225            if len(access_keys) == 0:
226                for k in ["%s/.ssh/id_rsa.pub", "%s/.ssh/id_dsa.pub", 
227                        "%s/.ssh/identity.pub"]:
228                    try_key = k % pw[5];
229                    if os.access(try_key, os.R_OK):
230                        access_keys.append(access_method(file=try_key,
231                            type=access_method.type_ssh))
232                        break
233        return (user, try_cert)
234
235    def do_rpc(self, req_dict, url, transport, cert, trusted, tracefile=None,
236            serialize_only=False):
237        """
238        The work of sending and parsing the RPC as either XMLRPC or SOAP
239        """
240
241        context = None
242        while context == None:
243            try:
244                context = fedd_ssl_context(cert, trusted)
245            except SSL.SSLError, e:
246                # Yes, doing this on message type is not ideal.  The string
247                # comes from OpenSSL, so check there is this stops working.
248                if str(e) == "bad decrypt": 
249                    print >>sys.stderr, "Bad Passphrase given."
250                else: raise
251
252        if transport == "soap":
253            loc = feddServiceLocator();
254            port = loc.getfeddPortType(url,
255                    transport=M2Crypto.httpslib.HTTPSConnection, 
256                    transdict={ 'ssl_context' : context },
257                    tracefile=tracefile)
258
259            req = self.RequestMessage()
260
261            set_req = getattr(req, "set_element_%s" % self.RequestBody, None)
262            set_req(pack_soap(req, self.RequestBody, req_dict))
263
264            if serialize_only:
265                sw = SoapWriter()
266                sw.serialize(req)
267                print str(sw)
268                sys.exit(0)
[6ff0b91]269
[03e0290]270            try:
271                method_call = getattr(port, self.method, None)
272                resp = method_call(req)
273            except ZSI.ParseException, e:
274                raise RuntimeError("Malformed response (XMLPRC?): %s" % e)
275            except ZSI.FaultException, e:
276                resp = e.fault.detail[0]
277
278            if resp:
279                resp_call = getattr(resp, "get_element_%s" %self.ResponseBody,
280                        None)
281                if resp_call: 
282                    resp_body = resp_call()
283                    if ( resp_body != None): 
284                        try:
285                            return unpack_soap(resp_body)
286                        except RuntimeError, e:
287                            raise RuntimeError("Bad response. %s" % e.message)
288                elif 'get_element_FeddFaultBody' in dir(resp): 
289                    resp_body = resp.get_element_FeddFaultBody()
290                    if resp_body != None: 
291                        try:
292                            fb = unpack_soap(resp_body)
293                        except RuntimeError, e:
294                            raise RuntimeError("Bad response. %s" % e.message)
295                        raise self.RPCException(fb)
296                else: 
297                    raise RuntimeError("No body in response!?")
298            else: 
299                raise RuntimeError("No response?!?")
300        elif transport == "xmlrpc":
301            if serialize_only:
302                ser = dumps((req_dict,))
303                print ser
304                sys.exit(0)
305
306            xtransport = SSL_Transport(context)
307            port = ServerProxy(url, transport=xtransport)
[6ff0b91]308
[03e0290]309            try:
310                method_call = getattr(port, self.method, None)
311                resp = method_call(
312                        encapsulate_binaries({ self.RequestBody: msg},\
313                            ('fedid',)))
314            except Error, e:
315                resp = { 'FeddFaultBody': \
316                        { 'errstr' : e.faultCode, 'desc' : e.faultString } }
317            if resp:
318                if resp.has_key(self.ResponseBody): 
319                    return resp[self.ResponseBody]
320                elif resp.has_key('FeddFaultBody'):
321                    raise self.RPCException(resp['FeddFaultBody'])
322                else: 
323                    raise RuntimeError("No body in response!?")
324            else: 
325                raise RuntimeError("No response?!?")
326        else:
327            raise RuntimeError("Unknown RPC transport: %s" % transport)
328
329# Querying experiment data follows the same control flow regardless of the
330# specific data retrieved.  This class encapsulates that control flow.
331class exp_data(fedd_rpc):
332    def __init__(self, op): 
333        """
334        Specialize the class for the type of data requested (op)
335        """
336
337        fedd_rpc.__init__(self, op)
338        if op =='Vtopo':
339            self.key="vtopo"
340            self.xml='experiment'
341        elif op == 'Vis':
342            self.key="vis"
343            self.xml='vis'
344        else:
345            raise TypeError("Bad op: %s" % op)
346
347    def print_xml(self, d, out=sys.stdout):
348        """
349        Print the retrieved data is a simple xml representation of the dict.
350        """
351        str = "<%s>\n" % self.xml
352        for t in ('node', 'lan'):
353            if d.has_key(t): 
354                for x in d[t]:
355                    str += "<%s>" % t
356                    for k in x.keys():
357                        str += "<%s>%s</%s>" % (k, x[k],k)
358                    str += "</%s>\n" % t
359        str+= "</%s>" % self.xml
360        print >>out, str
361
362    def __call__(self):
363        """
364        The control flow.  Compose the request and print the response.
365        """
366        # Process the options using the customized option parser defined above
367        parser = fedd_exp_data_opts()
368
369        (opts, args) = parser.parse_args()
370
371        if opts.trusted != None:
372            if ( not os.access(opts.trusted, os.R_OK) ) :
373                sys.exit("Cannot read trusted certificates (%s)" % opts.trusted)
374        else:
375            parser.error("--trusted is required")
[6ff0b91]376
[03e0290]377        if opts.debug > 0: opts.tracefile=sys.stderr
[6ff0b91]378
[03e0290]379        if opts.cert != None: cert = opts.cert
[6ff0b91]380
[03e0290]381        if cert == None:
382            sys.exit("No certificate given (--cert) or found")
[6ff0b91]383
[03e0290]384        if os.access(cert, os.R_OK):
385            fid = fedid(file=cert)
386        else:
387            sys.exit("Cannot read certificate (%s)" % cert)
388
389        if opts.exp_certfile:
390            exp_fedid = fedid(file=opts.exp_certfile)
391        else:
392            sys.exit("Experiment certfile required")
393
394        try:
395            resp_dict = self.do_rpc({ 'experiment': { 'fedid': exp_fedid } }, 
396                    opts.url, opts.transport, cert, opts.trusted, 
397                    serialize_only=opts.serialize_only,
398                    tracefile=opts.tracefile)
399        except self.RPCException, e:
400            exit_with_fault(\
401                    {'desc': e.desc, 'errstr': e.errstr, 'code': e.code})
402        except RuntimeError, e:
403            sys.exit("Error processing RPC: %s" % e.message)
404
405        try:
406            if resp_dict.has_key(self.key):
407                self.print_xml(resp_dict[self.key])
408        except RuntimeError, e:
409            sys.exit("Bad response. %s" % e.message)
410
411class create(fedd_rpc):
412    def __init__(self): 
413        fedd_rpc.__init__(self, "Create")
414    def __call__(self):
415        access_keys = []
416        # Process the options using the customized option parser defined above
417        parser = fedd_create_opts(access_keys, self.add_ssh_key,
418                self.add_x509_cert)
419
420        (opts, args) = parser.parse_args()
421
422        if opts.trusted != None:
423            if ( not os.access(opts.trusted, os.R_OK) ) :
424                sys.exit("Cannot read trusted certificates (%s)" % opts.trusted)
425        else:
426            parser.error("--trusted is required")
427
428        if opts.debug > 0: opts.tracefile=sys.stderr
429
430        (user, cert) = self.get_user_info(access_keys)
431
432        if opts.user: user = opts.user
433
434        if opts.cert != None: cert = opts.cert
435
436        if cert == None:
437            sys.exit("No certificate given (--cert) or found")
438
439        if os.access(cert, os.R_OK):
440            fid = fedid(file=cert)
441            if opts.use_fedid == True:
442                user = fid
443        else:
444            sys.exit("Cannot read certificate (%s)" % cert)
445
446        if opts.file:
447            exp_desc = ""
448            try:
449                f = open(opts.file, 'r')
450                for line in f:
451                    exp_desc += line
452                f.close()
453            except IOError:
454                sys.exit("Cannot read description file (%s)" %opts.file)
455        else:
456            sys.exit("Must specify an experiment description (--file)")
457
458        if not opts.master:
459            sys.exit("Must specify a master testbed (--master)")
460
461        out_certfile = opts.out_certfile
462
463        msg = {
464                'experimentdescription': exp_desc,
465                'master': opts.master,
466                'user' : [ {\
467                        'userID': pack_id(user), \
468                        'access': [ { a.type: a.buf } for a in access_keys]\
469                        } ]
[6ff0b91]470                }
[03e0290]471
472        if opts.debug > 1: print >>sys.stderr, msg
473
474        try:
475            resp_dict = self.do_rpc(msg, 
476                    opts.url, opts.transport, cert, opts.trusted, 
477                    serialize_only=opts.serialize_only,
478                    tracefile=opts.tracefile)
479        except self.RPCException, e:
480            exit_with_fault(\
481                    {'desc': e.desc, 'errstr': e.errstr, 'code': e.code})
482        except RuntimeError, e:
483            sys.exit("Error processing RPC: %s" % e.message)
484
485        if opts.debug > 1: print >>sys.stderr, resp_dict
486
487        ea = resp_dict.get('experimentAccess', None)
488        if out_certfile and ea and ea.has_key('X509'):
[0c0b13c]489            try:
[03e0290]490                f = open(out_certfile, "w")
491                print >>f, ea['X509']
492                f.close()
493            except IOError:
494                sys.exit('Could not write to %s' %  out_certfile)
495
496class access(fedd_rpc):
497    def __init__(self):
498        fedd_rpc.__init__(self, "RequestAccess")
499
500    def print_response_as_testbed(self, resp, label, out=sys.stdout):
501        """Print the response as input to the splitter script"""
502
503        e = resp['emulab']
504        p = e['project']
505        fields = { 
506                "Boss": e['boss'],
507                "OpsNode": e['ops'],
508                "Domain": e['domain'],
509                "FileServer": e['fileServer'],
510                "EventServer": e['eventServer'],
511                "Project": unpack_id(p['name'])
512                }
513        if (label != None): print >> out, "[%s]" % label
514
515        for l, v in fields.iteritems():
516            print >>out, "%s: %s" % (l, v)
517
518        for u in p['user']:
519            print >>out, "User: %s" % unpack_id(u['userID'])
520
521        for a in e['fedAttr']:
522            print >>out, "%s: %s" % (a['attribute'], a['value'])
523
524    def __call__(self):
525        access_keys = []
526        node_descs = []
527        proj = None
528
529        # Process the options using the customized option parser defined above
530        parser = fedd_access_opts(access_keys, node_descs, self.add_ssh_key,
531                self.add_x509_cert, self.add_node_desc)
532
533        (opts, args) = parser.parse_args()
534
535        if opts.testbed == None:
536            parser.error("--testbed is required")
537
538        if opts.trusted != None:
539            if ( not os.access(opts.trusted, os.R_OK) ) :
540                sys.exit("Cannot read trusted certificates (%s)" % opts.trusted)
541        else:
542            parser.error("--trusted is required")
543
544        if opts.debug > 0: opts.tracefile=sys.stderr
545
546        (user, cert) = self.get_user_info(access_keys)
547
548        if opts.user: user = opts.user
549
550        if opts.cert != None: cert = opts.cert
551
552        if cert == None:
553            sys.exit("No certificate given (--cert) or found")
554
555        if os.access(cert, os.R_OK):
556            fid = fedid(file=cert)
557            if opts.use_fedid == True:
558                user = fid
559        else:
560            sys.exit("Cannot read certificate (%s)" % cert)
561
562        msg = {
563                'allocID': pack_id('test alloc'),
564                'destinationTestbed': pack_id(opts.testbed),
565                'access' : [ { a.type: a.buf } for a in access_keys ],
566                }
567
568        if len(node_descs) > 0:
569            msg['resources'] = { 
570                    'node': [ 
571                        { 
572                            'image':  n.image ,
573                            'hardware':  n.hardware,
574                            'count': n.count,
575                        } for n in node_descs],
576                    }
577
578        if opts.project != None:
579            if not opts.anonymous and user != None:
580                msg['project'] = {
581                        'name': pack_id(opts.project),
582                        'user': [ { 'userID': pack_id(user) } ],
583                        }
584            else:
585                msg['project'] = { 'name': pack_id(opts.project) }
586        else:
587            if not opts.anonymous and user != None:
588                msg['user'] = [ { 'userID': pack_id(user) } ]
589            else:
590                msg['user'] = [];
591
592        if opts.debug > 1: print >>sys.stderr, msg
593
594        try:
595            resp_dict = self.do_rpc(msg, 
596                    opts.url, opts.transport, cert, opts.trusted, 
597                    serialize_only=opts.serialize_only,
598                    tracefile=opts.tracefile)
599        except self.RPCException, e:
600            exit_with_fault(\
601                    {'desc': e.desc, 'errstr': e.errstr, 'code': e.code})
602        except RuntimeError, e:
603            sys.exit("Error processing RPC: %s" % e.message)
604
605        if opts.debug > 1: print >>sys.stderr, resp_dict
606        self.print_response_as_testbed(resp_dict, opts.label)
607
608cmds = {\
609        'create': create(),\
610        'access': access(),\
611        'vtopo': exp_data('Vtopo'),\
612        'vis': exp_data('Vis'),\
613    }
614
615operation = cmds.get(sys.argv[1], None)
616if operation:
617    del sys.argv[1]
618    operation()
619else:
620    sys.exit("Bad command: %s.  Valid ones are: %s" % \
621            (sys.argv[1], ", ".join(cmds.keys())))
622
Note: See TracBrowser for help on using the repository browser.