Index: fedd/fedd_access.py
===================================================================
--- fedd/fedd_access.py	(revision 4ed10aefdf7652c7ea1c9a8488706ceb7c9436b8)
+++ fedd/fedd_access.py	(revision d81971a06d08832a3270b8db95e6dd8091450596)
@@ -14,4 +14,5 @@
 import string
 import copy
+import pickle
 
 from fedd_access_project import access_project
@@ -224,4 +225,12 @@
 	self.restricted = [ ]
 	self.fedid_category = { }
+	self.projects = { }
+	self.keys = { }
+	self.allocation = { }
+	self.state = { 
+	    'projects': self.projects,
+	    'allocation' : self.allocation,
+	    'keys' : self.keys
+	}
 	self.fedid_default = "testbed"
 	if config.has_option("access", "accessdb"):
@@ -230,5 +239,8 @@
 	    self.read_trust(config.get("access", "trustdb"))
 
+	self.state_filename = config.get("access", "access_state", "")
 	self.log = logging.getLogger("fedd.access")
+	self.read_state()
+
 
 	# Certs are promoted from the generic to the specific, so without a
@@ -279,9 +291,15 @@
 		RequestAccessRequestMessage.typecode,\
 		self.RequestAccess, RequestAccessResponseMessage,\
-		"RequestAccessResponseBody")\
+		"RequestAccessResponseBody"), \
+	    'ReleaseAccess': make_soap_handler(\
+		ReleaseAccessRequestMessage.typecode,\
+		self.ReleaseAccess, ReleaseAccessResponseMessage,\
+		"ReleaseAccessResponseBody")\
 	    }
 	self.xmlrpc_services =  {\
 	    'RequestAccess': make_xmlrpc_handler(\
-		self.RequestAccess, "RequestAccessResponseBody")\
+		self.RequestAccess, "RequestAccessResponseBody"),\
+	    'ReleaseAccess': make_xmlrpc_handler(\
+		self.ReleaseAccess, "ReleaseAccessResponseBody")\
 	    }
 
@@ -326,4 +344,44 @@
 	else:
 	    return None
+
+    def write_state(self):
+	try:
+	    f = open(self.state_filename, 'w')
+	    pickle.dump(self.state, f)
+	except IOError, e:
+	    self.log.error("Can't write file %s: %s" % \
+		    (self.state_filename, e))
+	except pickle.PicklingError, e:
+	    self.log.error("Pickling problem: %s" % e)
+	except TypeError, e:
+	    self.log.error("Pickling problem (TypeError): %s" % e)
+
+
+    def read_state(self):
+	"""
+	Read a new copy of access state.  Old state is overwritten.
+
+	State format is a simple pickling of the state dictionary.
+	"""
+	try:
+	    f = open(self.state_filename, "r")
+	    self.state = pickle.load(f)
+
+	    self.allocation = self.state['allocation']
+	    self.projects = self.state['projects']
+	    self.keys = self.state['keys']
+
+	    self.log.debug("[read_state]: Read state from %s" % \
+		    self.state_filename)
+	except IOError, e:
+	    self.log.warning("[read_state]: No saved state: Can't open %s: %s"\
+		    % (self.state_filename, e))
+	except EOFError, e:
+	    self.log.warning("[read_state]: Empty or damaged state file: %s:"\
+		    % self.state_filename)
+	except pickle.UnpicklingError, e:
+	    self.log.warning(("[read_state]: No saved state: " + \
+		    "Unpickling failed: %s") % e)
+
 
     def proxy_xmlrpc_request(self, dt, req):
@@ -649,5 +707,4 @@
 		    ap = self.allocate_project.dynamic_project(preq)
 		else:
