source: fedd/federation/emulab_access.py @ 35aa3ae

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

move the startcmd into the first lop

  • Property mode set to 100644
File size: 45.1 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        # The startcmds for master and slave testbeds
946        if master: 
947            gate_cmd = self.attrs.get('MasterConnectorStartCmd', '/bin/true')
948            node_cmd = self.attrs.get('MasterNodeStartCmd', 'bin/true')
949        else: 
950            gate_cmd = self.attrs.get('SlaveConnectorStartCmd', '/bin/true')
951            node_cmd = self.attrs.get('SlaveNodeStartCmd', 'bin/true')
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).  While we're
956        # touching all the elements, we also adjust paths from the original
957        # testbed to local testbed paths and put the federation commands into
958        # the start commands
959        for e in [e for e in t.elements]:
960            if isinstance(e, topdl.Segment):
961                t.elements.remove(e)
962            # Fix software paths
963            for s in getattr(e, 'software', []):
964                s.location = re.sub("^.*/", softdir, s.location)
965            if isinstance(e, topdl.Computer):
966                if e.get_attribute('portal') and gate_cmd:
967                    # Portals never have a user-specified start command
968                    e.set_attribute('startup', gate_cmd)
969                elif node_cmd:
970                    if e.get_attribute('startup'):
971                        s.set_attribute('startup', "%s \\$USER '%s'" % \
972                                (node_cmd, e.get_attribute('startup')))
973                    else:
974                        s.set_attribute('startup', node_cmd)
975
976                # Remove portal interfaces that do not connect to DRAGON
977                e.interface = [i for i in e.interface \
978                        if not i.get_attribute('portal') or \
979                            i.get_attribute('dragon_vlan')]
980
981        t.substrates = [ s.clone() for s in t.substrates ]
982        t.incorporate_elements()
983
984        # Customize the ns2 output for local portal commands and images
985        filters = []
986
987        # NB: these are extra commands issued for the node, not the startcmds
988        if master: cmd = self.attrs.get('MasterConnectorCmd', '')
989        else: cmd = self.attrs.get('SlaveConnectorCmd', '')
990
991        if self.attrs.has_key('dragon'):
992            add_filter = not_dragon
993            filters.append(dragon_commands)
994        else:
995            add_filter = None
996
997        if cmd:
998            filters.append(topdl.generate_portal_command_filter(cmd,
999                add_filter=add_filter))
1000
1001        if self.attrs.has_key('connectorImage'):
1002            filters.append(topdl.generate_portal_image_filter(
1003                self.attrs.get('connectorImage')))
1004
1005        if self.attrs.has_key('connectorType'):
1006            filters.append(topdl.generate_portal_hardware_filter(
1007                self.attrs.get('connectorType')))
1008
1009        # Convert to ns and write it out
1010        expfile = topdl.topology_to_ns2(t, filters)
1011        try:
1012            f = open(expfn, "w")
1013            print >>f, expfile
1014            f.close()
1015        except IOError:
1016            raise service_error(service_error.internal,
1017                    "Cannot write experiment file %s: %s" % (expfn,e))
1018
1019    def StartSegment(self, req, fid):
1020        def get_url(url, cf, destdir):
1021            po = urlparse(url)
1022            fn = po.path.rpartition('/')[2]
1023            try:
1024                conn = httplib.HTTPSConnection(po.hostname, port=po.port, 
1025                        cert_file=cf, key_file=cf)
1026                conn.putrequest('GET', po.path)
1027                conn.endheaders()
1028                response = conn.getresponse()
1029
1030                lf = open("%s/%s" % (destdir, fn), "w")
1031                buf = response.read(4096)
1032                while buf:
1033                    lf.write(buf)
1034                    buf = response.read(4096)
1035                lf.close()
1036            except IOError, e:
1037                raise service_error(service_error.internal,
1038                        "Erro writing tempfile: %s" %e)
1039            except httplib.HTTPException, e:
1040                raise service_error(service_error.internal, 
1041                        "Error retrieving data: %s" % e)
1042
1043        configs = set(('hosts', 'ssh_pubkey', 'ssh_secretkey'))
1044
1045        err = None  # Any service_error generated after tmpdir is created
1046        rv = None   # Return value from segment creation
1047
1048        try:
1049            req = req['StartSegmentRequestBody']
1050        except KeyError:
1051            raise service_error(server_error.req, "Badly formed request")
1052
1053        auth_attr = req['allocID']['fedid']
1054        aid = "%s" % auth_attr
1055        attrs = req.get('fedAttr', [])
1056        if not self.auth.check_attribute(fid, auth_attr):
1057            raise service_error(service_error.access, "Access denied")
1058
1059        if req.has_key('segmentdescription') and \
1060                req['segmentdescription'].has_key('topdldescription'):
1061            topo = \
1062                topdl.Topology(**req['segmentdescription']['topdldescription'])
1063        else:
1064            raise service_error(service_error.req, 
1065                    "Request missing segmentdescription'")
1066
1067        master = req.get('master', False)
1068
1069        certfile = "%s/%s.pem" % (self.certdir, auth_attr)
1070        try:
1071            tmpdir = tempfile.mkdtemp(prefix="access-")
1072            softdir = "%s/software" % tmpdir
1073        except IOError:
1074            raise service_error(service_error.internal, "Cannot create tmp dir")
1075
1076        # Try block alllows us to clean up temporary files.
1077        try:
1078            sw = set()
1079            for e in topo.elements:
1080                for s in getattr(e, 'software', []):
1081                    sw.add(s.location)
1082            if len(sw) > 0:
1083                os.mkdir(softdir)
1084            for s in sw:
1085                print "%s %s %s" % ( s, certfile, softdir)
1086                get_url(s, certfile, softdir)
1087
1088            for a in attrs:
1089                if a['attribute'] in configs:
1090                    get_url(a['value'], certfile, tmpdir)
1091                if a['attribute'] == 'ssh_pubkey':
1092                    pubkey_base = a['value'].rpartition('/')[2]
1093                if a['attribute'] == 'ssh_secretkey':
1094                    secretkey_base = a['value'].rpartition('/')[2]
1095                if a['attribute'] == 'experiment_name':
1096                    ename = a['value']
1097
1098            proj = None
1099            user = None
1100            self.state_lock.acquire()
1101            if self.allocation.has_key(aid):
1102                proj = self.allocation[aid].get('project', None)
1103                if not proj: 
1104                    proj = self.allocation[aid].get('sproject', None)
1105                user = self.allocation[aid].get('user', None)
1106                self.allocation[aid]['experiment'] = ename
1107                self.allocation[aid]['log'] = [ ]
1108                # Create a logger that logs to the experiment's state object as
1109                # well as to the main log file.
1110                alloc_log = logging.getLogger('fedd.access.%s' % ename)
1111                h = logging.StreamHandler(
1112                        list_log.list_log(self.allocation[aid]['log']))
1113                # XXX: there should be a global one of these rather than
1114                # repeating the code.
1115                h.setFormatter(logging.Formatter(
1116                    "%(asctime)s %(name)s %(message)s",
1117                            '%d %b %y %H:%M:%S'))
1118                alloc_log.addHandler(h)
1119                self.write_state()
1120            self.state_lock.release()
1121
1122            if not proj:
1123                raise service_error(service_error.internal, 
1124                        "Can't find project for %s" %aid)
1125
1126            if not user:
1127                raise service_error(service_error.internal, 
1128                        "Can't find creation user for %s" %aid)
1129
1130            expfile = "%s/experiment.tcl" % tmpdir
1131
1132            self.generate_portal_configs(topo, pubkey_base, 
1133                    secretkey_base, tmpdir, master)
1134            self.generate_ns2(topo, expfile, 
1135                    "/proj/%s/software/%s/" % (proj, ename), master)
1136
1137            starter = self.start_segment(keyfile=self.ssh_privkey_file, 
1138                    debug=self.create_debug, log=alloc_log)
1139            rv = starter(self, ename, proj, user, expfile, tmpdir)
1140        except service_error, e:
1141            err = e
1142
1143        # Walk up tmpdir, deleting as we go
1144        if self.cleanup:
1145            self.log.debug("[StartSegment]: removing %s" % tmpdir)
1146            for path, dirs, files in os.walk(tmpdir, topdown=False):
1147                for f in files:
1148                    os.remove(os.path.join(path, f))
1149                for d in dirs:
1150                    os.rmdir(os.path.join(path, d))
1151            os.rmdir(tmpdir)
1152        else:
1153            self.log.debug("[StartSegment]: not removing %s" % tmpdir)
1154
1155        if rv:
1156            # Grab the log (this is some anal locking, but better safe than
1157            # sorry)
1158            self.state_lock.acquire()
1159            logv = "".join(self.allocation[aid]['log'])
1160            self.state_lock.release()
1161
1162            return { 'allocID': req['allocID'], 'allocationLog': logv }
1163        elif err:
1164            raise service_error(service_error.federant,
1165                    "Swapin failed: %s" % err)
1166        else:
1167            raise service_error(service_error.federant, "Swapin failed")
1168
1169    def TerminateSegment(self, req, fid):
1170        try:
1171            req = req['TerminateSegmentRequestBody']
1172        except KeyError:
1173            raise service_error(server_error.req, "Badly formed request")
1174
1175        auth_attr = req['allocID']['fedid']
1176        aid = "%s" % auth_attr
1177        attrs = req.get('fedAttr', [])
1178        if not self.auth.check_attribute(fid, auth_attr):
1179            raise service_error(service_error.access, "Access denied")
1180
1181        self.state_lock.acquire()
1182        if self.allocation.has_key(aid):
1183            proj = self.allocation[aid].get('project', None)
1184            if not proj: 
1185                proj = self.allocation[aid].get('sproject', None)
1186            user = self.allocation[aid].get('user', None)
1187            ename = self.allocation[aid].get('experiment', None)
1188        else:
1189            proj = None
1190            user = None
1191            ename = None
1192        self.state_lock.release()
1193
1194        if not proj:
1195            raise service_error(service_error.internal, 
1196                    "Can't find project for %s" % aid)
1197
1198        if not user:
1199            raise service_error(service_error.internal, 
1200                    "Can't find creation user for %s" % aid)
1201        if not ename:
1202            raise service_error(service_error.internal, 
1203                    "Can't find experiment name for %s" % aid)
1204        stopper = self.stop_segment(keyfile=self.ssh_privkey_file,
1205                debug=self.create_debug)
1206        stopper(self, user, proj, ename)
1207        return { 'allocID': req['allocID'] }
Note: See TracBrowser for help on using the repository browser.