source: fedd/federation/protogeni_access.py @ c119839

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

Initial check in

  • Property mode set to 100644
File size: 37.0 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 access_project import access_project
16from fedid import fedid, generate_fedid
17from authorizer import authorizer
18from service_error import service_error
19from remote_service import xmlrpc_handler, soap_handler, service_caller
20
21import httplib
22import tempfile
23from urlparse import urlparse
24
25import topdl
26import list_log
27import proxy_protogeni_segment
28
29
30# Make log messages disappear if noone configures a fedd logger
31class nullHandler(logging.Handler):
32    def emit(self, record): pass
33
34fl = logging.getLogger("fedd.access")
35fl.addHandler(nullHandler())
36
37class access:
38    """
39    The implementation of access control based on mapping users to projects.
40
41    Users can be mapped to existing projects or have projects created
42    dynamically.  This implements both direct requests and proxies.
43    """
44
45    class parse_error(RuntimeError): pass
46
47
48    proxy_RequestAccess= service_caller('RequestAccess')
49    proxy_ReleaseAccess= service_caller('ReleaseAccess')
50
51    def __init__(self, config=None, auth=None):
52        """
53        Initializer.  Pulls parameters out of the ConfigParser's access section.
54        """
55
56        # Make sure that the configuration is in place
57        if not config: 
58            raise RunTimeError("No config to fedd.access")
59
60        self.project_priority = config.getboolean("access", "project_priority")
61        self.allow_proxy = config.getboolean("access", "allow_proxy")
62
63        self.domain = config.get("access", "domain")
64        self.certdir = config.get("access","certdir")
65        self.userconfdir = config.get("access","userconfdir")
66        self.userconfcmd = config.get("access","userconfcmd")
67        self.userconfurl = config.get("access","userconfurl")
68        self.ssh_port = config.get("access","ssh_port") or "22"
69        self.sshd = config.get("access","sshd")
70        self.sshd_config = config.get("access", "sshd_config")
71        self.create_debug = config.getboolean("access", "create_debug")
72        self.cleanup = not config.getboolean("access", "leave_tmpfiles")
73        self.access_type = config.get("access", "type")
74        self.staging_dir = config.get("access", "staging_dir") or "/tmp"
75        self.staging_host = config.get("access", "staging_host") \
76                or "ops.emulab.net"
77
78        self.ch_url = config.get("access", "ch_url")
79        self.sa_url = config.get("access", "sa_url")
80        self.cm_url = config.get("access", "cm_url")
81
82        self.attrs = { }
83        self.access = { }
84        self.restricted = [ ]
85        self.projects = { }
86        self.keys = { }
87        self.types = { }
88        self.allocation = { }
89        self.state = { 
90            'projects': self.projects,
91            'allocation' : self.allocation,
92            'keys' : self.keys,
93            'types': self.types
94        }
95        self.log = logging.getLogger("fedd.access")
96        set_log_level(config, "access", self.log)
97        self.state_lock = Lock()
98        # XXX: Configurable
99        self.exports = set(('SMB', 'seer', 'tmcd', 'userconfig'))
100        self.imports = set(('SMB', 'seer', 'userconfig'))
101
102        if auth: self.auth = auth
103        else:
104            self.log.error(\
105                    "[access]: No authorizer initialized, creating local one.")
106            auth = authorizer()
107
108        tb = config.get('access', 'testbed')
109        if tb: self.testbed = [ t.strip() for t in tb.split(',') ]
110        else: self.testbed = [ ]
111
112        if config.has_option("access", "accessdb"):
113            self.read_access(config.get("access", "accessdb"))
114
115        self.state_filename = config.get("access", "access_state")
116        print "Calling read_state %s" % self.state_filename
117        self.read_state()
118
119        # Keep cert_file and cert_pwd coming from the same place
120        self.cert_file = config.get("access", "cert_file")
121        if self.cert_file:
122            self.sert_pwd = config.get("access", "cert_pw")
123        else:
124            self.cert_file = config.get("globals", "cert_file")
125            self.sert_pwd = config.get("globals", "cert_pw")
126
127        self.trusted_certs = config.get("access", "trusted_certs") or \
128                config.get("globals", "trusted_certs")
129
130        self.start_segment = proxy_protogeni_segment.start_segment
131        self.stop_segment = proxy_protogeni_segment.stop_segment
132
133        self.call_SetValue = service_caller('SetValue')
134        self.call_GetValue = service_caller('GetValue')
135
136        self.soap_services = {\
137            'RequestAccess': soap_handler("RequestAccess", self.RequestAccess),
138            'ReleaseAccess': soap_handler("ReleaseAccess", self.ReleaseAccess),
139            'StartSegment': soap_handler("StartSegment", self.StartSegment),
140            'TerminateSegment': soap_handler("TerminateSegment", 
141                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    def read_access(self, config):
154        """
155        Read a configuration file and set internal parameters.
156
157        There are access lines of the
158        form (tb, proj, user) -> user that map the first tuple of
159        names to the user for for access purposes.  Names in the key (left side)
160        can include "<NONE> or <ANY>" to act as wildcards or to require the
161        fields to be empty.  Similarly aproj or auser can be <SAME> or
162        <DYNAMIC> indicating that either the matching key is to be used or a
163        dynamic user or project will be created.  These names can also be
164        federated IDs (fedid's) if prefixed with fedid:.  The user is the
165        ProtoGENI identity certificate.
166        Testbed attributes outside the forms above can be given using the
167        format attribute: name value: value.  The name is a single word and the
168        value continues to the end of the line.  Empty lines and lines startin
169        with a # are ignored.
170
171        Parsing errors result in a self.parse_error exception being raised.
172        """
173        lineno=0
174        name_expr = "["+string.ascii_letters + string.digits + "\/\.\-_]+"
175        fedid_expr = "fedid:[" + string.hexdigits + "]+"
176        key_name = "(<ANY>|<NONE>|"+fedid_expr + "|"+ name_expr + ")"
177
178        attr_re = re.compile('attribute:\s*([\._\-a-z0-9]+)\s+value:\s*(.*)',
179                re.IGNORECASE)
180        access_str = '\('+key_name+'\s*,\s*'+key_name+'\s*,\s*'+ \
181                key_name+'\s*\)\s*->\s*\(('+name_expr +')\s*,\s*('\
182                        + name_expr + ')\s*,\s*('+name_expr+')\s*,?\s*(' + \
183                        name_expr+ ')?\)'
184        access_re = re.compile(access_str, re.IGNORECASE)
185
186
187        def parse_name(n):
188            if n.startswith('fedid:'): return fedid(hexstr=n[len('fedid:'):])
189            else: return n
190       
191        def auth_name(n):
192            if isinstance(n, basestring):
193                if n =='<any>' or n =='<none>': return None
194                else: return unicode(n)
195            else:
196                return n
197
198        f = open(config, "r");
199        for line in f:
200            lineno += 1
201            line = line.strip();
202            if len(line) == 0 or line.startswith('#'):
203                continue
204
205            # Extended (attribute: x value: y) attribute line
206            m = attr_re.match(line)
207            if m != None:
208                attr, val = m.group(1,2)
209                self.attrs[attr] = val
210                continue
211
212            # Access line (t, p, u) -> (a, pw) line
213            # XXX: you are here
214            m = access_re.match(line)
215            if m != None:
216                access_key = tuple([ parse_name(x) for x in m.group(1,2,3)])
217                auth_key = tuple([ auth_name(x) for x in access_key])
218                cert = auth_name(parse_name(m.group(4)))
219                user_name = auth_name(parse_name(m.group(5)))
220                ssh_key = unicode(m.group(6))
221                if m.group(6): pw = unicode(m.group(7))
222                else: pw = None
223
224                self.access[access_key] = (cert, user_name, ssh_key, pw)
225                self.auth.set_attribute(auth_key, "access")
226                continue
227
228            # Nothing matched to here: unknown line - raise exception
229            f.close()
230            raise self.parse_error("Unknown statement at line %d of %s" % \
231                    (lineno, config))
232        f.close()
233
234# need this ?
235    def get_users(self, obj):
236        """
237        Return a list of the IDs of the users in dict
238        """
239        if obj.has_key('user'):
240            return [ unpack_id(u['userID']) \
241                    for u in obj['user'] if u.has_key('userID') ]
242        else:
243            return None
244# need this ?
245
246    def write_state(self):
247        if self.state_filename:
248            try:
249                f = open(self.state_filename, 'w')
250                pickle.dump(self.state, f)
251            except IOError, e:
252                self.log.error("Can't write file %s: %s" % \
253                        (self.state_filename, e))
254            except pickle.PicklingError, e:
255                self.log.error("Pickling problem: %s" % e)
256            except TypeError, e:
257                self.log.error("Pickling problem (TypeError): %s" % e)
258
259
260    def read_state(self):
261        """
262        Read a new copy of access state.  Old state is overwritten.
263
264        State format is a simple pickling of the state dictionary.
265        """
266        if self.state_filename:
267            try:
268                f = open(self.state_filename, "r")
269                self.state = pickle.load(f)
270                self.log.debug("[read_state]: Read state from %s" % \
271                        self.state_filename)
272            except IOError, e:
273                self.log.warning(("[read_state]: No saved state: " +\
274                        "Can't open %s: %s") % (self.state_filename, e))
275            except EOFError, e:
276                self.log.warning(("[read_state]: " +\
277                        "Empty or damaged state file: %s:") % \
278                        self.state_filename)
279            except pickle.UnpicklingError, e:
280                self.log.warning(("[read_state]: No saved state: " + \
281                        "Unpickling failed: %s") % e)
282
283            # Add the ownership attributes to the authorizer.  Note that the
284            # indices of the allocation dict are strings, but the attributes are
285            # fedids, so there is a conversion.
286            for k in self.state.get('allocation', {}).keys():
287                for o in self.state['allocation'][k].get('owners', []):
288                    self.auth.set_attribute(o, fedid(hexstr=k))
289                self.auth.set_attribute(fedid(hexstr=k),fedid(hexstr=k))
290
291            if self.allocation != self.state['allocation']:
292                self.allocation = self.state['allocation']
293
294    def permute_wildcards(self, a, p):
295        """Return a copy of a with various fields wildcarded.
296
297        The bits of p control the wildcards.  A set bit is a wildcard
298        replacement with the lowest bit being user then project then testbed.
299        """
300        if p & 1: user = ["<any>"]
301        else: user = a[2]
302        if p & 2: proj = "<any>"
303        else: proj = a[1]
304        if p & 4: tb = "<any>"
305        else: tb = a[0]
306
307        return (tb, proj, user)
308
309
310    def find_access(self, search):
311        """
312        Search the access DB for a match on this tuple.  Return the matching
313        user (protoGENI cert).
314       
315        NB, if the initial tuple fails to match we start inserting wildcards in
316        an order determined by self.project_priority.  Try the list of users in
317        order (when wildcarded, there's only one user in the list).
318        """
319        if self.project_priority: perm = (0, 1, 2, 3, 4, 5, 6, 7)
320        else: perm = (0, 2, 1, 3, 4, 6, 5, 7)
321
322        for p in perm: 
323            s = self.permute_wildcards(search, p)
324            # s[2] is None on an anonymous, unwildcarded request
325            if s[2] != None:
326                for u in s[2]:
327                    if self.access.has_key((s[0], s[1], u)):
328                        return self.access[(s[0], s[1], u)]
329            else:
330                if self.access.has_key(s):
331                    return self.access[s]
332        return None
333
334    def lookup_access(self, req, fid):
335        """
336        Determine the allowed access for this request.  Return the access and
337        which fields are dynamic.
338
339        The fedid is needed to construct the request
340        """
341        # Search keys
342        tb = None
343        project = None
344        user = None
345        # Return values
346        rp = access_project(None, ())
347        ru = None
348        user_re = re.compile("user:\s(.*)")
349        project_re = re.compile("project:\s(.*)")
350
351        user = [ user_re.findall(x)[0] for x in req.get('credential', []) \
352                if user_re.match(x)]
353        project = [ project_re.findall(x)[0] \
354                for x in req.get('credential', []) \
355                    if project_re.match(x)]
356
357        if len(project) == 1: project = project[0]
358        elif len(project) == 0: project = None
359        else: 
360            raise service_error(service_error.req, 
361                    "More than one project credential")
362
363
364        user_fedids = [ u for u in user if isinstance(u, fedid)]
365
366        # Determine how the caller is representing itself.  If its fedid shows
367        # up as a project or a singleton user, let that stand.  If neither the
368        # usernames nor the project name is a fedid, the caller is a testbed.
369        if project and isinstance(project, fedid):
370            if project == fid:
371                # The caller is the project (which is already in the tuple
372                # passed in to the authorizer)
373                owners = user_fedids
374                owners.append(project)
375            else:
376                raise service_error(service_error.req,
377                        "Project asserting different fedid")
378        else:
379            if fid not in user_fedids:
380                tb = fid
381                owners = user_fedids
382                owners.append(fid)
383            else:
384                if len(fedids) > 1:
385                    raise service_error(service_error.req,
386                            "User asserting different fedid")
387                else:
388                    # Which is a singleton
389                    owners = user_fedids
390        # Confirm authorization
391
392        for u in user:
393            self.log.debug("[lookup_access] Checking access for %s" % \
394                    ((tb, project, u),))
395            if self.auth.check_attribute((tb, project, u), 'access'):
396                self.log.debug("[lookup_access] Access granted")
397                break
398            else:
399                self.log.debug("[lookup_access] Access Denied")
400        else:
401            raise service_error(service_error.access, "Access denied")
402
403        # This maps a valid user to the ProtoGENI credentials to use
404        found = self.find_access((tb, project, user))
405       
406        if found == None:
407            raise service_error(service_error.access,
408                    "Access denied - cannot map access")
409        return found, owners
410
411    def get_handler(self, path, fid):
412        self.log.info("Get handler %s %s" % (path, fid))
413        if self.auth.check_attribute(fid, path) and self.userconfdir:
414            return ("%s/%s" % (self.userconfdir, path), "application/binary")
415        else:
416            return (None, None)
417
418    def export_userconf(self, project):
419        dev_null = None
420        confid, confcert = generate_fedid("test", dir=self.userconfdir, 
421                log=self.log)
422        conffilename = "%s/%s" % (self.userconfdir, str(confid))
423        cf = None
424        try:
425            cf = open(conffilename, "w")
426            os.chmod(conffilename, stat.S_IRUSR | stat.S_IWUSR)
427        except IOError, e:
428            raise service_error(service_error.internal, 
429                    "Cannot create user configuration data")
430
431        try:
432            dev_null = open("/dev/null", "a")
433        except IOError, e:
434            self.log.error("export_userconf: can't open /dev/null: %s" % e)
435
436        cmd = "%s %s" % (self.userconfcmd, project)
437        conf = subprocess.call(cmd.split(" "),
438                stdout=cf, stderr=dev_null, close_fds=True)
439
440        self.auth.set_attribute(confid, "/%s" % str(confid))
441
442        return confid, confcert
443
444
445    def export_services(self, sreq, project, user):
446        exp = [ ]
447        state = { }
448        # XXX: Filthy shortcut here using http: so urlparse will give the right
449        # answers.
450        for s in sreq:
451            sname = s.get('name', '')
452            svis = s.get('visibility', '')
453            if svis == 'export':
454                if sname in self.exports:
455                    outs = s.copy()
456                    if sname == 'SMB':
457                        outs = s.copy()
458                        outs['server'] = "http://fs:139" 
459                        outs['fedAttr'] = [
460                                { 'attribute': 'SMBSHARE', 'value': 'USERS' },
461                                { 'attribute': 'SMBUSER', 'value': user },
462                                { 'attribute': 'SMBPROJ', 'value': project },
463                            ]
464                    elif sname == 'seer':
465                        outs['server'] = "http://control:16606"
466                    elif sname == 'tmcd':
467                        outs['server'] = "http://boss:7777"
468                    elif sname == 'userconfig':
469                        if self.userconfdir and self.userconfcmd \
470                                and self.userconfurl:
471                            cid, cert = self.export_userconf(project)
472                            outs['server'] = "%s/%s" % \
473                                    (self.userconfurl, str(cid))
474                            outs['fedAttr'] = [
475                                    { 'attribute': 'cert', 'value': cert },
476                                ]
477                            state['userconfig'] = unicode(cid)
478                    exp.append(outs)
479        return (exp, state)
480
481    def build_response(self, alloc_id, ap, services):
482        """
483        Create the SOAP response.
484
485        Build the dictionary description of the response and use
486        fedd_utils.pack_soap to create the soap message.  ap is the allocate
487        project message returned from a remote project allocation (even if that
488        allocation was done locally).
489        """
490        # Because alloc_id is already a fedd_services_types.IDType_Holder,
491        # there's no need to repack it
492        msg = { 
493                'allocID': alloc_id,
494                'fedAttr': [
495                    { 'attribute': 'domain', 'value': self.domain } , 
496                    { 'attribute': 'project', 'value': 
497                        ap['project'].get('name', {}).get('localname', "???") },
498                ]
499            }
500        if len(self.attrs) > 0:
501            msg['fedAttr'].extend(
502                [ { 'attribute': x, 'value' : y } \
503                        for x,y in self.attrs.iteritems()])
504
505        if services:
506            msg['service'] = services
507        return msg
508
509    def RequestAccess(self, req, fid):
510        """
511        Handle the access request.  Proxy if not for us.
512
513        Parse out the fields and make the allocations or rejections if for us,
514        otherwise, assuming we're willing to proxy, proxy the request out.
515        """
516
517        # The dance to get into the request body
518        if req.has_key('RequestAccessRequestBody'):
519            req = req['RequestAccessRequestBody']
520        else:
521            raise service_error(service_error.req, "No request!?")
522
523        if req.has_key('destinationTestbed'):
524            dt = unpack_id(req['destinationTestbed'])
525
526        if dt == None or dt in self.testbed:
527            # Request for this fedd
528            found, owners = self.lookup_access(req, fid)
529            # keep track of what's been added
530            allocID, alloc_cert = generate_fedid(subj="alloc", log=self.log)
531            aid = unicode(allocID)
532
533            self.state_lock.acquire()
534            self.allocation[aid] = { }
535            # The protoGENI certificate
536            self.allocation[aid]['credentials'] = found
537            # The list of owner FIDs
538            self.allocation[aid]['owners'] = owners
539            self.write_state()
540            self.state_lock.release()
541            for o in owners:
542                self.auth.set_attribute(o, allocID)
543            self.auth.set_attribute(allocID, allocID)
544
545            try:
546                f = open("%s/%s.pem" % (self.certdir, aid), "w")
547                print >>f, alloc_cert
548                f.close()
549            except IOError, e:
550                raise service_error(service_error.internal, 
551                        "Can't open %s/%s : %s" % (self.certdir, aid, e))
552            return { 'allocID': { 'fedid': allocID } }
553        else:
554            if self.allow_proxy:
555                resp = self.proxy_RequestAccess.call_service(dt, req,
556                            self.cert_file, self.cert_pwd,
557                            self.trusted_certs)
558                if resp.has_key('RequestAccessResponseBody'):
559                    return resp['RequestAccessResponseBody']
560                else:
561                    return None
562            else:
563                raise service_error(service_error.access,
564                        "Access proxying denied")
565
566
567    def ReleaseAccess(self, req, fid):
568        # The dance to get into the request body
569        if req.has_key('ReleaseAccessRequestBody'):
570            req = req['ReleaseAccessRequestBody']
571        else:
572            raise service_error(service_error.req, "No request!?")
573
574        if req.has_key('destinationTestbed'):
575            dt = unpack_id(req['destinationTestbed'])
576        else:
577            dt = None
578
579        if dt == None or dt in self.testbed:
580            # Local request
581            try:
582                if req['allocID'].has_key('localname'):
583                    auth_attr = aid = req['allocID']['localname']
584                elif req['allocID'].has_key('fedid'):
585                    aid = unicode(req['allocID']['fedid'])
586                    auth_attr = req['allocID']['fedid']
587                else:
588                    raise service_error(service_error.req,
589                            "Only localnames and fedids are understood")
590            except KeyError:
591                raise service_error(service_error.req, "Badly formed request")
592
593            self.log.debug("[access] deallocation requested for %s", aid)
594            if not self.auth.check_attribute(fid, auth_attr):
595                self.log.debug("[access] deallocation denied for %s", aid)
596                raise service_error(service_error.access, "Access Denied")
597
598            self.state_lock.acquire()
599            if self.allocation.has_key(aid):
600                self.log.debug("Found allocation for %s" %aid)
601                del self.allocation[aid]
602                self.write_state()
603                self.state_lock.release()
604                # And remove the access cert
605                cf = "%s/%s.pem" % (self.certdir, aid)
606                self.log.debug("Removing %s" % cf)
607                os.remove(cf)
608                return { 'allocID': req['allocID'] } 
609            else:
610                self.state_lock.release()
611                raise service_error(service_error.req, "No such allocation")
612
613        else:
614            if self.allow_proxy:
615                resp = self.proxy_ReleaseAccess.call_service(dt, req,
616                            self.cert_file, self.cert_pwd,
617                            self.trusted_certs)
618                if resp.has_key('ReleaseAccessResponseBody'):
619                    return resp['ReleaseAccessResponseBody']
620                else:
621                    return None
622            else:
623                raise service_error(service_error.access,
624                        "Access proxying denied")
625
626    def import_store_info(self, cf, connInfo):
627        """
628        Pull any import parameters in connInfo in.  We translate them either
629        into known member names or fedAddrs.
630        """
631
632        for c in connInfo:
633            for p in [ p for p in c.get('parameter', []) \
634                    if p.get('type', '') == 'input']:
635                name = p.get('name', None)
636                key = p.get('key', None)
637                store = p.get('store', None)
638
639                if name and key and store :
640                    req = { 'name': key, 'wait': True }
641                    r = self.call_GetValue(store, req, cf)
642                    r = r.get('GetValueResponseBody', None)
643                    if r :
644                        if r.get('name', '') == key:
645                            v = r.get('value', None)
646                            if v is not None:
647                                if name == 'peer':
648                                    c['peer'] = v
649                                else:
650                                    if c.has_key('fedAttr'):
651                                        c['fedAttr'].append({
652                                            'attribute': name, 'value': v})
653                                    else:
654                                        c['fedAttr']= [{
655                                            'attribute': name, 'value': v}]
656                            else:
657                                raise service_error(service_error.internal, 
658                                        'None value exported for %s'  % key)
659                        else:
660                            raise service_error(service_error.internal, 
661                                    'Different name returned for %s: %s' \
662                                            % (key, r.get('name','')))
663                    else:
664                        raise service_error(service_error.internal, 
665                            'Badly formatted response: no GetValueResponseBody')
666                else:
667                    raise service_error(service_error.internal, 
668                        'Bad Services missing info for import %s' % c)
669
670    def generate_portal_configs(self, topo, pubkey_base, secretkey_base, 
671            tmpdir, master, leid, connInfo, services):
672
673        def conninfo_to_dict(key, info):
674            """
675            Make a cpoy of the connection information about key, and flatten it
676            into a single dict by parsing out any feddAttrs.
677            """
678
679            rv = None
680            for i in info:
681                if key == i.get('portal', "") or \
682                        key in [e.get('element', "") \
683                        for e in i.get('member', [])]:
684                    rv = i.copy()
685                    break
686
687            else:
688                return rv
689
690            if 'fedAttr' in rv:
691                for a in rv['fedAttr']:
692                    attr = a.get('attribute', "")
693                    val = a.get('value', "")
694                    if attr and attr not in rv:
695                        rv[attr] = val
696                del rv['fedAttr']
697            return rv
698
699        # XXX: un hardcode this
700        def client_null(f, s):
701            print >>f, "Service: %s" % s['name']
702
703        def client_smb(f, s):
704            print >>f, "Service: %s" % s['name']
705            smbshare = None
706            smbuser = None
707            smbproj = None
708            for a in s.get('fedAttr', []):
709                if a.get('attribute', '') == 'SMBSHARE':
710                    smbshare = a.get('value', None)
711                elif a.get('attribute', '') == 'SMBUSER':
712                    smbuser = a.get('value', None)
713                elif a.get('attribute', '') == 'SMBPROJ':
714                    smbproj = a.get('value', None)
715
716            if all((smbshare, smbuser, smbproj)):
717                print >>f, "SMBshare: %s" % smbshare
718                print >>f, "ProjectUser: %s" % smbuser
719                print >>f, "ProjectName: %s" % smbproj
720
721        client_service_out = {
722                'SMB': client_smb,
723                'tmcd': client_null,
724                'seer': client_null,
725                'userconfig': client_null,
726            }
727        # XXX: end un hardcode this
728
729
730        seer_out = False
731        client_out = False
732        for e in [ e for e in topo.elements \
733                if isinstance(e, topdl.Computer) and e.get_attribute('portal')]:
734            myname = e.name[0]
735            type = e.get_attribute('portal_type')
736
737            info = conninfo_to_dict(myname, connInfo)
738
739            if not info:
740                raise service_error(service_error.req,
741                        "No connectivity info for %s" % myname)
742
743            peer = info.get('peer', "")
744            ldomain = self.domain;
745
746            mexp = info.get('masterexperiment',"")
747            mproj, meid = mexp.split("/", 1)
748            mdomain = info.get('masterdomain',"")
749            muser = info.get('masteruser','root')
750            smbshare = info.get('smbshare', 'USERS')
751            ssh_port = info.get('ssh_port', '22')
752
753            active = info.get('active', 'False')
754
755            cfn = "%s/%s.gw.conf" % (tmpdir, myname.lower())
756            tunnelconfig = self.attrs.has_key('TunnelCfg')
757            try:
758                f = open(cfn, "w")
759                if active == 'True':
760                    print >>f, "active: True"
761                    if type in ('control', 'both'):
762                        for s in [s for s in services \
763                                if s.get('name', "") in self.imports]:
764                            p = urlparse(s.get('server', 'http://localhost'))
765                            print >>f, 'port: remote:%s:%s:%s' % \
766                                    (p.port, p.hostname, p.port)
767
768                if tunnelconfig:
769                    print >>f, "tunnelip: %s" % tunnelconfig
770                # XXX: send this an fedattr
771                #print >>f, "seercontrol: control.%s.%s%s" % \
772                        #(meid.lower(), mproj.lower(), mdomain)
773                print >>f, "peer: %s" % peer.lower()
774                print >>f, "ssh_pubkey: /usr/local/federation/etc/%s" % \
775                        pubkey_base
776                print >>f, "ssh_privkey: /usr/local/federation/etc/%s" % \
777                        secretkey_base
778                print >>f, "ssh_port: %s" % ssh_port
779                f.close()
780            except IOError, e:
781                raise service_error(service_error.internal,
782                        "Can't write protal config %s: %s" % (cfn, e))
783           
784            # XXX: This little seer config file needs to go away.
785            if not seer_out:
786                try:
787                    seerfn = "%s/seer.conf" % tmpdir
788                    f = open(seerfn, "w")
789                    if not master:
790                        print >>f, "ControlNode: control.%s.%s%s" % \
791                            (meid.lower(), mproj.lower(), mdomain)
792                    print >>f, "ExperimentID: %s" % mexp
793                    f.close()
794                except IOError, e:
795                    raise service_error(service_error.internal, 
796                            "Can't write seer.conf: %s" %e)
797                seer_out = True
798
799            if not client_out and type in ('control', 'both'):
800                try:
801                    f = open("%s/client.conf" % tmpdir, "w")
802                    print >>f, "ControlGateway: %s%s" % \
803                        (myname.lower(), ldomain.lower())
804                    for s in services:
805                        if s.get('name',"") in self.imports and \
806                                s.get('visibility','') == 'import':
807                            client_service_out[s['name']](f, s)
808                    # Does seer need this?
809                    # print >>f, "ExperimentID: %s/%s" % (mproj, meid)
810                    f.close()
811                except IOError, e:
812                    raise service_error(service_error.internal,
813                            "Cannot write client.conf: %s" %s)
814                client_out = True
815
816
817    def generate_rspec(self, topo, softdir, master, connInfo):
818        t = topo.clone()
819
820        starts = { }
821
822        # The startcmds for master and slave testbeds
823        if master: 
824            gate_cmd = self.attrs.get('MasterConnectorStartCmd', '/bin/true')
825            node_cmd = self.attrs.get('MasterNodeStartCmd', 'bin/true')
826        else: 
827            gate_cmd = self.attrs.get('SlaveConnectorStartCmd', '/bin/true')
828            node_cmd = self.attrs.get('SlaveNodeStartCmd', 'bin/true')
829
830        # Weed out the things we aren't going to instantiate: Segments, portal
831        # substrates, and portal interfaces.  (The copy in the for loop allows
832        # us to delete from e.elements in side the for loop).  While we're
833        # touching all the elements, we also adjust paths from the original
834        # testbed to local testbed paths and put the federation commands and
835        # startcommands into a dict so we can start them manually later.
836        # ProtoGENI requires setup before the federation commands run, so we
837        # run them by hand after we've seeded configurations.
838        for e in [e for e in t.elements]:
839            if isinstance(e, topdl.Segment):
840                t.elements.remove(e)
841            # Fix software paths
842            for s in getattr(e, 'software', []):
843                s.location = re.sub("^.*/", softdir, s.location)
844            if isinstance(e, topdl.Computer):
845                if e.get_attribute('portal') and gate_cmd:
846                    # Portals never have a user-specified start command
847                    starts[e.name[0]] = gate_cmd
848                elif node_cmd:
849                    if e.get_attribute('startup'):
850                        starts[e.name[0]] = "%s \\$USER '%s'" % \
851                                (node_cmd, e.get_attribute('startup'))
852                        e.remove_attribute('startup')
853                    else:
854                        starts[e.name[0]] = node_cmd
855
856                # Remove portal interfaces
857                e.interface = [i for i in e.interface \
858                        if not i.get_attribute('portal')]
859
860        t.substrates = [ s.clone() for s in t.substrates ]
861        t.incorporate_elements()
862
863        # Customize the ns2 output for local portal commands and images
864        filters = []
865
866        # NB: these are extra commands issued for the node, not the startcmds
867        if master: cmd = self.attrs.get('MasterConnectorCmd', '')
868        else: cmd = self.attrs.get('SlaveConnectorCmd', '')
869
870        if cmd:
871            filters.append(topdl.generate_portal_command_filter(cmd))
872
873        # Convert to rspec and return it
874        exp_rspec = topdl.topology_to_rspec(t, filters)
875
876        return exp_rspec
877
878    def StartSegment(self, req, fid):
879        def get_url(url, cf, destdir, fn=None):
880            po = urlparse(url)
881            if not fn:
882                fn = po.path.rpartition('/')[2]
883            try:
884                conn = httplib.HTTPSConnection(po.hostname, port=po.port, 
885                        cert_file=cf, key_file=cf)
886                conn.putrequest('GET', po.path)
887                conn.endheaders()
888                response = conn.getresponse()
889
890                lf = open("%s/%s" % (destdir, fn), "w")
891                buf = response.read(4096)
892                while buf:
893                    lf.write(buf)
894                    buf = response.read(4096)
895                lf.close()
896            except IOError, e:
897                print e
898                raise service_error(service_error.internal,
899                        "Error writing tempfile: %s" %e)
900            except httplib.HTTPException, e:
901                print e
902                raise service_error(service_error.internal, 
903                        "Error retrieving data: %s" % e)
904
905        configs = set(('hosts', 'ssh_pubkey', 'ssh_secretkey'))
906
907        err = None  # Any service_error generated after tmpdir is created
908        rv = None   # Return value from segment creation
909
910        try:
911            req = req['StartSegmentRequestBody']
912        except KeyError:
913            raise service_error(service_error.req, "Badly formed request")
914
915        connInfo = req.get('connection', [])
916        services = req.get('service', [])
917        auth_attr = req['allocID']['fedid']
918        aid = "%s" % auth_attr
919        attrs = req.get('fedAttr', [])
920        if not self.auth.check_attribute(fid, auth_attr):
921            raise service_error(service_error.access, "Access denied")
922
923
924        if req.has_key('segmentdescription') and \
925                req['segmentdescription'].has_key('topdldescription'):
926            topo = \
927                topdl.Topology(**req['segmentdescription']['topdldescription'])
928        else:
929            raise service_error(service_error.req, 
930                    "Request missing segmentdescription'")
931
932        master = req.get('master', False)
933
934        certfile = "%s/%s.pem" % (self.certdir, auth_attr)
935        try:
936            tmpdir = tempfile.mkdtemp(prefix="access-")
937            softdir = "%s/software" % tmpdir
938        except IOError:
939            raise service_error(service_error.internal, "Cannot create tmp dir")
940
941        # Try block alllows us to clean up temporary files.
942        try:
943            sw = set()
944            for e in topo.elements:
945                for s in getattr(e, 'software', []):
946                    sw.add(s.location)
947            if len(sw) > 0:
948                os.mkdir(softdir)
949            for s in sw:
950                self.log.debug("Retrieving %s" % s)
951                get_url(s, certfile, softdir)
952
953            for a in attrs:
954                if a['attribute'] in configs:
955                    get_url(a['value'], certfile, tmpdir)
956                if a['attribute'] == 'ssh_pubkey':
957                    pubkey_base = a['value'].rpartition('/')[2]
958                if a['attribute'] == 'ssh_secretkey':
959                    secretkey_base = a['value'].rpartition('/')[2]
960                if a['attribute'] == 'experiment_name':
961                    ename = a['value']
962
963            # If the userconf service was imported, collect the configuration
964            # data.
965            for s in services:
966                if s.get("name", "") == 'userconfig' \
967                        and s.get('visibility',"") == 'import':
968
969                    # Collect ther server and certificate info.
970                    u = s.get('server', None)
971                    for a in s.get('fedAttr', []):
972                        if a.get('attribute',"") == 'cert':
973                            cert = a.get('value', None)
974                            break
975                    else:
976                        cert = None
977
978                    if cert:
979                        # Make a temporary certificate file for get_url.  The
980                        # finally clause removes it whether something goes
981                        # wrong (including an exception from get_url) or not.
982                        try:
983                            tfos, tn = tempfile.mkstemp(suffix=".pem")
984                            tf = os.fdopen(tfos, 'w')
985                            print >>tf, cert
986                            tf.close()
987                            get_url(u, tn, tmpdir, "userconf")
988                        except IOError, e:
989                            raise service_error(service.error.internal, 
990                                    "Cannot create temp file for " + 
991                                    "userconfig certificates: %s e")
992                        finally:
993                            if tn: os.remove(tn)
994                    else:
995                        raise service_error(service_error.req,
996                                "No certificate for retreiving userconfig")
997                    break
998
999
1000
1001            self.state_lock.acquire()
1002            if self.allocation.has_key(aid):
1003                cf, user, ssh_key, cpw = self.allocation[aid]['credentials']
1004                self.allocation[aid]['experiment'] = ename
1005                self.allocation[aid]['log'] = [ ]
1006                # Create a logger that logs to the experiment's state object as
1007                # well as to the main log file.
1008                alloc_log = logging.getLogger('fedd.access.%s' % ename)
1009                h = logging.StreamHandler(
1010                        list_log.list_log(self.allocation[aid]['log']))
1011                # XXX: there should be a global one of these rather than
1012                # repeating the code.
1013                h.setFormatter(logging.Formatter(
1014                    "%(asctime)s %(name)s %(message)s",
1015                            '%d %b %y %H:%M:%S'))
1016                alloc_log.addHandler(h)
1017                self.write_state()
1018            else:
1019                self.log.error("No allocation for %s!?" % aid)
1020            self.state_lock.release()
1021
1022            # XXX: we really need to put the import and connection info
1023            # generation off longer.
1024            self.import_store_info(certfile, connInfo)
1025            self.generate_portal_configs(topo, pubkey_base, 
1026                    secretkey_base, tmpdir, master, ename, connInfo, 
1027                    services)
1028            rspec = self.generate_rspec(topo, "%s/%s/" \
1029                    % (self.staging_dir, ename), master, connInfo)
1030
1031            starter = self.start_segment(keyfile=ssh_key,
1032                    debug=self.create_debug, log=alloc_log,
1033                    ch_url = self.ch_url, sa_url=self.sa_url,
1034                    cm_url=self.cm_url)
1035            rv = starter(self, aid, user, rspec, pubkey_base, secretkey_base,
1036                    "%s/%s" % (self.staging_dir, ename), tmpdir, cf, cpw,
1037                    certfile, topo, connInfo)
1038        except service_error, e:
1039            err = e
1040        except e:
1041            err = service_error(service_error.internal, str(e))
1042
1043        # Walk up tmpdir, deleting as we go
1044        if self.cleanup:
1045            self.log.debug("[StartSegment]: removing %s" % tmpdir)
1046            for path, dirs, files in os.walk(tmpdir, topdown=False):
1047                for f in files:
1048                    os.remove(os.path.join(path, f))
1049                for d in dirs:
1050                    os.rmdir(os.path.join(path, d))
1051            os.rmdir(tmpdir)
1052        else:
1053            self.log.debug("[StartSegment]: not removing %s" % tmpdir)
1054
1055        if rv:
1056            # Grab the log (this is some anal locking, but better safe than
1057            # sorry)
1058            self.state_lock.acquire()
1059            logv = "".join(self.allocation[aid]['log'])
1060            self.state_lock.release()
1061
1062            return { 'allocID': req['allocID'], 'allocationLog': logv }
1063        elif err:
1064            raise service_error(service_error.federant,
1065                    "Swapin failed: %s" % err)
1066        else:
1067            raise service_error(service_error.federant, "Swapin failed")
1068
1069    def TerminateSegment(self, req, fid):
1070        try:
1071            req = req['TerminateSegmentRequestBody']
1072        except KeyError:
1073            raise service_error(service_error.req, "Badly formed request")
1074
1075        auth_attr = req['allocID']['fedid']
1076        aid = "%s" % auth_attr
1077        attrs = req.get('fedAttr', [])
1078        if not self.auth.check_attribute(fid, auth_attr):
1079            raise service_error(service_error.access, "Access denied")
1080
1081        self.state_lock.acquire()
1082        if self.allocation.has_key(aid):
1083            cf, user, ssh_key, cpw = self.allocation[aid]['credentials']
1084            slice_cred = self.allocation[aid].get('slice_credential', None)
1085            ename = self.allocation[aid].get('experiment', None)
1086        else:
1087            cf, user, ssh_key, cpw = (None, None, None, None)
1088            slice_cred = None
1089            ename = None
1090        self.state_lock.release()
1091
1092        if ename:
1093            staging = "%s/%s" % ( self.staging_dir, ename)
1094        else:
1095            self.log.warn("Can't find experiment name for %s" % aid)
1096            staging = None
1097
1098        stopper = self.stop_segment(keyfile=ssh_key, debug=self.create_debug,
1099                ch_url = self.ch_url, sa_url=self.sa_url, cm_url=self.cm_url)
1100        stopper(self, user, staging, slice_cred, cf, cpw)
1101        return { 'allocID': req['allocID'] }
Note: See TracBrowser for help on using the repository browser.