#!/usr/local/bin/python import os,sys import re import random import string import subprocess import tempfile from fedd_services import * from fedd_internal_services import * from fedd_util import * from fedid import fedid from fixed_resource import read_key_db, read_project_db from remote_service import xmlrpc_handler, soap_handler, service_caller from service_error import * import logging # Configure loggers to dump to /dev/null which avoids errors if calling classes # don't configure them. class nullHandler(logging.Handler): def emit(self, record): pass fl = logging.getLogger("fedd.allocate.local") fl.addHandler(nullHandler()) fl = logging.getLogger("fedd.allocate.remote") fl.addHandler(nullHandler()) class fedd_allocate_project_local: """ Allocate projects on this machine in response to an access request. """ dynamic_projects = 4 dynamic_keys= 2 confirm_keys = 1 none = 0 levels = { 'dynamic_projects': dynamic_projects, 'dynamic_keys': dynamic_keys, 'confirm_keys': confirm_keys, 'none': none, } def __init__(self, config, auth=None): """ Initializer. Parses a configuration if one is given. """ self.debug = config.get("access", "debug_project", False) self.wap = config.get('access', 'wap', '/usr/testbed/sbin/wap') self.newproj = config.get('access', 'newproj', '/usr/testbed/sbin/newproj') self.mkproj = config.get('access', 'mkproj', '/usr/testbed/sbin/mkproj') self.rmproj = config.get('access', 'rmproj', '/usr/testbed/sbin/rmproj') self.addpubkey = config.get('access', 'addpubkey', '/usr/testbed/sbin/taddpubkey') self.grantnodetype = config.get('access', 'grantnodetype', '/usr/testbed/sbin/grantnodetype') self.confirmkey = config.get('access', 'confirmkey', '/usr/testbed/sbin/taddpubkey') self.allocation_level = config.get("access", "allocation_level", "none") self.log = logging.getLogger("fedd.allocate.local") try: self.allocation_level = \ self.levels[self.allocation_level.strip().lower()] except KeyError: self.log.error("Bad allocation_level %s. Defaulting to none" % \ self.allocation_error) self.allocation_level = self.none set_log_level(config, "access", self.log) fixed_key_db = config.get("access", "fixed_keys", None) fixed_project_db = config.get("access", "fixed_projects", None) self.fixed_keys = set() self.fixed_projects = set() # initialize the fixed resource sets for db, rset, fcn in (\ (fixed_key_db, self.fixed_keys, read_key_db), \ (fixed_project_db, self.fixed_projects, read_project_db)): if db: try: rset.update(fcn(db)) except: self.log.debug("Can't read resources from %s" % db) # Internal services are SOAP only self.soap_services = {\ "AllocateProject": soap_handler(\ AllocateProjectRequestMessage.typecode,\ self.dynamic_project, AllocateProjectResponseMessage,\ "AllocateProjectResponseBody"), "StaticProject": soap_handler(\ StaticProjectRequestMessage.typecode,\ self.static_project, StaticProjectResponseMessage,\ "StaticProjectResponseBody"),\ "ReleaseProject": soap_handler(\ ReleaseProjectRequestMessage.typecode,\ self.release_project, ReleaseProjectResponseMessage,\ "ReleaseProjectResponseBody")\ } self.xmlrpc_services = { } def random_string(self, s, n=3): """Append n random ASCII characters to s and return the string""" rv = s for i in range(0,n): rv += random.choice(string.ascii_letters) return rv def write_attr_xml(self, file, root, lines): """ Write an emulab config file for a dynamic project. Format is lines[1] """ # Convert a pair to an attribute line out_attr = lambda a,v : \ '%s' % (a, v) f = os.fdopen(file, "w") f.write("<%s>\n" % root) f.write("\n".join([out_attr(*l) for l in lines])) f.write("\n" % root) f.close() def dynamic_project(self, req, fedid=None): """ Create a dynamic project with ssh access Req includes the project and resources as a dictionary """ if self.allocation_level < self.dynamic_projects: raise service_error(service_error.access, "[dynamic_project] dynamic project allocation not " + \ "permitted: check allocation level") # tempfiles for the parameter files uf, userfile = tempfile.mkstemp(prefix="usr", suffix=".xml", dir="/tmp") pf, projfile = tempfile.mkstemp(prefix="proj", suffix=".xml", dir="/tmp") if req.has_key('AllocateProjectRequestBody') and \ req['AllocateProjectRequestBody'].has_key('project'): proj = req['AllocateProjectRequestBody']['project'] else: raise service_error(service_error.req, "Badly formed allocation request") # Take the first user and ssh key name = proj.get('name', None) or self.random_string("proj",4) user = proj.get('user', None) if user != None: user = user[0] # User is a list, take the first entry if not user.has_key("userID"): uname = self.random_string("user", 3) else: uid = proj['userID'] # XXX: fedid uname = uid.get('localname', None) or \ uid.get('kerberosUsername', None) or \ uid.get('uri', None) if uname == None: raise fedd_proj.service_error(fedd_proj.service_error.req, "No ID for user"); access = user.get('access', None) if access != None: ssh = access[0].get('sshPubkey', None) if ssh == None: raise fedd_proj.service_error(fedd_proj.service_error.req, "No ssh key for user"); else: raise fedd_proj.service_error(fedd_proj.service_error.req, "No access information for project"); # uname, name and ssh are set user_fields = [ ("name", "Federation User %s" % uname), ("email", "%s-fed@isi.deterlab.net" % uname), ("password", self.random_string("", 8)), ("login", uname), ("address", "4676 Admiralty"), ("city", "Marina del Rey"), ("state", "CA"), ("zip", "90292"), ("country", "USA"), ("phone", "310-448-9190"), ("title", "None"), ("affiliation", "USC/ISI"), ("pubkey", ssh) ] proj_fields = [ ("name", name), ("short description", "dynamic federated project"), ("URL", "http://www.isi.edu/~faber"), ("funders", "USC/USU"), ("long description", "Federation access control"), ("public", "1"), ("num_pcs", "100"), ("linkedtous", "1"), ("newuser_xml", userfile) ] # Write out the files self.write_attr_xml(uf, "user", user_fields) self.write_attr_xml(pf, "project", proj_fields) # Generate the commands (only grantnodetype's are dynamic) cmds = [ (self.wap, self.newproj, projfile), (self.wap, self.mkproj, name) ] # Add commands to grant access to any resources in the request. for nt in [ h for r in req.get('resources', []) \ if r.has_key('node') and r['node'].has_key('hardware')\ for h in r['node']['hardware'] ] : cmds.append((self.wap, self.grantnodetype, '-p', name, nt)) # Create the projects rc = 0 for cmd in cmds: self.log.debug("[dynamic_project]: %s" % ' '.join(cmd)) if not self.debug: try: rc = subprocess.call(cmd) except OSerror, e: raise service_error(service_error.internal, "Dynamic project subprocess creation error "+ \ "[%s] (%s)" % (cmd[1], e.strerror)) if rc != 0: raise service_error(service_error.internal, "Dynamic project subprocess error " +\ "[%s] (%d)" % (cmd[1], rc)) # Clean up tempfiles os.unlink(userfile) os.unlink(projfile) rv = {\ 'project': {\ 'name': { 'localname': name }, 'user' : [ {\ 'userID': { 'localname' : uname }, 'access': [ { 'sshPubkey' : ssh } ], } ]\ }\ } return rv def static_project(self, req, fedid=None): """ Be certain that the local project in the request has access to the proper resources and users have correct keys. Add them if necessary. """ cmds = [] # While we should be more careful about this, for the short term, add # the keys to the specified users. try: users = req['StaticProjectRequestBody']['project']['user'] pname = req['StaticProjectRequestBody']['project']\ ['name']['localname'] resources = req['StaticProjectRequestBody'].get('resources', []) except KeyError: raise service_error(service_error.req, "Badly formed request") for u in users: try: name = u['userID']['localname'] except KeyError: raise service_error(service_error.req, "Badly formed user") for sk in [ k['sshPubkey'] for k in u.get('access', []) \ if k.has_key('sshPubkey')]: if self.allocation_level >= self.dynamic_keys: cmds.append((self.wap, self.addpubkey, '-w', \ '-u', name, '-k', sk)) elif self.allocation_level >= self.confirm_keys: cmds.append((self.wap, self.confirmkey, '-C', \ '-u', name, '-k', sk)) else: self.log.warning("[static_project] no checking of " + \ "static keys") # Add commands to grant access to any resources in the request. The # list comprehension pulls out the hardware types in the node entries # in the resources list. for nt in [ h for r in resources \ if r.has_key('node') and r['node'].has_key('hardware')\ for h in r['node']['hardware'] ] : if self.allocation_level >= self.confirm_keys: cmds.append((self.wap, self.grantnodetype, '-p', pname, nt)) # Run the commands rc = 0 for cmd in cmds: self.log.debug("[static_project]: %s" % ' '.join(cmd)) if not self.debug: try: rc = subprocess.call(cmd) except OSError, e: raise service_error(service_error.internal, "Static project subprocess creation error "+ \ "[%s] (%s)" % (cmd[0], e.strerror)) if rc != 0: raise service_error(service_error.internal, "Static project subprocess error " +\ "[%s] (%d)" % (cmd[0], rc)) return { 'project': req['StaticProjectRequestBody']['project']} def release_project(self, req, fedid=None): """ Remove user keys from users and delete dynamic projects. Only keys not in the set of fixed keys are deleted. and there are similar protections for projects. """ cmds = [] pname = None users = [] try: if req['ReleaseProjectRequestBody']['project'].has_key('name'): pname = req['ReleaseProjectRequestBody']['project']\ ['name']['localname'] if req['ReleaseProjectRequestBody']['project'].has_key('user'): users = req['ReleaseProjectRequestBody']['project']['user'] except KeyError: raise service_error(service_error.req, "Badly formed request") for u in users: try: name = u['userID']['localname'] except KeyError: raise service_error(service_error.req, "Badly formed user") for sk in [ k['sshPubkey'] for k in u.get('access', []) \ if k.has_key('sshPubkey')]: if (name.rstrip(), sk.rstrip()) not in self.fixed_keys: if self.allocation_level >= self.dynamic_keys: cmds.append((self.wap, self.addpubkey, '-R', '-w', \ '-u', name, '-k', sk)) if pname and pname not in self.fixed_projects and \ self.allocation_level >= self.dynamic_projects: cmds.append((self.wap, self.rmproj, pname)) # Run the commands rc = 0 for cmd in cmds: self.log.debug("[release_project]: %s" % ' '.join(cmd)) if not self.debug: try: rc = subprocess.call(cmd) except OSError, e: raise service_error(service_error.internal, "Release project subprocess creation error "+ \ "[%s] (%s)" % (cmd[0], e.strerror)) if rc != 0: raise service_error(service_error.internal, "Release project subprocess error " +\ "[%s] (%d)" % (cmd[0], rc)) return { 'project': req['ReleaseProjectRequestBody']['project']} class fedd_allocate_project_remote: """ Allocate projects on a remote machine using the internal SOAP interface """ class proxy(service_caller): """ This class is a proxy functor (callable) that has the same signature as a function called by soap_handler or xmlrpc_handler, but that used the service_caller class to call the function remotely. """ def __init__(self, url, cert_file, cert_pwd, trusted_certs, method, req_name, req_alloc, resp_name): service_caller.__init__(self, method, 'getfeddInternalPortType', feddInternalServiceLocator, req_alloc, req_name) self.url = url self.cert_file = cert_file self.cert_pwd = cert_pwd self.trusted_certs = trusted_certs self.resp_name = resp_name # Calling the proxy object directly invokes the proxy_call method, # not the service_call method. self.__call__ = self.proxy_call # Define the proxy, NB, the parameters to make_proxy are visible to the # definition of proxy. def proxy_call(self, req, fedid=None): """ Send req on to a remote project instantiator. Req is just the message to be sent. This function re-wraps it. It also rethrows any faults. """ if req.has_key(self.request_body_name): req = req[self.request_body_name] else: raise service_error(service_error.req, "Bad formated request"); r = self.call_service(self.url, req, self.cert_file, self.cert_pwd, self.trusted_certs) if r.has_key(self.resp_name): return r[self.resp_name] else: raise service_error(service_error.protocol, "Bad proxy response") # back to defining the fedd_allocate_project_remote class def __init__(self, config, auth=None): """ Initializer. Parses a configuration if one is given. """ self.debug = config.get("access", "debug_project", False) self.url = config.get("access", "project_allocation_uri", "") self.cert_file = config.get("access", "cert_file", None) self.cert_pwd = config.get("access", "cert_pwd", None) self.trusted_certs = config.get("access", "trusted_certs", None) # Certs are promoted from the generic to the specific, so without a if # no dynamic project certificates, then proxy certs are used, and if # none of those the main certs. if config.has_option("globals", "proxy_cert_file"): if not self.cert_file: self.cert_file = config.get("globals", "proxy_cert_file") if config.has_option("globals", "proxy_cert_pwd"): self.cert_pwd = config.get("globals", "proxy_cert_pwd") if config.has_option("globals", "proxy_trusted_certs") and \ not self.trusted_certs: self.trusted_certs = \ config.get("globals", "proxy_trusted_certs") if config.has_option("globals", "cert_file"): has_pwd = config.has_option("globals", "cert_pwd") if not self.cert_file: self.cert_file = config.get("globals", "cert_file") if has_pwd: self.cert_pwd = config.get("globals", "cert_pwd") if config.get("globals", "trusted_certs") and not self.trusted_certs: self.trusted_certs = \ config.get("globals", "trusted_certs") self.soap_services = { } self.xmlrpc_services = { } self.log = logging.getLogger("fedd.allocate.remote") set_log_level(config, "access", self.log) # The specializations of the proxy functions self.dynamic_project = self.proxy(self.url, self.cert_file, self.cert_pwd, self.trusted_certs, "AllocateProject", "AllocateProjectRequestBody", AllocateProjectRequestMessage, "AllocateProjectResponseBody") self.static_project = self.proxy(self.url, self.cert_file, self.cert_pwd, self.trusted_certs, "StaticProject", "StaticProjectRequestBody", StaticProjectRequestMessage, "StaticProjectResponseBody") self.release_project = self.proxy(self.url, self.cert_file, self.cert_pwd, self.trusted_certs, "ReleaseProject", "ReleaseProjectRequestBody", ReleaseProjectRequestMessage, "ReleaseProjectResponseBody")