source: fedd/federation/emulab_access.py @ e02cd14

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

Big hunk of the move to services and connectivity descriptors.

Config files are now generated from those params rather than from attributes on topdl nodes (mostly).

This is still quick and dirty in places.

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