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
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("-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")
121
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)")
142
143def exit_with_fault(dict, out=sys.stderr):
144    """ Print an error message and exit.
145
146    The dictionary contains the FeddFaultBody elements."""
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
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)
269
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)
308
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")
376
377        if opts.debug > 0: opts.tracefile=sys.stderr
378
379        if opts.cert != None: cert = opts.cert
380
381        if cert == None:
382            sys.exit("No certificate given (--cert) or found")
383
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                        } ]
470                }
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'):
489            try:
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.