-		    # XXX ssh key additions
 		    preq = {'StaticProjectRequestBody' : \
 			    { 'project': \
@@ -677,5 +734,39 @@
 		raise service_error(service_error.req, 
 			"SSH access parameters required")
-
+	    # keep track of what's been added
+	    if req['allocID'].has_key('fedid'):
+		aid = unicode(req['allocId']['fedid'])
+	    elif req['allocID'].has_key('localname'):
+		aid = req['allocID']['localname']
+	    else:
+		raise service_error(service_error.req, "Bad allocation ID")
+
+	    self.allocation[aid] = { }
+	    if dyn[1]:
+		try:
+		    pname = ap['project']['name']['localname']
+		except KeyError:
+		    raise service_error(service_error.internal,
+			    "Misformed allocation response?")
+		if self.projects.has_key(pname): self.projects[pname] += 1
+		else: self.projects[pname] = 1
+		self.allocation[aid]['project'] = pname
+
+	    self.allocation[aid]['keys'] = [ ]
+
+	    try:
+		for u in ap['project']['user']:
+		    uname = u['userID']['localname']
+		    for k in [ k['sshPubkey'] for k in u['access'] \
+			    if k.has_key('sshPubkey') ]:
+			kv = "%s:%s" % (uname, k)
+			if self.keys.has_key(kv): self.keys[kv] += 1
+			else: self.keys[kv] = 1
+			self.allocation[aid]['keys'].append((uname, k))
+	    except KeyError:
+		raise service_error(service_error.internal,
+			"Misformed allocation response?")
+
+	    self.write_state()
 	    resp = self.build_response(req['allocID'], ap)
 	    return resp
@@ -707,2 +798,69 @@
 			    service_error.proxy,
 			    "Undefined fault from proxy??");
+
+    def ReleaseAccess(self, req, fid):
+	# The dance to get into the request body
+	if req.has_key('ReleaseAccessRequestBody'):
+	    req = req['ReleaseAccessRequestBody']
+	else:
+	    raise service_error(service_error.req, "No request!?")
+
+	print req
+	try:
+	    if req['allocID'].has_key('localname'):
+		aid = req['allocID']['localname']
+	    elif req['allocID'].has_key('fedid'):
+		aid = unicode(req['allocID']['fedid'])
+	    else:
+		raise service_error(service_error.req,
+			"Only localnames and fedids are understood")
+	except KeyError:
+	    raise service_error(service_error.req, "Badly formed request")
+
+	# If we know this allocation, reduce the reference counts and remove
+	# the local allocations.  Otherwise report an error.  If there is an
+	# allocation to delete, del_users will be a dictonary of sets where the
+	# key is the user that owns the keys in the set.  We use a set to avoid
+	# duplicates.  del_project is just the name of any dynamic project to
+	# delete.
+	del_users = { }
+	del_project = None
+	if self.allocation.has_key(aid):
+	    for k in self.allocation[aid]['keys']:
+		kk = "%s:%s" % k
+		self.keys[kk] -= 1
+		if self.keys[kk] == 0:
+		    if not del_users.has_key(k[0]):
+			del_users[k[0]] = set()
+		    del_users[k[0]].add(k[1])
+		    del self.keys[kk]
+
+	    if self.allocation[aid].has_key('project'):
+		pname = self.allocation[aid]['project']
+		self.projects[pname] -= 1
+		if self.projects[pname] == 0:
+		    del_project = pname
+		    del self.projects[pname]
+
+	    del self.allocation[aid]
+	    # If we actually have resources to deallocate, prepare the call.
+	    if del_project or del_users:
+		msg = { 'project': { }}
+		if del_project:
+		    msg['project']['name']['localname'] =  del_project
+		users = [ ]
+		for u in del_users.keys():
+		    users.append({ 'userID': { 'localname': u },\
+			'access' :  \
+				[ {'sshPubkey' : s } for s in del_users[u]]\
+		    })
+		if users: 
+		    msg['project']['user'] = users
+		print msg
+	    self.write_state()
+	    return { 'allocID': req['allocID'] } 
+	else:
+	    raise service_error(service_error.req, "No such allocation")
+
+
+
