source: fedd/federation/emulab_access.py @ 3bddd24

axis_examplecompt_changesinfo-opsversion-3.01version-3.02
Last change on this file since 3bddd24 was 3bddd24, checked in by Ted Faber <faber@…>, 14 years ago

moving toward credentials, and away from emulab specifics

  • Property mode set to 100644
File size: 44.7 KB
Line 
1#!/usr/local/bin/python
2
3import os,sys
4import re
5import string
6import copy
7import pickle
8import logging
9
10from threading import *
11
12from util import *
13from allocate_project import allocate_project_local, allocate_project_remote
14from access_project import access_project
15from fedid import fedid, generate_fedid
16from authorizer import authorizer
17from service_error import service_error
18from remote_service import xmlrpc_handler, soap_handler, service_caller
19
20import httplib
21import tempfile
22from urlparse import urlparse
23
24import topdl
25import list_log
26import proxy_emulab_segment
27import local_emulab_segment
28
29
30# Make log messages disappear if noone configures a fedd logger
31class nullHandler(logging.Handler):
32    def emit(self, record): pass
33
34fl = logging.getLogger("fedd.access")
35fl.addHandler(nullHandler())
36
37class access:
38    """
39    The implementation of access control based on mapping users to projects.
40
41    Users can be mapped to existing projects or have projects created
42    dynamically.  This implements both direct requests and proxies.
43    """
44
45    class parse_error(RuntimeError): pass
46
47
48    proxy_RequestAccess= service_caller('RequestAccess')
49    proxy_ReleaseAccess= service_caller('ReleaseAccess')
50
51    def __init__(self, config=None, auth=None):
52        """
53        Initializer.  Pulls parameters out of the ConfigParser's access section.
54        """
55
56        # Make sure that the configuration is in place
57        if not config: 
58            raise RunTimeError("No config to fedd.access")
59
60        self.project_priority = config.getboolean("access", "project_priority")
61        self.allow_proxy = config.getboolean("access", "allow_proxy")
62
63        self.boss = config.get("access", "boss")
64        self.ops = config.get("access", "ops")
65        self.domain = config.get("access", "domain")
66        self.fileserver = config.get("access", "fileserver")
67        self.eventserver = config.get("access", "eventserver")
68        self.certdir = config.get("access","certdir")
69        self.ssh_privkey_file = config.get("access","ssh_privkey_file")
70        self.ssh_pubkey_file = config.get("access","ssh_pubkey_file")
71        self.create_debug = config.getboolean("access", "create_debug")
72        self.cleanup = not config.getboolean("access", "leave_tmpfiles")
73        self.access_type = config.get("access", "type")
74
75        self.access_type = self.access_type.lower()
76        if self.access_type == 'remote_emulab':
77            self.start_segment = proxy_emulab_segment.start_segment
78            self.stop_segment = proxy_emulab_segment.stop_segment
79        elif self.access_type == 'local_emulab':
80            self.start_segment = local_emulab_segment.start_segment
81            self.stop_segment = local_emulab_segment.stop_segment
82        else:
83            self.start_segment = None
84            self.stop_segment = None
85
86        self.attrs = { }
87        self.access = { }
88        self.restricted = [ ]
89        self.projects = { }
90        self.keys = { }
91        self.types = { }
92        self.allocation = { }
93        self.state = { 
94            'projects': self.projects,
95            'allocation' : self.allocation,
96            'keys' : self.keys,
97            'types': self.types
98        }
99        self.log = logging.getLogger("fedd.access")
100        set_log_level(config, "access", self.log)
101        self.state_lock = Lock()
102
103        if auth: self.auth = auth
104        else:
105            self.log.error(\
106                    "[access]: No authorizer initialized, creating local one.")
107            auth = authorizer()
108
109        tb = config.get('access', 'testbed')
110        if tb: self.testbed = [ t.strip() for t in tb.split(',') ]
111        else: self.testbed = [ ]
112
113        if config.has_option("access", "accessdb"):
114            self.read_access(config.get("access", "accessdb"))
115
116        self.state_filename = config.get("access", "access_state")
117        self.read_state()
118
119        # Keep cert_file and cert_pwd coming from the same place
120        self.cert_file = config.get("access", "cert_file")
121        if self.cert_file:
122            self.sert_pwd = config.get("access", "cert_pw")
123        else:
124            self.cert_file = config.get("globals", "cert_file")
125            self.sert_pwd = config.get("globals", "cert_pw")
126
127        self.trusted_certs = config.get("access", "trusted_certs") or \
128                config.get("globals", "trusted_certs")
129
130        self.soap_services = {\
131            'RequestAccess': soap_handler("RequestAccess", self.RequestAccess),
132            'ReleaseAccess': soap_handler("ReleaseAccess", self.ReleaseAccess),
133            'StartSegment': soap_handler("StartSegment", self.StartSegment),
134            'TerminateSegment': soap_handler("TerminateSegment", self.TerminateSegment),
135            }
136        self.xmlrpc_services =  {\
137            'RequestAccess': xmlrpc_handler('RequestAccess',
138                self.RequestAccess),
139            'ReleaseAccess': xmlrpc_handler('ReleaseAccess',
140                self.ReleaseAccess),
141            'StartSegment': xmlrpc_handler("StartSegment", self.StartSegment),
142            'TerminateSegment': xmlrpc_handler('TerminateSegment',
143                self.TerminateSegment),
144            }
145
146
147        if not config.has_option("allocate", "uri"):
148            self.allocate_project = \
149                allocate_project_local(config, auth)
150        else:
151            self.allocate_project = \
152                allocate_project_remote(config, auth)
153
154        # If the project allocator exports services, put them in this object's
155        # maps so that classes that instantiate this can call the services.
156        self.soap_services.update(self.allocate_project.soap_services)
157        self.xmlrpc_services.update(self.allocate_project.xmlrpc_services)
158
159
160    def read_access(self, config):
161        """
162        Read a configuration file and set internal parameters.
163
164        The format is more complex than one might hope.  The basic format is
165        attribute value pairs separated by colons(:) on a signle line.  The
166        attributes in bool_attrs, emulab_attrs and id_attrs can all be set
167        directly using the name: value syntax.  E.g.
168        boss: hostname
169        sets self.boss to hostname.  In addition, there are access lines of the
170        form (tb, proj, user) -> (aproj, auser) that map the first tuple of
171        names to the second for access purposes.  Names in the key (left side)
172        can include "<NONE> or <ANY>" to act as wildcards or to require the
173        fields to be empty.  Similarly aproj or auser can be <SAME> or
174        <DYNAMIC> indicating that either the matching key is to be used or a
175        dynamic user or project will be created.  These names can also be
176        federated IDs (fedid's) if prefixed with fedid:.  Finally, the aproj
177        can be followed with a colon-separated list of node types to which that
178        project has access (or will have access if dynamic).
179        Testbed attributes outside the forms above can be given using the
180        format attribute: name value: value.  The name is a single word and the
181        value continues to the end of the line.  Empty lines and lines startin
182        with a # are ignored.
183
184        Parsing errors result in a self.parse_error exception being raised.
185        """
186        lineno=0
187        name_expr = "["+string.ascii_letters + string.digits + "\.\-_]+"
188        fedid_expr = "fedid:[" + string.hexdigits + "]+"
189        key_name = "(<ANY>|<NONE>|"+fedid_expr + "|"+ name_expr + ")"
190        access_proj = "(<DYNAMIC>(?::" + name_expr +")*|"+ \
191                "<SAME>" + "(?::" + name_expr + ")*|" + \
192                fedid_expr + "(?::" + name_expr + ")*|" + \
193                name_expr + "(?::" + name_expr + ")*)"
194        access_name = "(<DYNAMIC>|<SAME>|" + fedid_expr + "|"+ name_expr + ")"
195
196        restricted_re = re.compile("restricted:\s*(.*)", re.IGNORECASE)
197        attr_re = re.compile('attribute:\s*([\._\-a-z0-9]+)\s+value:\s*(.*)',
198                re.IGNORECASE)
199        access_re = re.compile('\('+key_name+'\s*,\s*'+key_name+'\s*,\s*'+
200                key_name+'\s*\)\s*->\s*\('+access_proj + '\s*,\s*' + 
201                access_name + '\s*,\s*' + access_name + '\s*\)', re.IGNORECASE)
202
203        def parse_name(n):
204            if n.startswith('fedid:'): return fedid(hexstr=n[len('fedid:'):])
205            else: return n
206       
207        def auth_name(n):
208            if isinstance(n, basestring):
209                if n =='<any>' or n =='<none>': return None
210                else: return unicode(n)
211            else:
212                return n
213
214        f = open(config, "r");
215        for line in f:
216            lineno += 1
217            line = line.strip();
218            if len(line) == 0 or line.startswith('#'):
219                continue
220
221            # Extended (attribute: x value: y) attribute line
222            m = attr_re.match(line)
223            if m != None:
224                attr, val = m.group(1,2)
225                self.attrs[attr] = val
226                continue
227
228            # Restricted entry
229            m = restricted_re.match(line)
230            if m != None:
231                val = m.group(1)
232                self.restricted.append(val)
233                continue
234
235            # Access line (t, p, u) -> (ap, cu, su) line
236            m = access_re.match(line)
237            if m != None:
238                access_key = tuple([ parse_name(x) for x in m.group(1,2,3)])
239                auth_key = tuple([ auth_name(x) for x in access_key])
240                aps = m.group(4).split(":");
241                if aps[0] == 'fedid:':
242                    del aps[0]
243                    aps[0] = fedid(hexstr=aps[0])
244
245                cu = parse_name(m.group(5))
246                su = parse_name(m.group(6))
247
248                access_val = (access_project(aps[0], aps[1:]),
249                        parse_name(m.group(5)), parse_name(m.group(6)))
250
251                self.access[access_key] = access_val
252                self.auth.set_attribute(auth_key, "access")
253                continue
254
255            # Nothing matched to here: unknown line - raise exception
256            f.close()
257            raise self.parse_error("Unknown statement at line %d of %s" % \
258                    (lineno, config))
259        f.close()
260
261    def get_users(self, obj):
262        """
263        Return a list of the IDs of the users in dict
264        """
265        if obj.has_key('user'):
266            return [ unpack_id(u['userID']) \
267                    for u in obj['user'] if u.has_key('userID') ]
268        else:
269            return None
270
271    def write_state(self):
272        if self.state_filename:
273            try:
274                f = open(self.state_filename, 'w')
275                pickle.dump(self.state, f)
276            except IOError, e:
277                self.log.error("Can't write file %s: %s" % \
278                        (self.state_filename, e))
279            except pickle.PicklingError, e:
280                self.log.error("Pickling problem: %s" % e)
281            except TypeError, e:
282                self.log.error("Pickling problem (TypeError): %s" % e)
283
284
285    def read_state(self):
286        """
287        Read a new copy of access state.  Old state is overwritten.
288
289        State format is a simple pickling of the state dictionary.
290        """
291        if self.state_filename:
292            try:
293                f = open(self.state_filename, "r")
294                self.state = pickle.load(f)
295
296                self.allocation = self.state['allocation']
297                self.projects = self.state['projects']
298                self.keys = self.state['keys']
299                self.types = self.state['types']
300
301                self.log.debug("[read_state]: Read state from %s" % \
302                        self.state_filename)
303            except IOError, e:
304                self.log.warning(("[read_state]: No saved state: " +\
305                        "Can't open %s: %s") % (self.state_filename, e))
306            except EOFError, e:
307                self.log.warning(("[read_state]: " +\
308                        "Empty or damaged state file: %s:") % \
309                        self.state_filename)
310            except pickle.UnpicklingError, e:
311                self.log.warning(("[read_state]: No saved state: " + \
312                        "Unpickling failed: %s") % e)
313
314            # Add the ownership attributes to the authorizer.  Note that the
315            # indices of the allocation dict are strings, but the attributes are
316            # fedids, so there is a conversion.
317            for k in self.allocation.keys():
318                for o in self.allocation[k].get('owners', []):
319                    self.auth.set_attribute(o, fedid(hexstr=k))
320
321
322    def permute_wildcards(self, a, p):
323        """Return a copy of a with various fields wildcarded.
324
325        The bits of p control the wildcards.  A set bit is a wildcard
326        replacement with the lowest bit being user then project then testbed.
327        """
328        if p & 1: user = ["<any>"]
329        else: user = a[2]
330        if p & 2: proj = "<any>"
331        else: proj = a[1]
332        if p & 4: tb = "<any>"
333        else: tb = a[0]
334
335        return (tb, proj, user)
336
337    def find_access(self, search):
338        """
339        Search the access DB for a match on this tuple.  Return the matching
340        access tuple and the user that matched.
341       
342        NB, if the initial tuple fails to match we start inserting wildcards in
343        an order determined by self.project_priority.  Try the list of users in
344        order (when wildcarded, there's only one user in the list).
345        """
346        if self.project_priority: perm = (0, 1, 2, 3, 4, 5, 6, 7)
347        else: perm = (0, 2, 1, 3, 4, 6, 5, 7)
348
349        for p in perm: 
350            s = self.permute_wildcards(search, p)
351            # s[2] is None on an anonymous, unwildcarded request
352            if s[2] != None:
353                for u in s[2]:
354                    if self.access.has_key((s[0], s[1], u)):
355                        return (self.access[(s[0], s[1], u)], u)
356            else:
357                if self.access.has_key(s):
358                    return (self.access[s], None)
359        return None, None
360
361    def lookup_access(self, req, fid):
362        """
363        Determine the allowed access for this request.  Return the access and
364        which fields are dynamic.
365
366        The fedid is needed to construct the request
367        """
368        user_re = re.compile("user:\s(.*)")
369        project_re = re.compile("project:\s(.*)")
370
371        # Search keys
372        tb = None
373        project = None
374        user = None
375        # Return values
376        rp = access_project(None, ())
377        ru = None
378
379        user = [ user_re.findall(x)[0] for x in req.get('credential', []) \
380                if user_re.match(x)]
381        project = [ project_re.findall(x)[0] \
382                for x in req.get('credential', []) \
383                    if project_re.match(x)]
384
385        if len(project) == 1: project = project[0]
386        elif len(project) == 0: project = None
387        else: 
388            raise service_error(service_error.req, 
389                    "More than one project credential")
390
391
392        user_fedids = [ u for u in user if isinstance(u, fedid)]
393        # Determine how the caller is representing itself.  If its fedid shows
394        # up as a project or a singleton user, let that stand.  If neither the
395        # usernames nor the project name is a fedid, the caller is a testbed.
396        if project and isinstance(project, fedid):
397            if project == fid:
398                # The caller is the project (which is already in the tuple
399                # passed in to the authorizer)
400                owners = user_fedids
401                owners.append(project)
402            else:
403                raise service_error(service_error.req,
404                        "Project asserting different fedid")
405        else:
406            if fid not in user_fedids:
407                tb = fid
408                owners = user_fedids
409                owners.append(fid)
410            else:
411                if len(fedids) > 1:
412                    raise service_error(service_error.req,
413                            "User asserting different fedid")
414                else:
415                    # Which is a singleton
416                    owners = user_fedids
417        # Confirm authorization
418
419        for u in user:
420            self.log.debug("[lookup_access] Checking access for %s" % \
421                    ((tb, project, u),))
422            if self.auth.check_attribute((tb, project, u), 'access'):
423                self.log.debug("[lookup_access] Access granted")
424                break
425            else:
426                self.log.debug("[lookup_access] Access Denied")
427        else:
428            raise service_error(service_error.access, "Access denied")
429
430        # This maps a valid user to the Emulab projects and users to use
431        found, user_match = self.find_access((tb, project, user))
432       
433        if found == None:
434            raise service_error(service_error.access,
435                    "Access denied - cannot map access")
436
437        # resolve <dynamic> and <same> in found
438        dyn_proj = False
439        dyn_create_user = False
440        dyn_service_user = False
441
442        if found[0].name == "<same>":
443            if project != None:
444                rp.name = project
445            else : 
446                raise service_error(\
447                        service_error.server_config,
448                        "Project matched <same> when no project given")
449        elif found[0].name == "<dynamic>":
450            rp.name = None
451            dyn_proj = True
452        else:
453            rp.name = found[0].name
454        rp.node_types = found[0].node_types;
455
456        if found[1] == "<same>":
457            if user_match == "<any>":
458                if user != None: rcu = user[0]
459                else: raise service_error(\
460                        service_error.server_config,
461                        "Matched <same> on anonymous request")
462            else:
463                rcu = user_match
464        elif found[1] == "<dynamic>":
465            rcu = None
466            dyn_create_user = True
467        else:
468            rcu = found[1]
469       
470        if found[2] == "<same>":
471            if user_match == "<any>":
472                if user != None: rsu = user[0]
473                else: raise service_error(\
474                        service_error.server_config,
475                        "Matched <same> on anonymous request")
476            else:
477                rsu = user_match
478        elif found[2] == "<dynamic>":
479            rsu = None
480            dyn_service_user = True
481        else:
482            rsu = found[2]
483
484        return (rp, rcu, rsu), (dyn_create_user, dyn_service_user, dyn_proj),\
485                owners
486
487    def build_response(self, alloc_id, ap):
488        """
489        Create the SOAP response.
490
491        Build the dictionary description of the response and use
492        fedd_utils.pack_soap to create the soap message.  ap is the allocate
493        project message returned from a remote project allocation (even if that
494        allocation was done locally).
495        """
496        # Because alloc_id is already a fedd_services_types.IDType_Holder,
497        # there's no need to repack it
498        msg = { 
499                'allocID': alloc_id,
500                'emulab': { 
501                    'domain': self.domain,
502                    'boss': self.boss,
503                    'ops': self.ops,
504                    'fileServer': self.fileserver,
505                    'eventServer': self.eventserver,
506                    'project': ap['project']
507                },
508            }
509        if len(self.attrs) > 0:
510            msg['emulab']['fedAttr'] = \
511                [ { 'attribute': x, 'value' : y } \
512                        for x,y in self.attrs.iteritems()]
513        return msg
514
515    def RequestAccess(self, req, fid):
516        """
517        Handle the access request.  Proxy if not for us.
518
519        Parse out the fields and make the allocations or rejections if for us,
520        otherwise, assuming we're willing to proxy, proxy the request out.
521        """
522
523        def gateway_hardware(h):
524            if h == 'GWTYPE': return self.attrs.get('connectorType', 'GWTYPE')
525            else: return h
526
527        # The dance to get into the request body
528        if req.has_key('RequestAccessRequestBody'):
529            req = req['RequestAccessRequestBody']
530        else:
531            raise service_error(service_error.req, "No request!?")
532
533        if req.has_key('destinationTestbed'):
534            dt = unpack_id(req['destinationTestbed'])
535
536        if dt == None or dt in self.testbed:
537            # Request for this fedd
538            found, dyn, owners = self.lookup_access(req, fid)
539            restricted = None
540            ap = None
541
542            # If this is a request to export a project and the access project
543            # is not the project to export, access denied.
544            if req.has_key('exportProject'):
545                ep = unpack_id(req['exportProject'])
546                if ep != found[0].name:
547                    raise service_error(service_error.access,
548                            "Cannot export %s" % ep)
549
550            # Check for access to restricted nodes
551            if req.has_key('resources') and req['resources'].has_key('node'):
552                resources = req['resources']
553                restricted = [ gateway_hardware(t) for n in resources['node'] \
554                                if n.has_key('hardware') \
555                                    for t in n['hardware'] \
556                                        if gateway_hardware(t) \
557                                            in self.restricted ]
558                inaccessible = [ t for t in restricted \
559                                    if t not in found[0].node_types]
560                if len(inaccessible) > 0:
561                    raise service_error(service_error.access,
562                            "Access denied (nodetypes %s)" % \
563                            str(', ').join(inaccessible))
564
565            # These were passed around before, but now are hidden from users
566            # and configurators alike, beyond a configuration file entry.
567            create_ssh = [ self.ssh_pubkey_file ]
568            service_ssh = [ self.ssh_pubkey_file ]
569
570            if len(create_ssh) > 0 and len(service_ssh) >0: 
571                if dyn[1]: 
572                    # Compose the dynamic project request
573                    # (only dynamic, dynamic currently allowed)
574                    preq = { 'AllocateProjectRequestBody': \
575                                { 'project' : {\
576                                    'user': [ \
577                                    { \
578                                        'access': [ { 'sshPubkey': s } \
579                                            for s in service_ssh ], 
580                                         'role': "serviceAccess",\
581                                    }, \
582                                    { \
583                                        'access': [ { 'sshPubkey': s } \
584                                            for s in create_ssh ], 
585                                         'role': "experimentCreation",\
586                                    }, \
587                                    ], \
588                                    }\
589                                }\
590                            }
591                    if restricted != None and len(restricted) > 0:
592                        preq['AllocateProjectRequestBody']['resources'] = \
593                             {'node': [ { 'hardware' :  [ h ] } \
594                                    for h in restricted ] } 
595                    ap = self.allocate_project.dynamic_project(preq)
596                else:
597                    preq = {'StaticProjectRequestBody' : \
598                            { 'project': \
599                                { 'name' : { 'localname' : found[0].name },\
600                                  'user' : [ \
601                                    {\
602                                        'userID': { 'localname' : found[1] }, \
603                                        'access': [ { 'sshPubkey': s } 
604                                            for s in create_ssh ],
605                                        'role': 'experimentCreation'\
606                                    },\
607                                    {\
608                                        'userID': { 'localname' : found[2] }, \
609                                        'access': [ { 'sshPubkey': s } 
610                                            for s in service_ssh ],
611                                        'role': 'serviceAccess'\
612                                    },\
613                                ]}\
614                            }\
615                    }
616                    if restricted != None and len(restricted) > 0:
617                        preq['StaticProjectRequestBody']['resources'] = \
618                            {'node': [ { 'hardware' :  [ h ] } \
619                                    for h in restricted ] } 
620                    ap = self.allocate_project.static_project(preq)
621            else:
622                raise service_error(service_error.req, 
623                        "SSH access parameters required")
624            # keep track of what's been added
625            allocID, alloc_cert = generate_fedid(subj="alloc", log=self.log)
626            aid = unicode(allocID)
627
628            self.state_lock.acquire()
629            self.allocation[aid] = { }
630            try:
631                pname = ap['project']['name']['localname']
632            except KeyError:
633                pname = None
634
635            if dyn[1]:
636                if not pname:
637                    self.state_lock.release()
638                    raise service_error(service_error.internal,
639                            "Misformed allocation response?")
640                if self.projects.has_key(pname): self.projects[pname] += 1
641                else: self.projects[pname] = 1
642                self.allocation[aid]['project'] = pname
643            else:
644                # sproject is a static project associated with this allocation.
645                self.allocation[aid]['sproject'] = pname
646
647            if ap.has_key('resources'):
648                if not pname:
649                    self.state_lock.release()
650                    raise service_error(service_error.internal,
651                            "Misformed allocation response?")
652                self.allocation[aid]['types'] = set()
653                nodes = ap['resources'].get('node', [])
654                for n in nodes:
655                    for h in n.get('hardware', []):
656                        if self.types.has_key((pname, h)):
657                            self.types[(pname, h)] += 1
658                        else:
659                            self.types[(pname, h)] = 1
660                        self.allocation[aid]['types'].add((pname,h))
661
662
663            self.allocation[aid]['keys'] = [ ]
664
665            try:
666                for u in ap['project']['user']:
667                    uname = u['userID']['localname']
668                    if u['role'] == 'experimentCreation':
669                        self.allocation[aid]['user'] = uname
670                    for k in [ k['sshPubkey'] for k in u['access'] \
671                            if k.has_key('sshPubkey') ]:
672                        kv = "%s:%s" % (uname, k)
673                        if self.keys.has_key(kv): self.keys[kv] += 1
674                        else: self.keys[kv] = 1
675                        self.allocation[aid]['keys'].append((uname, k))
676            except KeyError:
677                self.state_lock.release()
678                raise service_error(service_error.internal,
679                        "Misformed allocation response?")
680
681
682            self.allocation[aid]['owners'] = owners
683            self.write_state()
684            self.state_lock.release()
685            for o in owners:
686                self.auth.set_attribute(o, allocID)
687            try:
688                f = open("%s/%s.pem" % (self.certdir, aid), "w")
689                print >>f, alloc_cert
690                f.close()
691            except IOError, e:
692                raise service_error(service_error.internal, 
693                        "Can't open %s/%s : %s" % (self.certdir, aid, e))
694            resp = self.build_response({ 'fedid': allocID } , ap)
695            return resp
696        else:
697            if self.allow_proxy:
698                resp = self.proxy_RequestAccess.call_service(dt, req,
699                            self.cert_file, self.cert_pwd,
700                            self.trusted_certs)
701                if resp.has_key('RequestAccessResponseBody'):
702                    return resp['RequestAccessResponseBody']
703                else:
704                    return None
705            else:
706                raise service_error(service_error.access,
707                        "Access proxying denied")
708
709    def ReleaseAccess(self, req, fid):
710        # The dance to get into the request body
711        if req.has_key('ReleaseAccessRequestBody'):
712            req = req['ReleaseAccessRequestBody']
713        else:
714            raise service_error(service_error.req, "No request!?")
715
716        if req.has_key('destinationTestbed'):
717            dt = unpack_id(req['destinationTestbed'])
718        else:
719            dt = None
720
721        if dt == None or dt in self.testbed:
722            # Local request
723            try:
724                if req['allocID'].has_key('localname'):
725                    auth_attr = aid = req['allocID']['localname']
726                elif req['allocID'].has_key('fedid'):
727                    aid = unicode(req['allocID']['fedid'])
728                    auth_attr = req['allocID']['fedid']
729                else:
730                    raise service_error(service_error.req,
731                            "Only localnames and fedids are understood")
732            except KeyError:
733                raise service_error(service_error.req, "Badly formed request")
734
735            self.log.debug("[access] deallocation requested for %s", aid)
736            if not self.auth.check_attribute(fid, auth_attr):
737                self.log.debug("[access] deallocation denied for %s", aid)
738                raise service_error(service_error.access, "Access Denied")
739
740            # If we know this allocation, reduce the reference counts and
741            # remove the local allocations.  Otherwise report an error.  If
742            # there is an allocation to delete, del_users will be a dictonary
743            # of sets where the key is the user that owns the keys in the set.
744            # We use a set to avoid duplicates.  del_project is just the name
745            # of any dynamic project to delete.  We're somewhat lazy about
746            # deleting authorization attributes.  Having access to something
747            # that doesn't exist isn't harmful.
748            del_users = { }
749            del_project = None
750            del_types = set()
751
752            if self.allocation.has_key(aid):
753                self.log.debug("Found allocation for %s" %aid)
754                self.state_lock.acquire()
755                for k in self.allocation[aid]['keys']:
756                    kk = "%s:%s" % k
757                    self.keys[kk] -= 1
758                    if self.keys[kk] == 0:
759                        if not del_users.has_key(k[0]):
760                            del_users[k[0]] = set()
761                        del_users[k[0]].add(k[1])
762                        del self.keys[kk]
763
764                if self.allocation[aid].has_key('project'):
765                    pname = self.allocation[aid]['project']
766                    self.projects[pname] -= 1
767                    if self.projects[pname] == 0:
768                        del_project = pname
769                        del self.projects[pname]
770
771                if self.allocation[aid].has_key('types'):
772                    for t in self.allocation[aid]['types']:
773                        self.types[t] -= 1
774                        if self.types[t] == 0:
775                            if not del_project: del_project = t[0]
776                            del_types.add(t[1])
777                            del self.types[t]
778
779                del self.allocation[aid]
780                self.write_state()
781                self.state_lock.release()
782                # If we actually have resources to deallocate, prepare the call.
783                if del_project or del_users:
784                    msg = { 'project': { }}
785                    if del_project:
786                        msg['project']['name']= {'localname': del_project}
787                    users = [ ]
788                    for u in del_users.keys():
789                        users.append({ 'userID': { 'localname': u },\
790                            'access' :  \
791                                    [ {'sshPubkey' : s } for s in del_users[u]]\
792                        })
793                    if users: 
794                        msg['project']['user'] = users
795                    if len(del_types) > 0:
796                        msg['resources'] = { 'node': \
797                                [ {'hardware': [ h ] } for h in del_types ]\
798                            }
799                    if self.allocate_project.release_project:
800                        msg = { 'ReleaseProjectRequestBody' : msg}
801                        self.allocate_project.release_project(msg)
802                # And remove the access cert
803                cf = "%s/%s.pem" % (self.certdir, aid)
804                self.log.debug("Removing %s" % cf)
805                os.remove(cf)
806                return { 'allocID': req['allocID'] } 
807            else:
808                raise service_error(service_error.req, "No such allocation")
809
810        else:
811            if self.allow_proxy:
812                resp = self.proxy_ReleaseAccess.call_service(dt, req,
813                            self.cert_file, self.cert_pwd,
814                            self.trusted_certs)
815                if resp.has_key('ReleaseAccessResponseBody'):
816                    return resp['ReleaseAccessResponseBody']
817                else:
818                    return None
819            else:
820                raise service_error(service_error.access,
821                        "Access proxying denied")
822
823    def generate_portal_configs(self, topo, pubkey_base, secretkey_base, 
824            tmpdir, master):
825
826        seer_out = False
827        client_out = False
828        for e in [ e for e in topo.elements \
829                if isinstance(e, topdl.Computer) and e.get_attribute('portal')]:
830            myname = e.name[0]
831            peer = e.get_attribute('peer')
832            lexp = e.get_attribute('experiment')
833            lproj, leid = lexp.split('/', 1)
834            ldomain = e.get_attribute('domain')
835            mexp = e.get_attribute('masterexperiment')
836            mproj, meid = mexp.split("/", 1)
837            mdomain = e.get_attribute('masterdomain')
838            muser = e.get_attribute('masteruser') or 'root'
839            smbshare = e.get_attribute('smbshare') or 'USERS'
840            scriptdir = e.get_attribute('scriptdir')
841            active = e.get_attribute('active')
842            type = e.get_attribute('portal_type')
843            segid = fedid(hexstr=e.get_attribute('peer_segment'))
844            for e in topo.elements:
845                if isinstance(e, topdl.Segment) and e.id.fedid == segid:
846                    seg = e
847                    break
848            else:
849                raise service_error(service_error.req, 
850                        "Can't find segment for portal %s" % myname)
851
852            is_ip = re.match('\d+\.\d+\.\d+\.\d+', peer)
853
854            rexp = seg.get_attribute('experiment')
855            rproj, reid = rexp.split("/", 1)
856            rdomain = seg.get_attribute('domain')
857            cfn = "%s/%s.%s.%s%s.gw.conf" % \
858                    (tmpdir, myname.lower(), leid.lower(),
859                            lproj.lower(), ldomain.lower())
860            tunnelconfig = self.attrs.has_key('TunnelCfg')
861            try:
862                f = open(cfn, "w")
863                print >>f, "Active: %s" % active
864                print >>f, "TunnelCfg: %s" % tunnelconfig
865                print >>f, "BossName: boss"
866                print >>f, "FsName: fs"
867                print >>f, "EventServerName: event-server%s" % ldomain
868                print >>f, "RemoteEventServerName: event-server%s" % rdomain
869                print >>f, "SeerControl: control.%s.%s%s" % \
870                        (meid.lower(), mproj.lower(), mdomain)
871                print >>f, "Type: %s" % type
872                print >>f, "RemoteExperiment: %s" % rexp
873                print >>f, "LocalExperiment: %s" % lexp
874                print >>f, "RemoteConfigFile: " + \
875                        "/proj/%s/exp/%s/tmp/%s.%s.%s%s.gw.conf" \
876                        % (rproj, reid, peer.lower(), reid.lower(),
877                                rproj.lower(), rdomain)
878                if is_ip:
879                    print >>f, "Peer: %s" % peer
880                else:
881                    print >>f, "Peer: %s.%s.%s%s" % \
882                            (peer.lower(), reid.lower(), 
883                                    rproj.lower(), rdomain)
884                print >>f, "RemoteScriptDir: %s" % scriptdir
885                print >>f, "Pubkeys: /proj/%s/exp/%s/tmp/%s" % \
886                        (lproj, leid, pubkey_base)
887                print >>f, "Privkeys: /proj/%s/exp/%s/tmp/%s" % \
888                        (lproj, leid, secretkey_base)
889                f.close()
890            except IOError, e:
891                raise service_error(service_error.internal,
892                        "Can't write protal config %s: %s" % (cfn, e))
893           
894            # XXX: This little seer config file needs to go away.
895            if not seer_out:
896                try:
897                    seerfn = "%s/seer.conf" % tmpdir
898                    f = open(seerfn, "w")
899                    if not master:
900                        print >>f, "ControlNode: control.%s.%s%s" % \
901                            (meid.lower(), mproj.lower(), mdomain)
902                    print >>f, "ExperimentID: %s" % mexp
903                    f.close()
904                except IOError, e:
905                    raise service_error(service_error.internal, 
906                            "Can't write seer.conf: %s" %e)
907                seer_out = True
908
909            if not client_out and type in ('control', 'both'):
910                try:
911                    f = open("%s/client.conf" % tmpdir, "w")
912                    print >>f, "ControlGateway: %s.%s.%s%s" % \
913                        (myname.lower(), leid.lower(), lproj.lower(),
914                                ldomain.lower())
915                    print >>f, "SMBshare: %s" % smbshare
916                    print >>f, "ProjectUser: %s" % muser
917                    print >>f, "ProjectName: %s" % mproj
918                    print >>f, "ExperimentID: %s/%s" % (mproj, meid)
919                    f.close()
920                except IOError, e:
921                    raise service_error(service_error.internal,
922                            "Cannot write client.conf: %s" %s)
923                client_out = True
924
925
926
927    def generate_ns2(self, topo, expfn, softdir, master):
928        def dragon_commands(e):
929            s = ""
930            if isinstance(e, topdl.Computer):
931                for i in e.interface:
932                    vlan = i.get_attribute('dragon_vlan')
933                    if vlan:
934                        type = i.get_attribute('dragon_type')
935                        ip = i.get_attribute('ip4_address')
936                        if type == 'link':
937                            s = ("tb-allow-external $%s dragonportal " + \
938                                    "ip %s vlan %s\n") % \
939                                    (topdl.to_tcl_name(e.name[0]), ip, vlan)
940                        elif type == 'lan':
941                            s = ("tb-allow-external $%s dragonportal " + \
942                                    "ip %s vlan %s usurp %s\n") % \
943                                    (topdl.to_tcl_name(e.name[0]), ip, vlan,
944                                        i.substrate[0])
945                        else:
946                            raise service_error(service_error_internal,
947                                    "Unknown DRAGON type %s" % type)
948            return s
949
950        def not_dragon(e):
951            return all([not i.get_attribute('dragon_vlan') \
952                    for i in e.interface])
953
954        t = topo.clone()
955
956        # Weed out the things we aren't going to instantiate: Segments, portal
957        # substrates, and portal interfaces.  (The copy in the for loop allows
958        # us to delete from e.elements in side the for loop).
959        for e in [e for e in t.elements]:
960            if isinstance(e, topdl.Segment):
961                t.elements.remove(e)
962            if isinstance(e, topdl.Computer):
963                e.interface = [i for i in e.interface \
964                        if not i.get_attribute('portal') or \
965                            i.get_attribute('dragon_vlan')]
966        t.substrates = [ s.clone() for s in t.substrates ]
967        #t.substrates = [ s for s in t.substrates \
968        #       if not s.get_attribute('portal')]
969        t.incorporate_elements()
970
971        # Localize the software locations
972        for e in t.elements:
973            for s in getattr(e, 'software', []):
974                s.location = re.sub("^.*/", softdir, s.location)
975
976        # Customize the ns2 output for local portal commands and images
977        filters = []
978
979        if master: cmdname = 'MasterConnectorCmd'
980        else:cmdname = 'SlaveConnectorCmd'
981
982        if self.attrs.has_key('dragon'):
983            add_filter = not_dragon
984            filters.append(dragon_commands)
985        else:
986            add_filter = None
987
988        if self.attrs.has_key(cmdname):
989            filters.append(topdl.generate_portal_command_filter(
990                self.attrs.get(cmdname), add_filter=add_filter))
991
992        if self.attrs.has_key('connectorImage'):
993            filters.append(topdl.generate_portal_image_filter(
994                self.attrs.get('connectorImage')))
995
996        if self.attrs.has_key('connectorType'):
997            filters.append(topdl.generate_portal_hardware_filter(
998                self.attrs.get('connectorType')))
999
1000        # Convert to ns and write it out
1001        expfile = topdl.topology_to_ns2(t, filters)
1002        try:
1003            f = open(expfn, "w")
1004            print >>f, expfile
1005            f.close()
1006        except IOError:
1007            raise service_error(service_error.internal,
1008                    "Cannot write experiment file %s: %s" % (expfn,e))
1009
1010    def StartSegment(self, req, fid):
1011        def get_url(url, cf, destdir):
1012            po = urlparse(url)
1013            fn = po.path.rpartition('/')[2]
1014            try:
1015                conn = httplib.HTTPSConnection(po.hostname, port=po.port, 
1016                        cert_file=cf, key_file=cf)
1017                conn.putrequest('GET', po.path)
1018                conn.endheaders()
1019                response = conn.getresponse()
1020
1021                lf = open("%s/%s" % (destdir, fn), "w")
1022                buf = response.read(4096)
1023                while buf:
1024                    lf.write(buf)
1025                    buf = response.read(4096)
1026                lf.close()
1027            except IOError, e:
1028                raise service_error(service_error.internal,
1029                        "Erro writing tempfile: %s" %e)
1030            except httplib.HTTPException, e:
1031                raise service_error(service_error.internal, 
1032                        "Error retrieving data: %s" % e)
1033
1034        configs = set(('hosts', 'ssh_pubkey', 'ssh_secretkey'))
1035
1036        err = None  # Any service_error generated after tmpdir is created
1037        rv = None   # Return value from segment creation
1038
1039        try:
1040            req = req['StartSegmentRequestBody']
1041        except KeyError:
1042            raise service_error(server_error.req, "Badly formed request")
1043
1044        auth_attr = req['allocID']['fedid']
1045        aid = "%s" % auth_attr
1046        attrs = req.get('fedAttr', [])
1047        if not self.auth.check_attribute(fid, auth_attr):
1048            raise service_error(service_error.access, "Access denied")
1049
1050        if req.has_key('segmentdescription') and \
1051                req['segmentdescription'].has_key('topdldescription'):
1052            topo = \
1053                topdl.Topology(**req['segmentdescription']['topdldescription'])
1054        else:
1055            raise service_error(service_error.req, 
1056                    "Request missing segmentdescription'")
1057
1058        master = req.get('master', False)
1059
1060        certfile = "%s/%s.pem" % (self.certdir, auth_attr)
1061        try:
1062            tmpdir = tempfile.mkdtemp(prefix="access-")
1063            softdir = "%s/software" % tmpdir
1064        except IOError:
1065            raise service_error(service_error.internal, "Cannot create tmp dir")
1066
1067        # Try block alllows us to clean up temporary files.
1068        try:
1069            sw = set()
1070            for e in topo.elements:
1071                for s in getattr(e, 'software', []):
1072                    sw.add(s.location)
1073            if len(sw) > 0:
1074                os.mkdir(softdir)
1075            for s in sw:
1076                print "%s %s %s" % ( s, certfile, softdir)
1077                get_url(s, certfile, softdir)
1078
1079            for a in attrs:
1080                if a['attribute'] in configs:
1081                    get_url(a['value'], certfile, tmpdir)
1082                if a['attribute'] == 'ssh_pubkey':
1083                    pubkey_base = a['value'].rpartition('/')[2]
1084                if a['attribute'] == 'ssh_secretkey':
1085                    secretkey_base = a['value'].rpartition('/')[2]
1086                if a['attribute'] == 'experiment_name':
1087                    ename = a['value']
1088
1089            proj = None
1090            user = None
1091            self.state_lock.acquire()
1092            if self.allocation.has_key(aid):
1093                proj = self.allocation[aid].get('project', None)
1094                if not proj: 
1095                    proj = self.allocation[aid].get('sproject', None)
1096                user = self.allocation[aid].get('user', None)
1097                self.allocation[aid]['experiment'] = ename
1098                self.allocation[aid]['log'] = [ ]
1099                # Create a logger that logs to the experiment's state object as
1100                # well as to the main log file.
1101                alloc_log = logging.getLogger('fedd.access.%s' % ename)
1102                h = logging.StreamHandler(
1103                        list_log.list_log(self.allocation[aid]['log']))
1104                # XXX: there should be a global one of these rather than
1105                # repeating the code.
1106                h.setFormatter(logging.Formatter(
1107                    "%(asctime)s %(name)s %(message)s",
1108                            '%d %b %y %H:%M:%S'))
1109                alloc_log.addHandler(h)
1110                self.write_state()
1111            self.state_lock.release()
1112
1113            if not proj:
1114                raise service_error(service_error.internal, 
1115                        "Can't find project for %s" %aid)
1116
1117            if not user:
1118                raise service_error(service_error.internal, 
1119                        "Can't find creation user for %s" %aid)
1120
1121            expfile = "%s/experiment.tcl" % tmpdir
1122
1123            self.generate_portal_configs(topo, pubkey_base, 
1124                    secretkey_base, tmpdir, master)
1125            self.generate_ns2(topo, expfile, 
1126                    "/proj/%s/software/%s/" % (proj, ename), master)
1127
1128            starter = self.start_segment(keyfile=self.ssh_privkey_file, 
1129                    debug=self.create_debug, log=alloc_log)
1130            rv = starter(self, ename, proj, user, expfile, tmpdir)
1131        except service_error, e:
1132            err = e
1133
1134        # Walk up tmpdir, deleting as we go
1135        if self.cleanup:
1136            self.log.debug("[StartSegment]: removing %s" % tmpdir)
1137            for path, dirs, files in os.walk(tmpdir, topdown=False):
1138                for f in files:
1139                    os.remove(os.path.join(path, f))
1140                for d in dirs:
1141                    os.rmdir(os.path.join(path, d))
1142            os.rmdir(tmpdir)
1143        else:
1144            self.log.debug("[StartSegment]: not removing %s" % tmpdir)
1145
1146        if rv:
1147            # Grab the log (this is some anal locking, but better safe than
1148            # sorry)
1149            self.state_lock.acquire()
1150            logv = "".join(self.allocation[aid]['log'])
1151            self.state_lock.release()
1152
1153            return { 'allocID': req['allocID'], 'allocationLog': logv }
1154        elif err:
1155            raise service_error(service_error.federant,
1156                    "Swapin failed: %s" % err)
1157        else:
1158            raise service_error(service_error.federant, "Swapin failed")
1159
1160    def TerminateSegment(self, req, fid):
1161        try:
1162            req = req['TerminateSegmentRequestBody']
1163        except KeyError:
1164            raise service_error(server_error.req, "Badly formed request")
1165
1166        auth_attr = req['allocID']['fedid']
1167        aid = "%s" % auth_attr
1168        attrs = req.get('fedAttr', [])
1169        if not self.auth.check_attribute(fid, auth_attr):
1170            raise service_error(service_error.access, "Access denied")
1171
1172        self.state_lock.acquire()
1173        if self.allocation.has_key(aid):
1174            proj = self.allocation[aid].get('project', None)
1175            if not proj: 
1176                proj = self.allocation[aid].get('sproject', None)
1177            user = self.allocation[aid].get('user', None)
1178            ename = self.allocation[aid].get('experiment', None)
1179        else:
1180            proj = None
1181            user = None
1182            ename = None
1183        self.state_lock.release()
1184
1185        if not proj:
1186            raise service_error(service_error.internal, 
1187                    "Can't find project for %s" % aid)
1188
1189        if not user:
1190            raise service_error(service_error.internal, 
1191                    "Can't find creation user for %s" % aid)
1192        if not ename:
1193            raise service_error(service_error.internal, 
1194                    "Can't find experiment name for %s" % aid)
1195        stopper = self.stop_segment(keyfile=self.ssh_privkey_file,
1196                debug=self.create_debug)
1197        stopper(self, user, proj, ename)
1198        return { 'allocID': req['allocID'] }
Note: See TracBrowser for help on using the repository browser.