#!/usr/local/bin/python import os,sys import re import string import copy import pickle import logging import random from util import * from deter import fedid, generate_fedid from authorizer import authorizer, abac_authorizer from service_error import service_error from remote_service import xmlrpc_handler, soap_handler, service_caller from deter import topdl from access import access_base # Make log messages disappear if noone configures a fedd logger. This is # something of an incantation, but basically it creates a logger object # registered to fedd.access if no other module above us has. It's an extra # belt for the suspenders. class nullHandler(logging.Handler): def emit(self, record): pass fl = logging.getLogger("fedd.access") fl.addHandler(nullHandler()) # The plug-in itself. class access(access_base): """ This is a demonstration plug-in for fedd. It responds to all the experiment_control requests and keeps internal state. The allocations it makes are simple integers associated with each valid request. It makes use of the general routines in access.access_base. Detailed comments in the code and info at """ @staticmethod def parse_access_string(s): """ Parse a parenthesized string from the access db by removing the parens. If the string isn't in parens, we just return it with whitespace trimmed in either case. """ st = s.strip() if st.startswith("(") and st.endswith(")"): return st[1:-1] else: return st def __init__(self, config=None, auth=None): """ Initializer. Pulls parameters out of the ConfigParser's access section, and initializes simple internal state. This version reads a maximum integer to assign from the configuration file, while most other configuration entries are read by the base class. An access database in the cannonical format is also read as well as a state database that is a hash of internal state. Routines to manipulate these are in the base class, but specializations appear here. The access database maps users to a simple string. """ # Calling the base initializer, which reads canonical configuration # information and initializes canonical members. access_base.__init__(self, config, auth) # Reading the maximum integer parameter from the configuration file self.maxint = config.getint("access", "maxint") or 5 # The available integers self.available_ints = set(range(0,self.maxint)) # authorization information self.auth_type = config.get('access', 'auth_type') \ or 'abac' self.auth_dir = config.get('access', 'auth_dir') accessdb = config.get("access", "accessdb") # initialize the authorization system. We make a call to # read the access database that maps from authorization information # into local information. The local information is parsed by the # translator above. if self.auth_type == 'abac': # Load the current authorization state self.auth = abac_authorizer(load=self.auth_dir) self.access = [ ] if accessdb: try: self.read_access(accessdb, self.parse_access_string) except EnvironmentError, e: self.log.error("Cannot read %s: %s" % \ (config.get("access", "accessdb"), e)) raise e else: raise service_error(service_error.internal, "Unknown auth_type: %s" % self.auth_type) # Clean the state self.state_lock.acquire() for k in self.state.keys(): # Remove any allocated integers from the available ones if 'integer' in self.state[k]: self.available_ints.discard(self.state[k]['integer']) self.state_lock.release() # These dictionaries register the plug-in's local routines for handline # these four messages with the server code above. There's a version # for SOAP and XMLRPC, depending on which interfaces the plugin # supports. There's rarely a technical reason not to support one or # the other - the plugin code almost never deals with the transport - # but if a plug-in writer wanted to disable XMLRPC, they could leave # the self.xmlrpc_services dictionary empty. self.soap_services = {\ 'RequestAccess': soap_handler("RequestAccess", self.RequestAccess), 'ReleaseAccess': soap_handler("ReleaseAccess", self.ReleaseAccess), 'StartSegment': soap_handler("StartSegment", self.StartSegment), 'TerminateSegment': soap_handler("TerminateSegment", self.TerminateSegment), } self.xmlrpc_services = {\ 'RequestAccess': xmlrpc_handler('RequestAccess', self.RequestAccess), 'ReleaseAccess': xmlrpc_handler('ReleaseAccess', self.ReleaseAccess), 'StartSegment': xmlrpc_handler("StartSegment", self.StartSegment), 'TerminateSegment': xmlrpc_handler('TerminateSegment', self.TerminateSegment), } # RequestAccess and ReleaseAccess come from the base class def StartSegment(self, req, fid): """ Start a segment. In this simple skeleton, this means to parse the request and assign an unassigned integer to it. We store the integer in the persistent state. """ try: req = req['StartSegmentRequestBody'] # Get the request topology. If not present, a KeyError is thrown. topref = req['segmentdescription']['topdldescription'] # The fedid of the allocation we're attaching resources to auth_attr = req['allocID']['fedid'] except KeyError: raise service_error(server_error.req, "Badly formed request") # String version of the allocation ID for keying aid = "%s" % auth_attr # Authorization check access_ok, proof = self.auth.check_attribute(fid, auth_attr, with_proof=True) if not access_ok: raise service_error(service_error.access, "Access denied", proof=proof) else: # See if this is a replay of an earlier succeeded StartSegment - # sometimes SSL kills 'em. If so, replay the response rather than # redoing the allocation. self.state_lock.acquire() retval = self.state[aid].get('started', None) self.state_lock.release() if retval: self.log.warning( "[StartSegment] Duplicate StartSegment for %s: " \ % aid + \ "replaying response") return retval certfile = "%s/%s.pem" % (self.certdir, aid) # Convert the topology into topdl data structures. Again, the # skeletion doesn't do anything with it, but this is how one parses a # topology request. if topref: topo = topdl.Topology(**topref) else: raise service_error(service_error.req, "Request missing segmentdescription'") # The attributes of the request. Not used by this plug-in, but that's # where they are. attrs = req.get('fedAttr', []) # Gather connection information. Used to send messages to those # waiting. connInfo = req.get('connection', []) # Do the assignment, A more complex plug-in would interface to the # facility here to create and configure the allocation. if len(self.available_ints) > 0: # NB: lock the data structure during allocation self.state_lock.acquire() assigned = random.choice([ i for i in self.available_ints]) self.available_ints.discard(assigned) self.state[aid]['integer'] = assigned self.write_state() self.state_lock.release() self.log.debug("[StartSegment] Allocated %d to %s" \ % (assigned, aid)) else: self.log.debug("[StartSegment] No remaining resources for %s" % aid) raise service_error(service_error.federant, "No available integers") # Save the information self.state_lock.acquire() # It's possible that the StartSegment call gets retried (!). # if the 'started' key is in the allocation, we'll return it rather # than redo the setup. The integer allocation was saved when we made # it. self.state[aid]['started'] = { 'allocID': req['allocID'], 'allocationLog': "Allocatation complete", 'segmentdescription': { 'topdldescription': topo.to_dict() }, 'proof': proof.to_dict(), } retval = copy.deepcopy(self.state[aid]['started']) self.write_state() self.state_lock.release() return retval def TerminateSegment(self, req, fid): """ Remove the resources associated with th eallocation and stop the music. In this example, this simply means removing the integer we allocated. """ # Gather the same access information as for Start Segment try: req = req['TerminateSegmentRequestBody'] except KeyError: raise service_error(server_error.req, "Badly formed request") auth_attr = req['allocID']['fedid'] aid = "%s" % auth_attr self.log.debug("Terminate request for %s" %aid) # Check authorization access_ok, proof = self.auth.check_attribute(fid, auth_attr, with_proof=True) if not access_ok: raise service_error(service_error.access, "Access denied", proof=proof) # Authorized: remove the integer from the allocation. A more complex # plug in would interface with the underlying facility to turn off the # experiment here. self.state_lock.acquire() if aid in self.state: assigned = self.state[aid].get('integer', None) self.available_ints.add(assigned) if 'integer' in self.state[aid]: del self.state[aid]['integer'] self.write_state() self.state_lock.release() return { 'allocID': req['allocID'], 'proof': proof.to_dict() }