source: fedd/federation/emulab_access.py @ 874e015

version-2.00
Last change on this file since 874e015 was 6e44258, checked in by Ted Faber <faber@…>, 15 years ago

that shouldn't be hard coded

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