hub config file, hander registry

- use a hub config file instead of PythonOptions
- now honors KojiDir
- scan for handlers once at startup instead of each call
This commit is contained in:
Mike McLean 2008-08-05 18:52:25 -04:00
parent 91d89bb99b
commit 0b8f313039
4 changed files with 260 additions and 80 deletions

View file

@ -25,6 +25,9 @@ install:
mkdir -p $(DESTDIR)/etc/httpd/conf.d
install -p -m 644 httpd.conf $(DESTDIR)/etc/httpd/conf.d/kojihub.conf
mkdir -p $(DESTDIR)/etc/koji-hub
install -p -m 644 hub.conf $(DESTDIR)/etc/koji-hub/hub.conf
mkdir -p $(DESTDIR)/$(SERVERDIR)
for p in $(PYFILES) ; do \
install -p -m 644 $$p $(DESTDIR)/$(SERVERDIR)/$$p; \

62
hub/hub.conf Normal file
View file

@ -0,0 +1,62 @@
[hub]
## Basic options ##
DBName = koji
DBUser = koji
DBHost = db.example.com
#DBPass = example_password
KojiDir = /mnt/koji
## Kerberos authentication options ##
# AuthPrincipal = kojihub@EXAMPLE.COM
# AuthKeytab = /etc/koji.keytab
# ProxyPrincipals = kojihub@EXAMPLE.COM
## format string for host principals (%s = hostname)
# HostPrincipalFormat = compile/%s@EXAMPLE.COM
## end Kerberos auth configuration
## SSL client certificate auth configuration ##
#note: ssl auth may also require editing the httpd config (conf.d/kojihub.conf)
## the client username is the common name of the subject of their client certificate
# DNUsernameComponent = CN
## separate multiple DNs with |
# ProxyDNs = /C=US/ST=Massachusetts/O=Example Org/OU=Example User/CN=example/emailAddress=example@example.com
## end SSL client certificate auth configuration
## Other options ##
LoginCreatesUser = On
KojiWebURL = http://kojiweb.example.com/koji
# The domain name that will be appended to Koji usernames
# when creating email notifications
#EmailDomain example.com
## Whether or not to report exception details for anticipated errors (i.e.
## koji's own exceptions -- subclasses of koji.GenericError).
# KojiDebug = On
## Determines how much detail about exceptions is reported to the client (via faults)
## Meaningful values:
## normal - a basic traceback (format_exception)
## extended - an extended traceback (format_exc_plus)
## anything else - no traceback, just the error message
## The extended traceback is intended for debugging only and should NOT be
## used in production, since it may contain sensitive information.
# KojiTraceback = normal
## These options are intended for planned outages
# ServerOffline = False
# OfflineMessage = temporary outage
# LockOut = False
## If ServerOffline is True, the server will always report a ServerOffline fault (with
## OfflineMessage as the fault string).
## If LockOut is True, the server will report a ServerOffline fault for all non-admin
## requests.

View file

