source: fedd/federation/emulab_access.py @ b4624b2

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

Generate configuration files for the new format of gateway nodes. Also insert
the startcmds in this module. The experiment countroller shouldn't be doing
it.

  • Property mode set to 100644
File size: 44.4 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.gw.conf" % (tmpdir, myname.lower())
855            tunnelconfig = self.attrs.has_key('TunnelCfg')
856            try:
857                f = open(cfn, "w")
858                if active == 'True':
859                    print >>f, "active: %s" % active
860                    if type in ('control', 'both'):
861                        print >>f, 'port: remote:139:fs:139'
862                        print >>f, 'port: remote:7777:boss:7777'
863                        print >>f, 'port: remote:16606:control:16606'
864
865                if tunnelconfig:
866                    print >>f, "tunnelip: %s" % tunnelconfig
867                print >>f, "seercontrol: control.%s.%s%s" % \
868                        (meid.lower(), mproj.lower(), mdomain)
869                if is_ip:
870                    print >>f, "peer: %s" % peer
871                else:
872                    print >>f, "peer: %s.%s.%s%s" % \
873                            (peer.lower(), reid.lower(), 
874                                    rproj.lower(), rdomain)
875                print >>f, "ssh_pubkey: /proj/%s/exp/%s/tmp/%s" % \
876                        (lproj, leid, pubkey_base)
877                print >>f, "ssh_privkey: /proj/%s/exp/%s/tmp/%s" % \
878                        (lproj, leid, secretkey_base)
879                f.close()
880            except IOError, e:
881                raise service_error(service_error.internal,
882                        "Can't write protal config %s: %s" % (cfn, e))
883           
884            # XXX: This little seer config file needs to go away.
885            if not seer_out:
886                try:
887                    seerfn = "%s/seer.conf" % tmpdir
888                    f = open(seerfn, "w")
889                    if not master:
890                        print >>f, "ControlNode: control.%s.%s%s" % \
891                            (meid.lower(), mproj.lower(), mdomain)
892                    print >>f, "ExperimentID: %s" % mexp
893                    f.close()
894                except IOError, e:
895                    raise service_error(service_error.internal, 
896                            "Can't write seer.conf: %s" %e)
897                seer_out = True
898
899            if not client_out and type in ('control', 'both'):
900                try:
901                    f = open("%s/client.conf" % tmpdir, "w")
902                    print >>f, "ControlGateway: %s.%s.%s%s" % \
903                        (myname.lower(), leid.lower(), lproj.lower(),
904                                ldomain.lower())
905                    print >>f, "SMBshare: %s" % smbshare
906                    print >>f, "ProjectUser: %s" % muser
907                    print >>f, "ProjectName: %s" % mproj
908                    print >>f, "ExperimentID: %s/%s" % (mproj, meid)
909                    f.close()
910                except IOError, e:
911                    raise service_error(service_error.internal,
912                            "Cannot write client.conf: %s" %s)
913                client_out = True
914
915
916    def generate_ns2(self, topo, expfn, softdir, master):
917        def dragon_commands(e):
918            s = ""
919            if isinstance(e, topdl.Computer):
920                for i in e.interface:
921                    vlan = i.get_attribute('dragon_vlan')
922                    if vlan:
923                        type = i.get_attribute('dragon_type')
924                        ip = i.get_attribute('ip4_address')
925                        if type == 'link':
926                            s = ("tb-allow-external $%s dragonportal " + \
927                                    "ip %s vlan %s\n") % \
928                                    (topdl.to_tcl_name(e.name[0]), ip, vlan)
929                        elif type == 'lan':
930                            s = ("tb-allow-external $%s dragonportal " + \
931                                    "ip %s vlan %s usurp %s\n") % \
932                                    (topdl.to_tcl_name(e.name[0]), ip, vlan,
933                                        i.substrate[0])
934                        else:
935                            raise service_error(service_error_internal,
936                                    "Unknown DRAGON type %s" % type)
937            return s
938
939        def not_dragon(e):
940            return all([not i.get_attribute('dragon_vlan') \
941                    for i in e.interface])
942
943        t = topo.clone()
944
945        # Weed out the things we aren't going to instantiate: Segments, portal
946        # substrates, and portal interfaces.  (The copy in the for loop allows
947        # us to delete from e.elements in side the for loop).
948        for e in [e for e in t.elements]:
949            if isinstance(e, topdl.Segment):
950                t.elements.remove(e)
951            if isinstance(e, topdl.Computer):
952                e.interface = [i for i in e.interface \
953                        if not i.get_attribute('portal') or \
954                            i.get_attribute('dragon_vlan')]
955        t.substrates = [ s.clone() for s in t.substrates ]
956        t.incorporate_elements()
957
958        if master: cmdname = 'MasterConnectorStartCmd'
959        else:cmdname = 'SlaveConnectorStartCmd'
960
961        # Localize the software locations and add
962        for e in t.elements:
963            for s in getattr(e, 'software', []):
964                s.location = re.sub("^.*/", softdir, s.location)
965            if isinstance(e, topdl.Computer) and e.get_attribute('portal'):
966                e.set_attribute('startup', self.attrs.get(cmdname))
967
968
969        # Customize the ns2 output for local portal commands and images
970        filters = []
971
972        if master: cmdname = 'MasterConnectorCmd'
973        else:cmdname = 'SlaveConnectorCmd'
974
975        if self.attrs.has_key('dragon'):
976            add_filter = not_dragon
977            filters.append(dragon_commands)
978        else:
979            add_filter = None
980
981        if self.attrs.has_key(cmdname):
982            filters.append(topdl.generate_portal_command_filter(
983                self.attrs.get(cmdname), add_filter=add_filter))
984
985        if self.attrs.has_key('connectorImage'):
986            filters.append(topdl.generate_portal_image_filter(
987                self.attrs.get('connectorImage')))
988
989        if self.attrs.has_key('connectorType'):
990            filters.append(topdl.generate_portal_hardware_filter(
991                self.attrs.get('connectorType')))
992
993        # Convert to ns and write it out
994        expfile = topdl.topology_to_ns2(t, filters)
995        try:
996            f = open(expfn, "w")
997            print >>f, expfile
998            f.close()
999        except IOError:
1000            raise service_error(service_error.internal,
1001                    "Cannot write experiment file %s: %s" % (expfn,e))
1002
1003    def StartSegment(self, req, fid):
1004        def get_url(url, cf, destdir):
1005            po = urlparse(url)
1006            fn = po.path.rpartition('/')[2]
1007            try:
1008                conn = httplib.HTTPSConnection(po.hostname, port=po.port, 
1009                        cert_file=cf, key_file=cf)
1010                conn.putrequest('GET', po.path)
1011                conn.endheaders()
1012                response = conn.getresponse()
1013
1014                lf = open("%s/%s" % (destdir, fn), "w")
1015                buf = response.read(4096)
1016                while buf:
1017                    lf.write(buf)
1018                    buf = response.read(4096)
1019                lf.close()
1020            except IOError, e:
1021                raise service_error(service_error.internal,
1022                        "Erro writing tempfile: %s" %e)
1023            except httplib.HTTPException, e:
1024                raise service_error(service_error.internal, 
1025                        "Error retrieving data: %s" % e)
1026
1027        configs = set(('hosts', 'ssh_pubkey', 'ssh_secretkey'))
1028
1029        err = None  # Any service_error generated after tmpdir is created
1030        rv = None   # Return value from segment creation
1031
1032        try:
1033            req = req['StartSegmentRequestBody']
1034        except KeyError:
1035            raise service_error(server_error.req, "Badly formed request")
1036
1037        auth_attr = req['allocID']['fedid']
1038        aid = "%s" % auth_attr
1039        attrs = req.get('fedAttr', [])
1040        if not self.auth.check_attribute(fid, auth_attr):
1041            raise service_error(service_error.access, "Access denied")
1042
1043        if req.has_key('segmentdescription') and \
1044                req['segmentdescription'].has_key('topdldescription'):
1045            topo = \
1046                topdl.Topology(**req['segmentdescription']['topdldescription'])
1047        else:
1048            raise service_error(service_error.req, 
1049                    "Request missing segmentdescription'")
1050
1051        master = req.get('master', False)
1052
1053        certfile = "%s/%s.pem" % (self.certdir, auth_attr)
1054        try:
1055            tmpdir = tempfile.mkdtemp(prefix="access-")
1056            softdir = "%s/software" % tmpdir
1057        except IOError:
1058            raise service_error(service_error.internal, "Cannot create tmp dir")
1059
1060        # Try block alllows us to clean up temporary files.
1061        try:
1062            sw = set()
1063            for e in topo.elements:
1064                for s in getattr(e, 'software', []):
1065                    sw.add(s.location)
1066            if len(sw) > 0:
1067                os.mkdir(softdir)
1068            for s in sw:
1069                print "%s %s %s" % ( s, certfile, softdir)
1070                get_url(s, certfile, softdir)
1071
1072            for a in attrs:
1073                if a['attribute'] in configs:
1074                    get_url(a['value'], certfile, tmpdir)
1075                if a['attribute'] == 'ssh_pubkey':
1076                    pubkey_base = a['value'].rpartition('/')[2]
1077                if a['attribute'] == 'ssh_secretkey':
1078                    secretkey_base = a['value'].rpartition('/')[2]
1079                if a['attribute'] == 'experiment_name':
1080                    ename = a['value']
1081
1082            proj = None
1083            user = None
1084            self.state_lock.acquire()
1085            if self.allocation.has_key(aid):
1086                proj = self.allocation[aid].get('project', None)
1087                if not proj: 
1088                    proj = self.allocation[aid].get('sproject', None)
1089                user = self.allocation[aid].get('user', None)
1090                self.allocation[aid]['experiment'] = ename
1091                self.allocation[aid]['log'] = [ ]
1092                # Create a logger that logs to the experiment's state object as
1093                # well as to the main log file.
1094                alloc_log = logging.getLogger('fedd.access.%s' % ename)
1095                h = logging.StreamHandler(
1096                        list_log.list_log(self.allocation[aid]['log']))
1097                # XXX: there should be a global one of these rather than
1098                # repeating the code.
1099                h.setFormatter(logging.Formatter(
1100                    "%(asctime)s %(name)s %(message)s",
1101                            '%d %b %y %H:%M:%S'))
1102                alloc_log.addHandler(h)
1103                self.write_state()
1104            self.state_lock.release()
1105
1106            if not proj:
1107                raise service_error(service_error.internal, 
1108                        "Can't find project for %s" %aid)
1109
1110            if not user:
1111                raise service_error(service_error.internal, 
1112                        "Can't find creation user for %s" %aid)
1113
1114            expfile = "%s/experiment.tcl" % tmpdir
1115
1116            self.generate_portal_configs(topo, pubkey_base, 
1117                    secretkey_base, tmpdir, master)
1118            self.generate_ns2(topo, expfile, 
1119                    "/proj/%s/software/%s/" % (proj, ename), master)
1120
1121            starter = self.start_segment(keyfile=self.ssh_privkey_file, 
1122                    debug=self.create_debug, log=alloc_log)
1123            rv = starter(self, ename, proj, user, expfile, tmpdir)
1124        except service_error, e:
1125            err = e
1126
1127        # Walk up tmpdir, deleting as we go
1128        if self.cleanup:
1129            self.log.debug("[StartSegment]: removing %s" % tmpdir)
1130            for path, dirs, files in os.walk(tmpdir, topdown=False):
1131                for f in files:
1132                    os.remove(os.path.join(path, f))
1133                for d in dirs:
1134                    os.rmdir(os.path.join(path, d))
1135            os.rmdir(tmpdir)
1136        else:
1137            self.log.debug("[StartSegment]: not removing %s" % tmpdir)
1138
1139        if rv:
1140            # Grab the log (this is some anal locking, but better safe than
1141            # sorry)
1142            self.state_lock.acquire()
1143            logv = "".join(self.allocation[aid]['log'])
1144            self.state_lock.release()
1145
1146            return { 'allocID': req['allocID'], 'allocationLog': logv }
1147        elif err:
1148            raise service_error(service_error.federant,
1149                    "Swapin failed: %s" % err)
1150        else:
1151            raise service_error(service_error.federant, "Swapin failed")
1152
1153    def TerminateSegment(self, req, fid):
1154        try:
1155            req = req['TerminateSegmentRequestBody']
1156        except KeyError:
1157            raise service_error(server_error.req, "Badly formed request")
1158
1159        auth_attr = req['allocID']['fedid']
1160        aid = "%s" % auth_attr
1161        attrs = req.get('fedAttr', [])
1162        if not self.auth.check_attribute(fid, auth_attr):
1163            raise service_error(service_error.access, "Access denied")
1164
1165        self.state_lock.acquire()
1166        if self.allocation.has_key(aid):
1167            proj = self.allocation[aid].get('project', None)
1168            if not proj: 
1169                proj = self.allocation[aid].get('sproject', None)
1170            user = self.allocation[aid].get('user', None)
1171            ename = self.allocation[aid].get('experiment', None)
1172        else:
1173            proj = None
1174            user = None
1175            ename = None
1176        self.state_lock.release()
1177
1178        if not proj:
1179            raise service_error(service_error.internal, 
1180                    "Can't find project for %s" % aid)
1181
1182        if not user:
1183            raise service_error(service_error.internal, 
1184                    "Can't find creation user for %s" % aid)
1185        if not ename:
1186            raise service_error(service_error.internal, 
1187                    "Can't find experiment name for %s" % aid)
1188        stopper = self.stop_segment(keyfile=self.ssh_privkey_file,
1189                debug=self.create_debug)
1190        stopper(self, user, proj, ename)
1191        return { 'allocID': req['allocID'] }
Note: See TracBrowser for help on using the repository browser.