#!/usr/local/bin/python import sys import pwd import os import os.path from string import join from datetime import datetime from fedid 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 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)