#!/usr/local/bin/python import sys import os import pwd from fedd_services import * from M2Crypto import SSL, X509 from M2Crypto.m2xmlrpclib import SSL_Transport import M2Crypto.httpslib from xmlrpclib import ServerProxy, Error, dumps, loads from ZSI import SoapWriter from ZSI.TC import QName, String, URI, AnyElement, UNBOUNDED, Any from ZSI.wstools.Namespaces import SOAP from ZSI.fault import FaultType, Detail import xmlrpclib from fedd_util import fedid, fedd_ssl_context, pack_soap, unpack_soap, \ pack_id, unpack_id, encapsulate_binaries, decapsulate_binaries from optparse import OptionParser, OptionValueError import parse_detail # Turn off the matching of hostname to certificate ID SSL.Connection.clientPostConnectionCheck = None class IDFormatException(RuntimeError): pass class access_method: """Encapsulates an access method generically.""" (type_ssh, type_x509, type_pgp) = ('sshPubkey', 'X509', 'pgpPubkey') default_type = type_ssh def __init__(self, buf=None, type=None, file=None): self.buf = buf if type != None: self.type = type else: self.type = access_method.default_type if file != None: self.readfile(file) def readfile(self, file, type=None): f = open(file, "r") self.buf = f.read(); f.close() if type == None: if self.type == None: self.type = access_method.default_type else: self.type = type; class node_desc: def __init__(self, image, hardware, count=1): if getattr(image, "__iter__", None) == None: if image == None: self.image = [ ] else: self.image = [ image ] else: self.image = image if getattr(hardware, "__iter__", None) == None: if hardware == None: self.hardware = [ ] else: self.hardware = [ hardware ] else: self.hardware = hardware if count != None: self.count = int(count) else: self.count = 1 class fedd_client_opts(OptionParser): """Encapsulate option processing in this class, rather than in main""" def __init__(self): OptionParser.__init__(self, usage="%prog [opts] (--help for details)", version="0.1") self.add_option("-c","--cert", action="store", dest="cert", type="string", help="my certificate file") self.add_option("-d", "--debug", action="count", dest="debug", default=0, help="Set debug. Repeat for more information") self.add_option("-s", "--serializeOnly", action="store_true", dest="serialize_only", default=False, help="Print the SOAP request that would be sent and exit") self.add_option("-T","--trusted", action="store", dest="trusted", type="string", help="Trusted certificates (required)") self.add_option("-u", "--url", action="store", dest="url", type="string",default="https://localhost:23235", help="URL to connect to (default %default)") self.add_option("-x","--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") class fedd_create_opts(fedd_client_opts): def __init__(self, access_keys, add_key_callback=None, add_cert_callback=None): fedd_client_opts.__init__(self) self.add_option("-e", "--experiment_cert", dest="out_certfile", type="string", help="output certificate file") self.add_option("-F","--useFedid", action="store_true", dest="use_fedid", default=False, help="Use a fedid derived from my certificate as user identity") self.add_option("-f", "--file", dest="file", help="experiment description file") if add_key_callback: self.add_option("-k", "--sshKey", action="callback", type="string", callback=add_key_callback, callback_args=(access_keys,), help="ssh key for access (can be supplied more than once") if add_cert_callback: self.add_option("-K", "--x509Key", action="callback", type="string", callback=add_cert_callback, callback_args=(access_keys,), help="X509 certificate for access " + \ "(can be supplied more than once") self.add_option("-m", "--master", dest="master", help="Master testbed in the federation") self.add_option("-U", "--username", action="store", dest="user", type="string", help="Use this username instead of the uid") def exit_with_fault(dict, out=sys.stderr): """ Print an error message and exit. The dictionary contains the FeddFaultBody elements.""" codestr = "" if dict.has_key('errstr'): codestr = "Error: %s" % dict['errstr'] if dict.has_key('code'): if len(codestr) > 0 : codestr += " (%d)" % dict['code'] else: codestr = "Error Code: %d" % dict['code'] print>>out, codestr print>>out, "Description: %s" % dict['desc'] sys.exit(dict.get('code', 20)) class fedd_exp_data_opts(fedd_client_opts): def __init__(self): fedd_client_opts.__init__(self) self.add_option("-e", "--experiment_cert", dest="exp_certfile", type="string", help="output certificate file") # Base class that will do a the SOAP/XMLRPC exchange for a request. class fedd_rpc: class RPCException: def __init__(self, fb): self.desc = fb.get('desc', None) self.code = fb.get('code', None) self.errstr = fb.get('errstr', None) def __init__(self, pre): """ Specialize the class for the prc method """ self.RequestMessage = globals()["%sRequestMessage" % pre] self.ResponseMessage = globals()["%sResponseMessage" % pre] self.RequestBody="%sRequestBody" % pre self.ResponseBody="%sResponseBody" % pre self.method = pre self.RPCException = fedd_rpc.RPCException def do_rpc(self, req_dict, url, transport, cert, trusted, tracefile=None, serialize_only=False): """ The work of sending and parsing the RPC as either XMLRPC or SOAP """ context = None while context == None: try: context = fedd_ssl_context(cert, trusted) except SSL.SSLError, 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": loc = feddServiceLocator(); port = loc.getfeddPortType(url, transport=M2Crypto.httpslib.HTTPSConnection, transdict={ 'ssl_context' : context }, tracefile=tracefile) req = self.RequestMessage() set_req = getattr(req, "set_element_%s" % self.RequestBody, None) set_req(pack_soap(req, self.RequestBody, req_dict)) if serialize_only: sw = SoapWriter() sw.serialize(req) print str(sw) sys.exit(0) try: method_call = getattr(port, self.method, None) resp = method_call(req) except ZSI.ParseException, e: raise RuntimeError("Malformed response (XMLPRC?): %s" % e) except ZSI.FaultException, e: resp = e.fault.detail[0] if resp: resp_call = getattr(resp, "get_element_%s" %self.ResponseBody, None) if resp_call: resp_body = resp_call() if ( resp_body != None): try: return unpack_soap(resp_body) except RuntimeError, e: raise RuntimeError("Bad response. %s" % e.message) elif 'get_element_FeddFaultBody' in dir(resp): resp_body = resp.get_element_FeddFaultBody() if resp_body != None: try: fb = unpack_soap(resp_body) except RuntimeError, e: raise RuntimeError("Bad response. %s" % e.message) raise self.RPCException(fb) else: raise RuntimeError("No body in response!?") else: raise RuntimeError("No response?!?") elif transport == "xmlrpc": if serialize_only: ser = dumps((req_dict,)) print ser sys.exit(0) xtransport = SSL_Transport(context) port = ServerProxy(url, transport=xtransport) try: method_call = getattr(port, self.method, None) resp = method_call( encapsulate_binaries({ self.RequestBody: msg},\ ('fedid',))) except Error, e: resp = { 'FeddFaultBody': \ { 'errstr' : e.faultCode, 'desc' : e.faultString } } if resp: if resp.has_key(self.ResponseBody): return resp[self.ResponseBody] elif resp.has_key('FeddFaultBody'): raise self.RPCException(resp['FeddFaultBody']) else: raise RuntimeError("No body in response!?") else: raise RuntimeError("No response?!?") else: raise RuntimeError("Unknown RPC transport: %s" % transport) # Querying experiment data follows the same control flow regardless of the # specific data retrieved. This class encapsulates that control flow. class exp_data(fedd_rpc): def __init__(self, op): """ Specialize the class for the type of data requested (op) """ fedd_rpc.__init__(self, op) if op =='Vtopo': self.key="vtopo" self.xml='experiment' elif op == 'Vis': self.key="vis" self.xml='vis' else: raise TypeError("Bad op: %s" % op) def print_xml(self, d, out=sys.stdout): """ Print the retrieved data is a simple xml representation of the dict. """ 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 >>out, str def __call__(self): """ The control flow. Compose the request and print the response. """ # Process the options using the customized option parser defined above parser = fedd_exp_data_opts() (opts, args) = parser.parse_args() if opts.trusted != None: if ( not os.access(opts.trusted, os.R_OK) ) : sys.exit("Cannot read trusted certificates (%s)" % opts.trusted) else: parser.error("--trusted is required") if opts.debug > 0: opts.tracefile=sys.stderr if opts.cert != None: cert = opts.cert if cert == None: sys.exit("No certificate given (--cert) or found") if os.access(cert, os.R_OK): fid = fedid(file=cert) else: sys.exit("Cannot read certificate (%s)" % cert) if opts.exp_certfile: exp_fedid = fedid(file=opts.exp_certfile) else: sys.exit("Experiment certfile required") try: resp_dict = self.do_rpc({ 'experiment': { 'fedid': exp_fedid } }, opts.url, opts.transport, cert, opts.trusted, serialize_only=opts.serialize_only, tracefile=opts.tracefile) except self.RPCException, e: exit_with_fault(\ {'desc': e.desc, 'errstr': e.errstr, 'code': e.code}) except RuntimeError, e: sys.exit("Error processing RPC: %s" % e.message) try: if resp_dict.has_key(self.key): self.print_xml(resp_dict[self.key]) except RuntimeError, e: sys.exit("Bad response. %s" % e.message) class create(fedd_rpc): def __init__(self): fedd_rpc.__init__(self, "Create") def add_ssh_key(self, option, opt_str, value, parser, access_keys): try: access_keys.append(access_method(file=value, type=access_method.type_ssh)) except IOError, (errno, strerror): raise OptionValueError("Cannot generate sshPubkey from %s: "\ "%s (%d)" % (value,strerror,errno)) def add_x509_cert(self, option, opt_str, value, parser, access_keys): try: access_keys.append(access_method(file=value, type=access_method.type_x509)) except IOError, (errno, strerror): raise OptionValueError("Cannot read x509 cert from %s: %s (%d)" % (value,strerror,errno)) def get_user_info(self, access_keys): pw = pwd.getpwuid(os.getuid()); try_cert=None user = None if pw != None: user = pw[0] try_cert = "%s/.ssl/emulab.pem" % pw[5]; if not os.access(try_cert, os.R_OK): try_cert = None if len(access_keys) == 0: for k in ["%s/.ssh/id_rsa.pub", "%s/.ssh/id_dsa.pub", "%s/.ssh/identity.pub"]: try_key = k % pw[5]; if os.access(try_key, os.R_OK): access_keys.append(access_method(file=try_key, type=access_method.type_ssh)) break return (user, try_cert) def __call__(self): access_keys = [] # Process the options using the customized option parser defined above parser = fedd_create_opts(access_keys, self.add_ssh_key, self.add_x509_cert) (opts, args) = parser.parse_args() if opts.trusted != None: if ( not os.access(opts.trusted, os.R_OK) ) : sys.exit("Cannot read trusted certificates (%s)" % opts.trusted) else: parser.error("--trusted is required") if opts.debug > 0: opts.tracefile=sys.stderr (user, cert) = self.get_user_info(access_keys) if opts.user: user = opts.user if opts.cert != None: cert = opts.cert if cert == None: sys.exit("No certificate given (--cert) or found") if os.access(cert, os.R_OK): fid = fedid(file=cert) if opts.use_fedid == True: user = fid else: sys.exit("Cannot read certificate (%s)" % cert) if opts.file: exp_desc = "" try: f = open(opts.file, 'r') for line in f: exp_desc += line f.close() except IOError: sys.exit("Cannot read description file (%s)" %opts.file) else: sys.exit("Must specify an experiment description (--file)") if not opts.master: sys.exit("Must specify a master testbed (--master)") out_certfile = opts.out_certfile msg = { 'experimentdescription': exp_desc, 'master': opts.master, 'user' : [ {\ 'userID': pack_id(user), \ 'access': [ { a.type: a.buf } for a in access_keys]\ } ] } if opts.debug > 1: print >>sys.stderr, msg try: resp_dict = self.do_rpc(msg, opts.url, opts.transport, cert, opts.trusted, serialize_only=opts.serialize_only, tracefile=opts.tracefile) except self.RPCException, e: exit_with_fault(\ {'desc': e.desc, 'errstr': e.errstr, 'code': e.code}) except RuntimeError, e: sys.exit("Error processing RPC: %s" % e.message) if opts.debug > 1: print >>sys.stderr, resp_dict ea = resp_dict.get('experimentAccess', None) if out_certfile and ea and ea.has_key('X509'): try: f = open(out_certfile, "w") print >>f, ea['X509'] f.close() except IOError: sys.exit('Could not write to %s' % out_certfile) valid_cmds = ['create'] f = None if sys.argv[1] == 'create': del sys.argv[1] f = create() elif sys.argv[1] == 'vtopo': del sys.argv[1] f = exp_data('Vtopo') elif sys.argv[1] == 'vis': del sys.argv[1] f = exp_data('Vis') else: sys.exit("Bad command: %s. Valid ones are: %s" % \ (sys.argv[1], ", ".join(valid_cmds))) if f: f() else: sys.exit("Null function?!?")