source: fedd/fedd_client.py @ e40c7ee

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

more data on a create request, including user requested local name

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