source: fedd/federation/client_lib.py @ ad4b25a

Last change on this file since ad4b25a was c259a77, checked in by Ted Faber <faber@…>, 10 years ago

Service info in xml files.

  • Property mode set to 100755
File size: 12.8 KB
RevLine 
[d743d60]1#!/usr/local/bin/python
2
3import sys
4import pwd
[d39809f]5import os
[c259a77]6import re
[d39809f]7import os.path
[c259a77]8import xml.parsers.expat
[d39809f]9
10from string import join
[e83f2f2]11from datetime import datetime
[d743d60]12
[6bedbdba]13from deter import fedid
[62f3dd9]14from util import fedd_ssl_context, file_expanding_opts
[d743d60]15from remote_service import service_caller
16from service_error import service_error
17
18from optparse import OptionParser, OptionValueError
19
20
[62f3dd9]21class client_opts(file_expanding_opts):
[d743d60]22    """
23    Standatd set of options that all clients talking to fedd can probably use.
24    Client code usually specializes this.
25    """
[62f3dd9]26
[d743d60]27    def __init__(self):
[62f3dd9]28        file_expanding_opts.__init__(self,
29                usage="%prog [opts] (--help for details)",
[d743d60]30                version="0.1")
31
[62f3dd9]32        self.add_option("--cert", action="callback", dest="cert", 
33                callback=self.expand_file,
[d743d60]34                type="string", help="my certificate file")
[e83f2f2]35        self.add_option("--auth_log", action="callback", dest="auth_log",
36                callback=self.expand_file, default=None,
37                type="string", help="Log authentication decisions to this file")
[62f3dd9]38        self.add_option("--abac", action="callback", dest="abac_dir",
39                callback=self.expand_file,
40                type="string", default=os.path.expanduser('~/.abac'), 
41                help="Directory with abac certs")
42        self.add_option('--no_abac', action='store_const', const=None, 
43                dest='abac_dir', help='Do not use abac authorization')
[d743d60]44        self.add_option( "--debug", action="count", dest="debug", 
45                default=0, help="Set debug.  Repeat for more information")
46        self.add_option("--serialize_only", action="store_true", 
47                dest="serialize_only", default=False,
48                help="Print the SOAP request that would be sent and exit")
[62f3dd9]49        self.add_option("--trusted", action="callback", dest="trusted",
50                callback=self.expand_file,
[d743d60]51                type="string", help="Trusted certificates (required)")
52        self.add_option("--url", action="store", dest="url",
[5d854e1]53                type="string",default=None, help="URL to connect to")
[d743d60]54        self.add_option("--transport", action="store", type="choice",
55                choices=("xmlrpc", "soap"), default="soap",
56                help="Transport for request (xmlrpc|soap) (Default: %default)")
57        self.add_option("--trace", action="store_const", dest="tracefile", 
58                const=sys.stderr, help="Print SOAP exchange to stderr")
59
[e83f2f2]60def log_authentication(fn, action, outcome, proof):
61    f = open(fn, 'a')
[311ece3]62    print >>f, '<comment>%s %s at %s</comment>' % (action, outcome, datetime.now())
[e83f2f2]63    if isinstance(proof, list):
64        for p in proof:
65            print >>f, p.to_xml()
66    else:
67        print >>f, proof.to_xml()
68    f.close()
69
70
71def exit_with_fault(exc, action, opts, out=sys.stderr):
[d743d60]72    """
73    Print an error message and exit.  exc is the RPCException that caused the
74    failure.
75    """
76    codestr = ""
77
78    if exc.errstr: codestr = "Error: %s" % exc.errstr
79    else: codestr = ""
80
81    if exc.code:
82        if isinstance(exc.code, int): code = exc.code
83        else: code = -1
84
85        if len(codestr) > 0 : codestr += " (%d)" % code
86        else: codestr = "Error Code: %d" % code
87    else:
88        code = -1
89
[e83f2f2]90    if exc.code == service_error.access and opts.auth_log:
91        try:
92            log_authentication(opts.auth_log, action, 'failed', exc.proof)
93        except EnvironmentError, e:
94            print >>sys.stderr, "Failed to log to %s: %s" % \
95                    (e.filename, e.strerror)
96
[d743d60]97    print>>out, codestr
98    print>>out, "Description: %s" % exc.desc
99    sys.exit(code)
100
101class RPCException(RuntimeError):
102    """
103    An error during the RPC exchange.  It unifies errors from both SOAP and
104    XMLPRC calls.
105    """
[e83f2f2]106    def __init__(self, desc=None, code=None, errstr=None, proof=None):
[d743d60]107        RuntimeError.__init__(self)
108        self.desc = desc
109        if isinstance(code, int): self.code = code
110        else: self.code = -1
111        self.errstr = errstr
[e83f2f2]112        self.proof = proof
[d743d60]113
[7206e5a]114class CertificateMismatchError(RuntimeError): pass
115
[d743d60]116
117def get_user_cert():
[c573278]118    for c in ("~/.ssl/fedid.pem", "~/.ssl/emulab.pem"):
119        cert = os.path.expanduser(c)
120        if os.access(cert, os.R_OK):
121            break
122        else:
123            cert = None
[d743d60]124    return cert
125
[d39809f]126def get_abac_certs(dir):
127    '''
128    Return a list of the contents of the files in dir.  These should be abac
129    certificates, but that isn't checked.
130    '''
131    rv = [ ]
[62f3dd9]132    if dir and os.path.isdir(dir):
[d39809f]133        for fn in ["%s/%s" % (dir, p) for p in os.listdir(dir) \
134                if os.path.isfile("%s/%s" % (dir,p))]:
135            f = open(fn, 'r')
[7206e5a]136            rv.append(f.read())
[d39809f]137            f.close()
138    return rv
139
[c259a77]140ns_service_re = re.compile('^\\s*#\\s*SERVICE:\\s*([\\S]+)')
141xml_service_re = re.compile('SERVICE:\\s*([\\S]+)\\s*$')
142
143def parse_service(svc):
144    """
145    Pasre a service entry into a hash representing a service entry in a
146    message.  The format is:
147        svc_name:exporter(s):importer(s):attr=val,attr=val
148    The svc_name is the service name, exporter is the exporting testbeds
149    (comma-separated) importer is the importing testbeds (if any) and the rest
150    are attr=val pairs that will become attributes of the service.  These
151    include parameters and other such stuff.
152    """
153
154    terms = svc.split(':')
155    svcd = { }
156    if len(terms) < 2 or len(terms[0]) == 0 or len(terms[1]) == 0:
157        sys.exit("Bad service description '%s': Not enough terms" % svc)
158
159    svcd['name'] = terms[0]
160    svcd['export'] = terms[1].split(",")
161    if len(terms) > 2 and len(terms[2]) > 0:
162        svcd['import'] = terms[2].split(",")
163    if len(terms) > 3 and len(terms[3]) > 0:
164        svcd['fedAttr'] = [ ]
165        for t in terms[3].split(";"):
166            i = t.find("=")
167            if i != -1 :
168                svcd['fedAttr'].append(
169                        {'attribute': t[0:i], 'value': t[i+1:]})
170            else:
171                sys.exit("Bad service attribute '%s': no equals sign" % t)
172    return svcd
173
174def service_dict_to_line(d):
175    """
176    Convert a dict containing a service description into a colon separated
177    service description suitable for inclusion in a file or command line.
178    """
179
180    return 'SERVICE: %s' % (
181            ':'.join([
182                d.get('name', ''),
183                ','.join(d.get('export','')),
184                ','.join(d.get('import','')),
185                ','.join([
186                    '='.join((dd.get('attribute', ''), dd.get('value',''))) \
187                        for dd in d.get('fedAttr', [])
188                ])
189            ]))
190
191def extract_services_from_xml(string=None, file=None, filename=None):
192    class parser:
193        def __init__(self):
194            self.svcs = []
195        def comment_handler(self, data):
196            for l in data.split('\n'):
197                if xml_service_re.match(l):
198                    self.svcs.append(
199                            parse_service(xml_service_re.match(l).group(1)))
200
201    p = parser()
202    xp = xml.parsers.expat.ParserCreate()
203
204    xp.CommentHandler = p.comment_handler
205
206    num_set = len([ x for x in (string, filename, file)\
207            if x is not None ])
208
209    if num_set != 1:
210        raise RuntimeError("Exactly one one of file, filename and string " + \
211                "must be set")
212    elif filename:
213        f = open(filename, "r")
214        xp.ParseFile(f)
215        f.close()
216    elif file:
217        xp.ParseFile(file)
218    elif string:
219        xp.Parse(string, True)
220
221    return p.svcs
222
[d743d60]223def wrangle_standard_options(opts):
224    """
225    Look for the certificate to use for the call (check for the standard emulab
[3ff5e2a]226    file and any passed in.  Make sure a present cert file can be read.  Make
227    sure that any trusted cert file can be read and if the debug level is set,
228    set the tracefile attribute as well.  If any of these tests fail, raise a
229    RuntimeError.  Otherwise return the certificate file, the fedid, and the
230    fedd url.
[d743d60]231    """
[5d854e1]232    default_url="https://localhost:23235"
233
[d743d60]234    if opts.debug > 0: opts.tracefile=sys.stderr
235
236    if opts.trusted:
237        if ( not os.access(opts.trusted, os.R_OK) ) :
238            raise RuntimeError("Cannot read trusted certificates (%s)" \
239                    % opts.trusted)
240
241    cert = get_user_cert()
242    if opts.cert: cert = opts.cert
243
244    if cert is None:
245        raise RuntimeError("No certificate given (--cert) or found")
246
247    if os.access(cert, os.R_OK):
248        fid = fedid(file=cert)
249    else:
250        raise RuntimeError("Cannot read certificate (%s)" % cert)
251
[5d854e1]252    if opts.url: url = opts.url
253    elif 'FEDD_URL' in os.environ: url = os.environ['FEDD_URL']
254    else: url = default_url
255
[a0c2866]256    if opts.abac_dir:
257        if not os.access(opts.abac_dir, os.F_OK):
[77d05a5]258            try:
259                os.mkdir(opts.abac_dir, 0700)
260            except OSError, e:
261                raise RuntimeError("No ABAC directory (could not create): %s" \
262                        % opts.abac_dir)
263
264        if not os.path.isdir(opts.abac_dir):
[a0c2866]265            raise RuntimeError("ABAC directory not a directory: %s" \
266                    % opts.abac_dir)
267        elif not os.access(opts.abac_dir, os.W_OK):
268            raise RuntimeError("Cannot write to ABAC directory: %s" \
269                    % opts.abac_dir)
270
271
[5d854e1]272
273    return (cert, fid, url)
[d743d60]274
[7206e5a]275def save_certfile(out_certfile, ea, check_cert=None):
[d743d60]276    """
277    if the experiment authority section in ea has a certificate and the
278    out_certfile parameter has a place to put it, save the cert to the file.
[7206e5a]279    EnvronmentError s can come from the file operations.  If check_cert is
280    given, the certificate in ea is compared with it and if they are not equal,
281    a CertificateMismatchError is raised.
[d743d60]282    """
283    if out_certfile and ea and 'X509' in ea:
[7206e5a]284        out_cert = ea['X509']
285        if check_cert and check_cert != out_cert:
286            raise CertificateMismatchError()
[d743d60]287        f = open(out_certfile, "w")
[7206e5a]288        f.write(out_cert)
[d743d60]289        f.close()
290
291
292def do_rpc(req_dict, url, transport, cert, trusted, tracefile=None,
293        serialize_only=False, caller=None, responseBody=None):
294    """
295    The work of sending and parsing the RPC as either XMLRPC or SOAP
296    """
297
298    if caller is None: 
299        raise RuntimeError("Must provide caller to do_rpc")
300
301    context = None
302    while context == None:
303        try:
304            context = fedd_ssl_context(cert, trusted)
305        except Exception, e:
306            # Yes, doing this on message type is not ideal.  The string
307            # comes from OpenSSL, so check there is this stops working.
308            if str(e) == "bad decrypt": 
309                print >>sys.stderr, "Bad Passphrase given."
310            else: raise
311
312    if transport == "soap":
313        if serialize_only:
314            print caller.serialize_soap(req_dict) 
315            return { }
316        else:
317            try:
318                resp = caller.call_soap_service(url, req_dict, 
319                        context=context, tracefile=tracefile)
320            except service_error, e:
321                raise RPCException(desc=e.desc, code=e.code, 
[e83f2f2]322                        errstr=e.code_string(), proof=e.proof)
[d743d60]323    elif transport == "xmlrpc":
324        if serialize_only:
325            ser = dumps((req_dict,))
326            print ser
327            return { }
328        else:
329            try:
330                resp = caller.call_xmlrpc_service(url, req_dict, 
331                        context=context, tracefile=tracefile)
332            except service_error, e:
333                raise RPCException(desc=e.desc, code=e.code, 
334                        errstr=e.code_string())
335    else:
336        raise RuntimeError("Unknown RPC transport: %s" % transport)
337
338    if responseBody:
339        if responseBody in resp: return resp[responseBody]
340        else: raise RuntimeError("No body in response??")
341    else:
342        return resp
343
344def get_experiment_names(eid):
345    """
346    eid is the experimentID entry in one of a dict representing a fedd message.
347    Pull out the fedid and localname from that hash and return them as fedid,
348    localid)
349    """
350    fedid = None
351    local = None
352    for id in eid or []:
353        for k in id.keys():
354            if k =='fedid':
355                fedid = id[k]
356            elif k =='localname':
357                local = id[k]
358
359    return (fedid, local)
360
361class info_format:
362    def __init__(self, out=sys.stdout):
363        self.out = out
364        self.key = {
365                'vis': 'vis', 
366                'vtopo': 'vtopo',
367                'federant': 'federant',
368                'experimentdescription': 'experimentdescription',
369                'id': 'experimentID',
370                'status': 'experimentStatus',
371                'log': 'allocationLog',
[2fd8f8c]372                'embedding': 'embedding',
[d743d60]373            }
374        self.formatter = {
375                'vis':  self.print_vis_or_vtopo('vis', self.out),
376                'vtopo':  self.print_vis_or_vtopo('vtopo', self.out),
377                'federant':  self.print_xml,
378                'experimentdescription':  self.print_xml,
379                'id': self.print_id,
380                'status': self.print_string,
381                'log': self.print_string,
[2fd8f8c]382                'embedding':  self.print_xml,
[d743d60]383            }
384
385    def print_string(self, d):
386        """
387        Print the string to the class output.
388        """
389        print >>self.out, d
390
391    def print_id(self, d):
392        """
393        d is an array of ID dicts.  Print each one to the class output.
394        """
395        for id in d or []:
396            for k, i in id.items():
397                print >>self.out, "%s: %s" % (k, i)
398
399    def print_xml(self, d):
400        """
401        Very simple ugly xml formatter of the kinds of dicts that come back
402        from services.
403        """
404        if isinstance(d, dict):
405            for k, v in d.items():
406                print >>self.out, "<%s>" % k
407                self.print_xml(v)
408                print >>self.out, "</%s>" % k
409        elif isinstance(d, list):
410            for x in d:
411                self.print_xml(x)
412        else:
413            print >>self.out, d
414
415
416    class print_vis_or_vtopo:
417        """
418        Print the retrieved data is a simple xml representation of the dict.
419        """
420        def __init__(self, top, out=sys.stdout):
421            self.xml = top
422            self.out = out
423
424        def __call__(self, d, out=sys.stdout):
425            str = "<%s>\n" % self.xml
426            for t in ('node', 'lan'):
427                if d.has_key(t): 
428                    for x in d[t]:
429                        str += "<%s>" % t
430                        for k in x.keys():
431                            str += "<%s>%s</%s>" % (k, x[k],k)
432                        str += "</%s>\n" % t
433            str+= "</%s>" % self.xml
434            print >>self.out, str
435
436    def __call__(self, d, r):
437        """
438        Print the data of type r (one of the keys of key) to the class output.
439        """
440        k = self.key.get(r, None)
441        if k:
442            if k in d: self.formatter[r](d[k])
443            else: raise RuntimeError("Bad response: no %s" %k)
444        else:
445            raise RuntimeError("Don't understand datatype %s" %r)
446
Note: See TracBrowser for help on using the repository browser.