Index: fedd/Makefile/fedd_client.py
===================================================================
--- fedd/Makefile/fedd_client.py	(revision 88e7f2f76ed0f9ce0f3042e8d7ae0c319b72036b)
+++ fedd/Makefile/fedd_client.py	(revision 88e7f2f76ed0f9ce0f3042e8d7ae0c319b72036b)
@@ -0,0 +1,623 @@
+#!/usr/local/bin/python
+
+import sys
+import os
+import pwd
+import tempfile
+import subprocess
+import re
+import xml.parsers.expat
+import time
+
+from federation import fedid, service_error
+from federation.util import fedd_ssl_context, pack_id, unpack_id
+from federation.remote_service import service_caller
+from federation import topdl
+
+from optparse import OptionParser, OptionValueError
+
+
+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("--cert", action="store", dest="cert",
+                type="string", help="my certificate file")
+        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="store", dest="trusted",
+                type="string", help="Trusted certificates (required)")
+        self.add_option("--url", action="store", dest="url",
+                type="string",default="https://localhost:23235",  
+                help="URL to connect to (default %default)")
+        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")
+
+class fedd_access_opts(fedd_client_opts):
+    def __init__(self):
+        fedd_client_opts.__init__(self)
+        self.add_option("--experiment_cert", dest="out_certfile",
+                type="string", help="output certificate file")
+        self.add_option("--experiment_name", dest="exp_name",
+                type="string", help="Suggested experiment name")
+        self.add_option("--label", action="store", dest="label",
+                type="string", help="Label for output")
+	# Access has been busted for a while.
+        #if add_node_callback:
+        #    self.add_option("--node", action="callback", type="string", 
+        #            callback=add_node_callback, callback_args=(node_descs,),
+        #            help="Node description: image:hardware[:count]")
+        self.add_option("--testbed", action="store", dest="testbed",
+                type="string",
+                help="Testbed identifier (URI) to contact (required)")
+        self.add_option("--file", dest="file", 
+                help="experiment description file")
+        self.add_option("--project", action="store", dest="project", 
+                type="string",
+                help="Project to export from master")
+        self.add_option("--master", dest="master",
+                help="Master testbed in the federation (pseudo project export)")
+	self.add_option("--service", dest="service", action="append",
+		type="string", default=[],
+		help="Service description name:exporters:importers:attrs")
+
+class fedd_terminate_opts(fedd_client_opts):
+    def __init__(self):
+        fedd_client_opts.__init__(self)
+        self.add_option("--force", dest="force",
+                action="store_true", default=False,
+                help="Force termination if experiment is in strange state")
+        self.add_option("--logfile", dest="logfile", default=None,
+                help="File to write log to")
+        self.add_option("--print_log", dest="print_log", default=False,
+                action="store_true",
+                help="Print deallocation log to standard output")
+        self.add_option("--experiment_cert", dest="exp_certfile",
+                type="string", help="experiment certificate file")
+        self.add_option("--experiment_name", dest="exp_name",
+                type="string", help="human readable experiment name")
+        self.add_option("--data", dest="data", default=[],
+                action="append", type="choice",
+                choices=("id", "experimentdescription", "federant", "vtopo", 
+		    "vis", "log", "status"),
+                help="data to extract")
+
+class fedd_start_opts(fedd_client_opts):
+    def __init__(self):
+        fedd_client_opts.__init__(self)
+        self.add_option("--file", dest="file", 
+                help="experiment description file")
+
+
+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))
+
+# Base class for the various client operations.  It includes some commonly used
+# functions and classes the most important of which are the exception classes
+# and the service calling classes.
+class fedd_rpc:
+
+    class RPCException:
+	"""
+	An error during the RPC exchange.  It unifies errors from both SOAP and
+	XMLPRC calls.
+	"""
+        def __init__(self, fb):
+            self.desc = fb.get('desc', None)
+            self.code = fb.get('code', -1)
+            self.errstr = fb.get('errstr', None)
+
+    class caller(service_caller):
+	"""
+	The caller is used by fedd_rpc.do_rpc to make the rpc call.  The extra
+	stashed information is used to parse responses a little.
+	"""
+	def __init__(self, pre):
+	    self.ResponseBody="%sResponseBody" % pre
+	    self.method = pre
+	    service_caller.__init__(self, self.method)
+
+    def __init__(self): 
+        """
+        Specialize the class for the pre method
+        """
+        self.caller = fedd_rpc.caller
+        self.RPCException = fedd_rpc.RPCException
+
+
+    def add_node_desc(self, option, opt_str, value, parser, node_descs):
+        def none_if_zero(x):
+            if len(x) > 0: return x
+            else: return None
+
+        params = map(none_if_zero, value.split(":"));
+        
+        if len(params) < 4 and len(params) > 1:
+            node_descs.append(node_desc(*params))
+        else:
+            raise OptionValueError("Bad node description: %s" % value)
+
+    def get_user_info(self):
+        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
+        return (user, try_cert)
+
+    def do_rpc(self, req_dict, url, transport, cert, trusted, tracefile=None,
+            serialize_only=False, caller=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 self.RPCException( {\
+                            'code': e.code, 
+                            'desc': e.desc, 
+                            'errstr': e.code_string()\
+                        })
+        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 self.RPCException( {\
+                            'code': e.code, 
+                            'desc': e.desc, 
+                            'errstr': e.code_string()\
+                        })
+
+        else:
+            raise RuntimeError("Unknown RPC transport: %s" % transport)
+
+        if resp.has_key(caller.ResponseBody):
+            return resp[caller.ResponseBody]
+        else:
+            raise RuntimeError("No body in response??")
+
+class terminate_segment(fedd_rpc):
+    def __init__(self): 
+        """
+        Termination request
+        """
+
+        fedd_rpc.__init__(self)
+
+    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_terminate_opts()
+
+        (opts, args) = parser.parse_args()
+
+        (user, cert) = self.get_user_info()
+        if opts.trusted:
+            if ( not os.access(opts.trusted, os.R_OK) ) :
+                sys.exit("Cannot read trusted certificates (%s)" % opts.trusted)
+
+        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_name and opts.exp_certfile:
+            sys.exit("Only one of --experiment_cert and " +\
+                    "--experiment_name permitted")
+
+        if opts.print_log and opts.logfile:
+            sys.exit("Only one of --logfile and --print_log is permitted")
+        elif opts.print_log:
+            out = sys.stdout
+        elif opts.logfile:
+            try:
+                out = open(opts.logfile, "w")
+            except IOError,e:
+                sys.exit("Cannot open logfile: %s" %e)
+        else:
+            out = None
+
+        exp_id = None
+
+        if opts.exp_certfile:
+            exp_id = { 'fedid': fedid(file=opts.exp_certfile) }
+
+        if opts.exp_name:
+            exp_id = { 'localname' : opts.exp_name }
+
+        if not exp_id:
+            sys.exit("Must give one of --experiment_cert and " +\
+                    "--experiment_name");
+
+        req = { 'allocID': exp_id, 'force': opts.force }
+
+        try:
+            resp_dict = self.do_rpc(req,
+                    opts.url, opts.transport, cert, opts.trusted, 
+                    serialize_only=opts.serialize_only,
+                    tracefile=opts.tracefile,
+		    caller=self.caller('TerminateSegment'))
+        except self.RPCException, e:
+            exit_with_fault(\
+                    {'desc': e.desc, 'errstr': e.errstr, 'code': e.code})
+        except RuntimeError, e:
+            print e
+            sys.exit("Error processing RPC: %s" % e)
+
+        if out:
+            log = resp_dict.get('deallocationLog', None)
+            if log:
+                print >>out, log
+                out.close()
+            else:
+                out.close()
+                sys.exit("No log returned")
+
+class access(fedd_rpc):
+    def __init__(self):
+        fedd_rpc.__init__(self)
+
+    def print_response_as_testbed(self, resp, label, out=sys.stdout):
+        """Print the response as input to the splitter script"""
+
+        e = resp.get('emulab', None)
+	if e:
+	    p = e['project']
+	    fields = { 
+		    "Boss": e['boss'],
+		    "OpsNode": e['ops'],
+		    "Domain": e['domain'],
+		    "FileServer": e['fileServer'],
+		    "EventServer": e['eventServer'],
+		    "Project": unpack_id(p['name'])
+		    }
+	    if (label != None): print >> out, "[%s]" % label
+
+	    for l, v in fields.iteritems():
+		print >>out, "%s: %s" % (l, v)
+
+	    for u in p['user']:
+		print >>out, "User: %s" % unpack_id(u['userID'])
+
+	    for a in e['fedAttr']:
+		print >>out, "%s: %s" % (a['attribute'], a['value'])
+
+    def __call__(self):
+        node_descs = []
+        proj = None
+
+        # Process the options using the customized option parser defined above
+        parser = fedd_access_opts()
+
+        (opts, args) = parser.parse_args()
+
+        if opts.testbed == None:
+            parser.error("--testbed is required")
+
+        if opts.trusted:
+            if ( not os.access(opts.trusted, os.R_OK) ) :
+                sys.exit("Cannot read trusted certificates (%s)" % opts.trusted)
+
+        if opts.debug > 0: opts.tracefile=sys.stderr
+
+        (user, cert) = self.get_user_info()
+
+        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)
+
+        msg = {
+                'allocID': pack_id('test alloc'),
+                'destinationTestbed': pack_id(opts.testbed),
+                }
+
+        if len(node_descs) > 0:
+            msg['resources'] = { 
+                    'node': [ 
+                        { 
+                            'image':  n.image ,
+                            'hardware':  n.hardware,
+                            'count': n.count,
+                        } for n in node_descs],
+                    }
+
+        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,
+		    caller=self.caller('RequestAccess'))
+        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
+	if not opts.serialize_only:
+	    self.print_response_as_testbed(resp_dict, opts.label)
+
+class start_segment(fedd_rpc):
+    def __init__(self): 
+        fedd_rpc.__init__(self)
+    def __call__(self):
+        # Process the options using the customized option parser defined above
+        parser = fedd_start_opts()
+
+        (opts, args) = parser.parse_args()
+
+        if opts.trusted:
+            if ( not os.access(opts.trusted, os.R_OK) ) :
+                sys.exit("Cannot read trusted certificates (%s)" % opts.trusted)
+
+        if opts.debug > 0: opts.tracefile=sys.stderr
+
+        (user, cert) = self.get_user_info()
+
+        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.file:
+            try:
+		top = topdl.topology_from_xml(filename=opts.file, 
+			top='experiment')
+            except IOError:
+                sys.exit("Cannot read description file (%s)" %opts.file)
+        else:
+            sys.exit("Must specify an experiment description (--file)")
+
+        msg = {
+                'segmentdescription': { 'topdldescription': top.to_dict() },
+                'allocID': pack_id(fid),
+		'master': False,
+                }
+
+        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,
+		    caller=self.caller('StartSegment'))
+        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)
+
+        if opts.debug > 1: print >>sys.stderr, resp_dict
+	if not opts.serialize_only:
+	    print resp_dict
+
+class ftopo:
+    def __call__(self):
+	sys.exit("Use fedd_ftopo.py")
+
+class exp_data:
+    def __call__(self):
+	sys.exit("Use fedd_info.py")
+
+class vtopo:
+    def __call__(self):
+	sys.exit("Use fedd_info.py --data=vtopo")
+
+class vis:
+    def __call__(self):
+	sys.exit("Use fedd_info.py --data=vis")
+
+class status:
+    def __call__(self):
+	sys.exit("Use fedd_info.py --data=status")
+
+class multi_exp_data:
+    def __call__(self):
+	sys.exit("Use fedd_multiinfo")
+
+class multi_status:
+    def __call__(self):
+	sys.exit("Use fedd_multistatus")
+
+class image:
+    def __call__(self):
+	sys.exit("Use fedd_image.py")
+
+class topdl_image:
+    def __call__(self):
+	sys.exit("Use fedd_image.py")
+
+class ns_image:
+    def __call__(self):
+	sys.exit("Use fedd_image.py")
+
+class terminate:
+    def __call__(self):
+	sys.exit("Use fedd_terminate.py")
+
+class new:
+    def __call__(self):
+	sys.exit("Use fedd_new.py")
+
+class create:
+    def __call__(self):
+	sys.exit("Use fedd_create.py")
+
+class split:
+    def __call__(self):
+	sys.exit("Discontinued function")
+
+class spew_log:
+    def __call__(self):
+	sys.exit("Use fedd_spewlog.py")
+
+class ns_topdl:
+    def __call__(self):
+	sys.exit("Use fedd_ns2topdl.py")
+
+cmds = {\
+        'new': new(),\
+        'create': create(),\
+        'split': split(),\
+        'access': access(),\
+        'ftopo': ftopo(),\
+        'vtopo': vtopo(),\
+        'vis': vis(),\
+        'info': exp_data(),\
+        'multiinfo': multi_exp_data(),\
+        'multistatus': multi_status(),\
+        'image': image(),\
+        'ns_image': ns_image(),\
+        'status': status(),\
+        'terminate': terminate(),\
+        'spewlog': spew_log(),\
+        'topdl_image': topdl_image(),\
+        'start_segment': start_segment(),\
+        'terminate_segment': terminate_segment(),\
+        'ns_topdl': ns_topdl(),\
+    }
+if len(sys.argv) > 1:
+    operation = cmds.get(sys.argv[1], None)
+else:
+    sys.exit("first argument must be one of " + ",".join(cmds.keys()))
+
+if operation:
+    del sys.argv[1]
+    operation()
+else:
+    if sys.argv[1] == '--help':
+        sys.exit(\
+'''Only context sensitive help is available.  For one of the commands:
+
+%s
+
+type
+  %s command --help
+
+to get help, e.g., %s create --help
+''' % (", ".join(cmds.keys()), sys.argv[0], sys.argv[0]))
+    else:
+        sys.exit("Bad command: %s.  Valid ones are: %s" % \
+                (sys.argv[1], ", ".join(cmds.keys())))
+
