#!/usr/local/bin/python import os,sys import stat # for chmod constants import re import time import string import copy import pickle import logging import subprocess from threading import * from M2Crypto.SSL import SSLError from util import * from access_project import access_project from fedid import fedid, generate_fedid from authorizer import authorizer from service_error import service_error from remote_service import xmlrpc_handler, soap_handler, service_caller import httplib import tempfile from urlparse import urlparse import topdl import list_log import proxy_protogeni_segment # Make log messages disappear if noone configures a fedd logger class nullHandler(logging.Handler): def emit(self, record): pass fl = logging.getLogger("fedd.access") fl.addHandler(nullHandler()) class access: """ The implementation of access control based on mapping users to projects. Users can be mapped to existing projects or have projects created dynamically. This implements both direct requests and proxies. """ class parse_error(RuntimeError): pass proxy_RequestAccess= service_caller('RequestAccess') proxy_ReleaseAccess= service_caller('ReleaseAccess') def __init__(self, config=None, auth=None): """ Initializer. Pulls parameters out of the ConfigParser's access section. """ def software_list(v): l = [ ] if v: ps = v.split(" ") while len(ps): loc, file = ps[0:2] del ps[0:2] l.append((loc, file)) return l # Make sure that the configuration is in place if not config: raise RunTimeError("No config to fedd.access") self.project_priority = config.getboolean("access", "project_priority") self.allow_proxy = config.getboolean("access", "allow_proxy") self.domain = config.get("access", "domain") self.certdir = config.get("access","certdir") self.userconfdir = config.get("access","userconfdir") self.userconfcmd = config.get("access","userconfcmd") self.userconfurl = config.get("access","userconfurl") self.federation_software = config.get("access", "federation_software") self.portal_software = config.get("access", "portal_software") self.ssh_port = config.get("access","ssh_port") or "22" self.sshd = config.get("access","sshd") self.sshd_config = config.get("access", "sshd_config") self.create_debug = config.getboolean("access", "create_debug") self.cleanup = not config.getboolean("access", "leave_tmpfiles") self.access_type = config.get("access", "type") self.staging_dir = config.get("access", "staging_dir") or "/tmp" self.staging_host = config.get("access", "staging_host") \ or "ops.emulab.net" self.federation_software = software_list(self.federation_software) self.portal_software = software_list(self.portal_software) self.renewal_interval = config.get("access", "renewal") or (3 * 60 ) self.renewal_interval = int(self.renewal_interval) * 60 self.ch_url = config.get("access", "ch_url") self.sa_url = config.get("access", "sa_url") self.cm_url = config.get("access", "cm_url") self.attrs = { } self.access = { } self.restricted = [ ] self.projects = { } self.keys = { } self.types = { } self.allocation = { } self.state = { 'projects': self.projects, 'allocation' : self.allocation, 'keys' : self.keys, 'types': self.types } self.log = logging.getLogger("fedd.access") set_log_level(config, "access", self.log) self.state_lock = Lock() # XXX: Configurable self.exports = set(('SMB', 'seer', 'tmcd', 'userconfig')) self.imports = set(('SMB', 'seer', 'userconfig')) if auth: self.auth = auth else: self.log.error(\ "[access]: No authorizer initialized, creating local one.") auth = authorizer() tb = config.get('access', 'testbed') if tb: self.testbed = [ t.strip() for t in tb.split(',') ] else: self.testbed = [ ] if config.has_option("access", "accessdb"): self.read_access(config.get("access", "accessdb")) self.state_filename = config.get("access", "access_state") print "Calling read_state %s" % self.state_filename self.read_state() # Keep cert_file and cert_pwd coming from the same place self.cert_file = config.get("access", "cert_file") if self.cert_file: self.sert_pwd = config.get("access", "cert_pw") else: self.cert_file = config.get("globals", "cert_file") self.sert_pwd = config.get("globals", "cert_pw") self.trusted_certs = config.get("access", "trusted_certs") or \ config.get("globals", "trusted_certs") self.start_segment = proxy_protogeni_segment.start_segment self.stop_segment = proxy_protogeni_segment.stop_segment self.renew_segment = proxy_protogeni_segment.renew_segment self.call_SetValue = service_caller('SetValue') self.call_GetValue = service_caller('GetValue') self.RenewSlices() self.soap_services = {\ 'RequestAccess': soap_handler("RequestAccess", self.RequestAccess), 'ReleaseAccess': soap_handler("ReleaseAccess", self.ReleaseAccess), 'StartSegment': soap_handler("StartSegment", self.StartSegment), 'TerminateSegment': soap_handler("TerminateSegment", self.TerminateSegment), } self.xmlrpc_services = {\ 'RequestAccess': xmlrpc_handler('RequestAccess', self.RequestAccess), 'ReleaseAccess': xmlrpc_handler('ReleaseAccess', self.ReleaseAccess), 'StartSegment': xmlrpc_handler("StartSegment", self.StartSegment), 'TerminateSegment': xmlrpc_handler('TerminateSegment', self.TerminateSegment), } def read_access(self, config): """ Read a configuration file and set internal parameters. There are access lines of the form (tb, proj, user) -> user that map the first tuple of names to the user for for access purposes. Names in the key (left side) can include " or " to act as wildcards or to require the fields to be empty. Similarly aproj or auser can be or indicating that either the matching key is to be used or a dynamic user or project will be created. These names can also be federated IDs (fedid's) if prefixed with fedid:. The user is the ProtoGENI identity certificate. Testbed attributes outside the forms above can be given using the format attribute: name value: value. The name is a single word and the value continues to the end of the line. Empty lines and lines startin with a # are ignored. Parsing errors result in a self.parse_error exception being raised. """ lineno=0 name_expr = "["+string.ascii_letters + string.digits + "\/\.\-_]+" fedid_expr = "fedid:[" + string.hexdigits + "]+" key_name = "(||"+fedid_expr + "|"+ name_expr + ")" attr_re = re.compile('attribute:\s*([\._\-a-z0-9]+)\s+value:\s*(.*)', re.IGNORECASE) access_str = '\('+key_name+'\s*,\s*'+key_name+'\s*,\s*'+ \ key_name+'\s*\)\s*->\s*\(('+name_expr +')\s*,\s*('\ + name_expr + ')\s*,\s*('+name_expr+')\s*,?\s*(' + \ name_expr+ ')?\)' access_re = re.compile(access_str, re.IGNORECASE) def parse_name(n): if n.startswith('fedid:'): return fedid(hexstr=n[len('fedid:'):]) else: return n def auth_name(n): if isinstance(n, basestring): if n =='' or n =='': return None else: return unicode(n) else: return n f = open(config, "r"); for line in f: lineno += 1 line = line.strip(); if len(line) == 0 or line.startswith('#'): continue # Extended (attribute: x value: y) attribute line m = attr_re.match(line) if m != None: attr, val = m.group(1,2) self.attrs[attr] = val continue # Access line (t, p, u) -> (a, pw) line m = access_re.match(line) if m != None: access_key = tuple([ parse_name(x) for x in m.group(1,2,3)]) auth_key = tuple([ auth_name(x) for x in access_key]) cert = auth_name(parse_name(m.group(4))) user_name = auth_name(parse_name(m.group(5))) ssh_key = unicode(m.group(6)) if m.group(6): pw = unicode(m.group(7)) else: pw = None self.access[access_key] = (cert, user_name, ssh_key, pw) self.auth.set_attribute(auth_key, "access") continue # Nothing matched to here: unknown line - raise exception f.close() raise self.parse_error("Unknown statement at line %d of %s" % \ (lineno, config)) f.close() def write_state(self): if self.state_filename: try: f = open(self.state_filename, 'w') pickle.dump(self.state, f) except IOError, e: self.log.error("Can't write file %s: %s" % \ (self.state_filename, e)) except pickle.PicklingError, e: self.log.error("Pickling problem: %s" % e) except TypeError, e: self.log.error("Pickling problem (TypeError): %s" % e) def read_state(self): """ Read a new copy of access state. Old state is overwritten. State format is a simple pickling of the state dictionary. """ if self.state_filename: try: f = open(self.state_filename, "r") self.state = pickle.load(f) self.log.debug("[read_state]: Read state from %s" % \ self.state_filename) except IOError, e: self.log.warning(("[read_state]: No saved state: " +\ "Can't open %s: %s") % (self.state_filename, e)) except EOFError, e: self.log.warning(("[read_state]: " +\ "Empty or damaged state file: %s:") % \ self.state_filename) except pickle.UnpicklingError, e: self.log.warning(("[read_state]: No saved state: " + \ "Unpickling failed: %s") % e) # Add the ownership attributes to the authorizer. Note that the # indices of the allocation dict are strings, but the attributes are # fedids, so there is a conversion. for k in self.state.get('allocation', {}).keys(): for o in self.state['allocation'][k].get('owners', []): self.auth.set_attribute(o, fedid(hexstr=k)) self.auth.set_attribute(fedid(hexstr=k),fedid(hexstr=k)) if self.allocation != self.state['allocation']: self.allocation = self.state['allocation'] def permute_wildcards(self, a, p): """Return a copy of a with various fields wildcarded. The bits of p control the wildcards. A set bit is a wildcard replacement with the lowest bit being user then project then testbed. """ if p & 1: user = [""] else: user = a[2] if p & 2: proj = "" else: proj = a[1] if p & 4: tb = "" else: tb = a[0] return (tb, proj, user) def find_access(self, search): """ Search the access DB for a match on this tuple. Return the matching user (protoGENI cert). NB, if the initial tuple fails to match we start inserting wildcards in an order determined by self.project_priority. Try the list of users in order (when wildcarded, there's only one user in the list). """ if self.project_priority: perm = (0, 1, 2, 3, 4, 5, 6, 7) else: perm = (0, 2, 1, 3, 4, 6, 5, 7) for p in perm: s = self.permute_wildcards(search, p) # s[2] is None on an anonymous, unwildcarded request if s[2] != None: for u in s[2]: if self.access.has_key((s[0], s[1], u)): return self.access[(s[0], s[1], u)] else: if self.access.has_key(s): return self.access[s] return None def lookup_access(self, req, fid): """ Determine the allowed access for this request. Return the access and which fields are dynamic. The fedid is needed to construct the request """ # Search keys tb = None project = None user = None # Return values rp = access_project(None, ()) ru = None user_re = re.compile("user:\s(.*)") project_re = re.compile("project:\s(.*)") user = [ user_re.findall(x)[0] for x in req.get('credential', []) \ if user_re.match(x)] project = [ project_re.findall(x)[0] \ for x in req.get('credential', []) \ if project_re.match(x)] if len(project) == 1: project = project[0] elif len(project) == 0: project = None else: raise service_error(service_error.req, "More than one project credential") user_fedids = [ u for u in user if isinstance(u, fedid)] # Determine how the caller is representing itself. If its fedid shows # up as a project or a singleton user, let that stand. If neither the # usernames nor the project name is a fedid, the caller is a testbed. if project and isinstance(project, fedid): if project == fid: # The caller is the project (which is already in the tuple # passed in to the authorizer) owners = user_fedids owners.append(project) else: raise service_error(service_error.req, "Project asserting different fedid") else: if fid not in user_fedids: tb = fid owners = user_fedids owners.append(fid) else: if len(fedids) > 1: raise service_error(service_error.req, "User asserting different fedid") else: # Which is a singleton owners = user_fedids # Confirm authorization for u in user: self.log.debug("[lookup_access] Checking access for %s" % \ ((tb, project, u),)) if self.auth.check_attribute((tb, project, u), 'access'): self.log.debug("[lookup_access] Access granted") break else: self.log.debug("[lookup_access] Access Denied") else: raise service_error(service_error.access, "Access denied") # This maps a valid user to the ProtoGENI credentials to use found = self.find_access((tb, project, user)) if found == None: raise service_error(service_error.access, "Access denied - cannot map access") return found, owners def get_handler(self, path, fid): self.log.info("Get handler %s %s" % (path, fid)) if self.auth.check_attribute(fid, path) and self.userconfdir: return ("%s/%s" % (self.userconfdir, path), "application/binary") else: return (None, None) def export_userconf(self, project): dev_null = None confid, confcert = generate_fedid("test", dir=self.userconfdir, log=self.log) conffilename = "%s/%s" % (self.userconfdir, str(confid)) cf = None try: cf = open(conffilename, "w") os.chmod(conffilename, stat.S_IRUSR | stat.S_IWUSR) except IOError, e: raise service_error(service_error.internal, "Cannot create user configuration data") try: dev_null = open("/dev/null", "a") except IOError, e: self.log.error("export_userconf: can't open /dev/null: %s" % e) cmd = "%s %s" % (self.userconfcmd, project) conf = subprocess.call(cmd.split(" "), stdout=cf, stderr=dev_null, close_fds=True) self.auth.set_attribute(confid, "/%s" % str(confid)) return confid, confcert def export_services(self, sreq, project, user): exp = [ ] state = { } # XXX: Filthy shortcut here using http: so urlparse will give the right # answers. for s in sreq: sname = s.get('name', '') svis = s.get('visibility', '') if svis == 'export': if sname in self.exports: outs = s.copy() if sname == 'SMB': outs = s.copy() outs['server'] = "http://fs:139" outs['fedAttr'] = [ { 'attribute': 'SMBSHARE', 'value': 'USERS' }, { 'attribute': 'SMBUSER', 'value': user }, { 'attribute': 'SMBPROJ', 'value': project }, ] elif sname == 'seer': outs['server'] = "http://control:16606" elif sname == 'tmcd': outs['server'] = "http://boss:7777" elif sname == 'userconfig': if self.userconfdir and self.userconfcmd \ and self.userconfurl: cid, cert = self.export_userconf(project) outs['server'] = "%s/%s" % \ (self.userconfurl, str(cid)) outs['fedAttr'] = [ { 'attribute': 'cert', 'value': cert }, ] state['userconfig'] = unicode(cid) exp.append(outs) return (exp, state) def build_response(self, alloc_id, ap, services): """ Create the SOAP response. Build the dictionary description of the response and use fedd_utils.pack_soap to create the soap message. ap is the allocate project message returned from a remote project allocation (even if that allocation was done locally). """ # Because alloc_id is already a fedd_services_types.IDType_Holder, # there's no need to repack it msg = { 'allocID': alloc_id, 'fedAttr': [ { 'attribute': 'domain', 'value': self.domain } , { 'attribute': 'project', 'value': ap['project'].get('name', {}).get('localname', "???") }, ] } if len(self.attrs) > 0: msg['fedAttr'].extend( [ { 'attribute': x, 'value' : y } \ for x,y in self.attrs.iteritems()]) if services: msg['service'] = services return msg def RequestAccess(self, req, fid): """ Handle the access request. Proxy if not for us. Parse out the fields and make the allocations or rejections if for us, otherwise, assuming we're willing to proxy, proxy the request out. """ # The dance to get into the request body if req.has_key('RequestAccessRequestBody'): req = req['RequestAccessRequestBody'] else: raise service_error(service_error.req, "No request!?") if req.has_key('destinationTestbed'): dt = unpack_id(req['destinationTestbed']) if dt == None or dt in self.testbed: # Request for this fedd found, owners = self.lookup_access(req, fid) # keep track of what's been added allocID, alloc_cert = generate_fedid(subj="alloc", log=self.log) aid = unicode(allocID) self.state_lock.acquire() self.allocation[aid] = { } # The protoGENI certificate self.allocation[aid]['credentials'] = found # The list of owner FIDs self.allocation[aid]['owners'] = owners self.write_state() self.state_lock.release() for o in owners: self.auth.set_attribute(o, allocID) self.auth.set_attribute(allocID, allocID) try: f = open("%s/%s.pem" % (self.certdir, aid), "w") print >>f, alloc_cert f.close() except IOError, e: raise service_error(service_error.internal, "Can't open %s/%s : %s" % (self.certdir, aid, e)) return { 'allocID': { 'fedid': allocID } } else: if self.allow_proxy: resp = self.proxy_RequestAccess.call_service(dt, req, self.cert_file, self.cert_pwd, self.trusted_certs) if resp.has_key('RequestAccessResponseBody'): return resp['RequestAccessResponseBody'] else: return None else: raise service_error(service_error.access, "Access proxying denied") def ReleaseAccess(self, req, fid): # The dance to get into the request body if req.has_key('ReleaseAccessRequestBody'): req = req['ReleaseAccessRequestBody'] else: raise service_error(service_error.req, "No request!?") if req.has_key('destinationTestbed'): dt = unpack_id(req['destinationTestbed']) else: dt = None if dt == None or dt in self.testbed: # Local request try: if req['allocID'].has_key('localname'): auth_attr = aid = req['allocID']['localname'] elif req['allocID'].has_key('fedid'): aid = unicode(req['allocID']['fedid']) auth_attr = req['allocID']['fedid'] else: raise service_error(service_error.req, "Only localnames and fedids are understood") except KeyError: raise service_error(service_error.req, "Badly formed request") self.log.debug("[access] deallocation requested for %s", aid) if not self.auth.check_attribute(fid, auth_attr): self.log.debug("[access] deallocation denied for %s", aid) raise service_error(service_error.access, "Access Denied") self.state_lock.acquire() if self.allocation.has_key(aid): self.log.debug("Found allocation for %s" %aid) del self.allocation[aid] self.write_state() self.state_lock.release() # And remove the access cert cf = "%s/%s.pem" % (self.certdir, aid) self.log.debug("Removing %s" % cf) os.remove(cf) return { 'allocID': req['allocID'] } else: self.state_lock.release() raise service_error(service_error.req, "No such allocation") else: if self.allow_proxy: resp = self.proxy_ReleaseAccess.call_service(dt, req, self.cert_file, self.cert_pwd, self.trusted_certs) if resp.has_key('ReleaseAccessResponseBody'): return resp['ReleaseAccessResponseBody'] else: return None else: raise service_error(service_error.access, "Access proxying denied") def import_store_info(self, cf, connInfo): """ Pull any import parameters in connInfo in. We translate them either into known member names or fedAddrs. """ for c in connInfo: for p in [ p for p in c.get('parameter', []) \ if p.get('type', '') == 'input']: name = p.get('name', None) key = p.get('key', None) store = p.get('store', None) if name and key and store : req = { 'name': key, 'wait': True } r = self.call_GetValue(store, req, cf) r = r.get('GetValueResponseBody', None) if r : if r.get('name', '') == key: v = r.get('value', None) if v is not None: if name == 'peer': c['peer'] = v else: if c.has_key('fedAttr'): c['fedAttr'].append({ 'attribute': name, 'value': v}) else: c['fedAttr']= [{ 'attribute': name, 'value': v}] else: raise service_error(service_error.internal, 'None value exported for %s' % key) else: raise service_error(service_error.internal, 'Different name returned for %s: %s' \ % (key, r.get('name',''))) else: raise service_error(service_error.internal, 'Badly formatted response: no GetValueResponseBody') else: raise service_error(service_error.internal, 'Bad Services missing info for import %s' % c) def generate_portal_configs(self, topo, pubkey_base, secretkey_base, tmpdir, master, leid, connInfo, services): def conninfo_to_dict(key, info): """ Make a cpoy of the connection information about key, and flatten it into a single dict by parsing out any feddAttrs. """ rv = None for i in info: if key == i.get('portal', "") or \ key in [e.get('element', "") \ for e in i.get('member', [])]: rv = i.copy() break else: return rv if 'fedAttr' in rv: for a in rv['fedAttr']: attr = a.get('attribute', "") val = a.get('value', "") if attr and attr not in rv: rv[attr] = val del rv['fedAttr'] return rv # XXX: un hardcode this def client_null(f, s): print >>f, "Service: %s" % s['name'] def client_smb(f, s): print >>f, "Service: %s" % s['name'] smbshare = None smbuser = None smbproj = None for a in s.get('fedAttr', []): if a.get('attribute', '') == 'SMBSHARE': smbshare = a.get('value', None) elif a.get('attribute', '') == 'SMBUSER': smbuser = a.get('value', None) elif a.get('attribute', '') == 'SMBPROJ': smbproj = a.get('value', None) if all((smbshare, smbuser, smbproj)): print >>f, "SMBshare: %s" % smbshare print >>f, "ProjectUser: %s" % smbuser print >>f, "ProjectName: %s" % smbproj client_service_out = { 'SMB': client_smb, 'tmcd': client_null, 'seer': client_null, 'userconfig': client_null, } # XXX: end un hardcode this seer_out = False client_out = False for e in [ e for e in topo.elements \ if isinstance(e, topdl.Computer) and e.get_attribute('portal')]: myname = e.name[0] type = e.get_attribute('portal_type') info = conninfo_to_dict(myname, connInfo) if not info: raise service_error(service_error.req, "No connectivity info for %s" % myname) peer = info.get('peer', "") ldomain = self.domain; mexp = info.get('masterexperiment',"") mproj, meid = mexp.split("/", 1) mdomain = info.get('masterdomain',"") muser = info.get('masteruser','root') smbshare = info.get('smbshare', 'USERS') ssh_port = info.get('ssh_port', '22') active = info.get('active', 'False') cfn = "%s/%s.gw.conf" % (tmpdir, myname.lower()) tunnelconfig = self.attrs.has_key('TunnelCfg') try: f = open(cfn, "w") if active == 'True': print >>f, "active: True" print >>f, "ssh_port: %s" % ssh_port if type in ('control', 'both'): for s in [s for s in services \ if s.get('name', "") in self.imports]: p = urlparse(s.get('server', 'http://localhost')) print >>f, 'port: remote:%s:%s:%s' % \ (p.port, p.hostname, p.port) if tunnelconfig: print >>f, "tunnelip: %s" % tunnelconfig # XXX: send this an fedattr #print >>f, "seercontrol: control.%s.%s%s" % \ #(meid.lower(), mproj.lower(), mdomain) print >>f, "peer: %s" % peer.lower() print >>f, "ssh_pubkey: /usr/local/federation/etc/%s" % \ pubkey_base print >>f, "ssh_privkey: /usr/local/federation/etc/%s" % \ secretkey_base f.close() except IOError, e: raise service_error(service_error.internal, "Can't write protal config %s: %s" % (cfn, e)) # XXX: This little seer config file needs to go away. if not seer_out: try: seerfn = "%s/seer.conf" % tmpdir f = open(seerfn, "w") if not master: print >>f, "ControlNode: control.%s.%s%s" % \ (meid.lower(), mproj.lower(), mdomain) print >>f, "ExperimentID: %s" % mexp f.close() except IOError, e: raise service_error(service_error.internal, "Can't write seer.conf: %s" %e) seer_out = True if not client_out and type in ('control', 'both'): try: f = open("%s/client.conf" % tmpdir, "w") print >>f, "ControlGateway: %s%s" % \ (myname.lower(), ldomain.lower()) for s in services: if s.get('name',"") in self.imports and \ s.get('visibility','') == 'import': client_service_out[s['name']](f, s) # Does seer need this? # print >>f, "ExperimentID: %s/%s" % (mproj, meid) f.close() except IOError, e: raise service_error(service_error.internal, "Cannot write client.conf: %s" %s) client_out = True def generate_rspec(self, topo, softdir, master, connInfo): t = topo.clone() starts = { } # The startcmds for master and slave testbeds if master: gate_cmd = self.attrs.get('MasterConnectorStartCmd', '/bin/true') node_cmd = self.attrs.get('MasterNodeStartCmd', 'bin/true') else: gate_cmd = self.attrs.get('SlaveConnectorStartCmd', '/bin/true') node_cmd = self.attrs.get('SlaveNodeStartCmd', 'bin/true') # Weed out the things we aren't going to instantiate: Segments, portal # substrates, and portal interfaces. (The copy in the for loop allows # us to delete from e.elements in side the for loop). While we're # touching all the elements, we also adjust paths from the original # testbed to local testbed paths and put the federation commands and # startcommands into a dict so we can start them manually later. # ProtoGENI requires setup before the federation commands run, so we # run them by hand after we've seeded configurations. for e in [e for e in t.elements]: if isinstance(e, topdl.Segment): t.elements.remove(e) # Fix software paths for s in getattr(e, 'software', []): s.location = re.sub("^.*/", softdir, s.location) if isinstance(e, topdl.Computer): if e.get_attribute('portal') and gate_cmd: # Portals never have a user-specified start command starts[e.name[0]] = gate_cmd elif node_cmd: if e.get_attribute('startup'): starts[e.name[0]] = "%s \\$USER '%s'" % \ (node_cmd, e.get_attribute('startup')) e.remove_attribute('startup') else: starts[e.name[0]] = node_cmd # Remove portal interfaces e.interface = [i for i in e.interface \ if not i.get_attribute('portal')] t.substrates = [ s.clone() for s in t.substrates ] t.incorporate_elements() # Customize the ns2 output for local portal commands and images filters = [] # NB: these are extra commands issued for the node, not the startcmds if master: cmd = self.attrs.get('MasterConnectorCmd', '') else: cmd = self.attrs.get('SlaveConnectorCmd', '') if cmd: filters.append(topdl.generate_portal_command_filter(cmd)) # Convert to rspec and return it exp_rspec = topdl.topology_to_rspec(t, filters) return exp_rspec def StartSegment(self, req, fid): configs = set(('hosts', 'ssh_pubkey', 'ssh_secretkey')) err = None # Any service_error generated after tmpdir is created rv = None # Return value from segment creation try: req = req['StartSegmentRequestBody'] except KeyError: raise service_error(service_error.req, "Badly formed request") connInfo = req.get('connection', []) services = req.get('service', []) auth_attr = req['allocID']['fedid'] aid = "%s" % auth_attr attrs = req.get('fedAttr', []) if not self.auth.check_attribute(fid, auth_attr): raise service_error(service_error.access, "Access denied") else: # See if this is a replay of an earlier succeeded StartSegment - # sometimes SSL kills 'em. If so, replay the response rather than # redoing the allocation. self.state_lock.acquire() retval = self.allocation[aid].get('started', None) self.state_lock.release() if retval: self.log.warning("Duplicate StartSegment for %s: " % aid + \ "replaying response") return retval if req.has_key('segmentdescription') and \ req['segmentdescription'].has_key('topdldescription'): topo = \ topdl.Topology(**req['segmentdescription']['topdldescription']) else: raise service_error(service_error.req, "Request missing segmentdescription'") master = req.get('master', False) certfile = "%s/%s.pem" % (self.certdir, auth_attr) try: tmpdir = tempfile.mkdtemp(prefix="access-") softdir = "%s/software" % tmpdir except IOError: raise service_error(service_error.internal, "Cannot create tmp dir") # Try block alllows us to clean up temporary files. try: sw = set() for e in topo.elements: for s in getattr(e, 'software', []): sw.add(s.location) os.mkdir(softdir) for s in sw: self.log.debug("Retrieving %s" % s) try: get_url(s, certfile, softdir) except: t, v, st = sys.exc_info() raise service_error(service_error.internal, "Error retrieving %s: %s" % (s, v)) # Copy local portal node software to the tempdir for s in (self.portal_software, self.federation_software): for l, f in s: base = os.path.basename(f) copy_file(f, "%s/%s" % (softdir, base)) # Ick. Put this python rpm in a place that it will get moved into # the staging area. It's a hack to install a modern (in a Roman # sense of modern) python on ProtoGENI python_rpm ="python2.4-2.4-1pydotorg.i586.rpm" if os.access("./%s" % python_rpm, os.R_OK): copy_file("./%s" % python_rpm, "%s/%s" % (softdir, python_rpm)) for a in attrs: if a['attribute'] in configs: try: self.log.debug("Retrieving %s" % a['value']) get_url(a['value'], certfile, tmpdir) except: t, v, st = sys.exc_info() raise service_error(service_error.internal, "Error retrieving %s: %s" % (s, v)) if a['attribute'] == 'ssh_pubkey': pubkey_base = a['value'].rpartition('/')[2] if a['attribute'] == 'ssh_secretkey': secretkey_base = a['value'].rpartition('/')[2] if a['attribute'] == 'experiment_name': ename = a['value'] # If the userconf service was imported, collect the configuration # data. for s in services: if s.get("name", "") == 'userconfig' \ and s.get('visibility',"") == 'import': # Collect ther server and certificate info. u = s.get('server', None) for a in s.get('fedAttr', []): if a.get('attribute',"") == 'cert': cert = a.get('value', None) break else: cert = None if cert: # Make a temporary certificate file for get_url. The # finally clause removes it whether something goes # wrong (including an exception from get_url) or not. try: tfos, tn = tempfile.mkstemp(suffix=".pem") tf = os.fdopen(tfos, 'w') print >>tf, cert tf.close() get_url(u, tn, tmpdir, "userconf") except IOError, e: raise service_error(service.error.internal, "Cannot create temp file for " + "userconfig certificates: %s e") except: t, v, st = sys.exc_info() raise service_error(service_error.internal, "Error retrieving %s: %s" % (u, v)) finally: if tn: os.remove(tn) else: raise service_error(service_error.req, "No certificate for retreiving userconfig") break self.state_lock.acquire() if self.allocation.has_key(aid): cf, user, ssh_key, cpw = self.allocation[aid]['credentials'] self.allocation[aid]['experiment'] = ename self.allocation[aid]['log'] = [ ] # Create a logger that logs to the experiment's state object as # well as to the main log file. alloc_log = logging.getLogger('fedd.access.%s' % ename) h = logging.StreamHandler( list_log.list_log(self.allocation[aid]['log'])) # XXX: there should be a global one of these rather than # repeating the code. h.setFormatter(logging.Formatter( "%(asctime)s %(name)s %(message)s", '%d %b %y %H:%M:%S')) alloc_log.addHandler(h) self.write_state() else: self.log.error("No allocation for %s!?" % aid) self.state_lock.release() # XXX: we really need to put the import and connection info # generation off longer. self.import_store_info(certfile, connInfo) #self.generate_portal_configs(topo, pubkey_base, #secretkey_base, tmpdir, master, ename, connInfo, #services) rspec = self.generate_rspec(topo, "%s/%s/" \ % (self.staging_dir, ename), master, connInfo) starter = self.start_segment(keyfile=ssh_key, debug=self.create_debug, log=alloc_log, ch_url = self.ch_url, sa_url=self.sa_url, cm_url=self.cm_url) rv = starter(self, aid, user, rspec, pubkey_base, secretkey_base, master, ename, "%s/%s" % (self.staging_dir, ename), tmpdir, cf, cpw, certfile, topo, connInfo, services) except service_error, e: err = e except e: err = service_error(service_error.internal, str(e)) # Walk up tmpdir, deleting as we go if self.cleanup: self.log.debug("[StartSegment]: removing %s" % tmpdir) for path, dirs, files in os.walk(tmpdir, topdown=False): for f in files: os.remove(os.path.join(path, f)) for d in dirs: os.rmdir(os.path.join(path, d)) os.rmdir(tmpdir) else: self.log.debug("[StartSegment]: not removing %s" % tmpdir) if rv: # Grab the log (this is some anal locking, but better safe than # sorry) self.state_lock.acquire() logv = "".join(self.allocation[aid]['log']) # It's possible that the StartSegment call gets retried (!). # if the 'started' key is in the allocation, we'll return it rather # than redo the setup. self.allocation[aid]['started'] = { 'allocID': req['allocID'], 'allocationLog': logv, } retval = self.allocation[aid]['started'] self.write_state() self.state_lock.release() return retval elif err: raise service_error(service_error.federant, "Swapin failed: %s" % err) else: raise service_error(service_error.federant, "Swapin failed") def TerminateSegment(self, req, fid): try: req = req['TerminateSegmentRequestBody'] except KeyError: raise service_error(service_error.req, "Badly formed request") auth_attr = req['allocID']['fedid'] aid = "%s" % auth_attr attrs = req.get('fedAttr', []) if not self.auth.check_attribute(fid, auth_attr): raise service_error(service_error.access, "Access denied") self.state_lock.acquire() if self.allocation.has_key(aid): cf, user, ssh_key, cpw = self.allocation[aid]['credentials'] slice_cred = self.allocation[aid].get('slice_credential', None) ename = self.allocation[aid].get('experiment', None) else: cf, user, ssh_key, cpw = (None, None, None, None) slice_cred = None ename = None self.state_lock.release() if ename: staging = "%s/%s" % ( self.staging_dir, ename) else: self.log.warn("Can't find experiment name for %s" % aid) staging = None stopper = self.stop_segment(keyfile=ssh_key, debug=self.create_debug, ch_url = self.ch_url, sa_url=self.sa_url, cm_url=self.cm_url) stopper(self, user, staging, slice_cred, cf, cpw) return { 'allocID': req['allocID'] } def RenewSlices(self): self.log.info("Scanning for slices to renew") self.state_lock.acquire() aids = self.allocation.keys() self.state_lock.release() for aid in aids: self.state_lock.acquire() if self.allocation.has_key(aid): name = self.allocation[aid].get('slice_name', None) scred = self.allocation[aid].get('slice_credential', None) cf, user, ssh_key, cpw = self.allocation[aid]['credentials'] else: name = None scred = None self.state_lock.release() # There's a ProtoGENI slice associated with the segment; renew it. if name and scred: renewer = self.renew_segment(log=self.log, debug=self.create_debug, keyfile=ssh_key, cm_url = self.cm_url, sa_url = self.sa_url, ch_url = self.ch_url) new_scred = renewer(name, scred, self.renewal_interval, cf, cpw) if new_scred: self.log.info("Slice %s renewed until %s GMT" % \ (name, time.asctime(time.gmtime(\ time.time()+self.renewal_interval)))) self.state_lock.acquire() if self.allocation.has_key(aid): self.allocation[aid]['slice_credential'] = new_scred self.state_lock.release() else: self.log.info("Failed to renew slice %s " % name) # Let's do this all again soon. (4 tries before the slices time out) t = Timer(self.renewal_interval/4, self.RenewSlices) t.start()