source: fedd/federation/emulab_access.py @ bbd0039

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

commented out code

  • Property mode set to 100644
File size: 44.5 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                'fedAttr': [
501                    { 'attribute': 'domain', 'value': self.domain } , 
502                    { 'attribute': 'project', 'value': 
503                        ap['project'].get('name', {}).get('localname', "???") },
504                ]
505            }
506        if len(self.attrs) > 0:
507            msg['fedAttr'].extend(
508                [ { 'attribute': x, 'value' : y } \
509                        for x,y in self.attrs.iteritems()])
510        return msg
511
512    def RequestAccess(self, req, fid):
513        """
514        Handle the access request.  Proxy if not for us.
515
516        Parse out the fields and make the allocations or rejections if for us,
517        otherwise, assuming we're willing to proxy, proxy the request out.
518        """
519
520        def gateway_hardware(h):
521            if h == 'GWTYPE': return self.attrs.get('connectorType', 'GWTYPE')
522            else: return h
523
524        # The dance to get into the request body
525        if req.has_key('RequestAccessRequestBody'):
526            req = req['RequestAccessRequestBody']
527        else:
528            raise service_error(service_error.req, "No request!?")
529
530        if req.has_key('destinationTestbed'):
531            dt = unpack_id(req['destinationTestbed'])
532
533        if dt == None or dt in self.testbed:
534            # Request for this fedd
535            found, dyn, owners = self.lookup_access(req, fid)
536            restricted = None
537            ap = None
538
539            # If this is a request to export a project and the access project
540            # is not the project to export, access denied.
541            if req.has_key('exportProject'):
542                ep = unpack_id(req['exportProject'])
543                if ep != found[0].name:
544                    raise service_error(service_error.access,
545                            "Cannot export %s" % ep)
546
547            # Check for access to restricted nodes
548            if req.has_key('resources') and req['resources'].has_key('node'):
549                resources = req['resources']
550                restricted = [ gateway_hardware(t) for n in resources['node'] \
551                                if n.has_key('hardware') \
552                                    for t in n['hardware'] \
553                                        if gateway_hardware(t) \
554                                            in self.restricted ]
555                inaccessible = [ t for t in restricted \
556                                    if t not in found[0].node_types]
557                if len(inaccessible) > 0:
558                    raise service_error(service_error.access,
559                            "Access denied (nodetypes %s)" % \
560                            str(', ').join(inaccessible))
561
562            # These were passed around before, but now are hidden from users
563            # and configurators alike, beyond a configuration file entry.
564            create_ssh = [ self.ssh_pubkey_file ]
565            service_ssh = [ self.ssh_pubkey_file ]
566
567            if len(create_ssh) > 0 and len(service_ssh) >0: 
568                if dyn[1]: 
569                    # Compose the dynamic project request
570                    # (only dynamic, dynamic currently allowed)
571                    preq = { 'AllocateProjectRequestBody': \
572                                { 'project' : {\
573                                    'user': [ \
574                                    { \
575                                        'access': [ { 'sshPubkey': s } \
576                                            for s in service_ssh ], 
577                                         'role': "serviceAccess",\
578                                    }, \
579                                    { \
580                                        'access': [ { 'sshPubkey': s } \
581                                            for s in create_ssh ], 
582                                         'role': "experimentCreation",\
583                                    }, \
584                                    ], \
585                                    }\
586                                }\
587                            }
588                    if restricted != None and len(restricted) > 0:
589                        preq['AllocateProjectRequestBody']['resources'] = \
590                             {'node': [ { 'hardware' :  [ h ] } \
591                                    for h in restricted ] } 
592                    ap = self.allocate_project.dynamic_project(preq)
593                else:
594                    preq = {'StaticProjectRequestBody' : \
595                            { 'project': \
596                                { 'name' : { 'localname' : found[0].name },\
597                                  'user' : [ \
598                                    {\
599                                        'userID': { 'localname' : found[1] }, \
600                                        'access': [ { 'sshPubkey': s } 
601                                            for s in create_ssh ],
602                                        'role': 'experimentCreation'\
603                                    },\
604                                    {\
605                                        'userID': { 'localname' : found[2] }, \
606                                        'access': [ { 'sshPubkey': s } 
607                                            for s in service_ssh ],
608                                        'role': 'serviceAccess'\
609                                    },\
610                                ]}\
611                            }\
612                    }
613                    if restricted != None and len(restricted) > 0:
614                        preq['StaticProjectRequestBody']['resources'] = \
615                            {'node': [ { 'hardware' :  [ h ] } \
616                                    for h in restricted ] } 
617                    ap = self.allocate_project.static_project(preq)
618            else:
619                raise service_error(service_error.req, 
620                        "SSH access parameters required")
621            # keep track of what's been added
622            allocID, alloc_cert = generate_fedid(subj="alloc", log=self.log)
623            aid = unicode(allocID)
624
625            self.state_lock.acquire()
626            self.allocation[aid] = { }
627            try:
628                pname = ap['project']['name']['localname']
629            except KeyError:
630                pname = None
631
632            if dyn[1]:
633                if not pname:
634                    self.state_lock.release()
635                    raise service_error(service_error.internal,
636                            "Misformed allocation response?")
637                if self.projects.has_key(pname): self.projects[pname] += 1
638                else: self.projects[pname] = 1
639                self.allocation[aid]['project'] = pname
640            else:
641                # sproject is a static project associated with this allocation.
642                self.allocation[aid]['sproject'] = pname
643
644            if ap.has_key('resources'):
645                if not pname:
646                    self.state_lock.release()
647                    raise service_error(service_error.internal,
648                            "Misformed allocation response?")
649                self.allocation[aid]['types'] = set()
650                nodes = ap['resources'].get('node', [])
651                for n in nodes:
652                    for h in n.get('hardware', []):
653                        if self.types.has_key((pname, h)):
654                            self.types[(pname, h)] += 1
655                        else:
656                            self.types[(pname, h)] = 1
657                        self.allocation[aid]['types'].add((pname,h))
658
659
660            self.allocation[aid]['keys'] = [ ]
661
662            try:
663                for u in ap['project']['user']:
664                    uname = u['userID']['localname']
665                    if u['role'] == 'experimentCreation':
666                        self.allocation[aid]['user'] = uname
667                    for k in [ k['sshPubkey'] for k in u['access'] \
668                            if k.has_key('sshPubkey') ]:
669                        kv = "%s:%s" % (uname, k)
670                        if self.keys.has_key(kv): self.keys[kv] += 1
671                        else: self.keys[kv] = 1
672                        self.allocation[aid]['keys'].append((uname, k))
673            except KeyError:
674                self.state_lock.release()
675                raise service_error(service_error.internal,
676                        "Misformed allocation response?")
677
678
679            self.allocation[aid]['owners'] = owners
680            self.write_state()
681            self.state_lock.release()
682            for o in owners:
683                self.auth.set_attribute(o, allocID)
684            try:
685                f = open("%s/%s.pem" % (self.certdir, aid), "w")
686                print >>f, alloc_cert
687                f.close()
688            except IOError, e:
689                raise service_error(service_error.internal, 
690                        "Can't open %s/%s : %s" % (self.certdir, aid, e))
691            resp = self.build_response({ 'fedid': allocID } , ap)
692            return resp
693        else:
694            if self.allow_proxy:
695                resp = self.proxy_RequestAccess.call_service(dt, req,
696                            self.cert_file, self.cert_pwd,
697                            self.trusted_certs)
698                if resp.has_key('RequestAccessResponseBody'):
699                    return resp['RequestAccessResponseBody']
700                else:
701                    return None
702            else:
703                raise service_error(service_error.access,
704                        "Access proxying denied")
705
706    def ReleaseAccess(self, req, fid):
707        # The dance to get into the request body
708        if req.has_key('ReleaseAccessRequestBody'):
709            req = req['ReleaseAccessRequestBody']
710        else:
711            raise service_error(service_error.req, "No request!?")
712
713        if req.has_key('destinationTestbed'):
714            dt = unpack_id(req['destinationTestbed'])
715        else:
716            dt = None
717
718        if dt == None or dt in self.testbed:
719            # Local request
720            try:
721                if req['allocID'].has_key('localname'):
722                    auth_attr = aid = req['allocID']['localname']
723                elif req['allocID'].has_key('fedid'):
724                    aid = unicode(req['allocID']['fedid'])
725                    auth_attr = req['allocID']['fedid']
726                else:
727                    raise service_error(service_error.req,
728                            "Only localnames and fedids are understood")
729            except KeyError:
730                raise service_error(service_error.req, "Badly formed request")
731
732            self.log.debug("[access] deallocation requested for %s", aid)
733            if not self.auth.check_attribute(fid, auth_attr):
734                self.log.debug("[access] deallocation denied for %s", aid)
735                raise service_error(service_error.access, "Access Denied")
736
737            # If we know this allocation, reduce the reference counts and
738            # remove the local allocations.  Otherwise report an error.  If
739            # there is an allocation to delete, del_users will be a dictonary
740            # of sets where the key is the user that owns the keys in the set.
741            # We use a set to avoid duplicates.  del_project is just the name
742            # of any dynamic project to delete.  We're somewhat lazy about
743            # deleting authorization attributes.  Having access to something
744            # that doesn't exist isn't harmful.
745            del_users = { }
746            del_project = None
747            del_types = set()
748
749            if self.allocation.has_key(aid):
750                self.log.debug("Found allocation for %s" %aid)
751                self.state_lock.acquire()
752                for k in self.allocation[aid]['keys']:
753                    kk = "%s:%s" % k
754                    self.keys[kk] -= 1
755                    if self.keys[kk] == 0:
756                        if not del_users.has_key(k[0]):
757                            del_users[k[0]] = set()
758                        del_users[k[0]].add(k[1])
759                        del self.keys[kk]
760
761                if self.allocation[aid].has_key('project'):
762                    pname = self.allocation[aid]['project']
763                    self.projects[pname] -= 1
764                    if self.projects[pname] == 0:
765                        del_project = pname
766                        del self.projects[pname]
767
768                if self.allocation[aid].has_key('types'):
769                    for t in self.allocation[aid]['types']:
770                        self.types[t] -= 1
771                        if self.types[t] == 0:
772                            if not del_project: del_project = t[0]
773                            del_types.add(t[1])
774                            del self.types[t]
775
776                del self.allocation[aid]
777                self.write_state()
778                self.state_lock.release()
779                # If we actually have resources to deallocate, prepare the call.
780                if del_project or del_users:
781                    msg = { 'project': { }}
782                    if del_project:
783                        msg['project']['name']= {'localname': del_project}
784                    users = [ ]
785                    for u in del_users.keys():
786                        users.append({ 'userID': { 'localname': u },\
787                            'access' :  \
788                                    [ {'sshPubkey' : s } for s in del_users[u]]\
789                        })
790                    if users: 
791                        msg['project']['user'] = users
792                    if len(del_types) > 0:
793                        msg['resources'] = { 'node': \
794                                [ {'hardware': [ h ] } for h in del_types ]\
795                            }
796                    if self.allocate_project.release_project:
797                        msg = { 'ReleaseProjectRequestBody' : msg}
798                        self.allocate_project.release_project(msg)
799                # And remove the access cert
800                cf = "%s/%s.pem" % (self.certdir, aid)
801                self.log.debug("Removing %s" % cf)
802                os.remove(cf)
803                return { 'allocID': req['allocID'] } 
804            else:
805                raise service_error(service_error.req, "No such allocation")
806
807        else:
808            if self.allow_proxy:
809                resp = self.proxy_ReleaseAccess.call_service(dt, req,
810                            self.cert_file, self.cert_pwd,
811                            self.trusted_certs)
812                if resp.has_key('ReleaseAccessResponseBody'):
813                    return resp['ReleaseAccessResponseBody']
814                else:
815                    return None
816            else:
817                raise service_error(service_error.access,
818                        "Access proxying denied")
819
820    def generate_portal_configs(self, topo, pubkey_base, secretkey_base, 
821            tmpdir, master):
822
823        seer_out = False
824        client_out = False
825        for e in [ e for e in topo.elements \
826                if isinstance(e, topdl.Computer) and e.get_attribute('portal')]:
827            myname = e.name[0]
828            peer = e.get_attribute('peer')
829            lexp = e.get_attribute('experiment')
830            lproj, leid = lexp.split('/', 1)
831            ldomain = e.get_attribute('domain')
832            mexp = e.get_attribute('masterexperiment')
833            mproj, meid = mexp.split("/", 1)
834            mdomain = e.get_attribute('masterdomain')
835            muser = e.get_attribute('masteruser') or 'root'
836            smbshare = e.get_attribute('smbshare') or 'USERS'
837            scriptdir = e.get_attribute('scriptdir')
838            active = e.get_attribute('active')
839            type = e.get_attribute('portal_type')
840            segid = fedid(hexstr=e.get_attribute('peer_segment'))
841            for e in topo.elements:
842                if isinstance(e, topdl.Segment) and e.id.fedid == segid:
843                    seg = e
844                    break
845            else:
846                raise service_error(service_error.req, 
847                        "Can't find segment for portal %s" % myname)
848
849            is_ip = re.match('\d+\.\d+\.\d+\.\d+', peer)
850
851            rexp = seg.get_attribute('experiment')
852            rproj, reid = rexp.split("/", 1)
853            rdomain = seg.get_attribute('domain')
854            cfn = "%s/%s.%s.%s%s.gw.conf" % \
855                    (tmpdir, myname.lower(), leid.lower(),
856                            lproj.lower(), ldomain.lower())
857            tunnelconfig = self.attrs.has_key('TunnelCfg')
858            try:
859                f = open(cfn, "w")
860                print >>f, "Active: %s" % active
861                print >>f, "TunnelCfg: %s" % tunnelconfig
862                print >>f, "BossName: boss"
863                print >>f, "FsName: fs"
864                print >>f, "EventServerName: event-server%s" % ldomain
865                print >>f, "RemoteEventServerName: event-server%s" % rdomain
866                print >>f, "SeerControl: control.%s.%s%s" % \
867                        (meid.lower(), mproj.lower(), mdomain)
868                print >>f, "Type: %s" % type
869                print >>f, "RemoteExperiment: %s" % rexp
870                print >>f, "LocalExperiment: %s" % lexp
871                print >>f, "RemoteConfigFile: " + \
872                        "/proj/%s/exp/%s/tmp/%s.%s.%s%s.gw.conf" \
873                        % (rproj, reid, peer.lower(), reid.lower(),
874                                rproj.lower(), rdomain)
875                if is_ip:
876                    print >>f, "Peer: %s" % peer
877                else:
878                    print >>f, "Peer: %s.%s.%s%s" % \
879                            (peer.lower(), reid.lower(), 
880                                    rproj.lower(), rdomain)
881                print >>f, "RemoteScriptDir: %s" % scriptdir
882                print >>f, "Pubkeys: /proj/%s/exp/%s/tmp/%s" % \
883                        (lproj, leid, pubkey_base)
884                print >>f, "Privkeys: /proj/%s/exp/%s/tmp/%s" % \
885                        (lproj, leid, secretkey_base)
886                f.close()
887            except IOError, e:
888                raise service_error(service_error.internal,
889                        "Can't write protal config %s: %s" % (cfn, e))
890           
891            # XXX: This little seer config file needs to go away.
892            if not seer_out:
893                try:
894                    seerfn = "%s/seer.conf" % tmpdir
895                    f = open(seerfn, "w")
896                    if not master:
897                        print >>f, "ControlNode: control.%s.%s%s" % \
898                            (meid.lower(), mproj.lower(), mdomain)
899                    print >>f, "ExperimentID: %s" % mexp
900                    f.close()
901                except IOError, e:
902                    raise service_error(service_error.internal, 
903                            "Can't write seer.conf: %s" %e)
904                seer_out = True
905
906            if not client_out and type in ('control', 'both'):
907                try:
908                    f = open("%s/client.conf" % tmpdir, "w")
909                    print >>f, "ControlGateway: %s.%s.%s%s" % \
910                        (myname.lower(), leid.lower(), lproj.lower(),
911                                ldomain.lower())
912                    print >>f, "SMBshare: %s" % smbshare
913                    print >>f, "ProjectUser: %s" % muser
914                    print >>f, "ProjectName: %s" % mproj
915                    print >>f, "ExperimentID: %s/%s" % (mproj, meid)
916                    f.close()
917                except IOError, e:
918                    raise service_error(service_error.internal,
919                            "Cannot write client.conf: %s" %s)
920                client_out = True
921
922
923
924    def generate_ns2(self, topo, expfn, softdir, master):
925        def dragon_commands(e):
926            s = ""
927            if isinstance(e, topdl.Computer):
928                for i in e.interface:
929                    vlan = i.get_attribute('dragon_vlan')
930                    if vlan:
931                        type = i.get_attribute('dragon_type')
932                        ip = i.get_attribute('ip4_address')
933                        if type == 'link':
934                            s = ("tb-allow-external $%s dragonportal " + \
935                                    "ip %s vlan %s\n") % \
936                                    (topdl.to_tcl_name(e.name[0]), ip, vlan)
937                        elif type == 'lan':
938                            s = ("tb-allow-external $%s dragonportal " + \
939                                    "ip %s vlan %s usurp %s\n") % \
940                                    (topdl.to_tcl_name(e.name[0]), ip, vlan,
941                                        i.substrate[0])
942                        else:
943                            raise service_error(service_error_internal,
944                                    "Unknown DRAGON type %s" % type)
945            return s
946
947        def not_dragon(e):
948            return all([not i.get_attribute('dragon_vlan') \
949                    for i in e.interface])
950
951        t = topo.clone()
952
953        # Weed out the things we aren't going to instantiate: Segments, portal
954        # substrates, and portal interfaces.  (The copy in the for loop allows
955        # us to delete from e.elements in side the for loop).
956        for e in [e for e in t.elements]:
957            if isinstance(e, topdl.Segment):
958                t.elements.remove(e)
959            if isinstance(e, topdl.Computer):
960                e.interface = [i for i in e.interface \
961                        if not i.get_attribute('portal') or \
962                            i.get_attribute('dragon_vlan')]
963        t.substrates = [ s.clone() for s in t.substrates ]
964        t.incorporate_elements()
965
966        # Localize the software locations
967        for e in t.elements:
968            for s in getattr(e, 'software', []):
969                s.location = re.sub("^.*/", softdir, s.location)
970
971        # Customize the ns2 output for local portal commands and images
972        filters = []
973
974        if master: cmdname = 'MasterConnectorCmd'
975        else:cmdname = 'SlaveConnectorCmd'
976
977        if self.attrs.has_key('dragon'):
978            add_filter = not_dragon
979            filters.append(dragon_commands)
980        else:
981            add_filter = None
982
983        if self.attrs.has_key(cmdname):
984            filters.append(topdl.generate_portal_command_filter(
985                self.attrs.get(cmdname), add_filter=add_filter))
986
987        if self.attrs.has_key('connectorImage'):
988            filters.append(topdl.generate_portal_image_filter(
989                self.attrs.get('connectorImage')))
990
991        if self.attrs.has_key('connectorType'):
992            filters.append(topdl.generate_portal_hardware_filter(
993                self.attrs.get('connectorType')))
994
995        # Convert to ns and write it out
996        expfile = topdl.topology_to_ns2(t, filters)
997        try:
998            f = open(expfn, "w")
999            print >>f, expfile
1000            f.close()
1001        except IOError:
1002            raise service_error(service_error.internal,
1003                    "Cannot write experiment file %s: %s" % (expfn,e))
1004
1005    def StartSegment(self, req, fid):
1006        def get_url(url, cf, destdir):
1007            po = urlparse(url)
1008            fn = po.path.rpartition('/')[2]
1009            try:
1010                conn = httplib.HTTPSConnection(po.hostname, port=po.port, 
1011                        cert_file=cf, key_file=cf)
1012                conn.putrequest('GET', po.path)
1013                conn.endheaders()
1014                response = conn.getresponse()
1015
1016                lf = open("%s/%s" % (destdir, fn), "w")
1017                buf = response.read(4096)
1018                while buf:
1019                    lf.write(buf)
1020                    buf = response.read(4096)
1021                lf.close()
1022            except IOError, e:
1023                raise service_error(service_error.internal,
1024                        "Erro writing tempfile: %s" %e)
1025            except httplib.HTTPException, e:
1026                raise service_error(service_error.internal, 
1027                        "Error retrieving data: %s" % e)
1028
1029        configs = set(('hosts', 'ssh_pubkey', 'ssh_secretkey'))
1030
1031        err = None  # Any service_error generated after tmpdir is created
1032        rv = None   # Return value from segment creation
1033
1034        try:
1035            req = req['StartSegmentRequestBody']
1036        except KeyError:
1037            raise service_error(server_error.req, "Badly formed request")
1038
1039        auth_attr = req['allocID']['fedid']
1040        aid = "%s" % auth_attr
1041        attrs = req.get('fedAttr', [])
1042        if not self.auth.check_attribute(fid, auth_attr):
1043            raise service_error(service_error.access, "Access denied")
1044
1045        if req.has_key('segmentdescription') and \
1046                req['segmentdescription'].has_key('topdldescription'):
1047            topo = \
1048                topdl.Topology(**req['segmentdescription']['topdldescription'])
1049        else:
1050            raise service_error(service_error.req, 
1051                    "Request missing segmentdescription'")
1052
1053        master = req.get('master', False)
1054
1055        certfile = "%s/%s.pem" % (self.certdir, auth_attr)
1056        try:
1057            tmpdir = tempfile.mkdtemp(prefix="access-")
1058            softdir = "%s/software" % tmpdir
1059        except IOError:
1060            raise service_error(service_error.internal, "Cannot create tmp dir")
1061
1062        # Try block alllows us to clean up temporary files.
1063        try:
1064            sw = set()
1065            for e in topo.elements:
1066                for s in getattr(e, 'software', []):
1067                    sw.add(s.location)
1068            if len(sw) > 0:
1069                os.mkdir(softdir)
1070            for s in sw:
1071                print "%s %s %s" % ( s, certfile, softdir)
1072                get_url(s, certfile, softdir)
1073
1074            for a in attrs:
1075                if a['attribute'] in configs:
1076                    get_url(a['value'], certfile, tmpdir)
1077                if a['attribute'] == 'ssh_pubkey':
1078                    pubkey_base = a['value'].rpartition('/')[2]
1079                if a['attribute'] == 'ssh_secretkey':
1080                    secretkey_base = a['value'].rpartition('/')[2]
1081                if a['attribute'] == 'experiment_name':
1082                    ename = a['value']
1083
1084            proj = None
1085            user = None
1086            self.state_lock.acquire()
1087            if self.allocation.has_key(aid):
1088                proj = self.allocation[aid].get('project', None)
1089                if not proj: 
1090                    proj = self.allocation[aid].get('sproject', None)
1091                user = self.allocation[aid].get('user', None)
1092                self.allocation[aid]['experiment'] = ename
1093                self.allocation[aid]['log'] = [ ]
1094                # Create a logger that logs to the experiment's state object as
1095                # well as to the main log file.
1096                alloc_log = logging.getLogger('fedd.access.%s' % ename)
1097                h = logging.StreamHandler(
1098                        list_log.list_log(self.allocation[aid]['log']))
1099                # XXX: there should be a global one of these rather than
1100                # repeating the code.
1101                h.setFormatter(logging.Formatter(
1102                    "%(asctime)s %(name)s %(message)s",
1103                            '%d %b %y %H:%M:%S'))
1104                alloc_log.addHandler(h)
1105                self.write_state()
1106            self.state_lock.release()
1107
1108            if not proj:
1109                raise service_error(service_error.internal, 
1110                        "Can't find project for %s" %aid)
1111
1112            if not user:
1113                raise service_error(service_error.internal, 
1114                        "Can't find creation user for %s" %aid)
1115
1116            expfile = "%s/experiment.tcl" % tmpdir
1117
1118            self.generate_portal_configs(topo, pubkey_base, 
1119                    secretkey_base, tmpdir, master)
1120            self.generate_ns2(topo, expfile, 
1121                    "/proj/%s/software/%s/" % (proj, ename), master)
1122
1123            starter = self.start_segment(keyfile=self.ssh_privkey_file, 
1124                    debug=self.create_debug, log=alloc_log)
1125            rv = starter(self, ename, proj, user, expfile, tmpdir)
1126        except service_error, e:
1127            err = e
1128
1129        # Walk up tmpdir, deleting as we go
1130        if self.cleanup:
1131            self.log.debug("[StartSegment]: removing %s" % tmpdir)
1132            for path, dirs, files in os.walk(tmpdir, topdown=False):
1133                for f in files:
1134                    os.remove(os.path.join(path, f))
1135                for d in dirs:
1136                    os.rmdir(os.path.join(path, d))
1137            os.rmdir(tmpdir)
1138        else:
1139            self.log.debug("[StartSegment]: not removing %s" % tmpdir)
1140
1141        if rv:
1142            # Grab the log (this is some anal locking, but better safe than
1143            # sorry)
1144            self.state_lock.acquire()
1145            logv = "".join(self.allocation[aid]['log'])
1146            self.state_lock.release()
1147
1148            return { 'allocID': req['allocID'], 'allocationLog': logv }
1149        elif err:
1150            raise service_error(service_error.federant,
1151                    "Swapin failed: %s" % err)
1152        else:
1153            raise service_error(service_error.federant, "Swapin failed")
1154
1155    def TerminateSegment(self, req, fid):
1156        try:
1157            req = req['TerminateSegmentRequestBody']
1158        except KeyError:
1159            raise service_error(server_error.req, "Badly formed request")
1160
1161        auth_attr = req['allocID']['fedid']
1162        aid = "%s" % auth_attr
1163        attrs = req.get('fedAttr', [])
1164        if not self.auth.check_attribute(fid, auth_attr):
1165            raise service_error(service_error.access, "Access denied")
1166
1167        self.state_lock.acquire()
1168        if self.allocation.has_key(aid):
1169            proj = self.allocation[aid].get('project', None)
1170            if not proj: 
1171                proj = self.allocation[aid].get('sproject', None)
1172            user = self.allocation[aid].get('user', None)
1173            ename = self.allocation[aid].get('experiment', None)
1174        else:
1175            proj = None
1176            user = None
1177            ename = None
1178        self.state_lock.release()
1179
1180        if not proj:
1181            raise service_error(service_error.internal, 
1182                    "Can't find project for %s" % aid)
1183
1184        if not user:
1185            raise service_error(service_error.internal, 
1186                    "Can't find creation user for %s" % aid)
1187        if not ename:
1188            raise service_error(service_error.internal, 
1189                    "Can't find experiment name for %s" % aid)
1190        stopper = self.stop_segment(keyfile=self.ssh_privkey_file,
1191                debug=self.create_debug)
1192        stopper(self, user, proj, ename)
1193        return { 'allocID': req['allocID'] }
Note: See TracBrowser for help on using the repository browser.