source: fedd/federation/skeleton_access.py @ 807b5ca

version-3.01version-3.02
Last change on this file since 807b5ca was 7888aee, checked in by Ted Faber <faber@…>, 14 years ago

Add skeleton plug-in

  • Property mode set to 100644
File size: 13.1 KB
Line 
1#!/usr/local/bin/python
2
3import os,sys
4import re
5import string
6import copy
7import pickle
8import logging
9import random
10
11from util import *
12from fedid import fedid, generate_fedid
13from authorizer import authorizer
14from service_error import service_error
15from remote_service import xmlrpc_handler, soap_handler, service_caller
16
17import topdl
18
19from access import access_base
20
21# Make log messages disappear if noone configures a fedd logger.  This is
22# something of an incantation, but basically it creates a logger object
23# registered to fedd.access if no other module above us has.  It's an extra
24# belt for the suspenders.
25class nullHandler(logging.Handler):
26    def emit(self, record): pass
27
28fl = logging.getLogger("fedd.access")
29fl.addHandler(nullHandler())
30
31
32# The plug-in itself.
33class access(access_base):
34    """
35    This is a demonstration plug-in for fedd.  It responds to all the
36    experiment_control requests and keeps internal state.  The allocations it
37    makes are simple integers associated with each valid request.  It makes use
38    of the general routines in access.access_base.
39
40    Detailed comments in the code and info at
41    """
42
43    @staticmethod 
44    def parse_access_string(s):
45        """
46        Parse a parenthesized string from the access db by removing the parens.
47        If the string isn't in parens, we just return it with whitespace
48        trimmed in either case.
49        """
50        st = s.strip()
51        if st.startswith("(") and st.endswith(")"): return st[1:-1]
52        else: return st
53
54    def __init__(self, config=None, auth=None):
55        """
56        Initializer.  Pulls parameters out of the ConfigParser's access
57        section, and initializes simple internal state.  This version reads a
58        maximum integer to assign from the configuration file, while most other
59        configuration entries  are read by the base class. 
60
61        An access database in the cannonical format is also read as well as a
62        state database that is a hash of internal state.  Routines to
63        manipulate these are in the base class, but specializations appear
64        here.
65
66        The access database maps users to a simple string.  Use the sample at.
67        """
68
69        # Calling the base initializer, which reads canonical configuration
70        # information and initializes canonical members.
71        access_base.__init__(self, config, auth)
72        # Reading the maximum integer parameter from the configuration file
73        self.maxint = config.getint("access", "maxint") or 5
74        # The available integers
75        self.available_ints = set(range(0,self.maxint))
76        # This will contain the local data from a successful access request.
77        # Keys are the valid three-names, and values are the locally
78        # interpreted data.
79        self.access = { }
80        # Read the access values.  We read the accessDB in the derived class so
81        # that the derived class can specialize the reading of access info.  In
82        # this case, we gather the strings in the access db into self.access
83        # using the translator above.
84        if config.has_option("access", "accessdb"):
85            try:
86                self.read_access(config.get("access", "accessdb"), 
87                        self.parse_access_string)
88            except EnvironmentError, e:
89                self.log.error("Cannot read %s: %s" % \
90                        (config.get("access", "accessdb"), e))
91                raise e
92
93        # The base class initializer has read the state dictionary from the
94        # state file, if there is one.  The state variable includes information
95        # about each active allocation, keyed by the allocation identifier.
96        # This loop extracts the owners stored with each allocation and
97        # associates an access attribute with them.  Each owner is allowed to
98        # access each thing they own.  This is a specialization of the state
99        # handling.
100        self.state_lock.acquire()
101        for k in self.state.keys():
102            # Add the owners
103            for o in self.state[k].get('owners', []):
104                self.auth.set_attribute(o, fedid(hexstr=k))
105            # The principal represented by the allocation itself is also
106            # allowed to make accesses.
107            self.auth.set_attribute(fedid(hexstr=k),fedid(hexstr=k))
108            # Remove any allocated integers from the available ones
109            if 'integer' in self.state[k]:
110                self.available_ints.discard(self.state[k]['integer'])
111        self.state_lock.release()
112
113        # This access controller does not specialize the process of looking up
114        # local information.  This aliases the lookup_access method to be
115        # easier to read.
116        self.lookup_access = self.lookup_access_base
117
118        # These dictionaries register the plug-in's local routines for handline
119        # these four messages with the server code above.  There's a version
120        # for SOAP and XMLRPC, depending on which interfaces the plugin
121        # supports.  There's rarely a technical reason not to support one or
122        # the other - the plugin code almost never deals with the transport -
123        # but if a plug-in writer wanted to disable XMLRPC, they could leave
124        # the self.xmlrpc_services dictionary empty.
125        self.soap_services = {\
126            'RequestAccess': soap_handler("RequestAccess", self.RequestAccess),
127            'ReleaseAccess': soap_handler("ReleaseAccess", self.ReleaseAccess),
128            'StartSegment': soap_handler("StartSegment", self.StartSegment),
129            'TerminateSegment': soap_handler("TerminateSegment", 
130                self.TerminateSegment),
131            }
132        self.xmlrpc_services =  {\
133            'RequestAccess': xmlrpc_handler('RequestAccess',
134                self.RequestAccess),
135            'ReleaseAccess': xmlrpc_handler('ReleaseAccess',
136                self.ReleaseAccess),
137            'StartSegment': xmlrpc_handler("StartSegment", self.StartSegment),
138            'TerminateSegment': xmlrpc_handler('TerminateSegment',
139                self.TerminateSegment),
140            }
141
142    def RequestAccess(self, req, fid):
143        """
144        Handle an access request.  Success here maps the requester into the
145        local access control space and establishes state about that user keyed
146        to a fedid.  We also save a copy of the certificate underlying that
147        fedid so this allocation can access configuration information and
148        shared parameters on the experiment controller.
149        """
150
151        # The dance to get into the request body
152        if req.has_key('RequestAccessRequestBody'):
153            req = req['RequestAccessRequestBody']
154        else:
155            raise service_error(service_error.req, "No request!?")
156
157        # Base class lookup routine.  If this fails, it throws a service
158        # exception denying access that triggers a fault response back to the
159        # caller.
160        found, match = self.lookup_access(req, fid)
161        self.log.info(
162                "[RequestAccess] Access granted to %s with local creds %s" % \
163                (match, found))
164        # Make a fedid for this allocation
165        allocID, alloc_cert = generate_fedid(subj="alloc", log=self.log)
166        aid = unicode(allocID)
167
168        # Store the data about this allocation:
169        self.state_lock.acquire()
170        self.state[aid] = { }
171        self.state[aid]['user'] = found
172        self.state[aid]['owners'] = [ fid ]
173        self.write_state()
174        self.state_lock.release()
175        # Authorize the creating fedid and the principal representing the
176        # allocation to manipulate it.
177        self.auth.set_attribute(fid, allocID)
178        self.auth.set_attribute(allocID, allocID)
179
180        # Create a directory to stash the certificate in, ans stash it.
181        try:
182            f = open("%s/%s.pem" % (self.certdir, aid), "w")
183            print >>f, alloc_cert
184            f.close()
185        except EnvironmentError, e:
186            raise service_error(service_error.internal, 
187                    "Can't open %s/%s : %s" % (self.certdir, aid, e))
188        self.log.debug('[RequestAccess] Returning allocation ID: %s' % allocID)
189        return { 'allocID': { 'fedid': allocID } }
190
191    def ReleaseAccess(self, req, fid):
192        """
193        Release the allocation granted earlier.  Access to the allocation is
194        checked and if valid, the state and cached certificate are destroyed.
195        """
196        # The dance to get into the request body
197        if req.has_key('ReleaseAccessRequestBody'):
198            req = req['ReleaseAccessRequestBody']
199        else:
200            raise service_error(service_error.req, "No request!?")
201
202        # Pull a key out of the request.  One can request to delete an
203        # allocation by a local human readable name or by a fedid.  This finds
204        # both choices.
205        try:
206            if 'localname' in req['allocID']:
207                auth_attr = aid = req['allocID']['localname']
208            elif 'fedid' in req['allocID']:
209                aid = unicode(req['allocID']['fedid'])
210                auth_attr = req['allocID']['fedid']
211            else:
212                raise service_error(service_error.req,
213                        "Only localnames and fedids are understood")
214        except KeyError:
215            raise service_error(service_error.req, "Badly formed request")
216
217        self.log.debug("[ReleaseAccess] deallocation requested for %s", aid)
218        #  Confirm access
219        if not self.auth.check_attribute(fid, auth_attr):
220            self.log.debug("[ReleaseAccess] deallocation denied for %s", aid)
221            raise service_error(service_error.access, "Access Denied")
222
223        # If there is an allocation in the state, delete it.  Note the locking.
224        self.state_lock.acquire()
225        if aid in self.state:
226            self.log.debug("[ReleaseAccess] Found allocation for %s" %aid)
227            del self.state[aid]
228            self.write_state()
229            self.state_lock.release()
230            # And remove the access cert
231            cf = "%s/%s.pem" % (self.certdir, aid)
232            self.log.debug("[ReleaseAccess] Removing %s" % cf)
233            os.remove(cf)
234            return { 'allocID': req['allocID'] } 
235        else:
236            self.state_lock.release()
237            raise service_error(service_error.req, "No such allocation")
238
239    def StartSegment(self, req, fid):
240        """
241        Start a segment.  In this simple skeleton, this means to parse the
242        request and assign an unassigned integer to it.  We store the integer
243        in the persistent state.
244        """
245        try:
246            req = req['StartSegmentRequestBody']
247            # Get the request topology.  If not present, a KeyError is thrown.
248            topref = req['segmentdescription']['topdldescription']
249            # The fedid of the allocation we're attaching resources to
250            auth_attr = req['allocID']['fedid']
251        except KeyError:
252            raise service_error(server_error.req, "Badly formed request")
253
254        # String version of the allocation ID for keying
255        aid = "%s" % auth_attr
256        # Authorization check
257        if not self.auth.check_attribute(fid, auth_attr):
258            raise service_error(service_error.access, "Access denied")
259        else:
260            # See if this is a replay of an earlier succeeded StartSegment -
261            # sometimes SSL kills 'em.  If so, replay the response rather than
262            # redoing the allocation.
263            self.state_lock.acquire()
264            retval = self.state[aid].get('started', None)
265            self.state_lock.release()
266            if retval:
267                self.log.warning(
268                        "[StartSegment] Duplicate StartSegment for %s: " \
269                                % aid + \
270                        "replaying response")
271                return retval
272
273        certfile = "%s/%s.pem" % (self.certdir, aid)
274
275        # Convert the topology into topdl data structures.  Again, the
276        # skeletion doesn't do anything with it, but this is how one parses a
277        # topology request.
278        if topref: topo = topdl.Topology(**topref)
279        else:
280            raise service_error(service_error.req, 
281                    "Request missing segmentdescription'")
282
283        # The attributes of the request.  Not used by this plug-in, but that's
284        # where they are.
285        attrs = req.get('fedAttr', [])
286
287        # Gather connection information.  Used to send messages to those
288        # waiting.
289        connInfo = req.get('connection', [])
290
291        # Do the assignment,  A more complex plug-in would interface to the
292        # facility here to create and configure the allocation.
293        if len(self.available_ints) > 0:
294            # NB: lock the data structure during allocation
295            self.state_lock.acquire()
296            assigned = random.choice([ i for i in self.available_ints])
297            self.available_ints.discard(assigned)
298            self.state[aid]['integer'] = assigned
299            self.write_state()
300            self.state_lock.release()
301            self.log.debug("[StartSegment] Allocated %d to %s" \
302                    % (assigned, aid))
303        else:
304            self.log.debug("[StartSegment] No remaining resources for %s" % aid)
305            raise service_error(service_error.federant, "No available integers")
306
307        # Save the information
308        self.state_lock.acquire()
309        # It's possible that the StartSegment call gets retried (!).
310        # if the 'started' key is in the allocation, we'll return it rather
311        # than redo the setup.  The integer allocation was saved when we made
312        # it.
313        self.state[aid]['started'] = { 
314                'allocID': req['allocID'],
315                'allocationLog': "Allocatation complete",
316                'segmentdescription': { 'topdldescription': topo.to_dict() }
317                }
318        retval = copy.deepcopy(self.state[aid]['started'])
319        self.write_state()
320        self.state_lock.release()
321
322        return retval
323
324    def TerminateSegment(self, req, fid):
325        """
326        Remove the resources associated with th eallocation and stop the music.
327        In this example, this simply means removing the integer we allocated.
328        """
329        # Gather the same access information as for Start Segment
330        try:
331            req = req['TerminateSegmentRequestBody']
332        except KeyError:
333            raise service_error(server_error.req, "Badly formed request")
334
335        auth_attr = req['allocID']['fedid']
336        aid = "%s" % auth_attr
337
338        self.log.debug("Terminate request for %s" %aid)
339        # Check authorization
340        if not self.auth.check_attribute(fid, auth_attr):
341            raise service_error(service_error.access, "Access denied")
342
343        # Authorized: remove the integer from the allocation.  A more complex
344        # plug in would interface with the underlying facility to turn off the
345        # experiment here.
346        self.state_lock.acquire()
347        if aid in self.state:
348            assigned = self.state[aid].get('integer', None)
349            self.available_ints.add(assigned)
350            if 'integer' in self.state[aid]:
351                del self.state[aid]['integer']
352            self.write_state()
353        self.state_lock.release()
354   
355        return { 'allocID': req['allocID'] }
Note: See TracBrowser for help on using the repository browser.