source: fedd/federation/emulab_access.py @ 46bc576

axis_examplecompt_changesinfo-opsversion-2.00version-3.01version-3.02
Last change on this file since 46bc576 was 69692a9, checked in by Ted Faber <faber@…>, 15 years ago

Looks like Dragon is being called correctly. Internals remain a bit messy.

  • Property mode set to 100644
File size: 44.5 KB
Line 
1#!/usr/local/bin/python
2
3import os,sys
4import re
5import string
6import copy
7import pickle
8import logging
9
10from threading import *
11
12from util import *
13from allocate_project import allocate_project_local, allocate_project_remote
14from access_project import access_project
15from fedid import fedid, generate_fedid
16from authorizer import authorizer
17from service_error import service_error
18from remote_service import xmlrpc_handler, soap_handler, service_caller
19
20import httplib
21import tempfile
22from urlparse import urlparse
23
24import topdl
25import list_log
26import proxy_emulab_segment
27import local_emulab_segment
28
29
30# Make log messages disappear if noone configures a fedd logger
31class nullHandler(logging.Handler):
32    def emit(self, record): pass
33
34fl = logging.getLogger("fedd.access")
35fl.addHandler(nullHandler())
36
37class access:
38    """
39    The implementation of access control based on mapping users to projects.
40
41    Users can be mapped to existing projects or have projects created
42    dynamically.  This implements both direct requests and proxies.
43    """
44
45    class parse_error(RuntimeError): pass
46
47
48    proxy_RequestAccess= service_caller('RequestAccess')
49    proxy_ReleaseAccess= service_caller('ReleaseAccess')
50
51    def __init__(self, config=None, auth=None):
52        """
53        Initializer.  Pulls parameters out of the ConfigParser's access section.
54        """
55
56        # Make sure that the configuration is in place
57        if not config: 
58            raise RunTimeError("No config to fedd.access")
59
60        self.project_priority = config.getboolean("access", "project_priority")
61        self.allow_proxy = config.getboolean("access", "allow_proxy")
62
63        self.boss = config.get("access", "boss")
64        self.ops = config.get("access", "ops")
65        self.domain = config.get("access", "domain")
66        self.fileserver = config.get("access", "fileserver")
67        self.eventserver = config.get("access", "eventserver")
68        self.certdir = config.get("access","certdir")
69        self.ssh_privkey_file = config.get("access","ssh_privkey_file")
70        self.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            rexp = seg.get_attribute('experiment')
849            rproj, reid = rexp.split("/", 1)
850            rdomain = seg.get_attribute('domain')
851            cfn = "%s/%s.%s.%s%s.gw.conf" % \
852                    (tmpdir, myname.lower(), leid.lower(),
853                            lproj.lower(), ldomain.lower())
854            tunnelconfig = self.attrs.has_key('TunnelCfg')
855            try:
856                f = open(cfn, "w")
857                print >>f, "Active: %s" % active
858                print >>f, "TunnelCfg: %s" % tunnelconfig
859                print >>f, "BossName: boss"
860                print >>f, "FsName: fs"
861                print >>f, "EventServerName: event-server%s" % ldomain
862                print >>f, "RemoteEventServerName: event-server%s" % rdomain
863                print >>f, "SeerControl: control.%s.%s%s" % \
864                        (meid.lower(), mproj.lower(), mdomain)
865                print >>f, "Type: %s" % type
866                print >>f, "RemoteExperiment: %s" % rexp
867                print >>f, "LocalExperiment: %s" % lexp
868                print >>f, "RemoteConfigFile: " + \
869                        "/proj/%s/exp/%s/tmp/%s.%s.%s%s.gw.conf" \
870                        % (rproj, reid, peer.lower(), reid.lower(),
871                                rproj.lower(), rdomain)
872                print >>f, "Peer: %s.%s.%s%s" % \
873                        (peer.lower(), reid.lower(), rproj.lower(), rdomain)
874                print >>f, "RemoteScriptDir: %s" % scriptdir
875                print >>f, "Pubkeys: /proj/%s/exp/%s/tmp/%s" % \
876                        (lproj, leid, pubkey_base)
877                print >>f, "Privkeys: /proj/%s/exp/%s/tmp/%s" % \
878                        (lproj, leid, secretkey_base)
879                f.close()
880            except IOError, e:
881                raise service_error(service_error.internal,
882                        "Can't write protal config %s: %s" % (cfn, e))
883           
884            # XXX: This little seer config file needs to go away.
885            if not seer_out:
886                try:
887                    seerfn = "%s/seer.conf" % tmpdir
888                    f = open(seerfn, "w")
889                    if not master:
890                        print >>f, "ControlNode: control.%s.%s%s" % \
891                            (meid.lower(), mproj.lower(), mdomain)
892                    print >>f, "ExperimentID: %s" % mexp
893                    f.close()
894                except IOError, e:
895                    raise service_error(service_error.internal, 
896                            "Can't write seer.conf: %s" %e)
897                seer_out = True
898
899            if not client_out and type in ('control', 'both'):
900                try:
901                    f = open("%s/client.conf" % tmpdir, "w")
902                    print >>f, "ControlGateway: %s.%s.%s%s" % \
903                        (myname.lower(), leid.lower(), lproj.lower(),
904                                ldomain.lower())
905                    print >>f, "SMBshare: %s" % smbshare
906                    print >>f, "ProjectUser: %s" % muser
907                    print >>f, "ProjectName: %s" % mproj
908                    print >>f, "ExperimentID: %s/%s" % (mproj, meid)
909                    f.close()
910                except IOError, e:
911                    raise service_error(service_error.internal,
912                            "Cannot write client.conf: %s" %s)
913                client_out = True
914
915
916
917    def generate_ns2(self, topo, expfn, softdir, master):
918        def dragon_commands(e):
919            s = ""
920            if isinstance(e, topdl.Computer):
921                for i in e.interface:
922                    vlan = i.get_attribute('dragon_vlan')
923                    if vlan:
924                        type = i.get_attribute('dragon_type')
925                        ip = i.get_attribute('ip4_address')
926                        if type == 'link':
927                            s = ("tb-allow-external $%s dragonportal " + \
928                                    "ip %s vlan %s\n") % \
929                                    (topdl.to_tcl_name(e.name[0]), ip, vlan)
930                        elif type == 'lan':
931                            s = ("tb-allow-external $%s dragonportal " + \
932                                    "ip %s vlan %s usurp %s\n") % \
933                                    (topdl.to_tcl_name(e.name[0]), ip, vlan,
934                                        i.substrate[0])
935                        else:
936                            raise service_error(service_error_internal,
937                                    "Unknown DRAGON type %s" % type)
938            return s
939
940        def not_dragon(e):
941            return all([not i.get_attribute('dragon_vlan') \
942                    for i in e.interface])
943
944        t = topo.clone()
945
946        # Weed out the things we aren't going to instantiate: Segments, portal
947        # substrates, and portal interfaces.  (The copy in the for loop allows
948        # us to delete from e.elements in side the for loop).
949        for e in [e for e in t.elements]:
950            if isinstance(e, topdl.Segment):
951                t.elements.remove(e)
952            if isinstance(e, topdl.Computer):
953                e.interface = [i for i in e.interface \
954                        if not i.get_attribute('portal') or \
955                            i.get_attribute('dragon_vlan')]
956        t.substrates = [ s.clone() for s in t.substrates ]
957        #t.substrates = [ s for s in t.substrates \
958        #       if not s.get_attribute('portal')]
959        t.incorporate_elements()
960
961        # Localize the software locations
962        for e in t.elements:
963            for s in getattr(e, 'software', []):
964                s.location = re.sub("^.*/", softdir, s.location)
965
966        # Customize the ns2 output for local portal commands and images
967        filters = []
968
969        if master: cmdname = 'MasterConnectorCmd'
970        else:cmdname = 'SlaveConnectorCmd'
971
972        if self.attrs.has_key('dragon'):
973            add_filter = not_dragon
974            filters.append(dragon_commands)
975        else:
976            add_filter = None
977
978        if self.attrs.has_key(cmdname):
979            filters.append(topdl.generate_portal_command_filter(
980                self.attrs.get(cmdname), add_filter=add_filter))
981
982        if self.attrs.has_key('connectorImage'):
983            filters.append(topdl.generate_portal_image_filter(
984                self.attrs.get('connectorImage')))
985
986        if self.attrs.has_key('connectorType'):
987            filters.append(topdl.generate_portal_hardware_filter(
988                self.attrs.get('connectorType')))
989
990        # Convert to ns and write it out
991        expfile = topdl.topology_to_ns2(t, filters)
992        try:
993            f = open(expfn, "w")
994            print >>f, expfile
995            f.close()
996        except IOError:
997            raise service_error(service_error.internal,
998                    "Cannot write experiment file %s: %s" % (expfn,e))
999
1000    def StartSegment(self, req, fid):
1001        def get_url(url, cf, destdir):
1002            po = urlparse(url)
1003            fn = po.path.rpartition('/')[2]
1004            try:
1005                conn = httplib.HTTPSConnection(po.hostname, port=po.port, 
1006                        cert_file=cf, key_file=cf)
1007                conn.putrequest('GET', po.path)
1008                conn.endheaders()
1009                response = conn.getresponse()
1010
1011                lf = open("%s/%s" % (destdir, fn), "w")
1012                buf = response.read(4096)
1013                while buf:
1014                    lf.write(buf)
1015                    buf = response.read(4096)
1016                lf.close()
1017            except IOError, e:
1018                raise service_error(service_error.internal,
1019                        "Erro writing tempfile: %s" %e)
1020            except httplib.HTTPException, e:
1021                raise service_error(service_error.internal, 
1022                        "Error retrieving data: %s" % e)
1023
1024        configs = set(('hosts', 'ssh_pubkey', 'ssh_secretkey'))
1025
1026        err = None  # Any service_error generated after tmpdir is created
1027        rv = None   # Return value from segment creation
1028
1029        try:
1030            req = req['StartSegmentRequestBody']
1031        except KeyError:
1032            raise service_error(server_error.req, "Badly formed request")
1033
1034        auth_attr = req['allocID']['fedid']
1035        aid = "%s" % auth_attr
1036        attrs = req.get('fedAttr', [])
1037        if not self.auth.check_attribute(fid, auth_attr):
1038            raise service_error(service_error.access, "Access denied")
1039
1040        if req.has_key('segmentdescription') and \
1041                req['segmentdescription'].has_key('topdldescription'):
1042            topo = \
1043                topdl.Topology(**req['segmentdescription']['topdldescription'])
1044        else:
1045            raise service_error(service_error.req, 
1046                    "Request missing segmentdescription'")
1047
1048        master = req.get('master', False)
1049
1050        certfile = "%s/%s.pem" % (self.certdir, auth_attr)
1051        try:
1052            tmpdir = tempfile.mkdtemp(prefix="access-")
1053            softdir = "%s/software" % tmpdir
1054        except IOError:
1055            raise service_error(service_error.internal, "Cannot create tmp dir")
1056
1057        # Try block alllows us to clean up temporary files.
1058        try:
1059            sw = set()
1060            for e in topo.elements:
1061                for s in getattr(e, 'software', []):
1062                    sw.add(s.location)
1063            if len(sw) > 0:
1064                os.mkdir(softdir)
1065            for s in sw:
1066                get_url(s, certfile, softdir)
1067
1068            for a in attrs:
1069                if a['attribute'] in configs:
1070                    get_url(a['value'], certfile, tmpdir)
1071                if a['attribute'] == 'ssh_pubkey':
1072                    pubkey_base = a['value'].rpartition('/')[2]
1073                if a['attribute'] == 'ssh_secretkey':
1074                    secretkey_base = a['value'].rpartition('/')[2]
1075                if a['attribute'] == 'experiment_name':
1076                    ename = a['value']
1077
1078            proj = None
1079            user = None
1080            self.state_lock.acquire()
1081            if self.allocation.has_key(aid):
1082                proj = self.allocation[aid].get('project', None)
1083                if not proj: 
1084                    proj = self.allocation[aid].get('sproject', None)
1085                user = self.allocation[aid].get('user', None)
1086                self.allocation[aid]['experiment'] = ename
1087                self.allocation[aid]['log'] = [ ]
1088                # Create a logger that logs to the experiment's state object as
1089                # well as to the main log file.
1090                alloc_log = logging.getLogger('fedd.access.%s' % ename)
1091                h = logging.StreamHandler(
1092                        list_log.list_log(self.allocation[aid]['log']))
1093                # XXX: there should be a global one of these rather than
1094                # repeating the code.
1095                h.setFormatter(logging.Formatter(
1096                    "%(asctime)s %(name)s %(message)s",
1097                            '%d %b %y %H:%M:%S'))
1098                alloc_log.addHandler(h)
1099                self.write_state()
1100            self.state_lock.release()
1101
1102            if not proj:
1103                raise service_error(service_error.internal, 
1104                        "Can't find project for %s" %aid)
1105
1106            if not user:
1107                raise service_error(service_error.internal, 
1108                        "Can't find creation user for %s" %aid)
1109
1110            expfile = "%s/experiment.tcl" % tmpdir
1111
1112            self.generate_portal_configs(topo, pubkey_base, 
1113                    secretkey_base, tmpdir, master)
1114            self.generate_ns2(topo, expfile, 
1115                    "/proj/%s/software/%s/" % (proj, ename), master)
1116
1117            starter = self.start_segment(keyfile=self.ssh_privkey_file, 
1118                    debug=self.create_debug, log=alloc_log)
1119            rv = starter(self, ename, proj, user, expfile, tmpdir)
1120        except service_error, e:
1121            err = e
1122
1123        # Walk up tmpdir, deleting as we go
1124        if self.cleanup:
1125            self.log.debug("[StartSegment]: removing %s" % tmpdir)
1126            for path, dirs, files in os.walk(tmpdir, topdown=False):
1127                for f in files:
1128                    os.remove(os.path.join(path, f))
1129                for d in dirs:
1130                    os.rmdir(os.path.join(path, d))
1131            os.rmdir(tmpdir)
1132        else:
1133            self.log.debug("[StartSegment]: not removing %s" % tmpdir)
1134
1135        if rv:
1136            # Grab the log (this is some anal locking, but better safe than
1137            # sorry)
1138            self.state_lock.acquire()
1139            logv = "".join(self.allocation[aid]['log'])
1140            self.state_lock.release()
1141
1142            return { 'allocID': req['allocID'], 'allocationLog': logv }
1143        elif err:
1144            raise service_error(service_error.federant,
1145                    "Swapin failed: %s" % err)
1146        else:
1147            raise service_error(service_error.federant, "Swapin failed")
1148
1149    def TerminateSegment(self, req, fid):
1150        try:
1151            req = req['TerminateSegmentRequestBody']
1152        except KeyError:
1153            raise service_error(server_error.req, "Badly formed request")
1154
1155        auth_attr = req['allocID']['fedid']
1156        aid = "%s" % auth_attr
1157        attrs = req.get('fedAttr', [])
1158        if not self.auth.check_attribute(fid, auth_attr):
1159            raise service_error(service_error.access, "Access denied")
1160
1161        self.state_lock.acquire()
1162        if self.allocation.has_key(aid):
1163            proj = self.allocation[aid].get('project', None)
1164            if not proj: 
1165                proj = self.allocation[aid].get('sproject', None)
1166            user = self.allocation[aid].get('user', None)
1167            ename = self.allocation[aid].get('experiment', None)
1168        else:
1169            proj = None
1170            user = None
1171            ename = None
1172        self.state_lock.release()
1173
1174        if not proj:
1175            raise service_error(service_error.internal, 
1176                    "Can't find project for %s" % aid)
1177
1178        if not user:
1179            raise service_error(service_error.internal, 
1180                    "Can't find creation user for %s" % aid)
1181        if not ename:
1182            raise service_error(service_error.internal, 
1183                    "Can't find experiment name for %s" % aid)
1184        stopper = self.stop_segment(keyfile=self.ssh_privkey_file,
1185                debug=self.create_debug)
1186        stopper(self, user, proj, ename)
1187        return { 'allocID': req['allocID'] }
Note: See TracBrowser for help on using the repository browser.