#!/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("%s>\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")