source: fedd/federation/emulab_access.py @ eeb0088

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

Beginnings of userconf

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