Index: fedd/fedd/allocate_project.py
===================================================================
--- fedd/fedd/allocate_project.py	(revision ec4fb429a3fab87363f4ff5ebc4b52e33b6c841b)
+++ fedd/fedd/allocate_project.py	(revision ec4fb429a3fab87363f4ff5ebc4b52e33b6c841b)
@@ -0,0 +1,484 @@
+#!/usr/local/bin/python
+
+import os,sys
+import re
+import random
+import string
+import subprocess
+import tempfile
+
+from fedd_services import *
+from fedd_internal_services import *
+from util import *
+from fedid import fedid
+from fixed_resource import read_key_db, read_project_db
+from remote_service import xmlrpc_handler, soap_handler, service_caller
+from service_error import service_error
+import logging
+
+
+# Configure loggers to dump to /dev/null which avoids errors if calling classes
+# don't configure them.
+class nullHandler(logging.Handler):
+    def emit(self, record): pass
+
+fl = logging.getLogger("fedd.allocate.local")
+fl.addHandler(nullHandler())
+fl = logging.getLogger("fedd.allocate.remote")
+fl.addHandler(nullHandler())
+
+
+class allocate_project_local:
+    """
+    Allocate projects on this machine in response to an access request.
+    """
+    dynamic_projects = 4
+    dynamic_keys= 2
+    confirm_keys = 1
+    none = 0
+
+    levels = {
+	    'dynamic_projects': dynamic_projects,
+	    'dynamic_keys': dynamic_keys,
+	    'confirm_keys': confirm_keys,
+	    'none': none,
+    }
+
+    def __init__(self, config, auth=None):
+	"""
+	Initializer.  Parses a configuration if one is given.
+	"""
+
+	self.debug = config.get("access", "debug_project", False)
+	self.wap = config.get('access', 'wap', '/usr/testbed/sbin/wap')
+	self.newproj = config.get('access', 'newproj',
+		'/usr/testbed/sbin/newproj')
+	self.mkproj = config.get('access', 'mkproj', '/usr/testbed/sbin/mkproj')
+	self.rmproj = config.get('access', 'rmproj', '/usr/testbed/sbin/rmproj')
+	self.addpubkey = config.get('access', 'addpubkey', 
+		'/usr/testbed/sbin/taddpubkey')
+	self.grantnodetype = config.get('access', 'grantnodetype', 
+		'/usr/testbed/sbin/grantnodetype')
+	self.confirmkey = config.get('access', 'confirmkey', 
+		'/usr/testbed/sbin/taddpubkey')
+	self.allocation_level = config.get("access", "allocation_level", "none")
+	self.log = logging.getLogger("fedd.allocate.local")
+
+	try:
+	    self.allocation_level = \
+		    self.levels[self.allocation_level.strip().lower()]
+	except KeyError:
+	    self.log.error("Bad allocation_level %s.  Defaulting to none" % \
+		    self.allocation_error)
+	    self.allocation_level = self.none
+
+
+	set_log_level(config, "access", self.log)
+	fixed_key_db = config.get("access", "fixed_keys", None)
+	fixed_project_db = config.get("access", "fixed_projects", None)
+	self.fixed_keys = set()
+	self.fixed_projects = set()
+
+	# initialize the fixed resource sets
+	for db, rset, fcn in (\
+		(fixed_key_db, self.fixed_keys, read_key_db), \
+		(fixed_project_db, self.fixed_projects, read_project_db)):
+	    if db:
+		try:
+		    rset.update(fcn(db))
+		except:
+		    self.log.debug("Can't read resources from %s" % db)
+	
+	# Internal services are SOAP only
+	self.soap_services = {\
+		"AllocateProject": soap_handler(\
+		AllocateProjectRequestMessage.typecode,\
+		self.dynamic_project, AllocateProjectResponseMessage,\
+		"AllocateProjectResponseBody"), 
+		"StaticProject": soap_handler(\
+		StaticProjectRequestMessage.typecode,\
+		self.static_project, StaticProjectResponseMessage,\
+		"StaticProjectResponseBody"),\
+		"ReleaseProject": soap_handler(\
+		ReleaseProjectRequestMessage.typecode,\
+		self.release_project, ReleaseProjectResponseMessage,\
+		"ReleaseProjectResponseBody")\
+		}
+	self.xmlrpc_services = { }
+
+    def random_string(self, s, n=3):
+	"""Append n random ASCII characters to s and return the string"""
+	rv = s
+	for i in range(0,n):
+	    rv += random.choice(string.ascii_letters)
+	return rv
+
+    def write_attr_xml(self, file, root, lines):
+	"""
+	Write an emulab config file for a dynamic project.
+
+	Format is <root><attribute name=lines[0]>lines[1]</attribute></root>
+	"""
+	# Convert a pair to an attribute line
+	out_attr = lambda a,v : \
+		'<attribute name="%s"><value>%s</value></attribute>' % (a, v)
+
+	f = os.fdopen(file, "w")
+	f.write("<%s>\n" % root)
+	f.write("\n".join([out_attr(*l) for l in lines]))
+	f.write("</%s>\n" % root)
+	f.close()
+
+
+    def dynamic_project(self, req, fedid=None):
+	"""
+	Create a dynamic project with ssh access
+
+	Req includes the project and resources as a dictionary
+	"""
+
+	if self.allocation_level < self.dynamic_projects:
+	    raise service_error(service_error.access, 
+		    "[dynamic_project] dynamic project allocation not " + \
+			    "permitted: check allocation level")
+	# tempfiles for the parameter files
+	uf, userfile = tempfile.mkstemp(prefix="usr", suffix=".xml",
+		dir="/tmp")
+	pf, projfile = tempfile.mkstemp(prefix="proj", suffix=".xml",
+		dir="/tmp")
+
+	if req.has_key('AllocateProjectRequestBody') and \
+		req['AllocateProjectRequestBody'].has_key('project'):
+	    proj = req['AllocateProjectRequestBody']['project']
+	else:
+	    raise service_error(service_error.req, 
+		    "Badly formed allocation request")
+	# Take the first user and ssh key
+	name = proj.get('name', None) or self.random_string("proj",4)
+	user = proj.get('user', None)
+	if user != None:
+	    user = user[0]	# User is a list, take the first entry
+	    if not user.has_key("userID"):
+		uname = self.random_string("user", 3)
+	    else:
+		uid = proj['userID']
+		# XXX: fedid
+		uname = uid.get('localname', None) or \
+			uid.get('kerberosUsername', None) or \
+			uid.get('uri', None)
+		if uname == None:
+		    raise service_error(service_error.req, "No ID for user");
+
+	    access = user.get('access', None)
+	    if access != None:
+		ssh = access[0].get('sshPubkey', None)
+		if ssh == None:
+		    raise service_error(service_error.req, 
+			    "No ssh key for user");
+	else:
+	    raise service_error(service_error.req, 
+		    "No access information for project");
+
+	# uname, name and ssh are set
+	user_fields = [
+		("name", "Federation User %s" % uname),
+		("email", "%s-fed@isi.deterlab.net" % uname),
+		("password", self.random_string("", 8)),
+		("login", uname),
+		("address", "4676 Admiralty"),
+		("city", "Marina del Rey"),
+		("state", "CA"),
+		("zip", "90292"),
+		("country", "USA"),
+		("phone", "310-448-9190"),
+		("title", "None"),
+		("affiliation", "USC/ISI"),
+		("pubkey", ssh)
+	]
+
+	proj_fields = [
+		("name", name),
+		("short description", "dynamic federated project"),
+		("URL", "http://www.isi.edu/~faber"),
+		("funders", "USC/USU"),
+		("long description", "Federation access control"),
+		("public", "1"),
+		("num_pcs", "100"),
+		("linkedtous", "1"),
+		("newuser_xml", userfile)
+	]
+	
+
+	# Write out the files
+	self.write_attr_xml(uf, "user", user_fields)
+	self.write_attr_xml(pf, "project", proj_fields)
+
+	# Generate the commands (only grantnodetype's are dynamic)
+	cmds = [
+		(self.wap, self.newproj, projfile),
+		(self.wap, self.mkproj, name)
+		]
+
+	# Add commands to grant access to any resources in the request.
+	for nt in [ h for r in req.get('resources', []) \
+		if r.has_key('node') and r['node'].has_key('hardware')\
+		    for h in r['node']['hardware'] ] :
+	    cmds.append((self.wap, self.grantnodetype, '-p', name, nt))
+
+	# Create the projects
+	rc = 0
+	for cmd in cmds:
+	    self.log.debug("[dynamic_project]: %s" % ' '.join(cmd))
+	    if not self.debug:
+		try:
+		    rc = subprocess.call(cmd)
+		except OSerror, e:
+		    raise service_error(service_error.internal,
+			    "Dynamic project subprocess creation error "+ \
+				    "[%s] (%s)" %  (cmd[1], e.strerror))
+
+	    if rc != 0: 
+		raise service_error(service_error.internal,
+			"Dynamic project subprocess error " +\
+				"[%s] (%d)" % (cmd[1], rc))
+	# Clean up tempfiles
+	os.unlink(userfile)
+	os.unlink(projfile)
+	rv = {\
+	    'project': {\
+		'name': { 'localname': name }, 
+		'user' : [ {\
+		    'userID': { 'localname' : uname },
+		    'access': [ { 'sshPubkey' : ssh } ],
+		} ]\
+	    }\
+	}
+	return rv
+
+    def static_project(self, req, fedid=None):
+	"""
+	Be certain that the local project in the request has access to the
+	proper resources and users have correct keys.  Add them if necessary.
+	"""
+
+	cmds =  []
+
+	# While we should be more careful about this, for the short term, add
+	# the keys to the specified users.
+
+	try:
+	    users = req['StaticProjectRequestBody']['project']['user']
+	    pname = req['StaticProjectRequestBody']['project']\
+		    ['name']['localname']
+	    resources = req['StaticProjectRequestBody'].get('resources', [])
+	except KeyError:
+	    raise service_error(service_error.req, "Badly formed request")
+
+
+	for u in users:
+	    try:
+		name = u['userID']['localname']
+	    except KeyError:
+		raise service_error(service_error.req, "Badly formed user")
+	    for sk in [ k['sshPubkey'] for k in u.get('access', []) \
+		    if k.has_key('sshPubkey')]:
+		if self.allocation_level >= self.dynamic_keys:
+		    cmds.append((self.wap, self.addpubkey, '-w', \
+			    '-u', name, '-k', sk))
+		elif self.allocation_level >= self.confirm_keys:
+		    cmds.append((self.wap, self.confirmkey, '-C', \
+			    '-u', name, '-k', sk))
+		else:
+		    self.log.warning("[static_project] no checking of " + \
+			    "static keys")
+	
+
+	# Add commands to grant access to any resources in the request.  The
+	# list comprehension pulls out the hardware types in the node entries
+	# in the resources list.
+	for nt in [ h for r in resources \
+		if r.has_key('node') and r['node'].has_key('hardware')\
+		    for h in r['node']['hardware'] ] :
+	    if self.allocation_level >= self.confirm_keys:
+		cmds.append((self.wap, self.grantnodetype, '-p', pname, nt))
+
+	# Run the commands 
+	rc = 0
+	for cmd in cmds:
+	    self.log.debug("[static_project]: %s" % ' '.join(cmd))
+	    if not self.debug:
+		try:
+		    rc = subprocess.call(cmd)
+		except OSError, e:
+		    raise service_error(service_error.internal,
+			    "Static project subprocess creation error "+ \
+				    "[%s] (%s)" %  (cmd[0], e.strerror))
+
+	    if rc != 0: 
+		raise service_error(service_error.internal,
+			"Static project subprocess error " +\
+				"[%s] (%d)" % (cmd[0], rc))
+
+	return { 'project': req['StaticProjectRequestBody']['project']}
+
+    def release_project(self, req, fedid=None):
+	"""
+	Remove user keys from users and delete dynamic projects.
+
+	Only keys not in the set of fixed keys are deleted. and there are
+	similar protections for projects.
+	"""
+
+	cmds = []
+	pname = None
+	users = []
+
+	try:
+	    if req['ReleaseProjectRequestBody']['project'].has_key('name'):
+		pname = req['ReleaseProjectRequestBody']['project']\
+			['name']['localname']
+	    if req['ReleaseProjectRequestBody']['project'].has_key('user'):
+		users = req['ReleaseProjectRequestBody']['project']['user']
+	except KeyError:
+	    raise service_error(service_error.req, "Badly formed request")
+
+	for u in users:
+	    try:
+		name = u['userID']['localname']
+	    except KeyError:
+		raise service_error(service_error.req, "Badly formed user")
+	    for sk in [ k['sshPubkey'] for k in u.get('access', []) \
+		    if k.has_key('sshPubkey')]:
+		if (name.rstrip(), sk.rstrip()) not in self.fixed_keys:
+		    if self.allocation_level >= self.dynamic_keys:
+			cmds.append((self.wap, self.addpubkey, '-R', '-w', \
+				'-u', name, '-k', sk))
+	if pname and pname not in self.fixed_projects and \
+		self.allocation_level >= self.dynamic_projects:
+	    cmds.append((self.wap, self.rmproj, pname))
+
+	# Run the commands 
+	rc = 0
+	for cmd in cmds:
+	    self.log.debug("[release_project]: %s" % ' '.join(cmd))
+	    if not self.debug:
+		try:
+		    rc = subprocess.call(cmd)
+		except OSError, e:
+		    raise service_error(service_error.internal,
+			    "Release project subprocess creation error "+ \
+				    "[%s] (%s)" %  (cmd[0], e.strerror))
+
+	    if rc != 0: 
+		raise service_error(service_error.internal,
+			"Release project subprocess error " +\
+				"[%s] (%d)" % (cmd[0], rc))
+
+	return { 'project': req['ReleaseProjectRequestBody']['project']}
+
+class allocate_project_remote:
+    """
+    Allocate projects on a remote machine using the internal SOAP interface
+    """
+    class proxy(service_caller):
+	"""
+	This class is a proxy functor (callable) that has the same signature as
+	a function called by soap_handler or xmlrpc_handler, but that used the
+	service_caller class to call the function remotely.
+	"""
+
+	def __init__(self, url, cert_file, cert_pwd, trusted_certs, 
+		method, req_name, req_alloc, resp_name):
+	    service_caller.__init__(self, method, 'getfeddInternalPortType',
+		    feddInternalServiceLocator, req_alloc, req_name)
+	    self.url = url
+	    self.cert_file = cert_file
+	    self.cert_pwd = cert_pwd
+	    self.trusted_certs = trusted_certs
+	    self.resp_name = resp_name
+	    # Calling the proxy object directly invokes the proxy_call method,
+	    # not the service_call method.
+	    self.__call__ = self.proxy_call
+	    
+
+	# Define the proxy, NB, the parameters to make_proxy are visible to the
+	# definition of proxy.
+	def proxy_call(self, req, fedid=None):
+	    """
+	    Send req on to a remote project instantiator.
+
+	    Req is just the message to be sent.  This function re-wraps it.
+	    It also rethrows any faults.
+	    """
+
+	    if req.has_key(self.request_body_name):
+		req = req[self.request_body_name]
+	    else:
+		raise service_error(service_error.req, "Bad formated request");
+
+	    r = self.call_service(self.url, req, self.cert_file, self.cert_pwd,
+		    self.trusted_certs)
+	    if r.has_key(self.resp_name):
+		return r[self.resp_name]
+	    else:
+		raise service_error(service_error.protocol, 
+			"Bad proxy response")
+
+    # back to defining the allocate_project_remote class
+    def __init__(self, config, auth=None):
+	"""
+	Initializer.  Parses a configuration if one is given.
+	"""
+
+	self.debug = config.get("access", "debug_project", False)
+	self.url = config.get("access", "project_allocation_uri", "")
+
+	self.cert_file = config.get("access", "cert_file", None)
+	self.cert_pwd = config.get("access", "cert_pwd", None)
+	self.trusted_certs = config.get("access", "trusted_certs", None)
+
+	# Certs are promoted from the generic to the specific, so without a if
+	# no dynamic project certificates, then proxy certs are used, and if
+	# none of those the main certs.
+
+	if config.has_option("globals", "proxy_cert_file"):
+	    if not self.cert_file:
+		self.cert_file = config.get("globals", "proxy_cert_file")
+		if config.has_option("globals", "proxy_cert_pwd"):
+		    self.cert_pwd = config.get("globals", "proxy_cert_pwd")
+
+	if config.has_option("globals", "proxy_trusted_certs") and \
+		not self.trusted_certs:
+		self.trusted_certs = \
+			config.get("globals", "proxy_trusted_certs")
+
+	if config.has_option("globals", "cert_file"):
+	    has_pwd = config.has_option("globals", "cert_pwd")
+	    if not self.cert_file:
+		self.cert_file = config.get("globals", "cert_file")
+		if has_pwd: 
+		    self.cert_pwd = config.get("globals", "cert_pwd")
+
+	if config.get("globals", "trusted_certs") and not self.trusted_certs:
+		self.trusted_certs = \
+			config.get("globals", "trusted_certs")
+
+	self.soap_services = { }
+	self.xmlrpc_services = { }
+	self.log = logging.getLogger("fedd.allocate.remote")
+	set_log_level(config, "access", self.log)
+
+	# The specializations of the proxy functions
+	self.dynamic_project = self.proxy(self.url, self.cert_file, 
+		self.cert_pwd, self.trusted_certs, "AllocateProject",
+		"AllocateProjectRequestBody", AllocateProjectRequestMessage,
+		"AllocateProjectResponseBody")
+	self.static_project = self.proxy(self.url, self.cert_file, 
+		self.cert_pwd, self.trusted_certs, "StaticProject",
+		"StaticProjectRequestBody", StaticProjectRequestMessage,
+		"StaticProjectResponseBody")
+	self.release_project = self.proxy(self.url, self.cert_file,
+		self.cert_pwd, self.trusted_certs, "ReleaseProject",
+		"ReleaseProjectRequestBody", ReleaseProjectRequestMessage,
+		"ReleaseProjectResponseBody")
+