@ -20,6 +20,7 @@
# Authors:
# Mike McLean <mikem@redhat.com>
from ConfigParser import ConfigParser
import sys
import time
import traceback
@ -35,33 +36,30 @@ from kojihub import HostExports
from koji.context import context
def _opt_bool(opts, name):
"""Convert a string option into a boolean
True or False value. The following values
will be considered True (case-insensitive):
yes, on, true, 1
Anything else will be considered False."""
val = opts.get(name, 'no')
if val is None:
val = ''
if val.lower() in ('yes', 'on', 'true', '1'):
return True
else:
return False
"""Convert option into a boolean if necessary
class ModXMLRPCRequestHandler(object):
"""Simple XML-RPC handler for mod_python environment"""
For strings, the following values
will be considered True (case-insensitive):
yes, on, true, 1
Any other strings will be considered False."""
val = opts.get(name, False)
if isinstance(val, bool):
return val
elif isinstance(val, basestring):
if val.lower() in ('yes', 'on', 'true', '1'):
return True
return False
class HandlerRegistry(object):
"""Track handlers for RPC calls"""
def __init__(self):
self.funcs = {}
self.traceback = False
#introspection functions
self.register_function(self.list_api, name="_listapi")
self.register_function(self.system_listMethods, name="system.listMethods")
self.register_function(self.system_methodSignature, name="system.methodSignature")
self.register_function(self.system_methodHelp, name="system.methodHelp")
self.register_function(self.multiCall)
# Also register it as system.multicall for standards compliance
self.register_function(self.multiCall, name="system.multicall")
def register_function(self, function, name = None):
if name is None:
@ -91,6 +89,72 @@ class ModXMLRPCRequestHandler(object):
def register_instance(self,instance):
self.register_module(instance)
def list_api(self):
funcs = []
for name,func in self.funcs.items():
#the keys in self.funcs determine the name of the method as seen over xmlrpc
#func.__name__ might differ (e.g. for dotted method names)
args = self._getFuncArgs(func)
funcs.append({'name': name,
'doc': func.__doc__,
'args': args})
return funcs
def _getFuncArgs(self, func):
args = []
for x in range(0, func.func_code.co_argcount):
if x == 0 and func.func_code.co_varnames[x] == "self":
continue
if func.func_defaults and func.func_code.co_argcount - x <= len(func.func_defaults):
args.append((func.func_code.co_varnames[x], func.func_defaults[x - func.func_code.co_argcount + len(func.func_defaults)]))
else:
args.append(func.func_code.co_varnames[x])
return args
def system_listMethods(self):
return self.funcs.keys()
def system_methodSignature(self, method):
#it is not possible to autogenerate this data
return 'signatures not supported'
def system_methodHelp(self, method):
func = self.funcs.get(method)
if func is None:
return ""
arglist = []
for arg in self._getFuncArgs(func):
if isinstance(arg,str):
arglist.append(arg)
else:
arglist.append('%s=%s' % (arg[0], arg[1]))
ret = '%s(%s)' % (method, ", ".join(arglist))
if func.__doc__:
ret += "\ndescription: %s" % func.__doc__
return ret
def get(self, name):
func = self.funcs.get(name, None)
if func is None:
raise koji.GenericError, "Invalid method: %s" % name
return func
class ModXMLRPCRequestHandler(object):
"""Simple XML-RPC handler for mod_python environment"""
def __init__(self, handlers):
self.traceback = False
self.handlers = handlers #expecting HandlerRegistry instance
def _get_handler(self, name):
# just a wrapper so we can handle multicall ourselves
# we don't register multicall since the registry will outlive our instance
if name in ('multiCall', 'system.multicall'):
return self.multiCall
else:
return self.handlers.get(name)
def _marshaled_dispatch(self, data):
"""Dispatches an XML-RPC method from marshalled (XML) data."""
@ -140,9 +204,7 @@ class ModXMLRPCRequestHandler(object):
return response
def _dispatch(self,method,params):
func = self.funcs.get(method,None)
if func is None:
raise koji.GenericError, "Invalid method: %s" % method
func = self._get_handler(method)
context.method = method
if not hasattr(context,"session"):
#we may be called again by one of our meta-calls (like multiCall)
@ -157,7 +219,7 @@ class ModXMLRPCRequestHandler(object):
if _opt_bool(context.opts, 'LockOut') and \
method not in ('login', 'krbLogin', 'sslLogin', 'logout'):
if not context.session.hasPerm('admin'):
raise koji.GenericError, "Server disabled for maintenance"
raise koji.ServerOffline, "Server disabled for maintenance"
# handle named parameters
params,opts = koji.decode_args(*params)
@ -203,50 +265,6 @@ class ModXMLRPCRequestHandler(object):
return results
def list_api(self):
funcs = []
for name,func in self.funcs.items():
#the keys in self.funcs determine the name of the method as seen over xmlrpc
#func.__name__ might differ (e.g. for dotted method names)
args = self._getFuncArgs(func)
funcs.append({'name': name,
'doc': func.__doc__,
'args': args})
return funcs
def _getFuncArgs(self, func):
args = []
for x in range(0, func.func_code.co_argcount):
if x == 0 and func.func_code.co_varnames[x] == "self":
continue
if func.func_defaults and func.func_code.co_argcount - x <= len(func.func_defaults):
args.append((func.func_code.co_varnames[x], func.func_defaults[x - func.func_code.co_argcount + len(func.func_defaults)]))
else:
args.append(func.func_code.co_varnames[x])
return args
def system_listMethods(self):
return self.funcs.keys()
def system_methodSignature(self, method):
#it is not possible to autogenerate this data
return 'signatures not supported'
def system_methodHelp(self, method):
func = self.funcs.get(method)
if func is None:
return ""
arglist = []
for arg in self._getFuncArgs(func):
if isinstance(arg,str):
arglist.append(arg)
else:
arglist.append('%s=%s' % (arg[0], arg[1]))
ret = '%s(%s)' % (method, ", ".join(arglist))
if func.__doc__:
ret += "\ndescription: %s" % func.__doc__
return ret
def handle_request(self,req):
"""Handle a single XML-RPC request"""
@ -274,11 +292,102 @@ def offline_reply(req, msg=None):
req.set_content_length(len(response))
req.write(response)
def load_config(req):
"""Load configuration options
Options are read from a config file. The config file location is
controlled by the PythonOption ConfigFile in the httpd config.
Backwards compatibility:
- if ConfigFile is not set, opts are loaded from http config
- if ConfigFile is set, then the http config must not provide Koji options
- In a future version we will load the default hub config regardless
- all PythonOptions (except ConfigFile) are now deprecated and support for them
will disappear in a future version of Koji
"""
#get our config file
modpy_opts = req.get_options()
#cf = modpy_opts.get('ConfigFile', '/etc/koji-hub/hub.conf')
cf = modpy_opts.get('ConfigFile', None)
if cf:
# to aid in the transition from PythonOptions to hub.conf, we only load
# the configfile if it is explicitly configured
config = ConfigParser()
config.read(cf)
else:
sys.stderr.write('Warning: configuring Koji via PythonOptions is deprecated. Use hub.conf\n')
sys.stderr.flush()
cfgmap = [
#option, type, default
['DBName', 'string', None],
['DBUser', 'string', None],
['DBHost', 'string', None],
['DBPass', 'string', None],
['KojiDir', 'string', None],
['AuthPrincipal', 'string', None],
['AuthKeytab', 'string', None],
['ProxyPrincipals', 'string', None],
['HostPrincipalFormat', 'string', None],
['DNUsernameComponent', 'string', None],
['ProxyDNs', 'string', None],
['LoginCreatesUser', 'boolean', True],
['KojiWebURL', 'string', 'http://localhost.localdomain/koji'],
['EmailDomain', 'string', None],
['KojiDebug', 'boolean', False],
['KojiTraceback', 'string', None],
['EnableFunctionDebug', 'boolean', False],
['LockOut', 'boolean', False],
['ServerOffline', 'string', False],
['OfflineMessage', 'string', None],
]
opts = {}
for name, dtype, default in cfgmap:
if cf:
key = ('hub', name)
if config.has_option(*key):
if dtype == 'integer':
opts[name] = config.getint(*key)
elif dtype == 'boolean':
opts[name] = config.getboolean(*key)
else:
opts[name] = config.get(*key)
else:
opts[name] = default
else:
if modpy_opts.get(name, None) is not None:
if dtype == 'integer':
opts[name] = int(modpy_opts.get(name))
elif dtype == 'boolean':
opts[name] = modpy_opts.get(name).lower() in ('yes', 'on', 'true', '1')
else:
opts[name] = modpy_opts.get(name)
else:
opts[name] = default
# use configured KojiDir
if opts.get('KojiDir') is not None:
koji.BASEDIR = opts['KojiDir']
koji.pathinfo.topdir = opts['KojiDir']
return opts
#
# mod_python handler
#
firstcall = True
def handler(req, profiling=False):
global firstcall, registry, opts
if firstcall:
registry = get_registry()
opts = load_config(req)
firstcall = False
if profiling:
import profile, pstats, StringIO, tempfile
global _profiling_req
@ -293,7 +402,6 @@ def handler(req, profiling=False):
req.write("<pre>" + strstream.getvalue() + "</pre>")
_profiling_req = None
else:
opts = req.get_options()
try:
if _opt_bool(opts, 'ServerOffline'):
offline_reply(req, msg=opts.get("OfflineMessage", None))
@ -311,19 +419,7 @@ def handler(req, profiling=False):
except Exception:
offline_reply(req, msg="database outage")
return apache.OK
functions = RootExports()
hostFunctions = HostExports()
h = ModXMLRPCRequestHandler()
h.register_instance(functions)
h.register_module(hostFunctions,"host")
h.register_function(koji.auth.login)
h.register_function(koji.auth.krbLogin)
h.register_function(koji.auth.sslLogin)
h.register_function(koji.auth.logout)
h.register_function(koji.auth.subsession)
h.register_function(koji.auth.logoutChild)
h.register_function(koji.auth.exclusiveSession)
h.register_function(koji.auth.sharedSession)
h = ModXMLRPCRequestHandler(registry)
h.handle_request(req)
if h.traceback:
#rollback
@ -336,3 +432,21 @@ def handler(req, profiling=False):
context.cnx.close()
context._threadclear()
return apache.OK
def get_registry():
# Create and populate handler registry
registry = HandlerRegistry()
functions = RootExports()
hostFunctions = HostExports()
registry.register_instance(functions)
registry.register_module(hostFunctions,"host")
registry.register_function(koji.auth.login)
registry.register_function(koji.auth.krbLogin)
registry.register_function(koji.auth.sslLogin)
registry.register_function(koji.auth.logout)
registry.register_function(koji.auth.subsession)
registry.register_function(koji.auth.logoutChild)
registry.register_function(koji.auth.exclusiveSession)
registry.register_function(koji.auth.sharedSession)
return registry

View file

@ -110,6 +110,7 @@ rm -rf $RPM_BUILD_ROOT
%defattr(-,root,root)
%{_datadir}/koji-hub
%config(noreplace) /etc/httpd/conf.d/kojihub.conf
%config(noreplace) /etc/koji-hub/hub.conf
%files utils
%defattr(-,root,root)