source: fedd/fedd_client.py @ c52c48d

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

add info and work with SEER attach

  • Property mode set to 100755
File size: 18.9 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        elif op == 'Info': pass
349        else:
350            raise TypeError("Bad op: %s" % op)
351
352    def print_xml(self, d, out=sys.stdout):
353        """
354        Print the retrieved data is a simple xml representation of the dict.
355        """
356        str = "<%s>\n" % self.xml
357        for t in ('node', 'lan'):
358            if d.has_key(t): 
359                for x in d[t]:
360                    str += "<%s>" % t
361                    for k in x.keys():
362                        str += "<%s>%s</%s>" % (k, x[k],k)
363                    str += "</%s>\n" % t
364        str+= "</%s>" % self.xml
365        print >>out, str
366
367    def __call__(self):
368        """
369        The control flow.  Compose the request and print the response.
370        """
371        # Process the options using the customized option parser defined above
372        parser = fedd_exp_data_opts()
373
374        (opts, args) = parser.parse_args()
375
376        if opts.trusted != None:
377            if ( not os.access(opts.trusted, os.R_OK) ) :
378                sys.exit("Cannot read trusted certificates (%s)" % opts.trusted)
379        else:
380            parser.error("--trusted is required")
381
382        if opts.debug > 0: opts.tracefile=sys.stderr
383
384        if opts.cert != None: cert = opts.cert
385
386        if cert == None:
387            sys.exit("No certificate given (--cert) or found")
388
389        if os.access(cert, os.R_OK):
390            fid = fedid(file=cert)
391        else:
392            sys.exit("Cannot read certificate (%s)" % cert)
393
394        if opts.exp_name and opts.exp_certfile:
395            sys.exit("Only one of --experiment_cert and " +\
396                    "--experiment_name permitted");
397
398        if opts.exp_certfile:
399            exp_id = { 'fedid': fedid(file=opts.exp_certfile) }
400
401        if opts.exp_name:
402            exp_id = { 'localname' : opts.exp_name }
403
404        req = { 'experiment': exp_id }
405
406        try:
407            resp_dict = self.do_rpc(req,
408                    opts.url, opts.transport, cert, opts.trusted, 
409                    serialize_only=opts.serialize_only,
410                    tracefile=opts.tracefile)
411        except self.RPCException, e:
412            exit_with_fault(\
413                    {'desc': e.desc, 'errstr': e.errstr, 'code': e.code})
414        except RuntimeError, e:
415            print e
416            sys.exit("Error processing RPC: %s" % e)
417
418        if getattr(self, 'key', None):
419            try:
420                if resp_dict.has_key(self.key):
421                    self.print_xml(resp_dict[self.key])
422            except RuntimeError, e:
423                sys.exit("Bad response. %s" % e.message)
424        else:
425            print resp_dict
426
427class create(fedd_rpc):
428    def __init__(self): 
429        fedd_rpc.__init__(self, "Create")
430    def __call__(self):
431        access_keys = []
432        # Process the options using the customized option parser defined above
433        parser = fedd_create_opts(access_keys, self.add_ssh_key,
434                self.add_x509_cert)
435
436        (opts, args) = parser.parse_args()
437
438        if opts.trusted != None:
439            if ( not os.access(opts.trusted, os.R_OK) ) :
440                sys.exit("Cannot read trusted certificates (%s)" % opts.trusted)
441        else:
442            parser.error("--trusted is required")
443
444        if opts.debug > 0: opts.tracefile=sys.stderr
445
446        (user, cert) = self.get_user_info(access_keys)
447
448        if opts.user: user = opts.user
449
450        if opts.cert != None: cert = opts.cert
451
452        if cert == None:
453            sys.exit("No certificate given (--cert) or found")
454
455        if os.access(cert, os.R_OK):
456            fid = fedid(file=cert)
457            if opts.use_fedid == True:
458                user = fid
459        else:
460            sys.exit("Cannot read certificate (%s)" % cert)
461
462        if opts.file:
463            exp_desc = ""
464            try:
465                f = open(opts.file, 'r')
466                for line in f:
467                    exp_desc += line
468                f.close()
469            except IOError:
470                sys.exit("Cannot read description file (%s)" %opts.file)
471        else:
472            sys.exit("Must specify an experiment description (--file)")
473
474        if not opts.master:
475            sys.exit("Must specify a master testbed (--master)")
476
477        out_certfile = opts.out_certfile
478
479        msg = {
480                'experimentdescription': exp_desc,
481                'master': opts.master,
482                'user' : [ {\
483                        'userID': pack_id(user), \
484                        'access': [ { a.type: a.buf } for a in access_keys]\
485                        } ]
486                }
487
488        if opts.exp_name:
489            msg['experimentID'] = { 'localname': opts.exp_name }
490
491        if opts.debug > 1: print >>sys.stderr, msg
492
493        try:
494            resp_dict = self.do_rpc(msg, 
495                    opts.url, opts.transport, cert, opts.trusted, 
496                    serialize_only=opts.serialize_only,
497                    tracefile=opts.tracefile)
498        except self.RPCException, e:
499            exit_with_fault(\
500                    {'desc': e.desc, 'errstr': e.errstr, 'code': e.code})
501        except RuntimeError, e:
502            sys.exit("Error processing RPC: %s" % e.message)
503
504        if opts.debug > 1: print >>sys.stderr, resp_dict
505
506        ea = resp_dict.get('experimentAccess', None)
507        if out_certfile and ea and ea.has_key('X509'):
508            try:
509                f = open(out_certfile, "w")
510                print >>f, ea['X509']
511                f.close()
512            except IOError:
513                sys.exit('Could not write to %s' %  out_certfile)
514        eid = resp_dict.get('experimentID', None)
515        if eid:
516            for id in eid:
517                for k in id.keys():
518                    if k == 'fedid': print "%s: %s" % (k,fedid(bits=id[k]))
519                    else: print "%s: %s" % (k, id[k])
520
521class access(fedd_rpc):
522    def __init__(self):
523        fedd_rpc.__init__(self, "RequestAccess")
524
525    def print_response_as_testbed(self, resp, label, out=sys.stdout):
526        """Print the response as input to the splitter script"""
527
528        e = resp['emulab']
529        p = e['project']
530        fields = { 
531                "Boss": e['boss'],
532                "OpsNode": e['ops'],
533                "Domain": e['domain'],
534                "FileServer": e['fileServer'],
535                "EventServer": e['eventServer'],
536                "Project": unpack_id(p['name'])
537                }
538        if (label != None): print >> out, "[%s]" % label
539
540        for l, v in fields.iteritems():
541            print >>out, "%s: %s" % (l, v)
542
543        for u in p['user']:
544            print >>out, "User: %s" % unpack_id(u['userID'])
545
546        for a in e['fedAttr']:
547            print >>out, "%s: %s" % (a['attribute'], a['value'])
548
549    def __call__(self):
550        access_keys = []
551        node_descs = []
552        proj = None
553
554        # Process the options using the customized option parser defined above
555        parser = fedd_access_opts(access_keys, node_descs, self.add_ssh_key,
556                self.add_x509_cert, self.add_node_desc)
557
558        (opts, args) = parser.parse_args()
559
560        if opts.testbed == None:
561            parser.error("--testbed is required")
562
563        if opts.trusted != None:
564            if ( not os.access(opts.trusted, os.R_OK) ) :
565                sys.exit("Cannot read trusted certificates (%s)" % opts.trusted)
566        else:
567            parser.error("--trusted is required")
568
569        if opts.debug > 0: opts.tracefile=sys.stderr
570
571        (user, cert) = self.get_user_info(access_keys)
572
573        if opts.user: user = opts.user
574
575        if opts.cert != None: cert = opts.cert
576
577        if cert == None:
578            sys.exit("No certificate given (--cert) or found")
579
580        if os.access(cert, os.R_OK):
581            fid = fedid(file=cert)
582            if opts.use_fedid == True:
583                user = fid
584        else:
585            sys.exit("Cannot read certificate (%s)" % cert)
586
587        msg = {
588                'allocID': pack_id('test alloc'),
589                'destinationTestbed': pack_id(opts.testbed),
590                'access' : [ { a.type: a.buf } for a in access_keys ],
591                }
592
593        if len(node_descs) > 0:
594            msg['resources'] = { 
595                    'node': [ 
596                        { 
597                            'image':  n.image ,
598                            'hardware':  n.hardware,
599                            'count': n.count,
600                        } for n in node_descs],
601                    }
602
603        if opts.project != None:
604            if not opts.anonymous and user != None:
605                msg['project'] = {
606                        'name': pack_id(opts.project),
607                        'user': [ { 'userID': pack_id(user) } ],
608                        }
609            else:
610                msg['project'] = { 'name': pack_id(opts.project) }
611        else:
612            if not opts.anonymous and user != None:
613                msg['user'] = [ { 'userID': pack_id(user) } ]
614            else:
615                msg['user'] = [];
616
617        if opts.debug > 1: print >>sys.stderr, msg
618
619        try:
620            resp_dict = self.do_rpc(msg, 
621                    opts.url, opts.transport, cert, opts.trusted, 
622                    serialize_only=opts.serialize_only,
623                    tracefile=opts.tracefile)
624        except self.RPCException, e:
625            exit_with_fault(\
626                    {'desc': e.desc, 'errstr': e.errstr, 'code': e.code})
627        except RuntimeError, e:
628            sys.exit("Error processing RPC: %s" % e.message)
629
630        if opts.debug > 1: print >>sys.stderr, resp_dict
631        self.print_response_as_testbed(resp_dict, opts.label)
632
633cmds = {\
634        'create': create(),\
635        'access': access(),\
636        'vtopo': exp_data('Vtopo'),\
637        'vis': exp_data('Vis'),\
638        'info': exp_data('Info'),\
639    }
640
641operation = cmds.get(sys.argv[1], None)
642if operation:
643    del sys.argv[1]
644    operation()
645else:
646    sys.exit("Bad command: %s.  Valid ones are: %s" % \
647            (sys.argv[1], ", ".join(cmds.keys())))
648
Note: See TracBrowser for help on using the repository browser.