source: fedd/federation/protogeni_access.py @ 73e0a61

axis_examplecompt_changesinfo-opsversion-3.01version-3.02
Last change on this file since 73e0a61 was 593e901, checked in by Ted Faber <faber@…>, 15 years ago

Checkpoint working federation w/PG (w/o routing yet...)

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