source: fedd/federation/emulab_access.py @ 4b68c58

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

Specify local SEER node image correctly

  • Property mode set to 100644
File size: 60.0 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
[19cc408]12
[f8582c9]13from threading import *
[8e6fe4d]14from M2Crypto.SSL import SSLError
[f8582c9]15
[ec4fb42]16from util import *
17from allocate_project import allocate_project_local, allocate_project_remote
18from access_project import access_project
[51cc9df]19from fedid import fedid, generate_fedid
[3f6bc5f]20from authorizer import authorizer
[6a0c9f4]21from service_error import service_error
[9460b1e]22from remote_service import xmlrpc_handler, soap_handler, service_caller
[11a08b0]23
[6c57fe9]24import httplib
25import tempfile
26from urlparse import urlparse
27
[11860f52]28import topdl
29import list_log
30import proxy_emulab_segment
31import local_emulab_segment
32
[0ea11af]33
34# Make log messages disappear if noone configures a fedd logger
[11a08b0]35class nullHandler(logging.Handler):
36    def emit(self, record): pass
37
38fl = logging.getLogger("fedd.access")
39fl.addHandler(nullHandler())
[19cc408]40
[ec4fb42]41class access:
[19cc408]42    """
43    The implementation of access control based on mapping users to projects.
44
45    Users can be mapped to existing projects or have projects created
46    dynamically.  This implements both direct requests and proxies.
47    """
48
[4ed10ae]49    class parse_error(RuntimeError): pass
50
[72ed6e4]51
[f069052]52    proxy_RequestAccess= service_caller('RequestAccess')
53    proxy_ReleaseAccess= service_caller('ReleaseAccess')
[c35207d]54
[3f6bc5f]55    def __init__(self, config=None, auth=None):
[866c983]56        """
57        Initializer.  Pulls parameters out of the ConfigParser's access section.
58        """
59
[9b3627e]60        def software_list(v):
61            l = [ ]
62            if v:
63                ps = v.split(" ")
64                while len(ps):
65                    loc, file = ps[0:2]
66                    del ps[0:2]
67                    l.append((loc, file))
68            return l
69
70
[866c983]71        # Make sure that the configuration is in place
72        if not config: 
73            raise RunTimeError("No config to fedd.access")
74
75        self.project_priority = config.getboolean("access", "project_priority")
76        self.allow_proxy = config.getboolean("access", "allow_proxy")
77
78        self.boss = config.get("access", "boss")
79        self.ops = config.get("access", "ops")
80        self.domain = config.get("access", "domain")
81        self.fileserver = config.get("access", "fileserver")
82        self.eventserver = config.get("access", "eventserver")
[6c57fe9]83        self.certdir = config.get("access","certdir")
[eeb0088]84        self.userconfdir = config.get("access","userconfdir")
85        self.userconfcmd = config.get("access","userconfcmd")
[fe28bb2]86        self.userconfurl = config.get("access","userconfurl")
[9b3627e]87        self.federation_software = config.get("access", "federation_software")
88        self.portal_software = config.get("access", "portal_software")
[e76f38a]89        self.local_seer_software = config.get("access", "local_seer_software")
90        self.local_seer_image = config.get("access", "local_seer_image")
91        self.local_seer_start = config.get("access", "local_seer_start")
[ecca6eb]92        self.ssh_privkey_file = config.get("access","ssh_privkey_file")
[3bddd24]93        self.ssh_pubkey_file = config.get("access","ssh_pubkey_file")
[6280b1f]94        self.ssh_port = config.get("access","ssh_port") or "22"
[5ae3857]95        self.create_debug = config.getboolean("access", "create_debug")
[69692a9]96        self.cleanup = not config.getboolean("access", "leave_tmpfiles")
[11860f52]97        self.access_type = config.get("access", "type")
[9b3627e]98   
99        self.federation_software = software_list(self.federation_software)
100        self.portal_software = software_list(self.portal_software)
[e76f38a]101        self.local_seer_software = software_list(self.local_seer_software)
[11860f52]102
103        self.access_type = self.access_type.lower()
104        if self.access_type == 'remote_emulab':
105            self.start_segment = proxy_emulab_segment.start_segment
106            self.stop_segment = proxy_emulab_segment.stop_segment
107        elif self.access_type == 'local_emulab':
108            self.start_segment = local_emulab_segment.start_segment
109            self.stop_segment = local_emulab_segment.stop_segment
110        else:
111            self.start_segment = None
112            self.stop_segment = None
[866c983]113
114        self.attrs = { }
115        self.access = { }
116        self.restricted = [ ]
117        self.projects = { }
118        self.keys = { }
119        self.types = { }
120        self.allocation = { }
121        self.state = { 
122            'projects': self.projects,
123            'allocation' : self.allocation,
124            'keys' : self.keys,
125            'types': self.types
126        }
127        self.log = logging.getLogger("fedd.access")
128        set_log_level(config, "access", self.log)
129        self.state_lock = Lock()
[e02cd14]130        # XXX: Configurable
[43197eb]131        self.exports = set(('SMB', 'seer', 'tmcd', 'userconfig', 
[e76f38a]132            'project_export', 'local_seer_control'))
[b7f6ccc]133        self.imports = set(('SMB', 'seer', 'userconfig'))
[866c983]134
[e76f38a]135        if not self.local_seer_image or not self.local_seer_software or \
136                not self.local_seer_start:
137            self.exports.discard('local_seer_control')
138
[866c983]139        if auth: self.auth = auth
140        else:
141            self.log.error(\
142                    "[access]: No authorizer initialized, creating local one.")
143            auth = authorizer()
144
145        tb = config.get('access', 'testbed')
146        if tb: self.testbed = [ t.strip() for t in tb.split(',') ]
147        else: self.testbed = [ ]
148
149        if config.has_option("access", "accessdb"):
150            self.read_access(config.get("access", "accessdb"))
151
152        self.state_filename = config.get("access", "access_state")
153        self.read_state()
154
155        # Keep cert_file and cert_pwd coming from the same place
156        self.cert_file = config.get("access", "cert_file")
157        if self.cert_file:
158            self.sert_pwd = config.get("access", "cert_pw")
159        else:
160            self.cert_file = config.get("globals", "cert_file")
161            self.sert_pwd = config.get("globals", "cert_pw")
162
163        self.trusted_certs = config.get("access", "trusted_certs") or \
164                config.get("globals", "trusted_certs")
165
166        self.soap_services = {\
167            'RequestAccess': soap_handler("RequestAccess", self.RequestAccess),
168            'ReleaseAccess': soap_handler("ReleaseAccess", self.ReleaseAccess),
[cc8d8e9]169            'StartSegment': soap_handler("StartSegment", self.StartSegment),
[e76f38a]170            'TerminateSegment': soap_handler("TerminateSegment",
171                self.TerminateSegment),
[866c983]172            }
173        self.xmlrpc_services =  {\
174            'RequestAccess': xmlrpc_handler('RequestAccess',
175                self.RequestAccess),
176            'ReleaseAccess': xmlrpc_handler('ReleaseAccess',
177                self.ReleaseAccess),
[5ae3857]178            'StartSegment': xmlrpc_handler("StartSegment", self.StartSegment),
179            'TerminateSegment': xmlrpc_handler('TerminateSegment',
180                self.TerminateSegment),
[866c983]181            }
182
[2761484]183        self.call_SetValue = service_caller('SetValue')
[2ee4226]184        self.call_GetValue = service_caller('GetValue', log=self.log)
[866c983]185
186        if not config.has_option("allocate", "uri"):
187            self.allocate_project = \
188                allocate_project_local(config, auth)
189        else:
190            self.allocate_project = \
191                allocate_project_remote(config, auth)
192
[e76f38a]193
[866c983]194        # If the project allocator exports services, put them in this object's
195        # maps so that classes that instantiate this can call the services.
196        self.soap_services.update(self.allocate_project.soap_services)
197        self.xmlrpc_services.update(self.allocate_project.xmlrpc_services)
[f8582c9]198
[e76f38a]199    @staticmethod
200    def add_kit(e, kit):
201        """
202        Add a Software object created from the list of (install, location)
203        tuples passed as kit  to the software attribute of an object e.  We
204        do this enough to break out the code, but it's kind of a hack to
205        avoid changing the old tuple rep.
206        """
207
208        s = [ topdl.Software(install=i, location=l) for i, l in kit]
209
210        if isinstance(e.software, list): e.software.extend(s)
211        else: e.software = s
212
[72ed6e4]213
214    def read_access(self, config):
[866c983]215        """
216        Read a configuration file and set internal parameters.
217
218        The format is more complex than one might hope.  The basic format is
219        attribute value pairs separated by colons(:) on a signle line.  The
220        attributes in bool_attrs, emulab_attrs and id_attrs can all be set
221        directly using the name: value syntax.  E.g.
222        boss: hostname
223        sets self.boss to hostname.  In addition, there are access lines of the
224        form (tb, proj, user) -> (aproj, auser) that map the first tuple of
225        names to the second for access purposes.  Names in the key (left side)
226        can include "<NONE> or <ANY>" to act as wildcards or to require the
227        fields to be empty.  Similarly aproj or auser can be <SAME> or
228        <DYNAMIC> indicating that either the matching key is to be used or a
229        dynamic user or project will be created.  These names can also be
230        federated IDs (fedid's) if prefixed with fedid:.  Finally, the aproj
231        can be followed with a colon-separated list of node types to which that
232        project has access (or will have access if dynamic).
233        Testbed attributes outside the forms above can be given using the
234        format attribute: name value: value.  The name is a single word and the
235        value continues to the end of the line.  Empty lines and lines startin
236        with a # are ignored.
237
238        Parsing errors result in a self.parse_error exception being raised.
239        """
240        lineno=0
241        name_expr = "["+string.ascii_letters + string.digits + "\.\-_]+"
242        fedid_expr = "fedid:[" + string.hexdigits + "]+"
243        key_name = "(<ANY>|<NONE>|"+fedid_expr + "|"+ name_expr + ")"
244        access_proj = "(<DYNAMIC>(?::" + name_expr +")*|"+ \
245                "<SAME>" + "(?::" + name_expr + ")*|" + \
246                fedid_expr + "(?::" + name_expr + ")*|" + \
247                name_expr + "(?::" + name_expr + ")*)"
248        access_name = "(<DYNAMIC>|<SAME>|" + fedid_expr + "|"+ name_expr + ")"
249
250        restricted_re = re.compile("restricted:\s*(.*)", re.IGNORECASE)
251        attr_re = re.compile('attribute:\s*([\._\-a-z0-9]+)\s+value:\s*(.*)',
252                re.IGNORECASE)
253        access_re = re.compile('\('+key_name+'\s*,\s*'+key_name+'\s*,\s*'+
254                key_name+'\s*\)\s*->\s*\('+access_proj + '\s*,\s*' + 
255                access_name + '\s*,\s*' + access_name + '\s*\)', re.IGNORECASE)
256
257        def parse_name(n):
258            if n.startswith('fedid:'): return fedid(hexstr=n[len('fedid:'):])
259            else: return n
260       
261        def auth_name(n):
262            if isinstance(n, basestring):
263                if n =='<any>' or n =='<none>': return None
264                else: return unicode(n)
265            else:
266                return n
267
268        f = open(config, "r");
269        for line in f:
270            lineno += 1
271            line = line.strip();
272            if len(line) == 0 or line.startswith('#'):
273                continue
274
275            # Extended (attribute: x value: y) attribute line
276            m = attr_re.match(line)
277            if m != None:
278                attr, val = m.group(1,2)
279                self.attrs[attr] = val
280                continue
281
282            # Restricted entry
283            m = restricted_re.match(line)
284            if m != None:
285                val = m.group(1)
286                self.restricted.append(val)
287                continue
288
289            # Access line (t, p, u) -> (ap, cu, su) line
290            m = access_re.match(line)
291            if m != None:
292                access_key = tuple([ parse_name(x) for x in m.group(1,2,3)])
293                auth_key = tuple([ auth_name(x) for x in access_key])
294                aps = m.group(4).split(":");
295                if aps[0] == 'fedid:':
296                    del aps[0]
297                    aps[0] = fedid(hexstr=aps[0])
298
299                cu = parse_name(m.group(5))
300                su = parse_name(m.group(6))
301
302                access_val = (access_project(aps[0], aps[1:]),
303                        parse_name(m.group(5)), parse_name(m.group(6)))
304
305                self.access[access_key] = access_val
306                self.auth.set_attribute(auth_key, "access")
307                continue
308
309            # Nothing matched to here: unknown line - raise exception
310            f.close()
311            raise self.parse_error("Unknown statement at line %d of %s" % \
312                    (lineno, config))
313        f.close()
[72ed6e4]314
[19cc408]315    def get_users(self, obj):
[866c983]316        """
317        Return a list of the IDs of the users in dict
318        """
319        if obj.has_key('user'):
320            return [ unpack_id(u['userID']) \
321                    for u in obj['user'] if u.has_key('userID') ]
322        else:
323            return None
[19cc408]324
[d81971a]325    def write_state(self):
[866c983]326        if self.state_filename:
327            try:
328                f = open(self.state_filename, 'w')
329                pickle.dump(self.state, f)
330            except IOError, e:
331                self.log.error("Can't write file %s: %s" % \
332                        (self.state_filename, e))
333            except pickle.PicklingError, e:
334                self.log.error("Pickling problem: %s" % e)
335            except TypeError, e:
336                self.log.error("Pickling problem (TypeError): %s" % e)
[d81971a]337
338
339    def read_state(self):
[866c983]340        """
341        Read a new copy of access state.  Old state is overwritten.
342
343        State format is a simple pickling of the state dictionary.
344        """
345        if self.state_filename:
346            try:
347                f = open(self.state_filename, "r")
348                self.state = pickle.load(f)
349
350                self.allocation = self.state['allocation']
351                self.projects = self.state['projects']
352                self.keys = self.state['keys']
353                self.types = self.state['types']
354
355                self.log.debug("[read_state]: Read state from %s" % \
356                        self.state_filename)
357            except IOError, e:
358                self.log.warning(("[read_state]: No saved state: " +\
359                        "Can't open %s: %s") % (self.state_filename, e))
360            except EOFError, e:
361                self.log.warning(("[read_state]: " +\
362                        "Empty or damaged state file: %s:") % \
363                        self.state_filename)
364            except pickle.UnpicklingError, e:
365                self.log.warning(("[read_state]: No saved state: " + \
366                        "Unpickling failed: %s") % e)
367
368            # Add the ownership attributes to the authorizer.  Note that the
369            # indices of the allocation dict are strings, but the attributes are
370            # fedids, so there is a conversion.
371            for k in self.allocation.keys():
372                for o in self.allocation[k].get('owners', []):
373                    self.auth.set_attribute(o, fedid(hexstr=k))
[eeb0088]374                if self.allocation[k].has_key('userconfig'):
[fe28bb2]375                    sfid = self.allocation[k]['userconfig']
[eeb0088]376                    fid = fedid(hexstr=sfid)
[fe28bb2]377                    self.auth.set_attribute(fid, "/%s" % sfid)
[3f6bc5f]378
379
[19cc408]380    def permute_wildcards(self, a, p):
[866c983]381        """Return a copy of a with various fields wildcarded.
[19cc408]382
[866c983]383        The bits of p control the wildcards.  A set bit is a wildcard
384        replacement with the lowest bit being user then project then testbed.
385        """
386        if p & 1: user = ["<any>"]
387        else: user = a[2]
388        if p & 2: proj = "<any>"
389        else: proj = a[1]
390        if p & 4: tb = "<any>"
391        else: tb = a[0]
[19cc408]392
[866c983]393        return (tb, proj, user)
[19cc408]394
395    def find_access(self, search):
[866c983]396        """
397        Search the access DB for a match on this tuple.  Return the matching
398        access tuple and the user that matched.
399       
400        NB, if the initial tuple fails to match we start inserting wildcards in
401        an order determined by self.project_priority.  Try the list of users in
402        order (when wildcarded, there's only one user in the list).
403        """
404        if self.project_priority: perm = (0, 1, 2, 3, 4, 5, 6, 7)
405        else: perm = (0, 2, 1, 3, 4, 6, 5, 7)
406
407        for p in perm: 
408            s = self.permute_wildcards(search, p)
409            # s[2] is None on an anonymous, unwildcarded request
410            if s[2] != None:
411                for u in s[2]:
412                    if self.access.has_key((s[0], s[1], u)):
413                        return (self.access[(s[0], s[1], u)], u)
414            else:
415                if self.access.has_key(s):
416                    return (self.access[s], None)
417        return None, None
[19cc408]418
419    def lookup_access(self, req, fid):
[866c983]420        """
421        Determine the allowed access for this request.  Return the access and
422        which fields are dynamic.
423
424        The fedid is needed to construct the request
425        """
[3bddd24]426        user_re = re.compile("user:\s(.*)")
427        project_re = re.compile("project:\s(.*)")
428
[866c983]429        # Search keys
430        tb = None
431        project = None
432        user = None
433        # Return values
434        rp = access_project(None, ())
435        ru = None
436
[3bddd24]437        user = [ user_re.findall(x)[0] for x in req.get('credential', []) \
438                if user_re.match(x)]
439        project = [ project_re.findall(x)[0] \
440                for x in req.get('credential', []) \
441                    if project_re.match(x)]
442
443        if len(project) == 1: project = project[0]
444        elif len(project) == 0: project = None
445        else: 
446            raise service_error(service_error.req, 
447                    "More than one project credential")
448
[866c983]449
450        user_fedids = [ u for u in user if isinstance(u, fedid)]
451        # Determine how the caller is representing itself.  If its fedid shows
452        # up as a project or a singleton user, let that stand.  If neither the
453        # usernames nor the project name is a fedid, the caller is a testbed.
454        if project and isinstance(project, fedid):
455            if project == fid:
456                # The caller is the project (which is already in the tuple
457                # passed in to the authorizer)
458                owners = user_fedids
459                owners.append(project)
460            else:
461                raise service_error(service_error.req,
462                        "Project asserting different fedid")
463        else:
464            if fid not in user_fedids:
465                tb = fid
466                owners = user_fedids
467                owners.append(fid)
468            else:
469                if len(fedids) > 1:
470                    raise service_error(service_error.req,
471                            "User asserting different fedid")
472                else:
473                    # Which is a singleton
474                    owners = user_fedids
475        # Confirm authorization
476
477        for u in user:
478            self.log.debug("[lookup_access] Checking access for %s" % \
479                    ((tb, project, u),))
480            if self.auth.check_attribute((tb, project, u), 'access'):
481                self.log.debug("[lookup_access] Access granted")
482                break
483            else:
484                self.log.debug("[lookup_access] Access Denied")
485        else:
486            raise service_error(service_error.access, "Access denied")
487
488        # This maps a valid user to the Emulab projects and users to use
489        found, user_match = self.find_access((tb, project, user))
490       
491        if found == None:
492            raise service_error(service_error.access,
493                    "Access denied - cannot map access")
494
495        # resolve <dynamic> and <same> in found
496        dyn_proj = False
497        dyn_create_user = False
498        dyn_service_user = False
499
500        if found[0].name == "<same>":
501            if project != None:
502                rp.name = project
503            else : 
504                raise service_error(\
505                        service_error.server_config,
506                        "Project matched <same> when no project given")
507        elif found[0].name == "<dynamic>":
508            rp.name = None
509            dyn_proj = True
510        else:
511            rp.name = found[0].name
512        rp.node_types = found[0].node_types;
513
514        if found[1] == "<same>":
515            if user_match == "<any>":
516                if user != None: rcu = user[0]
517                else: raise service_error(\
518                        service_error.server_config,
519                        "Matched <same> on anonymous request")
520            else:
521                rcu = user_match
522        elif found[1] == "<dynamic>":
523            rcu = None
524            dyn_create_user = True
525        else:
526            rcu = found[1]
527       
528        if found[2] == "<same>":
529            if user_match == "<any>":
530                if user != None: rsu = user[0]
531                else: raise service_error(\
532                        service_error.server_config,
533                        "Matched <same> on anonymous request")
534            else:
535                rsu = user_match
536        elif found[2] == "<dynamic>":
537            rsu = None
538            dyn_service_user = True
539        else:
540            rsu = found[2]
541
542        return (rp, rcu, rsu), (dyn_create_user, dyn_service_user, dyn_proj),\
543                owners
[19cc408]544
[eeb0088]545    def get_handler(self, path, fid):
546        self.log.info("Get handler %s %s" % (path, fid))
547        if self.auth.check_attribute(fid, path) and self.userconfdir:
548            return ("%s/%s" % (self.userconfdir, path), "application/binary")
549        else:
550            return (None, None)
551
552    def export_userconf(self, project):
553        dev_null = None
554        confid, confcert = generate_fedid("test", dir=self.userconfdir, 
555                log=self.log)
556        conffilename = "%s/%s" % (self.userconfdir, str(confid))
557        cf = None
558        try:
559            cf = open(conffilename, "w")
560            os.chmod(conffilename, stat.S_IRUSR | stat.S_IWUSR)
561        except IOError, e:
562            raise service_error(service_error.internal, 
563                    "Cannot create user configuration data")
564
565        try:
566            dev_null = open("/dev/null", "a")
567        except IOError, e:
568            self.log.error("export_userconf: can't open /dev/null: %s" % e)
569
570        cmd = "%s %s" % (self.userconfcmd, project)
571        conf = subprocess.call(cmd.split(" "),
572                stdout=cf, stderr=dev_null, close_fds=True)
573
[fe28bb2]574        self.auth.set_attribute(confid, "/%s" % str(confid))
[eeb0088]575
576        return confid, confcert
577
[43197eb]578    def export_SMB(self, id, state, project, user):
579        return { 
580                'id': id,
581                'name': 'SMB',
582                'visibility': 'export',
583                'server': 'http://fs:139',
584                'fedAttr': [
585                        { 'attribute': 'SMBSHARE', 'value': 'USERS' },
586                        { 'attribute': 'SMBUSER', 'value': user },
587                        { 'attribute': 'SMBPROJ', 'value': project },
588                    ]
589                }
590
591    def export_seer(self, id, state, project, user):
592        return { 
593                'id': id,
594                'name': 'seer',
595                'visibility': 'export',
596                'server': 'http://control:16606',
597                }
598
[e76f38a]599    def export_local_seer(self, id, state, project, user):
600        return { 
601                'id': id,
602                'name': 'local_seer_control',
603                'visibility': 'export',
604                'server': 'http://control:16606',
605                }
606
[43197eb]607    def export_tmcd(self, id, state, project, user):
608        return { 
609                'id': id,
610                'name': 'seer',
611                'visibility': 'export',
612                'server': 'http://boss:7777',
613                }
614
615    def export_userconfig(self, id, state, project, user):
616        if self.userconfdir and self.userconfcmd \
617                and self.userconfurl:
618            cid, cert = self.export_userconf(project)
619            state['userconfig'] = unicode(cid)
620            return {
621                    'id': id,
622                    'name': 'userconfig',
623                    'visibility': 'export',
624                    'server': "%s/%s" % (self.userconfurl, str(cid)),
625                    'fedAttr': [
626                        { 'attribute': 'cert', 'value': cert },
627                    ]
628                    }
629        else:
630            return None
[eeb0088]631
[e02cd14]632    def export_services(self, sreq, project, user):
633        exp = [ ]
[eeb0088]634        state = { }
635        # XXX: Filthy shortcut here using http: so urlparse will give the right
[e02cd14]636        # answers.
637        for s in sreq:
638            sname = s.get('name', '')
639            svis = s.get('visibility', '')
640            if svis == 'export':
641                if sname in self.exports:
[43197eb]642                    id = s.get('id', 'no_id')
[e02cd14]643                    if sname == 'SMB':
[43197eb]644                        exp.append(self.export_SMB(id, state, project, user))
[e02cd14]645                    elif sname == 'seer':
[43197eb]646                        exp.append(self.export_seer(id, state, project, user))
[e02cd14]647                    elif sname == 'tmcd':
[43197eb]648                        exp.append(self.export_tmcd(id, state, project, user))
[eeb0088]649                    elif sname == 'userconfig':
[43197eb]650                        exp.append(self.export_userconfig(id, state,
651                            project, user))
652                    elif sname == 'project_export':
653                        exp.append(self.export_SMB(id, state, project, user))
654                        exp.append(self.export_seer(id, state, project, user))
655                        exp.append(self.export_userconfig(id, state,
656                            project, user))
[e76f38a]657                    elif sname == 'local_seer_control':
658                        exp.append(self.export_local_seer(id, state, project,
659                            user))
[eeb0088]660        return (exp, state)
[e02cd14]661
662    def build_response(self, alloc_id, ap, services):
[866c983]663        """
664        Create the SOAP response.
665
666        Build the dictionary description of the response and use
667        fedd_utils.pack_soap to create the soap message.  ap is the allocate
668        project message returned from a remote project allocation (even if that
669        allocation was done locally).
670        """
671        # Because alloc_id is already a fedd_services_types.IDType_Holder,
672        # there's no need to repack it
673        msg = { 
674                'allocID': alloc_id,
[4afcfc4]675                'fedAttr': [
676                    { 'attribute': 'domain', 'value': self.domain } , 
677                    { 'attribute': 'project', 'value': 
678                        ap['project'].get('name', {}).get('localname', "???") },
679                ]
[866c983]680            }
681        if len(self.attrs) > 0:
[4afcfc4]682            msg['fedAttr'].extend(
[866c983]683                [ { 'attribute': x, 'value' : y } \
[4afcfc4]684                        for x,y in self.attrs.iteritems()])
[e02cd14]685
686        if services:
687            msg['service'] = services
[866c983]688        return msg
[19cc408]689
690    def RequestAccess(self, req, fid):
[866c983]691        """
692        Handle the access request.  Proxy if not for us.
693
694        Parse out the fields and make the allocations or rejections if for us,
695        otherwise, assuming we're willing to proxy, proxy the request out.
696        """
697
698        def gateway_hardware(h):
699            if h == 'GWTYPE': return self.attrs.get('connectorType', 'GWTYPE')
700            else: return h
701
[43197eb]702        def get_export_project(svcs):
703            """
704            if the service requests includes one to export a project, return
705            that project.
706            """
707            rv = None
708            for s in svcs:
709                if s.get('name', '') == 'project_export' and \
710                        s.get('visibility', '') == 'export':
711                    if not rv: 
712                        for a in s.get('feddAttr', []):
713                            if a.get('attribute', '') == 'project' \
714                                    and 'value' in a:
715                                rv = a['value']
716                    else:
717                        raise service_error(service_error, access, 
718                                'Requesting multiple project exports is ' + \
719                                        'not supported');
720            return rv
721
[866c983]722        # The dance to get into the request body
723        if req.has_key('RequestAccessRequestBody'):
724            req = req['RequestAccessRequestBody']
725        else:
726            raise service_error(service_error.req, "No request!?")
727
728        if req.has_key('destinationTestbed'):
729            dt = unpack_id(req['destinationTestbed'])
730
731        if dt == None or dt in self.testbed:
732            # Request for this fedd
733            found, dyn, owners = self.lookup_access(req, fid)
734            restricted = None
735            ap = None
736
[43197eb]737            # if this includes a project export request and the exported
738            # project is not the access project, access denied.
739            if 'service' in req:
740                ep = get_export_project(req['service'])
741                if ep and ep != found[0].name:
[866c983]742                    raise service_error(service_error.access,
743                            "Cannot export %s" % ep)
744
[d8442da]745            # XXX
[866c983]746            # Check for access to restricted nodes
747            if req.has_key('resources') and req['resources'].has_key('node'):
748                resources = req['resources']
749                restricted = [ gateway_hardware(t) for n in resources['node'] \
750                                if n.has_key('hardware') \
751                                    for t in n['hardware'] \
752                                        if gateway_hardware(t) \
753                                            in self.restricted ]
754                inaccessible = [ t for t in restricted \
755                                    if t not in found[0].node_types]
756                if len(inaccessible) > 0:
757                    raise service_error(service_error.access,
758                            "Access denied (nodetypes %s)" % \
759                            str(', ').join(inaccessible))
[d8442da]760            # XXX
[3bddd24]761
762            # These were passed around before, but now are hidden from users
763            # and configurators alike, beyond a configuration file entry.
764            create_ssh = [ self.ssh_pubkey_file ]
765            service_ssh = [ self.ssh_pubkey_file ]
[866c983]766
767            if len(create_ssh) > 0 and len(service_ssh) >0: 
768                if dyn[1]: 
769                    # Compose the dynamic project request
770                    # (only dynamic, dynamic currently allowed)
771                    preq = { 'AllocateProjectRequestBody': \
772                                { 'project' : {\
773                                    'user': [ \
774                                    { \
775                                        'access': [ { 'sshPubkey': s } \
776                                            for s in service_ssh ], 
777                                         'role': "serviceAccess",\
778                                    }, \
779                                    { \
780                                        'access': [ { 'sshPubkey': s } \
781                                            for s in create_ssh ], 
782                                         'role': "experimentCreation",\
783                                    }, \
784                                    ], \
785                                    }\
786                                }\
787                            }
788                    if restricted != None and len(restricted) > 0:
789                        preq['AllocateProjectRequestBody']['resources'] = \
790                             {'node': [ { 'hardware' :  [ h ] } \
791                                    for h in restricted ] } 
792                    ap = self.allocate_project.dynamic_project(preq)
793                else:
794                    preq = {'StaticProjectRequestBody' : \
795                            { 'project': \
796                                { 'name' : { 'localname' : found[0].name },\
797                                  'user' : [ \
798                                    {\
799                                        'userID': { 'localname' : found[1] }, \
800                                        'access': [ { 'sshPubkey': s } 
801                                            for s in create_ssh ],
802                                        'role': 'experimentCreation'\
803                                    },\
804                                    {\
805                                        'userID': { 'localname' : found[2] }, \
806                                        'access': [ { 'sshPubkey': s } 
807                                            for s in service_ssh ],
808                                        'role': 'serviceAccess'\
809                                    },\
810                                ]}\
811                            }\
812                    }
813                    if restricted != None and len(restricted) > 0:
814                        preq['StaticProjectRequestBody']['resources'] = \
815                            {'node': [ { 'hardware' :  [ h ] } \
816                                    for h in restricted ] } 
817                    ap = self.allocate_project.static_project(preq)
818            else:
819                raise service_error(service_error.req, 
820                        "SSH access parameters required")
821            # keep track of what's been added
822            allocID, alloc_cert = generate_fedid(subj="alloc", log=self.log)
823            aid = unicode(allocID)
824
825            self.state_lock.acquire()
826            self.allocation[aid] = { }
827            try:
828                pname = ap['project']['name']['localname']
829            except KeyError:
830                pname = None
831
832            if dyn[1]:
833                if not pname:
834                    self.state_lock.release()
835                    raise service_error(service_error.internal,
836                            "Misformed allocation response?")
837                if self.projects.has_key(pname): self.projects[pname] += 1
838                else: self.projects[pname] = 1
839                self.allocation[aid]['project'] = pname
[ecca6eb]840            else:
841                # sproject is a static project associated with this allocation.
842                self.allocation[aid]['sproject'] = pname
[866c983]843
844            if ap.has_key('resources'):
845                if not pname:
846                    self.state_lock.release()
847                    raise service_error(service_error.internal,
848                            "Misformed allocation response?")
849                self.allocation[aid]['types'] = set()
850                nodes = ap['resources'].get('node', [])
851                for n in nodes:
852                    for h in n.get('hardware', []):
853                        if self.types.has_key((pname, h)):
854                            self.types[(pname, h)] += 1
855                        else:
856                            self.types[(pname, h)] = 1
857                        self.allocation[aid]['types'].add((pname,h))
858
859
860            self.allocation[aid]['keys'] = [ ]
861
862            try:
863                for u in ap['project']['user']:
864                    uname = u['userID']['localname']
[ecca6eb]865                    if u['role'] == 'experimentCreation':
866                        self.allocation[aid]['user'] = uname
[866c983]867                    for k in [ k['sshPubkey'] for k in u['access'] \
868                            if k.has_key('sshPubkey') ]:
869                        kv = "%s:%s" % (uname, k)
870                        if self.keys.has_key(kv): self.keys[kv] += 1
871                        else: self.keys[kv] = 1
872                        self.allocation[aid]['keys'].append((uname, k))
873            except KeyError:
874                self.state_lock.release()
875                raise service_error(service_error.internal,
876                        "Misformed allocation response?")
877
878            self.allocation[aid]['owners'] = owners
[eeb0088]879            services, svc_state = self.export_services(req.get('service',[]),
880                    pname, uname)
881            # Store services state in global state
882            for k, v in svc_state.items():
883                self.allocation[aid][k] = v
[866c983]884            self.write_state()
885            self.state_lock.release()
886            for o in owners:
887                self.auth.set_attribute(o, allocID)
[6c57fe9]888            try:
889                f = open("%s/%s.pem" % (self.certdir, aid), "w")
890                print >>f, alloc_cert
891                f.close()
892            except IOError, e:
893                raise service_error(service_error.internal, 
894                        "Can't open %s/%s : %s" % (self.certdir, aid, e))
[e02cd14]895            resp = self.build_response({ 'fedid': allocID } , ap, services)
[866c983]896            return resp
897        else:
898            if self.allow_proxy:
899                resp = self.proxy_RequestAccess.call_service(dt, req,
900                            self.cert_file, self.cert_pwd,
901                            self.trusted_certs)
902                if resp.has_key('RequestAccessResponseBody'):
903                    return resp['RequestAccessResponseBody']
904                else:
905                    return None
906            else:
907                raise service_error(service_error.access,
908                        "Access proxying denied")
[d81971a]909
910    def ReleaseAccess(self, req, fid):
[866c983]911        # The dance to get into the request body
912        if req.has_key('ReleaseAccessRequestBody'):
913            req = req['ReleaseAccessRequestBody']
914        else:
915            raise service_error(service_error.req, "No request!?")
916
917        if req.has_key('destinationTestbed'):
918            dt = unpack_id(req['destinationTestbed'])
919        else:
920            dt = None
921
922        if dt == None or dt in self.testbed:
923            # Local request
924            try:
925                if req['allocID'].has_key('localname'):
926                    auth_attr = aid = req['allocID']['localname']
927                elif req['allocID'].has_key('fedid'):
928                    aid = unicode(req['allocID']['fedid'])
929                    auth_attr = req['allocID']['fedid']
930                else:
931                    raise service_error(service_error.req,
932                            "Only localnames and fedids are understood")
933            except KeyError:
934                raise service_error(service_error.req, "Badly formed request")
935
936            self.log.debug("[access] deallocation requested for %s", aid)
937            if not self.auth.check_attribute(fid, auth_attr):
938                self.log.debug("[access] deallocation denied for %s", aid)
939                raise service_error(service_error.access, "Access Denied")
940
941            # If we know this allocation, reduce the reference counts and
942            # remove the local allocations.  Otherwise report an error.  If
943            # there is an allocation to delete, del_users will be a dictonary
944            # of sets where the key is the user that owns the keys in the set.
945            # We use a set to avoid duplicates.  del_project is just the name
946            # of any dynamic project to delete.  We're somewhat lazy about
947            # deleting authorization attributes.  Having access to something
948            # that doesn't exist isn't harmful.
949            del_users = { }
950            del_project = None
951            del_types = set()
952
953            if self.allocation.has_key(aid):
954                self.log.debug("Found allocation for %s" %aid)
955                self.state_lock.acquire()
956                for k in self.allocation[aid]['keys']:
957                    kk = "%s:%s" % k
958                    self.keys[kk] -= 1
959                    if self.keys[kk] == 0:
960                        if not del_users.has_key(k[0]):
961                            del_users[k[0]] = set()
962                        del_users[k[0]].add(k[1])
963                        del self.keys[kk]
964
965                if self.allocation[aid].has_key('project'):
966                    pname = self.allocation[aid]['project']
967                    self.projects[pname] -= 1
968                    if self.projects[pname] == 0:
969                        del_project = pname
970                        del self.projects[pname]
971
972                if self.allocation[aid].has_key('types'):
973                    for t in self.allocation[aid]['types']:
974                        self.types[t] -= 1
975                        if self.types[t] == 0:
976                            if not del_project: del_project = t[0]
977                            del_types.add(t[1])
978                            del self.types[t]
979
980                del self.allocation[aid]
981                self.write_state()
982                self.state_lock.release()
983                # If we actually have resources to deallocate, prepare the call.
984                if del_project or del_users:
985                    msg = { 'project': { }}
986                    if del_project:
987                        msg['project']['name']= {'localname': del_project}
988                    users = [ ]
989                    for u in del_users.keys():
990                        users.append({ 'userID': { 'localname': u },\
991                            'access' :  \
992                                    [ {'sshPubkey' : s } for s in del_users[u]]\
993                        })
994                    if users: 
995                        msg['project']['user'] = users
996                    if len(del_types) > 0:
997                        msg['resources'] = { 'node': \
998                                [ {'hardware': [ h ] } for h in del_types ]\
999                            }
1000                    if self.allocate_project.release_project:
1001                        msg = { 'ReleaseProjectRequestBody' : msg}
1002                        self.allocate_project.release_project(msg)
[f07fa49]1003                # And remove the access cert
1004                cf = "%s/%s.pem" % (self.certdir, aid)
1005                self.log.debug("Removing %s" % cf)
1006                os.remove(cf)
[866c983]1007                return { 'allocID': req['allocID'] } 
1008            else:
1009                raise service_error(service_error.req, "No such allocation")
1010
1011        else:
1012            if self.allow_proxy:
1013                resp = self.proxy_ReleaseAccess.call_service(dt, req,
1014                            self.cert_file, self.cert_pwd,
1015                            self.trusted_certs)
1016                if resp.has_key('ReleaseAccessResponseBody'):
1017                    return resp['ReleaseAccessResponseBody']
1018                else:
1019                    return None
1020            else:
1021                raise service_error(service_error.access,
1022                        "Access proxying denied")
[d81971a]1023
[f9ef40b]1024    def generate_portal_configs(self, topo, pubkey_base, secretkey_base, 
[e02cd14]1025            tmpdir, master, lproj, leid, connInfo, services):
1026
1027        def conninfo_to_dict(key, info):
1028            """
1029            Make a cpoy of the connection information about key, and flatten it
1030            into a single dict by parsing out any feddAttrs.
1031            """
1032
1033            rv = None
1034            for i in info:
[617592b]1035                if key == i.get('portal', "") or \
1036                        key in [e.get('element', "") \
1037                        for e in i.get('member', [])]:
[e02cd14]1038                    rv = i.copy()
1039                    break
[617592b]1040
[e02cd14]1041            else:
1042                return rv
1043
1044            if 'fedAttr' in rv:
1045                for a in rv['fedAttr']:
1046                    attr = a.get('attribute', "")
1047                    val = a.get('value', "")
1048                    if attr and attr not in rv:
1049                        rv[attr] = val
1050                del rv['fedAttr']
1051            return rv
1052
1053        # XXX: un hardcode this
[b7f6ccc]1054        def client_null(f, s):
1055            print >>f, "Service: %s" % s['name']
[e02cd14]1056
1057        def client_smb(f, s):
[b7f6ccc]1058            print >>f, "Service: %s" % s['name']
[e02cd14]1059            smbshare = None
1060            smbuser = None
1061            smbproj = None
1062            for a in s.get('fedAttr', []):
1063                if a.get('attribute', '') == 'SMBSHARE':
1064                    smbshare = a.get('value', None)
1065                elif a.get('attribute', '') == 'SMBUSER':
1066                    smbuser = a.get('value', None)
1067                elif a.get('attribute', '') == 'SMBPROJ':
1068                    smbproj = a.get('value', None)
1069
1070            if all((smbshare, smbuser, smbproj)):
1071                print >>f, "SMBshare: %s" % smbshare
1072                print >>f, "ProjectUser: %s" % smbuser
1073                print >>f, "ProjectName: %s" % smbproj
1074
1075        client_service_out = {
1076                'SMB': client_smb,
1077                'tmcd': client_null,
1078                'seer': client_null,
1079                'userconfig': client_null,
[43197eb]1080                'project_export': client_null,
[e02cd14]1081            }
[9b3627e]1082
1083        def server_port(f, s):
1084            p = urlparse(s.get('server', 'http://localhost'))
1085            print >>f, 'port: remote:%s:%s:%s' % (p.port, p.hostname, p.port) 
1086
1087        def server_null(f,s): pass
1088
1089        def server_seer(f, s):
[43197eb]1090            print >>f, 'seer: True'
[9b3627e]1091
1092        server_service_out = {
1093                'SMB': server_port,
1094                'tmcd': server_port,
1095                'userconfig': server_null,
[43197eb]1096                'project_export': server_null,
[9b3627e]1097                'seer': server_seer,
1098            }
[e02cd14]1099        # XXX: end un hardcode this
1100
[ecca6eb]1101
1102        seer_out = False
[1da6a23]1103        client_out = False
[e76f38a]1104        mproj = None
1105        mexp = None
1106        control_gw = None
1107        testbed = ""
1108        # Create configuration files for the portals
[e2a7a413]1109        for e in [ e for e in topo.elements \
[f9ef40b]1110                if isinstance(e, topdl.Computer) and e.get_attribute('portal')]:
1111            myname = e.name[0]
[ecca6eb]1112            type = e.get_attribute('portal_type')
[f9ef40b]1113
[e02cd14]1114            info = conninfo_to_dict(myname, connInfo)
1115
1116            if not info:
1117                raise service_error(service_error.req,
1118                        "No connectivity info for %s" % myname)
1119
1120            peer = info.get('peer', "")
1121            ldomain = self.domain;
[593e901]1122            ssh_port = info.get('ssh_port', 22)
[e02cd14]1123
[e76f38a]1124            # Collect this for the client.conf file
1125            if 'masterexperiment' in info:
1126                mproj, meid = info['masterexperiment'].split("/", 1)
1127
1128            if type in ('control', 'both'):
1129                testbed = e.get_attribute('testbed')
1130                control_gw = myname
[e02cd14]1131
1132            active = info.get('active', 'False')
1133
[0297248]1134            cfn = "%s/%s.gw.conf" % (tmpdir, myname.lower())
[1da6a23]1135            tunnelconfig = self.attrs.has_key('TunnelCfg')
[f9ef40b]1136            try:
1137                f = open(cfn, "w")
[0297248]1138                if active == 'True':
[d87778f]1139                    print >>f, "active: True"
[593e901]1140                    print >>f, "ssh_port: %s" % ssh_port
[0297248]1141                    if type in ('control', 'both'):
[e02cd14]1142                        for s in [s for s in services \
1143                                if s.get('name', "") in self.imports]:
[9b3627e]1144                            server_service_out[s['name']](f, s)
[0297248]1145
1146                if tunnelconfig:
1147                    print >>f, "tunnelip: %s" % tunnelconfig
[e02cd14]1148                print >>f, "peer: %s" % peer.lower()
[0297248]1149                print >>f, "ssh_pubkey: /proj/%s/exp/%s/tmp/%s" % \
[f9ef40b]1150                        (lproj, leid, pubkey_base)
[0297248]1151                print >>f, "ssh_privkey: /proj/%s/exp/%s/tmp/%s" % \
[f9ef40b]1152                        (lproj, leid, secretkey_base)
1153                f.close()
1154            except IOError, e:
1155                raise service_error(service_error.internal,
1156                        "Can't write protal config %s: %s" % (cfn, e))
[1da6a23]1157
[e76f38a]1158        # Done with portals, write the client config file.
1159        try:
1160            f = open("%s/client.conf" % tmpdir, "w")
1161            if control_gw:
1162                print >>f, "ControlGateway: %s.%s.%s%s" % \
1163                    (myname.lower(), leid.lower(), lproj.lower(),
1164                            ldomain.lower())
1165            for s in services:
1166                if s.get('name',"") in self.imports and \
1167                        s.get('visibility','') == 'import':
1168                    client_service_out[s['name']](f, s)
1169            # Seer uses this.
1170            if mproj and meid:
1171                print >>f, "ExperimentID: %s/%s" % (mproj, meid)
1172            # Better way...
1173            if testbed == master:
1174                print >>f, "SEERBase: True"
1175            f.close()
1176        except IOError, e:
1177            raise service_error(service_error.internal,
1178                    "Cannot write client.conf: %s" %s)
[ecca6eb]1179
[617592b]1180    def generate_ns2(self, topo, expfn, softdir, master, connInfo):
1181        class dragon_commands:
1182            """
1183            Functor to spit out approrpiate dragon commands for nodes listed in
1184            the connectivity description.  The constructor makes a dict mapping
1185            dragon nodes to their parameters and the __call__ checks each
1186            element in turn for membership.
1187            """
1188            def __init__(self, map):
1189                self.node_info = map
1190
1191            def __call__(self, e):
1192                s = ""
1193                if isinstance(e, topdl.Computer):
1194                    if self.node_info.has_key(e.name[0]):
1195                        i = self.node_info[e.name[0]]
1196                        for ifname, vlan, type in i:
1197                            for i in e.interface:
1198                                if i.name == ifname:
1199                                    addr = i.get_attribute('ip4_address')
1200                                    subs = i.substrate[0]
1201                                    break
1202                            else:
1203                                raise service_error(service_error.internal,
1204                                        "No interface %s on element %s" % \
1205                                                (ifname, e.name[0]))
[1cf8e2c]1206                            # XXX: do netmask right
[617592b]1207                            if type =='link':
1208                                s = ("tb-allow-external $%s dragonportal " + \
[1cf8e2c]1209                                        "ip %s vlan %s netmask 255.255.255.0\n") % \
[617592b]1210                                        (e.name[0], addr, vlan)
1211                            elif type =='lan':
1212                                s = ("tb-allow-external $%s dragonportal " + \
1213                                        "ip %s vlan %s usurp %s\n") % \
1214                                        (e.name[0], addr, vlan, subs)
1215                            else:
1216                                raise service_error(service_error_internal,
1217                                        "Unknown DRAGON type %s" % type)
1218                return s
1219
1220        class not_dragon:
1221            def __init__(self, map):
1222                self.nodes = set(map.keys())
1223
1224            def __call__(self, e):
1225                return e.name[0] not in self.nodes
[69692a9]1226
[9b3627e]1227
[ecca6eb]1228        t = topo.clone()
1229
[617592b]1230        dragon_map = { }
1231        for i in [ i for i in connInfo if i['type'] == 'transit']:
1232            for a in i.get('fedAttr', []):
1233                if a['attribute'] == 'vlan_id':
1234                    vlan = a['value']
1235                    break
1236            else:
1237                raise service_error(service_error.internal, 
1238                        "No vlan tag")
1239            members = i.get('member', [])
1240            if len(members) > 1: type = 'lan'
1241            else: type = 'link'
1242
1243            try:
1244                for m in members:
1245                    if dragon_map.has_key(m['element']):
1246                        dragon_map[m['element']].append(( m['interface'], 
1247                            vlan, type))
1248                    else:
1249                        dragon_map[m['element']] = [( m['interface'], 
1250                            vlan, type),]
1251            except KeyError:
1252                raise service_error(service_error.req,
1253                        "Missing connectivity info")
1254
[43649f1]1255        # The startcmds for master and slave testbeds
1256        if master: 
1257            gate_cmd = self.attrs.get('MasterConnectorStartCmd', '/bin/true')
1258            node_cmd = self.attrs.get('MasterNodeStartCmd', 'bin/true')
1259        else: 
1260            gate_cmd = self.attrs.get('SlaveConnectorStartCmd', '/bin/true')
1261            node_cmd = self.attrs.get('SlaveNodeStartCmd', 'bin/true')
[0297248]1262
[35aa3ae]1263        # Weed out the things we aren't going to instantiate: Segments, portal
1264        # substrates, and portal interfaces.  (The copy in the for loop allows
1265        # us to delete from e.elements in side the for loop).  While we're
1266        # touching all the elements, we also adjust paths from the original
1267        # testbed to local testbed paths and put the federation commands into
1268        # the start commands
1269        for e in [e for e in t.elements]:
1270            if isinstance(e, topdl.Segment):
1271                t.elements.remove(e)
[43649f1]1272            if isinstance(e, topdl.Computer):
[e76f38a]1273                self.add_kit(e, self.federation_software)
[35aa3ae]1274                if e.get_attribute('portal') and gate_cmd:
[9b3627e]1275                    # Add local portal support software
[e76f38a]1276                    self.add_kit(e, self.portal_software)
[43649f1]1277                    # Portals never have a user-specified start command
1278                    e.set_attribute('startup', gate_cmd)
[35aa3ae]1279                elif node_cmd:
[43649f1]1280                    if e.get_attribute('startup'):
[d87778f]1281                        e.set_attribute('startup', "%s \\$USER '%s'" % \
[43649f1]1282                                (node_cmd, e.get_attribute('startup')))
1283                    else:
[d87778f]1284                        e.set_attribute('startup', node_cmd)
[0297248]1285
[617592b]1286                dinf = [i[0] for i in dragon_map.get(e.name[0], []) ]
[35aa3ae]1287                # Remove portal interfaces that do not connect to DRAGON
1288                e.interface = [i for i in e.interface \
[617592b]1289                        if not i.get_attribute('portal') or i.name in dinf ]
[9b3627e]1290            # Fix software paths
1291            for s in getattr(e, 'software', []):
1292                s.location = re.sub("^.*/", softdir, s.location)
[35aa3ae]1293
1294        t.substrates = [ s.clone() for s in t.substrates ]
1295        t.incorporate_elements()
[ecca6eb]1296
1297        # Customize the ns2 output for local portal commands and images
1298        filters = []
1299
[43649f1]1300        # NB: these are extra commands issued for the node, not the startcmds
1301        if master: cmd = self.attrs.get('MasterConnectorCmd', '')
1302        else: cmd = self.attrs.get('SlaveConnectorCmd', '')
[ecca6eb]1303
[69692a9]1304        if self.attrs.has_key('dragon'):
[617592b]1305            add_filter = not_dragon(dragon_map)
1306            filters.append(dragon_commands(dragon_map))
[69692a9]1307        else:
1308            add_filter = None
1309
[43649f1]1310        if cmd:
1311            filters.append(topdl.generate_portal_command_filter(cmd,
1312                add_filter=add_filter))
[ecca6eb]1313
1314        if self.attrs.has_key('connectorImage'):
1315            filters.append(topdl.generate_portal_image_filter(
1316                self.attrs.get('connectorImage')))
1317
1318        if self.attrs.has_key('connectorType'):
1319            filters.append(topdl.generate_portal_hardware_filter(
1320                self.attrs.get('connectorType')))
1321
1322        # Convert to ns and write it out
1323        expfile = topdl.topology_to_ns2(t, filters)
1324        try:
1325            f = open(expfn, "w")
1326            print >>f, expfile
1327            f.close()
1328        except IOError:
1329            raise service_error(service_error.internal,
1330                    "Cannot write experiment file %s: %s" % (expfn,e))
[f9ef40b]1331
[2761484]1332    def export_store_info(self, cf, proj, ename, connInfo):
1333        """
1334        For the export requests in the connection info, install the peer names
1335        at the experiment controller via SetValue calls.
1336        """
1337
1338        for c in connInfo:
1339            for p in [ p for p in c.get('parameter', []) \
1340                    if p.get('type', '') == 'output']:
1341
[6280b1f]1342                if p.get('name', '') == 'peer':
1343                    k = p.get('key', None)
1344                    surl = p.get('store', None)
1345                    if surl and k and k.index('/') != -1:
1346                        value = "%s.%s.%s%s" % \
1347                                (k[k.index('/')+1:], ename, proj, self.domain)
1348                        req = { 'name': k, 'value': value }
[d8442da]1349                        self.log.debug("Setting %s to %s on %s" % \
1350                                (k, value, surl))
[2ee4226]1351                        self.call_SetValue(surl, req, cf)
[6280b1f]1352                    else:
1353                        self.log.error("Bad export request: %s" % p)
1354                elif p.get('name', '') == 'ssh_port':
1355                    k = p.get('key', None)
1356                    surl = p.get('store', None)
1357                    if surl and k:
1358                        req = { 'name': k, 'value': self.ssh_port }
[d8442da]1359                        self.log.debug("Setting %s to %s on %s" % \
1360                                (k, self.ssh_port, surl))
[2ee4226]1361                        self.call_SetValue(surl, req, cf)
[6280b1f]1362                    else:
1363                        self.log.error("Bad export request: %s" % p)
1364                else:
[2761484]1365                    self.log.error("Unknown export parameter: %s" % \
1366                            p.get('name'))
1367                    continue
1368
1369    def import_store_info(self, cf, connInfo):
1370        """
1371        Pull any import parameters in connInfo in.  We translate them either
1372        into known member names or fedAddrs.
1373        """
1374
1375        for c in connInfo:
1376            for p in [ p for p in c.get('parameter', []) \
1377                    if p.get('type', '') == 'input']:
1378                name = p.get('name', None)
1379                key = p.get('key', None)
1380                store = p.get('store', None)
1381
1382                if name and key and store :
1383                    req = { 'name': key, 'wait': True }
[d8442da]1384                    self.log.debug("Waiting for %s (%s) from %s" % \
1385                            (name, key, store))
[2ee4226]1386                    r = self.call_GetValue(store, req, cf)
[2761484]1387                    r = r.get('GetValueResponseBody', None)
1388                    if r :
1389                        if r.get('name', '') == key:
1390                            v = r.get('value', None)
1391                            if v is not None:
1392                                if name == 'peer':
[d8442da]1393                                    self.log.debug("Got peer %s" % v)
[2761484]1394                                    c['peer'] = v
1395                                else:
[d8442da]1396                                    self.log.debug("Got %s %s" % (name, v))
[2761484]1397                                    if c.has_key('fedAttr'):
1398                                        c['fedAttr'].append({
[8353ac6]1399                                            'attribute': name, 'value': v})
[2761484]1400                                    else:
1401                                        c['fedAttr']= [{
[8353ac6]1402                                            'attribute': name, 'value': v}]
[2761484]1403                            else:
1404                                raise service_error(service_error.internal, 
1405                                        'None value exported for %s'  % key)
1406                        else:
1407                            raise service_error(service_error.internal, 
1408                                    'Different name returned for %s: %s' \
1409                                            % (key, r.get('name','')))
1410                    else:
1411                        raise service_error(service_error.internal, 
1412                            'Badly formatted response: no GetValueResponseBody')
1413                else:
1414                    raise service_error(service_error.internal, 
1415                        'Bad Services missing info for import %s' % c)
1416
[cc8d8e9]1417    def StartSegment(self, req, fid):
[6c57fe9]1418
1419        configs = set(('hosts', 'ssh_pubkey', 'ssh_secretkey'))
1420
[b770aa0]1421        err = None  # Any service_error generated after tmpdir is created
1422        rv = None   # Return value from segment creation
1423
[cc8d8e9]1424        try:
1425            req = req['StartSegmentRequestBody']
1426        except KeyError:
1427            raise service_error(server_error.req, "Badly formed request")
[ecca6eb]1428
[e02cd14]1429        connInfo = req.get('connection', [])
1430        services = req.get('service', [])
[cc8d8e9]1431        auth_attr = req['allocID']['fedid']
[ecca6eb]1432        aid = "%s" % auth_attr
[6c57fe9]1433        attrs = req.get('fedAttr', [])
[ecca6eb]1434        if not self.auth.check_attribute(fid, auth_attr):
1435            raise service_error(service_error.access, "Access denied")
[cd06678]1436        else:
1437            # See if this is a replay of an earlier succeeded StartSegment -
1438            # sometimes SSL kills 'em.  If so, replay the response rather than
1439            # redoing the allocation.
1440            self.state_lock.acquire()
1441            retval = self.allocation[aid].get('started', None)
1442            self.state_lock.release()
1443            if retval:
1444                self.log.warning("Duplicate StartSegment for %s: " % aid + \
1445                        "replaying response")
1446                return retval
1447
1448        # A new request.  Do it.
[6c57fe9]1449
1450        if req.has_key('segmentdescription') and \
1451                req['segmentdescription'].has_key('topdldescription'):
1452            topo = \
1453                topdl.Topology(**req['segmentdescription']['topdldescription'])
1454        else:
1455            raise service_error(service_error.req, 
1456                    "Request missing segmentdescription'")
[2761484]1457       
[ecca6eb]1458        master = req.get('master', False)
1459
[6c57fe9]1460        certfile = "%s/%s.pem" % (self.certdir, auth_attr)
1461        try:
1462            tmpdir = tempfile.mkdtemp(prefix="access-")
[ecca6eb]1463            softdir = "%s/software" % tmpdir
[c23684d]1464            os.mkdir(softdir)
[6c57fe9]1465        except IOError:
1466            raise service_error(service_error.internal, "Cannot create tmp dir")
1467
[b770aa0]1468        # Try block alllows us to clean up temporary files.
1469        try:
1470            sw = set()
1471            for e in topo.elements:
1472                for s in getattr(e, 'software', []):
1473                    sw.add(s.location)
1474            for s in sw:
[e02cd14]1475                self.log.debug("Retrieving %s" % s)
[1dcaff4]1476                try:
1477                    get_url(s, certfile, softdir)
1478                except:
1479                    t, v, st = sys.exc_info()
1480                    raise service_error(service_error.internal,
1481                            "Error retrieving %s: %s" % (s, v))
[b770aa0]1482
[1cf8e2c]1483            # Copy local federation and portal node software to the tempdir
1484            for s in (self.federation_software, self.portal_software):
1485                for l, f in s:
1486                    base = os.path.basename(f)
1487                    copy_file(f, "%s/%s" % (softdir, base))
[9b3627e]1488
[ab847bc]1489            ename = None
1490            pubkey_base = None
1491            secretkey_base = None
[b770aa0]1492            for a in attrs:
1493                if a['attribute'] in configs:
[1dcaff4]1494                    try:
[d8442da]1495                        self.log.debug("Retrieving %s from %s" % \
1496                                (a['attribute'], a['value']))
[1dcaff4]1497                        get_url(a['value'], certfile, tmpdir)
1498                    except:
1499                        t, v, st = sys.exc_info()
1500                        raise service_error(service_error.internal,
1501                                "Error retrieving %s: %s" % (s, v))
[b770aa0]1502                if a['attribute'] == 'ssh_pubkey':
1503                    pubkey_base = a['value'].rpartition('/')[2]
1504                if a['attribute'] == 'ssh_secretkey':
1505                    secretkey_base = a['value'].rpartition('/')[2]
1506                if a['attribute'] == 'experiment_name':
1507                    ename = a['value']
1508
[ab847bc]1509            if not ename:
1510                ename = ""
1511                for i in range(0,5):
1512                    ename += random.choice(string.ascii_letters)
1513                self.log.warn("No experiment name: picked one randomly: %s" \
1514                        % ename)
1515
1516            if not pubkey_base:
1517                raise service_error(service_error.req, 
1518                        "No public key attribute")
1519
1520            if not secretkey_base:
1521                raise service_error(service_error.req, 
1522                        "No secret key attribute")
1523
[fe28bb2]1524            # If the userconf service was imported, collect the configuration
1525            # data.
1526            for s in services:
[e76f38a]1527                s_name = s.get('name', '')
1528                s_vis = s.get('visibility','')
1529                if s_name  == 'userconfig' and s_vis == 'import':
[94a00cb]1530                    # Collect ther server and certificate info.
[fe28bb2]1531                    u = s.get('server', None)
1532                    for a in s.get('fedAttr', []):
1533                        if a.get('attribute',"") == 'cert':
1534                            cert = a.get('value', None)
1535                            break
1536                    else:
1537                        cert = None
1538
[94a00cb]1539                    if cert:
1540                        # Make a temporary certificate file for get_url.  The
1541                        # finally clause removes it whether something goes
1542                        # wrong (including an exception from get_url) or not.
1543                        try:
1544                            tfos, tn = tempfile.mkstemp(suffix=".pem")
1545                            tf = os.fdopen(tfos, 'w')
1546                            print >>tf, cert
1547                            tf.close()
[d8442da]1548                            self.log.debug("Getting userconf info: %s" % u)
[94a00cb]1549                            get_url(u, tn, tmpdir, "userconf")
[d8442da]1550                            self.log.debug("Got userconf info: %s" % u)
[94a00cb]1551                        except IOError, e:
1552                            raise service_error(service.error.internal, 
1553                                    "Cannot create temp file for " + 
1554                                    "userconfig certificates: %s e")
[1dcaff4]1555                        except:
1556                            t, v, st = sys.exc_info()
1557                            raise service_error(service_error.internal,
1558                                    "Error retrieving %s: %s" % (s, v))
[94a00cb]1559                        finally:
1560                            if tn: os.remove(tn)
1561                    else:
[fe28bb2]1562                        raise service_error(service_error.req,
1563                                "No certificate for retreiving userconfig")
1564                    break
[e76f38a]1565                if s_name  == 'local_seer_control'  and s_vis == 'export':
1566                    # Copy local seer control node software to the tempdir
1567                    for l, f in self.local_seer_software:
1568                        base = os.path.basename(f)
1569                        copy_file(f, "%s/%s" % (softdir, base))
1570
1571                    # Add a control node to the local topology
1572                    c_node = topdl.Computer(
1573                            name='control', 
[d2c2315]1574                            os= topdl.OperatingSystem(
1575                                attribute=[
1576                                { 'attribute': 'osid', 
[e76f38a]1577                                    'value': self.local_seer_image },
[d2c2315]1578                                ]),
1579                            attribute=[
[e76f38a]1580                                { 'attribute': 'startup',
1581                                    'value': self.local_seer_start },
1582                                ]
1583                            )
1584                    self.add_kit(c_node, self.local_seer_software)
1585                    topo.elements.append(c_node)
[fe28bb2]1586
[b770aa0]1587            proj = None
1588            user = None
1589            self.state_lock.acquire()
1590            if self.allocation.has_key(aid):
1591                proj = self.allocation[aid].get('project', None)
1592                if not proj: 
1593                    proj = self.allocation[aid].get('sproject', None)
1594                user = self.allocation[aid].get('user', None)
1595                self.allocation[aid]['experiment'] = ename
[f07fa49]1596                self.allocation[aid]['log'] = [ ]
1597                # Create a logger that logs to the experiment's state object as
1598                # well as to the main log file.
1599                alloc_log = logging.getLogger('fedd.access.%s' % ename)
1600                h = logging.StreamHandler(
1601                        list_log.list_log(self.allocation[aid]['log']))
1602                # XXX: there should be a global one of these rather than
1603                # repeating the code.
1604                h.setFormatter(logging.Formatter(
1605                    "%(asctime)s %(name)s %(message)s",
1606                            '%d %b %y %H:%M:%S'))
1607                alloc_log.addHandler(h)
[b770aa0]1608                self.write_state()
1609            self.state_lock.release()
1610
1611            if not proj:
1612                raise service_error(service_error.internal, 
1613                        "Can't find project for %s" %aid)
1614
1615            if not user:
1616                raise service_error(service_error.internal, 
1617                        "Can't find creation user for %s" %aid)
1618
[2761484]1619            self.export_store_info(certfile, proj, ename, connInfo)
1620            self.import_store_info(certfile, connInfo)
1621
[b770aa0]1622            expfile = "%s/experiment.tcl" % tmpdir
1623
1624            self.generate_portal_configs(topo, pubkey_base, 
[e02cd14]1625                    secretkey_base, tmpdir, master, proj, ename, connInfo, 
1626                    services)
[b770aa0]1627            self.generate_ns2(topo, expfile, 
[617592b]1628                    "/proj/%s/software/%s/" % (proj, ename), master, connInfo)
[f07fa49]1629
[b770aa0]1630            starter = self.start_segment(keyfile=self.ssh_privkey_file, 
[f07fa49]1631                    debug=self.create_debug, log=alloc_log)
[b770aa0]1632            rv = starter(self, ename, proj, user, expfile, tmpdir)
[b4b19c7]1633            # Copy the assigned names into the return topology
1634            rvtopo = topo.clone()
1635            for e in [ e for e in rvtopo.elements \
1636                   if isinstance(e, topdl.Computer)]:
1637                for n in e.name:
1638                    if n in starter.node:
[935e46f]1639                        e.set_attribute('hostname', "%s%s" % \
1640                                (starter.node[n], self.domain))
[b770aa0]1641        except service_error, e:
1642            err = e
[e02cd14]1643        except e:
1644            err = service_error(service_error.internal, str(e))
[b770aa0]1645
[574055e]1646        # Walk up tmpdir, deleting as we go
[69692a9]1647        if self.cleanup:
1648            self.log.debug("[StartSegment]: removing %s" % tmpdir)
1649            for path, dirs, files in os.walk(tmpdir, topdown=False):
1650                for f in files:
1651                    os.remove(os.path.join(path, f))
1652                for d in dirs:
1653                    os.rmdir(os.path.join(path, d))
1654            os.rmdir(tmpdir)
1655        else:
1656            self.log.debug("[StartSegment]: not removing %s" % tmpdir)
[574055e]1657
[fd556d1]1658        if rv:
[f07fa49]1659            # Grab the log (this is some anal locking, but better safe than
1660            # sorry)
1661            self.state_lock.acquire()
1662            logv = "".join(self.allocation[aid]['log'])
[cd06678]1663            # It's possible that the StartSegment call gets retried (!).
1664            # if the 'started' key is in the allocation, we'll return it rather
1665            # than redo the setup.
1666            self.allocation[aid]['started'] = { 
1667                    'allocID': req['allocID'],
1668                    'allocationLog': logv,
1669                    }
[b4b19c7]1670            retval = copy.copy(self.allocation[aid]['started'])
[1627c3d]1671            self.write_state()
[f07fa49]1672            self.state_lock.release()
[b4b19c7]1673            retval['segmentdescription'] =  \
1674                    { 'topdldescription': rvtopo.to_dict() }
[cd06678]1675            return retval
[b770aa0]1676        elif err:
1677            raise service_error(service_error.federant,
1678                    "Swapin failed: %s" % err)
[fd556d1]1679        else:
1680            raise service_error(service_error.federant, "Swapin failed")
[5ae3857]1681
1682    def TerminateSegment(self, req, fid):
1683        try:
1684            req = req['TerminateSegmentRequestBody']
1685        except KeyError:
1686            raise service_error(server_error.req, "Badly formed request")
1687
1688        auth_attr = req['allocID']['fedid']
1689        aid = "%s" % auth_attr
1690        attrs = req.get('fedAttr', [])
1691        if not self.auth.check_attribute(fid, auth_attr):
1692            raise service_error(service_error.access, "Access denied")
1693
1694        self.state_lock.acquire()
1695        if self.allocation.has_key(aid):
1696            proj = self.allocation[aid].get('project', None)
1697            if not proj: 
1698                proj = self.allocation[aid].get('sproject', None)
1699            user = self.allocation[aid].get('user', None)
1700            ename = self.allocation[aid].get('experiment', None)
[1d913e13]1701        else:
1702            proj = None
1703            user = None
1704            ename = None
[5ae3857]1705        self.state_lock.release()
1706
1707        if not proj:
1708            raise service_error(service_error.internal, 
1709                    "Can't find project for %s" % aid)
1710
1711        if not user:
1712            raise service_error(service_error.internal, 
1713                    "Can't find creation user for %s" % aid)
1714        if not ename:
1715            raise service_error(service_error.internal, 
1716                    "Can't find experiment name for %s" % aid)
[fd556d1]1717        stopper = self.stop_segment(keyfile=self.ssh_privkey_file,
1718                debug=self.create_debug)
[5ae3857]1719        stopper(self, user, proj, ename)
1720        return { 'allocID': req['allocID'] }
Note: See TracBrowser for help on using the repository browser.