#!/usr/local/bin/python import sys import pwd import os import re import os.path import xml.parsers.expat from string import join from datetime import datetime from deter import fedid from util import fedd_ssl_context, file_expanding_opts from remote_service import service_caller from service_error import service_error from optparse import OptionParser, OptionValueError class client_opts(file_expanding_opts): """ Standatd set of options that all clients talking to fedd can probably use. Client code usually specializes this. """ def __init__(self): file_expanding_opts.__init__(self, usage="%prog [opts] (--help for details)", version="0.1") self.add_option("--cert", action="callback", dest="cert", callback=self.expand_file, type="string", help="my certificate file") self.add_option("--auth_log", action="callback", dest="auth_log", callback=self.expand_file, default=None, type="string", help="Log authentication decisions to this file") self.add_option("--abac", action="callback", dest="abac_dir", callback=self.expand_file, type="string", default=os.path.expanduser('~/.abac'), help="Directory with abac certs") self.add_option('--no_abac', action='store_const', const=None, dest='abac_dir', help='Do not use abac authorization') self.add_option( "--debug", action="count", dest="debug", default=0, help="Set debug. Repeat for more information") self.add_option("--serialize_only", action="store_true", dest="serialize_only", default=False, help="Print the SOAP request that would be sent and exit") self.add_option("--trusted", action="callback", dest="trusted", callback=self.expand_file, type="string", help="Trusted certificates (required)") self.add_option("--url", action="store", dest="url", type="string",default=None, help="URL to connect to") self.add_option("--transport", action="store", type="choice", choices=("xmlrpc", "soap"), default="soap", help="Transport for request (xmlrpc|soap) (Default: %default)") self.add_option("--trace", action="store_const", dest="tracefile", const=sys.stderr, help="Print SOAP exchange to stderr") def log_authentication(fn, action, outcome, proof): f = open(fn, 'a') print >>f, '%s %s at %s' % (action, outcome, datetime.now()) if isinstance(proof, list): for p in proof: print >>f, p.to_xml() else: print >>f, proof.to_xml() f.close() def exit_with_fault(exc, action, opts, out=sys.stderr): """ Print an error message and exit. exc is the RPCException that caused the failure. """ codestr = "" if exc.errstr: codestr = "Error: %s" % exc.errstr else: codestr = "" if exc.code: if isinstance(exc.code, int): code = exc.code else: code = -1 if len(codestr) > 0 : codestr += " (%d)" % code else: codestr = "Error Code: %d" % code else: code = -1 if exc.code == service_error.access and opts.auth_log: try: log_authentication(opts.auth_log, action, 'failed', exc.proof) except EnvironmentError, e: print >>sys.stderr, "Failed to log to %s: %s" % \ (e.filename, e.strerror) print>>out, codestr print>>out, "Description: %s" % exc.desc sys.exit(code) class RPCException(RuntimeError): """ An error during the RPC exchange. It unifies errors from both SOAP and XMLPRC calls. """ def __init__(self, desc=None, code=None, errstr=None, proof=None): RuntimeError.__init__(self) self.desc = desc if isinstance(code, int): self.code = code else: self.code = -1 self.errstr = errstr self.proof = proof class CertificateMismatchError(RuntimeError): pass def get_user_cert(): for c in ("~/.ssl/fedid.pem", "~/.ssl/emulab.pem"): cert = os.path.expanduser(c) if os.access(cert, os.R_OK): break else: cert = None return cert def get_abac_certs(dir): ''' Return a list of the contents of the files in dir. These should be abac certificates, but that isn't checked. ''' rv = [ ] if dir and os.path.isdir(dir): for fn in ["%s/%s" % (dir, p) for p in os.listdir(dir) \ if os.path.isfile("%s/%s" % (dir,p))]: f = open(fn, 'r') rv.append(f.read()) f.close() return rv ns_service_re = re.compile('^\\s*#\\s*SERVICE:\\s*([\\S]+)') xml_service_re = re.compile('SERVICE:\\s*([\\S]+)\\s*$') def parse_service(svc): """ Pasre a service entry into a hash representing a service entry in a message. The format is: svc_name:exporter(s):importer(s):attr=val,attr=val The svc_name is the service name, exporter is the exporting testbeds (comma-separated) importer is the importing testbeds (if any) and the rest are attr=val pairs that will become attributes of the service. These include parameters and other such stuff. """ terms = svc.split(':') svcd = { } if len(terms) < 2 or len(terms[0]) == 0 or len(terms[1]) == 0: sys.exit("Bad service description '%s': Not enough terms" % svc) svcd['name'] = terms[0] svcd['export'] = terms[1].split(",") if len(terms) > 2 and len(terms[2]) > 0: svcd['import'] = terms[2].split(",") if len(terms) > 3 and len(terms[3]) > 0: svcd['fedAttr'] = [ ] for t in terms[3].split(";"): i = t.find("=") if i != -1 : svcd['fedAttr'].append( {'attribute': t[0:i], 'value': t[i+1:]}) else: sys.exit("Bad service attribute '%s': no equals sign" % t) return svcd def service_dict_to_line(d): """ Convert a dict containing a service description into a colon separated service description suitable for inclusion in a file or command line. """ return 'SERVICE: %s' % ( ':'.join([ d.get('name', ''), ','.join(d.get('export','')), ','.join(d.get('import','')), ','.join([ '='.join((dd.get('attribute', ''), dd.get('value',''))) \ for dd in d.get('fedAttr', []) ]) ])) def extract_services_from_xml(string=None, file=None, filename=None): class parser: def __init__(self): self.svcs = [] def comment_handler(self, data): for l in data.split('\n'): if xml_service_re.match(l): self.svcs.append( parse_service(xml_service_re.match(l).group(1))) p = parser() xp = xml.parsers.expat.ParserCreate() xp.CommentHandler = p.comment_handler num_set = len([ x for x in (string, filename, file)\ if x is not None ]) if num_set != 1: raise RuntimeError("Exactly one one of file, filename and string " + \ "must be set") elif filename: f = open(filename, "r") xp.ParseFile(f) f.close() elif file: xp.ParseFile(file) elif string: xp.Parse(string, True) return p.svcs def wrangle_standard_options(opts): """ Look for the certificate to use for the call (check for the standard emulab file and any passed in. Make sure a present cert file can be read. Make sure that any trusted cert file can be read and if the debug level is set, set the tracefile attribute as well. If any of these tests fail, raise a RuntimeError. Otherwise return the certificate file, the fedid, and the fedd url. """ default_url="https://localhost:23235" if opts.debug > 0: opts.tracefile=sys.stderr if opts.trusted: if ( not os.access(opts.trusted, os.R_OK) ) : raise RuntimeError("Cannot read trusted certificates (%s)" \ % opts.trusted) cert = get_user_cert() if opts.cert: cert = opts.cert if cert is None: raise RuntimeError("No certificate given (--cert) or found") if os.access(cert, os.R_OK): fid = fedid(file=cert) else: raise RuntimeError("Cannot read certificate (%s)" % cert) if opts.url: url = opts.url elif 'FEDD_URL' in os.environ: url = os.environ['FEDD_URL'] else: url = default_url if opts.abac_dir: if not os.access(opts.abac_dir, os.F_OK): try: os.mkdir(opts.abac_dir, 0700) except OSError, e: raise RuntimeError("No ABAC directory (could not create): %s" \ % opts.abac_dir) if not os.path.isdir(opts.abac_dir): raise RuntimeError("ABAC directory not a directory: %s" \ % opts.abac_dir) elif not os.access(opts.abac_dir, os.W_OK): raise RuntimeError("Cannot write to ABAC directory: %s" \ % opts.abac_dir) return (cert, fid, url) def save_certfile(out_certfile, ea, check_cert=None): """ if the experiment authority section in ea has a certificate and the out_certfile parameter has a place to put it, save the cert to the file. EnvronmentError s can come from the file operations. If check_cert is given, the certificate in ea is compared with it and if they are not equal, a CertificateMismatchError is raised. """ if out_certfile and ea and 'X509' in ea: out_cert = ea['X509'] if check_cert and check_cert != out_cert: raise CertificateMismatchError() f = open(out_certfile, "w") f.write(out_cert) f.close() def do_rpc(req_dict, url, transport, cert, trusted, tracefile=None, serialize_only=False, caller=None, responseBody=None): """ The work of sending and parsing the RPC as either XMLRPC or SOAP """ if caller is None: raise RuntimeError("Must provide caller to do_rpc") context = None while context == None: try: context = fedd_ssl_context(cert, trusted) except Exception, e: # Yes, doing this on message type is not ideal. The string # comes from OpenSSL, so check there is this stops working. if str(e) == "bad decrypt": print >>sys.stderr, "Bad Passphrase given." else: raise if transport == "soap": if serialize_only: print caller.serialize_soap(req_dict) return { } else: try: resp = caller.call_soap_service(url, req_dict, context=context, tracefile=tracefile) except service_error, e: raise RPCException(desc=e.desc, code=e.code, errstr=e.code_string(), proof=e.proof) elif transport == "xmlrpc": if serialize_only: ser = dumps((req_dict,)) print ser return { } else: try: resp = caller.call_xmlrpc_service(url, req_dict, context=context, tracefile=tracefile) except service_error, e: raise RPCException(desc=e.desc, code=e.code, errstr=e.code_string()) else: raise RuntimeError("Unknown RPC transport: %s" % transport) if responseBody: if responseBody in resp: return resp[responseBody] else: raise RuntimeError("No body in response??") else: return resp def get_experiment_names(eid): """ eid is the experimentID entry in one of a dict representing a fedd message. Pull out the fedid and localname from that hash and return them as fedid, localid) """ fedid = None local = None for id in eid or []: for k in id.keys(): if k =='fedid': fedid = id[k] elif k =='localname': local = id[k] return (fedid, local) class info_format: def __init__(self, out=sys.stdout): self.out = out self.key = { 'vis': 'vis', 'vtopo': 'vtopo', 'federant': 'federant', 'experimentdescription': 'experimentdescription', 'id': 'experimentID', 'status': 'experimentStatus', 'log': 'allocationLog', 'embedding': 'embedding', } self.formatter = { 'vis': self.print_vis_or_vtopo('vis', self.out), 'vtopo': self.print_vis_or_vtopo('vtopo', self.out), 'federant': self.print_xml, 'experimentdescription': self.print_xml, 'id': self.print_id, 'status': self.print_string, 'log': self.print_string, 'embedding': self.print_xml, } def print_string(self, d): """ Print the string to the class output. """ print >>self.out, d def print_id(self, d): """ d is an array of ID dicts. Print each one to the class output. """ for id in d or []: for k, i in id.items(): print >>self.out, "%s: %s" % (k, i) def print_xml(self, d): """ Very simple ugly xml formatter of the kinds of dicts that come back from services. """ if isinstance(d, dict): for k, v in d.items(): print >>self.out, "<%s>" % k self.print_xml(v) print >>self.out, "" % k elif isinstance(d, list): for x in d: self.print_xml(x) else: print >>self.out, d class print_vis_or_vtopo: """ Print the retrieved data is a simple xml representation of the dict. """ def __init__(self, top, out=sys.stdout): self.xml = top self.out = out def __call__(self, d, out=sys.stdout): str = "<%s>\n" % self.xml for t in ('node', 'lan'): if d.has_key(t): for x in d[t]: str += "<%s>" % t for k in x.keys(): str += "<%s>%s" % (k, x[k],k) str += "\n" % t str+= "" % self.xml print >>self.out, str def __call__(self, d, r): """ Print the data of type r (one of the keys of key) to the class output. """ k = self.key.get(r, None) if k: if k in d: self.formatter[r](d[k]) else: raise RuntimeError("Bad response: no %s" %k) else: raise RuntimeError("Don't understand datatype %s" %r)