source: fedd/federation/emulab_access.py @ 1f356d3

axis_examplecompt_changesinfo-ops
Last change on this file since 1f356d3 was dee164e, checked in by Ted Faber <faber@…>, 14 years ago

Looks like internal works now.

Had to add default entries to the access list to accomodate that, and discovered that ABAC requires strings - not unicode.

Moved lookup_access into the aceess class as most should be able to use it directly now.

  • Property mode set to 100644
File size: 37.9 KB
RevLine 
[19cc408]1#!/usr/local/bin/python
2
3import os,sys
[eeb0088]4import stat # for chmod constants
[19cc408]5import re
[ab847bc]6import random
[19cc408]7import string
8import copy
[d81971a]9import pickle
[c971895]10import logging
[eeb0088]11import subprocess
[06cc65b]12import traceback
[19cc408]13
[f8582c9]14from threading import *
[8e6fe4d]15from M2Crypto.SSL import SSLError
[f8582c9]16
[f771e2f]17from access import access_base
[78f2668]18from legacy_access import legacy_access
[f771e2f]19
[ec4fb42]20from util import *
21from allocate_project import allocate_project_local, allocate_project_remote
[51cc9df]22from fedid import fedid, generate_fedid
[6e63513]23from authorizer import authorizer, abac_authorizer
[6a0c9f4]24from service_error import service_error
[9460b1e]25from remote_service import xmlrpc_handler, soap_handler, service_caller
[11a08b0]26
[6c57fe9]27import httplib
28import tempfile
29from urlparse import urlparse
30
[11860f52]31import topdl
32import list_log
33import proxy_emulab_segment
34import local_emulab_segment
35
[0ea11af]36
37# Make log messages disappear if noone configures a fedd logger
[11a08b0]38class nullHandler(logging.Handler):
39    def emit(self, record): pass
40
41fl = logging.getLogger("fedd.access")
42fl.addHandler(nullHandler())
[19cc408]43
[78f2668]44class access(access_base, legacy_access):
[19cc408]45    """
46    The implementation of access control based on mapping users to projects.
47
48    Users can be mapped to existing projects or have projects created
49    dynamically.  This implements both direct requests and proxies.
50    """
51
[3f6bc5f]52    def __init__(self, config=None, auth=None):
[866c983]53        """
54        Initializer.  Pulls parameters out of the ConfigParser's access section.
55        """
56
[f771e2f]57        access_base.__init__(self, config, auth)
[866c983]58
59        self.allow_proxy = config.getboolean("access", "allow_proxy")
60
61        self.domain = config.get("access", "domain")
[eeb0088]62        self.userconfdir = config.get("access","userconfdir")
63        self.userconfcmd = config.get("access","userconfcmd")
[fe28bb2]64        self.userconfurl = config.get("access","userconfurl")
[9b3627e]65        self.federation_software = config.get("access", "federation_software")
66        self.portal_software = config.get("access", "portal_software")
[e76f38a]67        self.local_seer_software = config.get("access", "local_seer_software")
68        self.local_seer_image = config.get("access", "local_seer_image")
69        self.local_seer_start = config.get("access", "local_seer_start")
[49051fb]70        self.seer_master_start = config.get("access", "seer_master_start")
[ecca6eb]71        self.ssh_privkey_file = config.get("access","ssh_privkey_file")
[3bddd24]72        self.ssh_pubkey_file = config.get("access","ssh_pubkey_file")
[6280b1f]73        self.ssh_port = config.get("access","ssh_port") or "22"
[181aeb4]74        self.boss = config.get("access", "boss")
[814b5e5]75        self.ops = config.get("access", "ops")
[181aeb4]76        self.xmlrpc_cert = config.get("access", "xmlrpc_cert")
77        self.xmlrpc_certpw = config.get("access", "xmlrpc_certpw")
[5e1fb7b]78
79        self.dragon_endpoint = config.get("access", "dragon")
80        self.dragon_vlans = config.get("access", "dragon_vlans")
81        self.deter_internal = config.get("access", "deter_internal")
82
83        self.tunnel_config = config.getboolean("access", "tunnel_config")
84        self.portal_command = config.get("access", "portal_command")
85        self.portal_image = config.get("access", "portal_image")
86        self.portal_type = config.get("access", "portal_type") or "pc"
87        self.portal_startcommand = config.get("access", "portal_startcommand")
88        self.node_startcommand = config.get("access", "node_startcommand")
89
[f771e2f]90        self.federation_software = self.software_list(self.federation_software)
91        self.portal_software = self.software_list(self.portal_software)
92        self.local_seer_software = self.software_list(self.local_seer_software)
[11860f52]93
94        self.access_type = self.access_type.lower()
95        if self.access_type == 'remote_emulab':
96            self.start_segment = proxy_emulab_segment.start_segment
97            self.stop_segment = proxy_emulab_segment.stop_segment
98        elif self.access_type == 'local_emulab':
99            self.start_segment = local_emulab_segment.start_segment
100            self.stop_segment = local_emulab_segment.stop_segment
101        else:
102            self.start_segment = None
103            self.stop_segment = None
[866c983]104
105        self.restricted = [ ]
[6e63513]106        # XXX: this should go?
107        #if config.has_option("access", "accessdb"):
108        #    self.read_access(config.get("access", "accessdb"))
[866c983]109        tb = config.get('access', 'testbed')
110        if tb: self.testbed = [ t.strip() for t in tb.split(',') ]
111        else: self.testbed = [ ]
112
[6e63513]113        # authorization information
114        self.auth_type = config.get('access', 'auth_type') \
115                or 'legacy'
116        self.auth_dir = config.get('access', 'auth_dir')
117        accessdb = config.get("access", "accessdb")
118        # initialize the authorization system
119        if self.auth_type == 'legacy':
[c002cb2]120            self.access = { }
[6e63513]121            if accessdb:
[78f2668]122                self.legacy_read_access(accessdb, self.legacy_access_tuple)
[6e63513]123        elif self.auth_type == 'abac':
124            self.auth = abac_authorizer(load=self.auth_dir)
[c002cb2]125            self.access = [ ]
[6e63513]126            if accessdb:
[78f2668]127                self.read_access(accessdb, self.access_tuple)
[6e63513]128        else:
129            raise service_error(service_error.internal, 
130                    "Unknown auth_type: %s" % self.auth_type)
[f771e2f]131
[06cc65b]132        # read_state in the base_class
[f771e2f]133        self.state_lock.acquire()
[06cc65b]134        for a  in ('allocation', 'projects', 'keys', 'types'):
135            if a not in self.state:
136                self.state[a] = { }
[f771e2f]137        self.allocation = self.state['allocation']
138        self.projects = self.state['projects']
139        self.keys = self.state['keys']
140        self.types = self.state['types']
[6e63513]141        if self.auth_type == "legacy": 
142            # Add the ownership attributes to the authorizer.  Note that the
143            # indices of the allocation dict are strings, but the attributes are
144            # fedids, so there is a conversion.
145            for k in self.allocation.keys():
146                for o in self.allocation[k].get('owners', []):
147                    self.auth.set_attribute(o, fedid(hexstr=k))
148                if self.allocation[k].has_key('userconfig'):
149                    sfid = self.allocation[k]['userconfig']
150                    fid = fedid(hexstr=sfid)
151                    self.auth.set_attribute(fid, "/%s" % sfid)
[f771e2f]152        self.state_lock.release()
[a20a20f]153        self.exports = {
154                'SMB': self.export_SMB,
155                'seer': self.export_seer,
156                'tmcd': self.export_tmcd,
157                'userconfig': self.export_userconfig,
158                'project_export': self.export_project_export,
159                'local_seer_control': self.export_local_seer,
160                'seer_master': self.export_seer_master,
161                'hide_hosts': self.export_hide_hosts,
162                }
163
164        if not self.local_seer_image or not self.local_seer_software or \
165                not self.local_seer_start:
166            if 'local_seer_control' in self.exports:
167                del self.exports['local_seer_control']
168
169        if not self.local_seer_image or not self.local_seer_software or \
170                not self.seer_master_start:
171            if 'seer_master' in self.exports:
172                del self.exports['seer_master']
[866c983]173
174
175        self.soap_services = {\
176            'RequestAccess': soap_handler("RequestAccess", self.RequestAccess),
177            'ReleaseAccess': soap_handler("ReleaseAccess", self.ReleaseAccess),
[cc8d8e9]178            'StartSegment': soap_handler("StartSegment", self.StartSegment),
[e76f38a]179            'TerminateSegment': soap_handler("TerminateSegment",
180                self.TerminateSegment),
[866c983]181            }
182        self.xmlrpc_services =  {\
183            'RequestAccess': xmlrpc_handler('RequestAccess',
184                self.RequestAccess),
185            'ReleaseAccess': xmlrpc_handler('ReleaseAccess',
186                self.ReleaseAccess),
[5ae3857]187            'StartSegment': xmlrpc_handler("StartSegment", self.StartSegment),
188            'TerminateSegment': xmlrpc_handler('TerminateSegment',
189                self.TerminateSegment),
[866c983]190            }
191
[2761484]192        self.call_SetValue = service_caller('SetValue')
[2ee4226]193        self.call_GetValue = service_caller('GetValue', log=self.log)
[866c983]194
195        if not config.has_option("allocate", "uri"):
196            self.allocate_project = \
197                allocate_project_local(config, auth)
198        else:
199            self.allocate_project = \
200                allocate_project_remote(config, auth)
201
[e76f38a]202
[866c983]203        # If the project allocator exports services, put them in this object's
204        # maps so that classes that instantiate this can call the services.
205        self.soap_services.update(self.allocate_project.soap_services)
206        self.xmlrpc_services.update(self.allocate_project.xmlrpc_services)
[f8582c9]207
[e76f38a]208    @staticmethod
[78f2668]209    def legacy_access_tuple(str):
[06cc65b]210        """
[027b87b]211        Convert a string of the form (id[:resources:resouces], id, id) into a
212        tuple of the form (project, user, user) where users may be names or
213        fedids.  The resources strings are obsolete and ignored.
[06cc65b]214        """
[866c983]215        def parse_name(n):
216            if n.startswith('fedid:'): return fedid(hexstr=n[len('fedid:'):])
217            else: return n
[19cc408]218
[f771e2f]219        str = str.strip()
220        if str.startswith('(') and str.endswith(')'):
221            str = str[1:-1]
222            names = [ s.strip() for s in str.split(",")]
223            if len(names) > 3:
224                raise self.parse_error("More than three fields in name")
225            first = names[0].split(":")
226            if first == 'fedid:':
227                del first[0]
228                first[0] = fedid(hexstr=first[0])
[027b87b]229            names[0] = first[0]
[f771e2f]230
231            for i in range(1,2):
232                names[i] = parse_name(names[i])
233
234            return tuple(names)
235        else:
236            raise self.parse_error('Bad mapping (unbalanced parens)')
[19cc408]237
[6e63513]238    @staticmethod
[78f2668]239    def access_tuple(str):
[6e63513]240        """
241        Convert a string of the form (id, id) into an access_project.  This is
[78f2668]242        called by read_access to convert to local attributes.  It returns
[6e63513]243        a tuple of the form (project, user, user) where the two users are
244        always the same.
245        """
246
247        str = str.strip()
248        if str.startswith('(') and str.endswith(')') and str.count(',') == 1:
[725c55d]249            # The slice takes the parens off the string.
250            proj, user = str[1:-1].split(',')
[027b87b]251            return (proj.strip(), user.strip(), user.strip())
[6e63513]252        else:
253            raise self.parse_error(
254                    'Bad mapping (unbalanced parens or more than 1 comma)')
255
[19cc408]256
[06cc65b]257    # RequestAccess support routines
258
[78f2668]259    def legacy_lookup_access(self, req, fid):
[866c983]260        """
[06cc65b]261        Look up the local access control information mapped to this fedid and
262        credentials.  In this case it is a (project, create_user, access_user)
263        triple, and a triple of three booleans indicating which, if any will
264        need to be dynamically created.  Finally a list of owners for that
265        allocation is returned.
266
267        lookup_access_base pulls the first triple out, and it is parsed by this
268        routine into the boolean map.  Owners is always the controlling fedid.
[866c983]269        """
270        # Return values
[027b87b]271        rp = None
[866c983]272        ru = None
273        # This maps a valid user to the Emulab projects and users to use
[78f2668]274        found, match = self.legacy_lookup_access_base(req, fid)
[f771e2f]275        tb, project, user = match
[866c983]276       
277        if found == None:
278            raise service_error(service_error.access,
279                    "Access denied - cannot map access")
280
281        # resolve <dynamic> and <same> in found
282        dyn_proj = False
283        dyn_create_user = False
284        dyn_service_user = False
285
[027b87b]286        if found[0] == "<same>":
[866c983]287            if project != None:
[027b87b]288                rp = project
[866c983]289            else : 
290                raise service_error(\
291                        service_error.server_config,
292                        "Project matched <same> when no project given")
[027b87b]293        elif found[0] == "<dynamic>":
294            rp = None
[866c983]295            dyn_proj = True
296        else:
[027b87b]297            rp = found[0]
[866c983]298
299        if found[1] == "<same>":
300            if user_match == "<any>":
301                if user != None: rcu = user[0]
302                else: raise service_error(\
303                        service_error.server_config,
304                        "Matched <same> on anonymous request")
305            else:
306                rcu = user_match
307        elif found[1] == "<dynamic>":
308            rcu = None
309            dyn_create_user = True
310        else:
311            rcu = found[1]
312       
313        if found[2] == "<same>":
314            if user_match == "<any>":
315                if user != None: rsu = user[0]
316                else: raise service_error(\
317                        service_error.server_config,
318                        "Matched <same> on anonymous request")
319            else:
320                rsu = user_match
321        elif found[2] == "<dynamic>":
322            rsu = None
323            dyn_service_user = True
324        else:
325            rsu = found[2]
326
327        return (rp, rcu, rsu), (dyn_create_user, dyn_service_user, dyn_proj),\
[f771e2f]328                [ fid ]
[19cc408]329
[f771e2f]330    def do_project_allocation(self, dyn, project, user):
331        """
332        Call the project allocation routines and return the info.
333        """
334        if dyn: 
335            # Compose the dynamic project request
336            # (only dynamic, dynamic currently allowed)
337            preq = { 'AllocateProjectRequestBody': \
338                        { 'project' : {\
339                            'user': [ \
340                            { \
341                                'access': [ { 
342                                    'sshPubkey': self.ssh_pubkey_file } ],
343                                 'role': "serviceAccess",\
344                            }, \
345                            { \
346                                'access': [ { 
347                                    'sshPubkey': self.ssh_pubkey_file } ],
348                                 'role': "experimentCreation",\
349                            }, \
350                            ], \
351                            }\
352                        }\
353                    }
354            return self.allocate_project.dynamic_project(preq)
[eeb0088]355        else:
[f771e2f]356            preq = {'StaticProjectRequestBody' : \
357                    { 'project': \
358                        { 'name' : { 'localname' : project },\
359                          'user' : [ \
360                            {\
361                                'userID': { 'localname' : user }, \
362                                'access': [ { 
363                                    'sshPubkey': self.ssh_pubkey_file } ],
364                                'role': 'experimentCreation'\
365                            },\
366                            {\
367                                'userID': { 'localname' : user}, \
368                                'access': [ { 
369                                    'sshPubkey': self.ssh_pubkey_file } ],
370                                'role': 'serviceAccess'\
371                            },\
372                        ]}\
373                    }\
374            }
375            return self.allocate_project.static_project(preq)
[eeb0088]376
[f771e2f]377    def save_project_state(self, aid, ap, dyn, owners):
378        """
379        Parse out and save the information relevant to the project created for
380        this experiment.  That info is largely in ap and owners.  dyn indicates
381        that the project was created dynamically.  Return the user and project
382        names.
383        """
384        self.state_lock.acquire()
385        self.allocation[aid] = { }
[eeb0088]386        try:
[f771e2f]387            pname = ap['project']['name']['localname']
388        except KeyError:
389            pname = None
390
391        if dyn:
392            if not pname:
393                self.state_lock.release()
394                raise service_error(service_error.internal,
395                        "Misformed allocation response?")
396            if pname in self.projects: self.projects[pname] += 1
397            else: self.projects[pname] = 1
398            self.allocation[aid]['project'] = pname
[43197eb]399        else:
[f771e2f]400            # sproject is a static project associated with this allocation.
401            self.allocation[aid]['sproject'] = pname
[866c983]402
[f771e2f]403        self.allocation[aid]['keys'] = [ ]
[5e1fb7b]404
[f771e2f]405        try:
406            for u in ap['project']['user']:
407                uname = u['userID']['localname']
408                if u['role'] == 'experimentCreation':
409                    self.allocation[aid]['user'] = uname
410                for k in [ k['sshPubkey'] for k in u['access'] \
411                        if k.has_key('sshPubkey') ]:
412                    kv = "%s:%s" % (uname, k)
413                    if self.keys.has_key(kv): self.keys[kv] += 1
414                    else: self.keys[kv] = 1
415                    self.allocation[aid]['keys'].append((uname, k))
416        except KeyError:
417            self.state_lock.release()
418            raise service_error(service_error.internal,
419                    "Misformed allocation response?")
420
421        self.allocation[aid]['owners'] = owners
422        self.write_state()
423        self.state_lock.release()
424        return (pname, uname)
[19cc408]425
[06cc65b]426    # End of RequestAccess support routines
427
[19cc408]428    def RequestAccess(self, req, fid):
[866c983]429        """
430        Handle the access request.  Proxy if not for us.
431
432        Parse out the fields and make the allocations or rejections if for us,
433        otherwise, assuming we're willing to proxy, proxy the request out.
434        """
435
436        def gateway_hardware(h):
[5e1fb7b]437            if h == 'GWTYPE': return self.portal_type or 'GWTYPE'
[866c983]438            else: return h
439
[43197eb]440        def get_export_project(svcs):
441            """
442            if the service requests includes one to export a project, return
443            that project.
444            """
445            rv = None
446            for s in svcs:
447                if s.get('name', '') == 'project_export' and \
448                        s.get('visibility', '') == 'export':
449                    if not rv: 
[1f6a573]450                        for a in s.get('fedAttr', []):
[43197eb]451                            if a.get('attribute', '') == 'project' \
452                                    and 'value' in a:
453                                rv = a['value']
454                    else:
455                        raise service_error(service_error, access, 
456                                'Requesting multiple project exports is ' + \
457                                        'not supported');
458            return rv
459
[866c983]460        # The dance to get into the request body
461        if req.has_key('RequestAccessRequestBody'):
462            req = req['RequestAccessRequestBody']
463        else:
464            raise service_error(service_error.req, "No request!?")
465
[1f6a573]466        # if this includes a project export request, construct a filter such
467        # that only the ABAC attributes mapped to that project are checked for
468        # access.
469        if 'service' in req:
470            ep = get_export_project(req['service'])
471            pf = lambda(a): a.value[0] == ep
472        else:
473            ep = None
474            pf = None
475
[c573278]476        if self.auth.import_credentials(
477                data_list=req.get('abac_credential', [])):
478            self.auth.save()
[866c983]479
[6e63513]480        if self.auth_type == "legacy":
[78f2668]481            found, dyn, owners = self.legacy_lookup_access(req, fid)
[6e63513]482        elif self.auth_type == 'abac':
[1f6a573]483            found, dyn, owners = self.lookup_access(req, fid, filter=pf)
[6e63513]484        else:
485            raise service_error(service_error.internal, 
486                    'Unknown auth_type: %s' % self.auth_type)
[f771e2f]487        ap = None
[866c983]488
[1f6a573]489        # This only happens in legacy lookups, but if this user has access to
490        # the testbed but not the project to be exported, raise the error.
491        if ep and ep != found[0]:
492            raise service_error(service_error.access,
493                    "Cannot export %s" % ep)
[f771e2f]494
495        if self.ssh_pubkey_file:
[027b87b]496            ap = self.do_project_allocation(dyn[1], found[0], found[1])
[f771e2f]497        else:
498            raise service_error(service_error.internal, 
499                    "SSH access parameters required")
500        # keep track of what's been added
501        allocID, alloc_cert = generate_fedid(subj="alloc", log=self.log)
502        aid = unicode(allocID)
503
504        pname, uname = self.save_project_state(aid, ap, dyn[1], owners)
505
506        services, svc_state = self.export_services(req.get('service',[]),
507                pname, uname)
508        self.state_lock.acquire()
509        # Store services state in global state
510        for k, v in svc_state.items():
511            self.allocation[aid][k] = v
512        self.write_state()
513        self.state_lock.release()
514        # Give the owners the right to change this allocation
515        for o in owners:
516            self.auth.set_attribute(o, allocID)
[6e63513]517        self.auth.save()
[f771e2f]518        try:
519            f = open("%s/%s.pem" % (self.certdir, aid), "w")
520            print >>f, alloc_cert
521            f.close()
522        except EnvironmentError, e:
523            raise service_error(service_error.internal, 
524                    "Can't open %s/%s : %s" % (self.certdir, aid, e))
525        resp = self.build_access_response({ 'fedid': allocID } ,
526                ap, services)
527        return resp
[d81971a]528
[8cf2b90e]529    def do_release_project(self, del_project, del_users, del_types):
530        """
531        If a project and users has to be deleted, make the call.
532        """
533        msg = { 'project': { }}
534        if del_project:
535            msg['project']['name']= {'localname': del_project}
536        users = [ ]
537        for u in del_users.keys():
538            users.append({ 'userID': { 'localname': u },\
539                'access' :  \
540                        [ {'sshPubkey' : s } for s in del_users[u]]\
541            })
542        if users: 
543            msg['project']['user'] = users
544        if len(del_types) > 0:
545            msg['resources'] = { 'node': \
546                    [ {'hardware': [ h ] } for h in del_types ]\
547                }
548        if self.allocate_project.release_project:
549            msg = { 'ReleaseProjectRequestBody' : msg}
550            self.allocate_project.release_project(msg)
551
[d81971a]552    def ReleaseAccess(self, req, fid):
[866c983]553        # The dance to get into the request body
554        if req.has_key('ReleaseAccessRequestBody'):
555            req = req['ReleaseAccessRequestBody']
556        else:
557            raise service_error(service_error.req, "No request!?")
558
[8cf2b90e]559        try:
560            if req['allocID'].has_key('localname'):
561                auth_attr = aid = req['allocID']['localname']
562            elif req['allocID'].has_key('fedid'):
563                aid = unicode(req['allocID']['fedid'])
564                auth_attr = req['allocID']['fedid']
565            else:
566                raise service_error(service_error.req,
567                        "Only localnames and fedids are understood")
568        except KeyError:
569            raise service_error(service_error.req, "Badly formed request")
570
[725c55d]571        self.log.debug("[access] deallocation requested for %s by %s" % \
572                (aid, fid))
[8cf2b90e]573        if not self.auth.check_attribute(fid, auth_attr):
574            self.log.debug("[access] deallocation denied for %s", aid)
575            raise service_error(service_error.access, "Access Denied")
576
577        # If we know this allocation, reduce the reference counts and
578        # remove the local allocations.  Otherwise report an error.  If
579        # there is an allocation to delete, del_users will be a dictonary
580        # of sets where the key is the user that owns the keys in the set.
581        # We use a set to avoid duplicates.  del_project is just the name
582        # of any dynamic project to delete.  We're somewhat lazy about
583        # deleting authorization attributes.  Having access to something
584        # that doesn't exist isn't harmful.
585        del_users = { }
586        del_project = None
587        del_types = set()
588
589        self.state_lock.acquire()
590        if aid in self.allocation:
591            self.log.debug("Found allocation for %s" %aid)
592            for k in self.allocation[aid]['keys']:
593                kk = "%s:%s" % k
594                self.keys[kk] -= 1
595                if self.keys[kk] == 0:
596                    if not del_users.has_key(k[0]):
597                        del_users[k[0]] = set()
598                    del_users[k[0]].add(k[1])
599                    del self.keys[kk]
600
601            if 'project' in self.allocation[aid]:
602                pname = self.allocation[aid]['project']
603                self.projects[pname] -= 1
604                if self.projects[pname] == 0:
605                    del_project = pname
606                    del self.projects[pname]
607
608            if 'types' in self.allocation[aid]:
609                for t in self.allocation[aid]['types']:
610                    self.types[t] -= 1
611                    if self.types[t] == 0:
612                        if not del_project: del_project = t[0]
613                        del_types.add(t[1])
614                        del self.types[t]
615
616            del self.allocation[aid]
617            self.write_state()
618            self.state_lock.release()
619            # If we actually have resources to deallocate, prepare the call.
620            if del_project or del_users:
621                self.do_release_project(del_project, del_users, del_types)
622            # And remove the access cert
623            cf = "%s/%s.pem" % (self.certdir, aid)
624            self.log.debug("Removing %s" % cf)
625            os.remove(cf)
626            return { 'allocID': req['allocID'] } 
627        else:
628            self.state_lock.release()
629            raise service_error(service_error.req, "No such allocation")
[866c983]630
[06cc65b]631    # These are subroutines for StartSegment
[8cf2b90e]632    def generate_ns2(self, topo, expfn, softdir, connInfo):
[06cc65b]633        """
634        Convert topo into an ns2 file, decorated with appropriate commands for
635        the particular testbed setup.  Convert all requests for software, etc
636        to point at the staged copies on this testbed and add the federation
637        startcommands.
638        """
[617592b]639        class dragon_commands:
640            """
641            Functor to spit out approrpiate dragon commands for nodes listed in
642            the connectivity description.  The constructor makes a dict mapping
643            dragon nodes to their parameters and the __call__ checks each
644            element in turn for membership.
645            """
646            def __init__(self, map):
647                self.node_info = map
648
649            def __call__(self, e):
650                s = ""
651                if isinstance(e, topdl.Computer):
[49051fb]652                    if self.node_info.has_key(e.name):
[fefa026]653                        info = self.node_info[e.name]
654                        for ifname, vlan, type in info:
[617592b]655                            for i in e.interface:
656                                if i.name == ifname:
657                                    addr = i.get_attribute('ip4_address')
658                                    subs = i.substrate[0]
659                                    break
660                            else:
661                                raise service_error(service_error.internal,
662                                        "No interface %s on element %s" % \
[49051fb]663                                                (ifname, e.name))
[1cf8e2c]664                            # XXX: do netmask right
[617592b]665                            if type =='link':
[06cc65b]666                                s = ("tb-allow-external ${%s} " + \
667                                        "dragonportal ip %s vlan %s " + \
668                                        "netmask 255.255.255.0\n") % \
[e07c8f3]669                                        (topdl.to_tcl_name(e.name), addr, vlan)
[617592b]670                            elif type =='lan':
[06cc65b]671                                s = ("tb-allow-external ${%s} " + \
672                                        "dragonportal " + \
[617592b]673                                        "ip %s vlan %s usurp %s\n") % \
[e07c8f3]674                                        (topdl.to_tcl_name(e.name), addr, 
675                                                vlan, subs)
[617592b]676                            else:
677                                raise service_error(service_error_internal,
678                                        "Unknown DRAGON type %s" % type)
679                return s
680
681        class not_dragon:
[06cc65b]682            """
683            Return true if a node is in the given map of dragon nodes.
684            """
[617592b]685            def __init__(self, map):
686                self.nodes = set(map.keys())
687
688            def __call__(self, e):
[49051fb]689                return e.name not in self.nodes
[69692a9]690
[06cc65b]691        # Main line of generate_ns2
[ecca6eb]692        t = topo.clone()
693
[06cc65b]694        # Create the map of nodes that need direct connections (dragon
695        # connections) from the connInfo
[617592b]696        dragon_map = { }
697        for i in [ i for i in connInfo if i['type'] == 'transit']:
698            for a in i.get('fedAttr', []):
699                if a['attribute'] == 'vlan_id':
700                    vlan = a['value']
701                    break
702            else:
[8cf2b90e]703                raise service_error(service_error.internal, "No vlan tag")
[617592b]704            members = i.get('member', [])
705            if len(members) > 1: type = 'lan'
706            else: type = 'link'
707
708            try:
709                for m in members:
[8cf2b90e]710                    if m['element'] in dragon_map:
[617592b]711                        dragon_map[m['element']].append(( m['interface'], 
712                            vlan, type))
713                    else:
714                        dragon_map[m['element']] = [( m['interface'], 
715                            vlan, type),]
716            except KeyError:
717                raise service_error(service_error.req,
718                        "Missing connectivity info")
719
[35aa3ae]720        # Weed out the things we aren't going to instantiate: Segments, portal
721        # substrates, and portal interfaces.  (The copy in the for loop allows
722        # us to delete from e.elements in side the for loop).  While we're
723        # touching all the elements, we also adjust paths from the original
724        # testbed to local testbed paths and put the federation commands into
725        # the start commands
726        for e in [e for e in t.elements]:
727            if isinstance(e, topdl.Segment):
728                t.elements.remove(e)
[43649f1]729            if isinstance(e, topdl.Computer):
[e76f38a]730                self.add_kit(e, self.federation_software)
[5e1fb7b]731                if e.get_attribute('portal') and self.portal_startcommand:
[9b3627e]732                    # Add local portal support software
[e76f38a]733                    self.add_kit(e, self.portal_software)
[43649f1]734                    # Portals never have a user-specified start command
[5e1fb7b]735                    e.set_attribute('startup', self.portal_startcommand)
736                elif self.node_startcommand:
[43649f1]737                    if e.get_attribute('startup'):
[d87778f]738                        e.set_attribute('startup', "%s \\$USER '%s'" % \
[5e1fb7b]739                                (self.node_startcommand, 
740                                    e.get_attribute('startup')))
[43649f1]741                    else:
[5e1fb7b]742                        e.set_attribute('startup', self.node_startcommand)
[0297248]743
[49051fb]744                dinf = [i[0] for i in dragon_map.get(e.name, []) ]
[35aa3ae]745                # Remove portal interfaces that do not connect to DRAGON
746                e.interface = [i for i in e.interface \
[617592b]747                        if not i.get_attribute('portal') or i.name in dinf ]
[9b3627e]748            # Fix software paths
749            for s in getattr(e, 'software', []):
750                s.location = re.sub("^.*/", softdir, s.location)
[35aa3ae]751
752        t.substrates = [ s.clone() for s in t.substrates ]
753        t.incorporate_elements()
[ecca6eb]754
755        # Customize the ns2 output for local portal commands and images
756        filters = []
757
[5e1fb7b]758        if self.dragon_endpoint:
[617592b]759            add_filter = not_dragon(dragon_map)
760            filters.append(dragon_commands(dragon_map))
[69692a9]761        else:
762            add_filter = None
763
[5e1fb7b]764        if self.portal_command:
765            filters.append(topdl.generate_portal_command_filter(
766                self.portal_command, add_filter=add_filter))
[ecca6eb]767
[5e1fb7b]768        if self.portal_image:
[ecca6eb]769            filters.append(topdl.generate_portal_image_filter(
[5e1fb7b]770                self.portal_image))
[ecca6eb]771
[5e1fb7b]772        if self.portal_type:
[ecca6eb]773            filters.append(topdl.generate_portal_hardware_filter(
[5e1fb7b]774                self.portal_type))
[ecca6eb]775
776        # Convert to ns and write it out
777        expfile = topdl.topology_to_ns2(t, filters)
778        try:
779            f = open(expfn, "w")
780            print >>f, expfile
781            f.close()
[d3c8759]782        except EnvironmentError:
[ecca6eb]783            raise service_error(service_error.internal,
784                    "Cannot write experiment file %s: %s" % (expfn,e))
[f9ef40b]785
[2c1fd21]786    def export_store_info(self, cf, proj, ename, connInfo):
787        """
788        For the export requests in the connection info, install the peer names
789        at the experiment controller via SetValue calls.
790        """
791
792        for c in connInfo:
793            for p in [ p for p in c.get('parameter', []) \
794                    if p.get('type', '') == 'output']:
795
796                if p.get('name', '') == 'peer':
797                    k = p.get('key', None)
798                    surl = p.get('store', None)
799                    if surl and k and k.index('/') != -1:
800                        value = "%s.%s.%s%s" % \
801                                (k[k.index('/')+1:], ename, proj, self.domain)
802                        req = { 'name': k, 'value': value }
803                        self.log.debug("Setting %s to %s on %s" % \
804                                (k, value, surl))
805                        self.call_SetValue(surl, req, cf)
806                    else:
807                        self.log.error("Bad export request: %s" % p)
808                elif p.get('name', '') == 'ssh_port':
809                    k = p.get('key', None)
810                    surl = p.get('store', None)
811                    if surl and k:
812                        req = { 'name': k, 'value': self.ssh_port }
813                        self.log.debug("Setting %s to %s on %s" % \
814                                (k, self.ssh_port, surl))
815                        self.call_SetValue(surl, req, cf)
816                    else:
817                        self.log.error("Bad export request: %s" % p)
818                else:
819                    self.log.error("Unknown export parameter: %s" % \
820                            p.get('name'))
821                    continue
822
[49051fb]823    def add_seer_node(self, topo, name, startup):
824        """
825        Add a seer node to the given topology, with the startup command passed
[06cc65b]826        in.  Used by configure seer_services.
[49051fb]827        """
828        c_node = topdl.Computer(
829                name=name, 
830                os= topdl.OperatingSystem(
831                    attribute=[
832                    { 'attribute': 'osid', 
833                        'value': self.local_seer_image },
834                    ]),
835                attribute=[
836                    { 'attribute': 'startup', 'value': startup },
837                    ]
838                )
839        self.add_kit(c_node, self.local_seer_software)
840        topo.elements.append(c_node)
841
842    def configure_seer_services(self, services, topo, softdir):
[8cf2b90e]843        """
844        Make changes to the topology required for the seer requests being made.
845        Specifically, add any control or master nodes required and set up the
846        start commands on the nodes to interconnect them.
847        """
848        local_seer = False      # True if we need to add a control node
849        collect_seer = False    # True if there is a seer-master node
850        seer_master= False      # True if we need to add the seer-master
[49051fb]851        for s in services:
852            s_name = s.get('name', '')
853            s_vis = s.get('visibility','')
854
[8cf2b90e]855            if s_name  == 'local_seer_control' and s_vis == 'export':
[49051fb]856                local_seer = True
857            elif s_name == 'seer_master':
858                if s_vis == 'import':
859                    collect_seer = True
860                elif s_vis == 'export':
861                    seer_master = True
862       
863        # We've got the whole picture now, so add nodes if needed and configure
864        # them to interconnect properly.
865        if local_seer or seer_master:
866            # Copy local seer control node software to the tempdir
867            for l, f in self.local_seer_software:
868                base = os.path.basename(f)
869                copy_file(f, "%s/%s" % (softdir, base))
870        # If we're collecting seers somewhere the controllers need to talk to
871        # the master.  In testbeds that export the service, that will be a
872        # local node that we'll add below.  Elsewhere it will be the control
873        # portal that will port forward to the exporting master.
874        if local_seer:
875            if collect_seer:
[acaa9b9]876                startup = "%s -C %s" % (self.local_seer_start, "seer-master")
[49051fb]877            else:
878                startup = self.local_seer_start
879            self.add_seer_node(topo, 'control', startup)
880        # If this is the seer master, add that node, too.
881        if seer_master:
[e07c8f3]882            self.add_seer_node(topo, 'seer-master', 
883                    "%s -R -n -R seer-master -R -A -R sink" % \
884                            self.seer_master_start)
[49051fb]885
[06cc65b]886    def retrieve_software(self, topo, certfile, softdir):
887        """
888        Collect the software that nodes in the topology need loaded and stage
889        it locally.  This implies retrieving it from the experiment_controller
890        and placing it into softdir.  Certfile is used to prove that this node
891        has access to that data (it's the allocation/segment fedid).  Finally
892        local portal and federation software is also copied to the same staging
893        directory for simplicity - all software needed for experiment creation
894        is in softdir.
895        """
896        sw = set()
897        for e in topo.elements:
898            for s in getattr(e, 'software', []):
899                sw.add(s.location)
900        for s in sw:
901            self.log.debug("Retrieving %s" % s)
902            try:
903                get_url(s, certfile, softdir)
904            except:
905                t, v, st = sys.exc_info()
906                raise service_error(service_error.internal,
907                        "Error retrieving %s: %s" % (s, v))
[49051fb]908
[06cc65b]909        # Copy local federation and portal node software to the tempdir
910        for s in (self.federation_software, self.portal_software):
911            for l, f in s:
912                base = os.path.basename(f)
913                copy_file(f, "%s/%s" % (softdir, base))
[49051fb]914
[6c57fe9]915
[06cc65b]916    def initialize_experiment_info(self, attrs, aid, certfile, tmpdir):
917        """
918        Gather common configuration files, retrieve or create an experiment
919        name and project name, and return the ssh_key filenames.  Create an
920        allocation log bound to the state log variable as well.
921        """
[6c57fe9]922        configs = set(('hosts', 'ssh_pubkey', 'ssh_secretkey'))
[06cc65b]923        ename = None
924        pubkey_base = None
925        secretkey_base = None
926        proj = None
927        user = None
928        alloc_log = None
929
930        for a in attrs:
931            if a['attribute'] in configs:
932                try:
933                    self.log.debug("Retrieving %s from %s" % \
934                            (a['attribute'], a['value']))
935                    get_url(a['value'], certfile, tmpdir)
936                except:
937                    t, v, st = sys.exc_info()
938                    raise service_error(service_error.internal,
939                            "Error retrieving %s: %s" % (a.get('value', ""), v))
940            if a['attribute'] == 'ssh_pubkey':
941                pubkey_base = a['value'].rpartition('/')[2]
942            if a['attribute'] == 'ssh_secretkey':
943                secretkey_base = a['value'].rpartition('/')[2]
944            if a['attribute'] == 'experiment_name':
945                ename = a['value']
946
947        if not ename:
948            ename = ""
949            for i in range(0,5):
950                ename += random.choice(string.ascii_letters)
951            self.log.warn("No experiment name: picked one randomly: %s" \
952                    % ename)
953
954        if not pubkey_base:
955            raise service_error(service_error.req, 
956                    "No public key attribute")
957
958        if not secretkey_base:
959            raise service_error(service_error.req, 
960                    "No secret key attribute")
[6c57fe9]961
[06cc65b]962        self.state_lock.acquire()
963        if aid in self.allocation:
964            proj = self.allocation[aid].get('project', None)
965            if not proj: 
966                proj = self.allocation[aid].get('sproject', None)
967            user = self.allocation[aid].get('user', None)
968            self.allocation[aid]['experiment'] = ename
969            self.allocation[aid]['log'] = [ ]
970            # Create a logger that logs to the experiment's state object as
971            # well as to the main log file.
972            alloc_log = logging.getLogger('fedd.access.%s' % ename)
973            h = logging.StreamHandler(
974                    list_log.list_log(self.allocation[aid]['log']))
975            # XXX: there should be a global one of these rather than
976            # repeating the code.
977            h.setFormatter(logging.Formatter(
978                "%(asctime)s %(name)s %(message)s",
979                        '%d %b %y %H:%M:%S'))
980            alloc_log.addHandler(h)
981            self.write_state()
982        self.state_lock.release()
983
984        if not proj:
985            raise service_error(service_error.internal, 
986                    "Can't find project for %s" %aid)
987
988        if not user:
989            raise service_error(service_error.internal, 
990                    "Can't find creation user for %s" %aid)
991
992        return (ename, proj, user, pubkey_base, secretkey_base, alloc_log)
993
994    def finalize_experiment(self, starter, topo, aid, alloc_id):
995        """
996        Store key bits of experiment state in the global repository, including
997        the response that may need to be replayed, and return the response.
998        """
999        # Copy the assigned names into the return topology
1000        embedding = [ ]
1001        for n in starter.node:
1002            embedding.append({ 
1003                'toponame': n,
1004                'physname': ["%s%s" %  (starter.node[n], self.domain)],
1005                })
1006        # Grab the log (this is some anal locking, but better safe than
1007        # sorry)
1008        self.state_lock.acquire()
1009        logv = "".join(self.allocation[aid]['log'])
1010        # It's possible that the StartSegment call gets retried (!).
1011        # if the 'started' key is in the allocation, we'll return it rather
1012        # than redo the setup.
1013        self.allocation[aid]['started'] = { 
1014                'allocID': alloc_id,
1015                'allocationLog': logv,
1016                'segmentdescription': { 
1017                    'topdldescription': topo.clone().to_dict()
1018                    },
1019                'embedding': embedding
1020                }
1021        retval = copy.copy(self.allocation[aid]['started'])
1022        self.write_state()
1023        self.state_lock.release()
1024        return retval
1025   
1026    # End of StartSegment support routines
1027
1028    def StartSegment(self, req, fid):
[b770aa0]1029        err = None  # Any service_error generated after tmpdir is created
1030        rv = None   # Return value from segment creation
1031
[cc8d8e9]1032        try:
1033            req = req['StartSegmentRequestBody']
[06cc65b]1034            auth_attr = req['allocID']['fedid']
1035            topref = req['segmentdescription']['topdldescription']
[cc8d8e9]1036        except KeyError:
1037            raise service_error(server_error.req, "Badly formed request")
[ecca6eb]1038
[e02cd14]1039        connInfo = req.get('connection', [])
1040        services = req.get('service', [])
[ecca6eb]1041        aid = "%s" % auth_attr
[6c57fe9]1042        attrs = req.get('fedAttr', [])
[ecca6eb]1043        if not self.auth.check_attribute(fid, auth_attr):
1044            raise service_error(service_error.access, "Access denied")
[cd06678]1045        else:
1046            # See if this is a replay of an earlier succeeded StartSegment -
1047            # sometimes SSL kills 'em.  If so, replay the response rather than
1048            # redoing the allocation.
1049            self.state_lock.acquire()
1050            retval = self.allocation[aid].get('started', None)
1051            self.state_lock.release()
1052            if retval:
1053                self.log.warning("Duplicate StartSegment for %s: " % aid + \
1054                        "replaying response")
1055                return retval
1056
1057        # A new request.  Do it.
[6c57fe9]1058
[06cc65b]1059        if topref: topo = topdl.Topology(**topref)
[6c57fe9]1060        else:
1061            raise service_error(service_error.req, 
1062                    "Request missing segmentdescription'")
[2761484]1063       
[6c57fe9]1064        certfile = "%s/%s.pem" % (self.certdir, auth_attr)
1065        try:
1066            tmpdir = tempfile.mkdtemp(prefix="access-")
[ecca6eb]1067            softdir = "%s/software" % tmpdir
[c23684d]1068            os.mkdir(softdir)
[d3c8759]1069        except EnvironmentError:
[6c57fe9]1070            raise service_error(service_error.internal, "Cannot create tmp dir")
1071
[b770aa0]1072        # Try block alllows us to clean up temporary files.
1073        try:
[06cc65b]1074            self.retrieve_software(topo, certfile, softdir)
1075            ename, proj, user, pubkey_base, secretkey_base, alloc_log = \
1076                    self.initialize_experiment_info(attrs, aid, 
1077                            certfile, tmpdir)
[9b3627e]1078
[06cc65b]1079            # Set up userconf and seer if needed
[c200d36]1080            self.configure_userconf(services, tmpdir)
[49051fb]1081            self.configure_seer_services(services, topo, softdir)
[06cc65b]1082            # Get and send synch store variables
[2761484]1083            self.export_store_info(certfile, proj, ename, connInfo)
1084            self.import_store_info(certfile, connInfo)
1085
[b770aa0]1086            expfile = "%s/experiment.tcl" % tmpdir
1087
1088            self.generate_portal_configs(topo, pubkey_base, 
[06cc65b]1089                    secretkey_base, tmpdir, proj, ename, connInfo, services)
[b770aa0]1090            self.generate_ns2(topo, expfile, 
[8cf2b90e]1091                    "/proj/%s/software/%s/" % (proj, ename), connInfo)
[f07fa49]1092
[b770aa0]1093            starter = self.start_segment(keyfile=self.ssh_privkey_file, 
[181aeb4]1094                    debug=self.create_debug, log=alloc_log, boss=self.boss,
1095                    cert=self.xmlrpc_cert)
[b770aa0]1096            rv = starter(self, ename, proj, user, expfile, tmpdir)
1097        except service_error, e:
1098            err = e
[06cc65b]1099        except:
1100            t, v, st = sys.exc_info()
1101            err = service_error(service_error.internal, "%s: %s" % \
1102                    (v, traceback.extract_tb(st)))
[b770aa0]1103
[574055e]1104        # Walk up tmpdir, deleting as we go
[06cc65b]1105        if self.cleanup: self.remove_dirs(tmpdir)
1106        else: self.log.debug("[StartSegment]: not removing %s" % tmpdir)
[574055e]1107
[fd556d1]1108        if rv:
[06cc65b]1109            return self.finalize_experiment(starter, topo, aid, req['allocID'])
[b770aa0]1110        elif err:
1111            raise service_error(service_error.federant,
1112                    "Swapin failed: %s" % err)
[fd556d1]1113        else:
1114            raise service_error(service_error.federant, "Swapin failed")
[5ae3857]1115
1116    def TerminateSegment(self, req, fid):
1117        try:
1118            req = req['TerminateSegmentRequestBody']
1119        except KeyError:
1120            raise service_error(server_error.req, "Badly formed request")
1121
1122        auth_attr = req['allocID']['fedid']
1123        aid = "%s" % auth_attr
1124        attrs = req.get('fedAttr', [])
1125        if not self.auth.check_attribute(fid, auth_attr):
1126            raise service_error(service_error.access, "Access denied")
1127
1128        self.state_lock.acquire()
[06cc65b]1129        if aid in self.allocation:
[5ae3857]1130            proj = self.allocation[aid].get('project', None)
1131            if not proj: 
1132                proj = self.allocation[aid].get('sproject', None)
1133            user = self.allocation[aid].get('user', None)
1134            ename = self.allocation[aid].get('experiment', None)
[1d913e13]1135        else:
1136            proj = None
1137            user = None
1138            ename = None
[5ae3857]1139        self.state_lock.release()
1140
1141        if not proj:
1142            raise service_error(service_error.internal, 
1143                    "Can't find project for %s" % aid)
1144
1145        if not user:
1146            raise service_error(service_error.internal, 
1147                    "Can't find creation user for %s" % aid)
1148        if not ename:
1149            raise service_error(service_error.internal, 
1150                    "Can't find experiment name for %s" % aid)
[fd556d1]1151        stopper = self.stop_segment(keyfile=self.ssh_privkey_file,
[181aeb4]1152                debug=self.create_debug, boss=self.boss, cert=self.xmlrpc_cert)
[5ae3857]1153        stopper(self, user, proj, ename)
1154        return { 'allocID': req['allocID'] }
Note: See TracBrowser for help on using the repository browser.