From 54c0ed84382eebd030ba1a1d44c37b4a753afb10 Mon Sep 17 00:00:00 2001 From: Mike McLean Date: Thu, 10 May 2012 17:19:22 -0400 Subject: [PATCH] Support wsgi in koji-hub and koji-web - mod_python still supported, but deprecated - mod_wsgi is the default - koji-web now configured via web.conf - new wsgi-friendly publisher for koji-web - koji-web now has logging --- hub/httpd.conf | 31 +- hub/kojihub.py | 4 - hub/kojixmlrpc.py | 236 +++++----- koji.spec | 1 + koji/auth.py | 37 +- koji/server.py | 182 ++++++++ koji/util.py | 47 ++ util/koji-shadow | 2 +- www/conf/Makefile | 1 + www/conf/kojiweb.conf | 48 +- www/conf/web.conf | 24 + www/kojiweb/index.py | 820 ++++++++++++++++++---------------- www/kojiweb/wsgi_publisher.py | 461 +++++++++++++++++++ www/lib/kojiweb/handlers.py | 7 - www/lib/kojiweb/publisher.py | 42 -- www/lib/kojiweb/util.py | 74 +-- 16 files changed, 1392 insertions(+), 625 deletions(-) create mode 100644 koji/server.py create mode 100644 www/conf/web.conf create mode 100644 www/kojiweb/wsgi_publisher.py delete mode 100644 www/lib/kojiweb/handlers.py delete mode 100644 www/lib/kojiweb/publisher.py diff --git a/hub/httpd.conf b/hub/httpd.conf index 04c86305..5ecfb097 100644 --- a/hub/httpd.conf +++ b/hub/httpd.conf @@ -2,20 +2,29 @@ # koji-hub is an xmlrpc interface to the Koji database # -Alias /kojihub "/usr/share/koji-hub/XMLRPC" -Alias /kojifiles "/mnt/koji/" +Alias /kojihub /usr/share/koji-hub/kojixmlrpc.py - - SetHandler mod_python - PythonHandler kojixmlrpc - - PythonOption ConfigFile /etc/koji-hub/hub.conf - - PythonDebug Off - # autoreload is mostly useless to us (it would only reload kojixmlrpc.py) - PythonAutoReload Off + + Options ExecCGI + SetHandler wsgi-script + Order allow,deny + Allow from all +# Support for mod_python is DEPRECATED. If you still need mod_python support, +# then use the following directory settings instead: +# +# +# SetHandler mod_python +# PythonHandler kojixmlrpc +# PythonOption ConfigFile /etc/koji-hub/hub.conf +# PythonDebug Off +# PythonAutoReload Off +# + +# Also serve /mnt/koji +Alias /kojifiles "/mnt/koji/" + Options Indexes AllowOverride None diff --git a/hub/kojihub.py b/hub/kojihub.py index a4c81506..8b55e369 100644 --- a/hub/kojihub.py +++ b/hub/kojihub.py @@ -56,10 +56,6 @@ from koji.context import context logger = logging.getLogger('koji.hub') def log_error(msg): - #if hasattr(context,'req'): - # context.req.log_error(msg) - #else: - # sys.stderr.write(msg + "\n") logger.error(msg) diff --git a/hub/kojixmlrpc.py b/hub/kojixmlrpc.py index 0dd490b4..461ee36c 100644 --- a/hub/kojixmlrpc.py +++ b/hub/kojixmlrpc.py @@ -1,7 +1,5 @@ -# mod_python script - # kojixmlrpc - an XMLRPC interface for koji. -# Copyright (c) 2005-2007 Red Hat +# Copyright (c) 2005-2012 Red Hat # # Koji is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -29,17 +27,14 @@ import traceback import types import pprint import resource -from xmlrpclib import loads,dumps,Fault -from mod_python import apache +from xmlrpclib import getparser,dumps,Fault +from koji.server import WSGIWrapper import koji import koji.auth import koji.db import koji.plugin import koji.policy -import kojihub -from kojihub import RootExports -from kojihub import HostExports from koji.context import context from koji.util import setup_rlimits @@ -183,14 +178,21 @@ class ModXMLRPCRequestHandler(object): else: return self.handlers.get(name) - def _marshaled_dispatch(self, data): + def _read_request(self, stream): + parser, unmarshaller = getparser() + while True: + chunk = stream.read(8192) + if not chunk: + break + parser.feed(chunk) + parser.close() + return unmarshaller.close(), unmarshaller.getmethodname() + + def _marshaled_dispatch(self, environ): """Dispatches an XML-RPC method from marshalled (XML) data.""" - - params, method = loads(data) - start = time.time() - # generate response try: + params, method = self._read_request(environ['wsgi.input']) response = self._dispatch(method, params) # wrap response in a singleton tuple response = (response,) @@ -292,41 +294,11 @@ class ModXMLRPCRequestHandler(object): def handle_request(self,req): """Handle a single XML-RPC request""" - start = time.time() - # XMLRPC uses POST only. Reject anything else - if req.method != 'POST': - req.allow_methods(['POST'],1) - raise apache.SERVER_RETURN, apache.HTTP_METHOD_NOT_ALLOWED - - context.connection = req.connection - response = self._marshaled_dispatch(req.read()) - - req.content_type = "text/xml" - req.set_content_length(len(response)) - req.write(response) - self.logger.debug("Returning %d bytes after %f seconds", len(response), - time.time() - start) + pass + #XXX no longer used - -def dump_req(req): - data = [ - "request: %s\n" % req.the_request, - "uri: %s\n" % req.uri, - "args: %s\n" % req.args, - "protocol: %s\n" % req.protocol, - "method: %s\n" % req.method, - "https: %s\n" % req.is_https(), - "auth_name: %s\n" % req.auth_name(), - "auth_type: %s\n" % req.auth_type(), - "headers:\n%s\n" % pprint.pformat(req.headers_in), - ] - for part in data: - sys.stderr.write(part) - sys.stderr.flush() - - -def offline_reply(req, msg=None): +def offline_reply(start_response, msg=None): """Send a ServerOffline reply""" faultCode = koji.ServerOffline.faultCode if msg is None: @@ -334,11 +306,14 @@ def offline_reply(req, msg=None): else: faultString = msg response = dumps(Fault(faultCode, faultString)) - req.content_type = "text/xml" - req.set_content_length(len(response)) - req.write(response) + headers = [ + ('Content-Length', str(len(response))), + ('Content-Type', "text/xml"), + ] + start_response('200 OK', headers) + return [response] -def load_config(req): +def load_config(environ): """Load configuration options Options are read from a config file. The config file location is @@ -351,18 +326,21 @@ def load_config(req): - all PythonOptions (except ConfigFile) are now deprecated and support for them will disappear in a future version of Koji """ + logger = logging.getLogger("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 'modpy.opts' in environ: + modpy_opts = environ.get('modpy.opts') + cf = modpy_opts.get('ConfigFile', None) + else: + cf = environ.get('koji.hub.ConfigFile', '/etc/koji-hub/hub.conf') + modpy_opts = {} if cf: # to aid in the transition from PythonOptions to hub.conf, we only load # the configfile if it is explicitly configured config = RawConfigParser() config.read(cf) else: - sys.stderr.write('Warning: configuring Koji via PythonOptions is deprecated. Use hub.conf\n') - sys.stderr.flush() + logger.warn('Warning: configuring Koji via PythonOptions is deprecated. Use hub.conf') cfgmap = [ #option, type, default ['DBName', 'string', None], @@ -534,8 +512,10 @@ class HubFormatter(logging.Formatter): def format(self, record): record.method = getattr(context, 'method', None) - if hasattr(context, 'connection'): - record.remoteaddr = "%s:%s" % (context.connection.remote_addr) + if hasattr(context, 'environ'): + record.remoteaddr = "%s:%s" % ( + context.environ.get('REMOTE_ADDR', '?'), + context.environ.get('REMOTE_PORT', '?')) else: record.remoteaddr = "?:?" if hasattr(context, 'session'): @@ -550,8 +530,21 @@ class HubFormatter(logging.Formatter): record.user_name = None return logging.Formatter.format(self, record) +def setup_logging1(): + """Set up basic logging, before options are loaded""" + global log_handler + logger = logging.getLogger("koji") + logger.setLevel(logging.WARNING) + #stderr logging (stderr goes to httpd logs) + log_handler = logging.StreamHandler() + log_format = '%(asctime)s [%(levelname)s] SETUP p=%(process)s %(name)s: %(message)s' + log_handler.setFormatter(HubFormatter(log_format)) + log_handler.setLevel(logging.DEBUG) + logger.addHandler(log_handler) -def setup_logging(opts): +def setup_logging2(opts): + global log_handler + """Adjust logging based on configuration options""" #determine log level level = opts['LogLevel'] valid_levels = ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL') @@ -585,19 +578,26 @@ def setup_logging(opts): elif default is None: #LogLevel did not configure a default level logger.setLevel(logging.WARNING) - #for now, just stderr logging (stderr goes to httpd logs) - handler = logging.StreamHandler() - handler.setFormatter(HubFormatter(opts['LogFormat'])) - handler.setLevel(logging.DEBUG) - logger.addHandler(handler) + #log_handler defined in setup_logging1 + log_handler.setFormatter(HubFormatter(opts['LogFormat'])) + + +def load_scripts(environ): + """Update path and import our scripts files""" + global kojihub + scriptsdir = os.path.dirname(environ['SCRIPT_FILENAME']) + sys.path.insert(0, scriptsdir) + import kojihub + # # mod_python handler # -firstcall = True -ready = False -opts = {} +def handler(req): + wrapper = WSGIWrapper(req) + return wrapper.run(application) + def get_memory_usage(): pagesize = resource.getpagesize() @@ -605,58 +605,80 @@ def get_memory_usage(): size, res, shr, text, lib, data, dirty = statm return res - shr -def handler(req, profiling=False): - global firstcall, ready, registry, opts, plugins, policy - if firstcall: - firstcall = False - opts = load_config(req) - setup_logging(opts) +def server_setup(environ): + global opts, plugins, registry, policy + logger = logging.getLogger('koji') + try: + setup_logging1() + opts = load_config(environ) + setup_logging2(opts) + load_scripts(environ) setup_rlimits(opts) plugins = load_plugins(opts) registry = get_registry(opts, plugins) policy = get_policy(opts, plugins) - ready = True - if not ready: - #this will happen on subsequent passes if an error happens in the firstcall code - opts['ServerOffline'] = True - opts['OfflineMessage'] = 'server startup error' - if profiling: - import profile, pstats, StringIO, tempfile - global _profiling_req - _profiling_req = req - temp = tempfile.NamedTemporaryFile() - profile.run("import kojixmlrpc; kojixmlrpc.handler(kojixmlrpc._profiling_req, False)", temp.name) - stats = pstats.Stats(temp.name) - strstream = StringIO.StringIO() - sys.stdout = strstream - stats.sort_stats("time") - stats.print_stats() - req.write("
" + strstream.getvalue() + "
") - _profiling_req = None - else: + koji.db.provideDBopts(database = opts["DBName"], + user = opts["DBUser"], + password = opts.get("DBPass",None), + host = opts.get("DBHost", None)) + except Exception: + tb_str = ''.join(traceback.format_exception(*sys.exc_info())) + logger.error(tb_str) + opts = { + 'ServerOffline': True, + 'OfflineMessage': 'server startup error', + } + + +# +# wsgi handler +# + +firstcall = True + +def application(environ, start_response): + global firstcall + if firstcall: + server_setup(environ) + firstcall = False + # XMLRPC uses POST only. Reject anything else + if environ['REQUEST_METHOD'] != 'POST': + headers = [ + ('Allow', 'POST'), + ] + start_response('405 Method Not Allowed', headers) + response = "Method Not Allowed\nThis is an XML-RPC server. Only POST requests are accepted." + headers = [ + ('Content-Length', str(len(response))), + ('Content-Type', "text/plain"), + ] + return [response] + if opts.get('ServerOffline'): + return offline_reply(start_response, msg=opts.get("OfflineMessage", None)) + # XXX check request length + # XXX most of this should be moved elsewhere + if 1: try: + start = time.time() memory_usage_at_start = get_memory_usage() - if opts.get('ServerOffline'): - offline_reply(req, msg=opts.get("OfflineMessage", None)) - return apache.OK context._threadclear() context.commit_pending = False context.opts = opts context.handlers = HandlerAccess(registry) - context.req = req + context.environ = environ context.policy = policy - koji.db.provideDBopts(database = opts["DBName"], - user = opts["DBUser"], - password = opts.get("DBPass",None), - host = opts.get("DBHost", None)) try: context.cnx = koji.db.connect() except Exception: - offline_reply(req, msg="database outage") - return apache.OK + return offline_reply(start_response, msg="database outage") h = ModXMLRPCRequestHandler(registry) - h.handle_request(req) + response = h._marshaled_dispatch(environ) + headers = [ + ('Content-Length', str(len(response))), + ('Content-Type', "text/xml"), + ] + start_response('200 OK', headers) if h.traceback: #rollback context.cnx.rollback() @@ -668,6 +690,8 @@ def handler(req, profiling=False): if len(paramstr) > 120: paramstr = paramstr[:117] + "..." h.logger.warning("Memory usage of process %d grew from %d KiB to %d KiB (+%d KiB) processing request %s with args %s" % (os.getpid(), memory_usage_at_start, memory_usage_at_end, memory_usage_at_end - memory_usage_at_start, context.method, paramstr)) + h.logger.debug("Returning %d bytes after %f seconds", len(response), + time.time() - start) finally: #make sure context gets cleaned up if hasattr(context,'cnx'): @@ -676,13 +700,14 @@ def handler(req, profiling=False): except Exception: pass context._threadclear() - return apache.OK + return [response] #XXX + def get_registry(opts, plugins): # Create and populate handler registry registry = HandlerRegistry() - functions = RootExports() - hostFunctions = HostExports() + functions = kojihub.RootExports() + hostFunctions = kojihub.HostExports() registry.register_instance(functions) registry.register_module(hostFunctions,"host") registry.register_function(koji.auth.login) @@ -696,4 +721,3 @@ def get_registry(opts, plugins): for name in opts.get('Plugins', '').split(): registry.register_plugin(plugins.get(name)) return registry - diff --git a/koji.spec b/koji.spec index 8495b1c8..0fc8f09f 100644 --- a/koji.spec +++ b/koji.spec @@ -182,6 +182,7 @@ rm -rf $RPM_BUILD_ROOT %{_datadir}/koji-web %{_sysconfdir}/kojiweb %config(noreplace) %{_sysconfdir}/httpd/conf.d/kojiweb.conf +%config(noreplace) %{_sysconfdir}/kojiweb/web.conf %files builder %defattr(-,root,root) diff --git a/koji/auth.py b/koji/auth.py index cc35688d..d7dace2c 100644 --- a/koji/auth.py +++ b/koji/auth.py @@ -1,5 +1,5 @@ # authentication module -# Copyright (c) 2005-2007 Red Hat +# Copyright (c) 2005-2012 Red Hat # # Koji is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -26,7 +26,6 @@ import krbV import koji import cgi #for parse_qs from context import context -from mod_python import apache # 1 - load session if provided # - check uri for session id @@ -65,14 +64,15 @@ class Session(object): self.callnum = None #get session data from request if args is None: - req = getattr(context,'req',None) - args = getattr(req,'args',None) + environ = getattr(context,'environ',{}) + args = environ.get('QUERY_STRING','') if not args: self.message = 'no session args' return args = cgi.parse_qs(args,strict_parsing=True) if hostip is None: - hostip = context.req.get_remote_host(apache.REMOTE_NOLOOKUP) + hostip = context.environ['REMOTE_ADDR'] + #XXX - REMOTE_ADDR not promised by wsgi spec if hostip == '127.0.0.1': hostip = socket.gethostbyname(socket.gethostname()) try: @@ -260,7 +260,8 @@ class Session(object): raise koji.GenericError, "Already logged in" hostip = opts.get('hostip') if hostip is None: - hostip = context.req.get_remote_host(apache.REMOTE_NOLOOKUP) + hostip = context.environ['REMOTE_ADDR'] + #XXX - REMOTE_ADDR not promised by wsgi spec if hostip == '127.0.0.1': hostip = socket.gethostbyname(socket.gethostname()) @@ -329,7 +330,8 @@ class Session(object): self.checkLoginAllowed(user_id) - hostip = context.req.connection.remote_ip + hostip = context.environ['REMOTE_ADDR'] + #XXX - REMOTE_ADDR not promised by wsgi spec if hostip == '127.0.0.1': hostip = socket.gethostbyname(socket.gethostname()) @@ -354,15 +356,13 @@ class Session(object): # See: http://lists.planet-lab.org/pipermail/devel-community/2005-June/001084.html # local_ip seems to always be set to the same value as remote_ip, # so get the local ip via a different method - # local_ip = context.req.connection.local_ip - local_ip = socket.gethostbyname(context.req.hostname) - remote_ip = context.req.connection.remote_ip + local_ip = socket.gethostbyname(context.environ['SERVER_NAME']) + remote_ip = context.environ['REMOTE_ADDR'] + #XXX - REMOTE_ADDR not promised by wsgi spec # it appears that calling setports() with *any* value results in authentication # failing with "Incorrect net address", so return 0 (which prevents # python-krbV from calling setports()) - # local_port = context.req.connection.local_addr[1] - # remote_port = context.req.connection.remote_addr[1] local_port = 0 remote_port = 0 @@ -372,14 +372,10 @@ class Session(object): if self.logged_in: raise koji.AuthError, "Already logged in" - # populate standard CGI variables - context.req.add_common_vars() - env = context.req.subprocess_env - - if env.get('HTTPS') != 'on': + if context.environ.get('HTTPS') not in ['on', '1']: raise koji.AuthError, 'cannot call sslLogin() via a non-https connection' - if env.get('SSL_CLIENT_VERIFY') != 'SUCCESS': + if context.environ.get('SSL_CLIENT_VERIFY') != 'SUCCESS': raise koji.AuthError, 'could not verify client: %s' % env.get('SSL_CLIENT_VERIFY') name_dn_component = context.opts.get('DNUsernameComponent', 'CN') @@ -412,8 +408,9 @@ class Session(object): raise koji.AuthError, 'Unknown user: %s' % username self.checkLoginAllowed(user_id) - - hostip = context.req.connection.remote_ip + + hostip = context.environ['REMOTE_ADDR'] + #XXX - REMOTE_ADDR not promised by wsgi spec if hostip == '127.0.0.1': hostip = socket.gethostbyname(socket.gethostname()) diff --git a/koji/server.py b/koji/server.py new file mode 100644 index 00000000..8171db0e --- /dev/null +++ b/koji/server.py @@ -0,0 +1,182 @@ +# common server code for koji +# +# Copyright (c) 2012 Red Hat +# +# Koji is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; +# version 2.1 of the License. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this software; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# +# Authors: +# Mike McLean + +import koji +import sys +import traceback +from koji.util import LazyDict + +try: + from mod_python import apache +except ImportError: + apache = None + + +class ServerError(Exception): + """Base class for our server-side-only exceptions""" + +class ServerRedirect(ServerError): + """Used to handle redirects""" + + +class WSGIWrapper(object): + """A very thin wsgi compat layer for mod_python + + This class is highly specific to koji and is not fit for general use. + It does not support the full wsgi spec + """ + + def __init__(self, req): + self.req = req + self._env = None + host, port = req.connection.remote_addr + environ = { + 'REMOTE_ADDR' : req.connection.remote_ip, + # or remote_addr[0]? + # or req.get_remote_host(apache.REMOTE_NOLOOKUP)? + 'REMOTE_PORT' : str(req.connection.remote_addr[1]), + 'REMOTE_USER' : req.user, + 'REQUEST_METHOD' : req.method, + 'REQUEST_URI' : req.uri, + 'PATH_INFO' : req.path_info, + 'SCRIPT_FILENAME' : req.filename, + 'QUERY_STRING' : req.args or '', + 'SERVER_NAME' : req.hostname, + 'SERVER_PORT' : str(req.connection.local_addr[1]), + 'wsgi.version' : (1, 0), + 'wsgi.input' : InputWrapper(req), + 'wsgi.errors' : sys.stderr, + #TODO - file_wrapper support + } + environ = LazyDict(environ) + if req.is_https(): + environ['HTTPS'] = 'on' + environ['wsgi.url_scheme'] = 'https' + else: + environ['HTTPS'] = 'off' + environ['wsgi.url_scheme'] = 'http' + environ.lazyset('modpy.env', self.env, []) + environ.lazyset('modpy.opts', req.get_options, []) + environ.lazyset('SCRIPT_NAME', self.script_name, [], cache=True) + env_keys = ['SSL_CLIENT_VERIFY'] + for key in env_keys: + environ.lazyset(key, self.envget, [key]) + #gather the headers we care about + for key in req.headers_in: + k2 = key.upper() + k2 = k2.replace('-', '_') + if k2 not in ['CONTENT_TYPE', 'CONTENT_LENGTH']: + k2 = 'HTTP_' + k2 + environ[k2] = req.headers_in[key] + self.environ = environ + self.set_headers = False + + def env(self): + if self._env is None: + self.req.add_common_vars() + self._env = self.req.subprocess_env + return self._env + + def envget(self, *args): + self.env().get(*args) + + def script_name(self): + uri = self.req.uri + path_info = self.req.path_info + if uri.endswith(path_info): + uri = uri[:-len(path_info)] + uri = uri.rstrip('/') + return uri + + def no_write(self, string): + """a fake write() callable returned by start_response + + we don't use the write() callable in koji, so it will raise an error if called + """ + raise RuntimeError, "wsgi write() callable not supported" + + def start_response(self, status, headers, exc_info=None): + #XXX we don't deal with exc_info + if self.set_headers: + raise RuntimeError, "start_response() already called" + self.req.status = int(status[:3]) + for key, val in headers: + if key.lower() == 'content-length': + self.req.set_content_length(int(val)) + elif key.lower() == 'content-type': + self.req.content_type = val + else: + self.req.headers_out.add(key, val) + self.set_headers = True + return self.no_write + + def run(self, handler): + try: + result = handler(self.environ, self.start_response) + self.write_result(result) + return apache.OK + except: + sys.stderr.write(''.join(traceback.format_exception(*sys.exc_info()))) + sys.stderr.flush() + raise apache.SERVER_RETURN, apache.HTTP_INTERNAL_SERVER_ERROR + + def write_result(self, result): + """called by run() to handle the application's result value""" + req = self.req + write = req.write + if self.set_headers: + for chunk in result: + write(chunk) + else: + #slower version -- need to check for set_headers + for chunk in result: + if chunk and not self.set_headers: + raise RuntimeError, "write() called before start_response()" + write(data) + if not req.bytes_sent: + #application sent nothing back + req.set_content_length(0) + + + +class InputWrapper(object): + + def __init__(self, req): + self.req = req + + def close(self): + pass + + def read(self, size=-1): + return self.req.read(size) + + def readline(self): + return self.req.readline() + + def readlines(self, hint=-1): + return self.req.readlines(hint) + + def __iter__(self): + line = self.readline() + while line: + yield line + line = self.readline() + diff --git a/koji/util.py b/koji/util.py index 7de7991a..53e7d3cc 100644 --- a/koji/util.py +++ b/koji/util.py @@ -132,6 +132,19 @@ def dslice(dict, keys, strict=True): return ret +class HiddenValue(object): + """A wrapper that prevents a value being accidentally printed""" + + def __init__(self, value): + self.value = value + + def __str__(self): + return "[value hidden]" + + def __repr__(self): + return "HiddenValue()" + + class LazyValue(object): """Used to represent a value that is generated by a function call at access time """ @@ -153,6 +166,13 @@ class LazyValue(object): return value +class LazyString(LazyValue): + """Lazy values that should be expanded when printed""" + + def __str__(self): + return str(self.get()) + + def lazy_eval(value): if isinstance(value, LazyValue): return value.get() @@ -199,6 +219,33 @@ class LazyDict(dict): return key, lazy_eval(val) +class LazyRecord(object): + """A object whose attributes can reference lazy data + + Use lazysetattr to set lazy attributes, or just set them to a LazyValue + object directly""" + + def __init__(self, base=None): + if base is not None: + self.__dict__.update(base.__dict__) + self._base_record = base + + def __getattribute__(self, name): + try: + val = object.__getattribute__(self, name) + except AttributeError: + base = object.__getattribute__(self, '_base_record') + val = getattr(base, name) + return lazy_eval(val) + + +def lazysetattr(object, name, func, args, kwargs=None, cache=False): + if not isinstance(object, LazyRecord): + raise TypeError, 'object does not support lazy attributes' + value = LazyValue(func, args, kwargs=kwargs, cache=cache) + setattr(object, name, value) + + def rmtree(path): """Delete a directory tree without crossing fs boundaries""" st = os.lstat(path) diff --git a/util/koji-shadow b/util/koji-shadow index c9e0d987..6d844b32 100755 --- a/util/koji-shadow +++ b/util/koji-shadow @@ -1270,7 +1270,7 @@ if __name__ == "__main__": #XXX - config! remote_opts = {'anon_retry': True} for k in ('debug_xmlrpc', 'debug'): - session_opts[k] = getattr(options,k) + remote_opts[k] = getattr(options,k) remote = koji.ClientSession(options.remote, remote_opts) rv = 0 try: diff --git a/www/conf/Makefile b/www/conf/Makefile index 8dce50e7..c57c4f5a 100644 --- a/www/conf/Makefile +++ b/www/conf/Makefile @@ -16,3 +16,4 @@ install: install -p -m 644 kojiweb.conf $(DESTDIR)/etc/httpd/conf.d/kojiweb.conf mkdir -p $(DESTDIR)/etc/kojiweb + install -p -m 644 web.conf $(DESTDIR)/etc/kojiweb/web.conf diff --git a/www/conf/kojiweb.conf b/www/conf/kojiweb.conf index a82c0506..a350a234 100644 --- a/www/conf/kojiweb.conf +++ b/www/conf/kojiweb.conf @@ -1,33 +1,29 @@ -Alias /koji "/usr/share/koji-web/scripts/" +#We use wsgi by default +Alias /koji "/usr/share/koji-web/scripts/wsgi_publisher.py" +#(configuration goes in /etc/kojiweb/web.conf) - # Config for the publisher handler - SetHandler mod_python - # Use kojiweb's publisher (which handles errors more gracefully) - # You can also use mod_python.publisher, but you will lose the pretty tracebacks - PythonHandler kojiweb.publisher - - # General settings - PythonDebug On - PythonOption SiteName Koji - PythonOption KojiHubURL http://hub.example.com/kojihub - PythonOption KojiFilesURL http://server.example.com/mnt/koji - #PythonOption KojiTheme mytheme - PythonOption WebPrincipal koji/web@EXAMPLE.COM - PythonOption WebKeytab /etc/httpd.keytab - PythonOption WebCCache /var/tmp/kojiweb.ccache - PythonOption KrbService host - PythonOption WebCert /etc/kojiweb/kojiweb.crt - PythonOption ClientCA /etc/kojiweb/clientca.crt - PythonOption KojiHubCA /etc/kojiweb/kojihubca.crt - PythonOption LoginTimeout 72 - # This must be changed before deployment - PythonOption Secret CHANGE_ME - PythonPath "sys.path + ['/usr/share/koji-web/lib']" - PythonCleanupHandler kojiweb.handlers::cleanup - PythonAutoReload Off + Options ExecCGI + SetHandler wsgi-script + Order allow,deny + Allow from all +# Support for mod_python is DEPRECATED. If you still need mod_python support, +# then use the following directory settings instead: +# +# +# # Config for the publisher handler +# SetHandler mod_python +# # Use kojiweb's publisher (provides wsgi compat layer) +# # mod_python's publisher is no longer supported +# PythonHandler wsgi_publisher +# PythonAutoReload Off +# # Configuration via PythonOptions is DEPRECATED. Use /etc/kojiweb/web.conf +# Order allow,deny +# Allow from all +# + # uncomment this to enable authentication via Kerberos # # AuthType Kerberos diff --git a/www/conf/web.conf b/www/conf/web.conf new file mode 100644 index 00000000..22b1b923 --- /dev/null +++ b/www/conf/web.conf @@ -0,0 +1,24 @@ +[web] +SiteName = koji +#KojiTheme = mytheme + +# Key urls +KojiHubURL = http://hub.example.com/kojihub +KojiFilesURL = http://server.example.com/kojifiles + +# Kerberos authentication options +# WebPrincipal = koji/web@EXAMPLE.COM +# WebKeytab = /etc/httpd.keytab +# WebCCache = /var/tmp/kojiweb.ccache + +# SSL authentication options +# WebCert = /etc/kojiweb/kojiweb.crt +# ClientCA = /etc/kojiweb/clientca.crt +# KojiHubCA = /etc/kojiweb/kojihubca.crt + +LoginTimeout = 72 + +# This must be changed and uncommented before deployment +# Secret = CHANGE_ME + +LibPath = /usr/share/koji-web/lib diff --git a/www/kojiweb/index.py b/www/kojiweb/index.py index ab6f7dcc..46efb6fd 100644 --- a/www/kojiweb/index.py +++ b/www/kojiweb/index.py @@ -1,68 +1,122 @@ +# core web interface handlers for koji +# +# Copyright (c) 2005-2012 Red Hat +# +# Koji is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; +# version 2.1 of the License. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this software; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# +# Authors: +# Mike Bonnet +# Mike McLean + import os import os.path import re import sys import mimetypes -import mod_python -import mod_python.Cookie +import Cookie import Cheetah.Filters import Cheetah.Template import datetime +import logging import time import koji import kojiweb.util +from koji.server import ServerRedirect from kojiweb.util import _initValues from kojiweb.util import _genHTML from kojiweb.util import _getValidTokens +from koji.util import sha1_constructor # Convenience definition of a commonly-used sort function _sortbyname = kojiweb.util.sortByKeyFunc('name') -def _setUserCookie(req, user): - options = req.get_options() +#loggers +authlogger = logging.getLogger('koji.auth') + +def _setUserCookie(environ, user): + options = environ['koji.options'] # include the current time in the cookie so we can verify that # someone is not using an expired cookie - cookie = mod_python.Cookie.SignedCookie('user', user + ':' + str(time.time()), - secret=options['Secret'], - secure=True, - path=os.path.dirname(req.uri), - expires=(time.time() + (int(options['LoginTimeout']) * 60 * 60))) - mod_python.Cookie.add_cookie(req, cookie) + value = user + ':' + str(int(time.time())) + shasum = sha1_constructor(value) + shasum.update(options['Secret'].value) + value = "%s:%s" % (shasum.hexdigest(), value) + cookies = Cookie.SimpleCookie() + cookies['user'] = value + c = cookies['user'] #morsel instance + c['secure'] = True + c['path'] = os.path.dirname(environ['SCRIPT_NAME']) + # the Cookie module treats integer expire times as relative seconds + c['expires'] = int(options['LoginTimeout']) * 60 * 60 + out = c.OutputString() + out += '; HttpOnly' + environ['koji.headers'].append(['Set-Cookie', out]) + environ['koji.headers'].append(['Cache-Control', 'no-cache="set-cookie"']) -def _clearUserCookie(req): - cookie = mod_python.Cookie.Cookie('user', '', - path=os.path.dirname(req.uri), - expires=0) - mod_python.Cookie.add_cookie(req, cookie) +def _clearUserCookie(environ): + cookies = Cookie.SimpleCookie() + cookies['user'] = '' + c = cookies['user'] #morsel instance + c['path'] = os.path.dirname(environ['SCRIPT_NAME']) + c['expires'] = 0 + out = c.OutputString() + environ['koji.headers'].append(['Set-Cookie', out]) -def _getUserCookie(req): - options = req.get_options() - cookies = mod_python.Cookie.get_cookies(req, - mod_python.Cookie.SignedCookie, - secret=options['Secret']) - if cookies.has_key('user') and \ - (type(cookies['user']) is mod_python.Cookie.SignedCookie): - value = cookies['user'].value - if ':' in value: - user, timestamp = value.split(':') - timestamp = float(timestamp) - if (time.time() - timestamp) < (int(options['LoginTimeout']) * 60 * 60): - # cookie is valid and was created more recently than the login - # timeout - return user +def _getUserCookie(environ): + options = environ['koji.options'] + cookies = Cookie.SimpleCookie(environ.get('HTTP_COOKIE','')) + if 'user' not in cookies: + return None + value = cookies['user'].value + parts = value.split(":", 1) + if len(parts) != 2: + authlogger.warn('malformed user cookie: %s' % value) + return None + sig, value = parts + shasum = sha1_constructor(value) + shasum.update(options['Secret'].value) + if shasum.hexdigest() != sig: + authlogger.warn('invalid user cookie: %s:%s', sig, value) + return None + parts = value.split(":", 1) + if len(parts) != 2: + authlogger.warn('invalid signed user cookie: %s:%s', sig, value) + # no embedded timestamp + return None + user, timestamp = parts + try: + timestamp = float(timestamp) + except ValueError: + authlogger.warn('invalid time in signed user cookie: %s:%s', sig, value) + return None + if (time.time() - timestamp) > (int(options['LoginTimeout']) * 60 * 60): + authlogger.info('expired user cookie: %s', value) + return None + # Otherwise, cookie is valid and current + return user - return None - -def _krbLogin(req, session, principal): - options = req.get_options() +def _krbLogin(environ, session, principal): + options = environ['koji.options'] wprinc = options['WebPrincipal'] keytab = options['WebKeytab'] ccache = options['WebCCache'] return session.krb_login(principal=wprinc, keytab=keytab, ccache=ccache, proxyuser=principal) -def _sslLogin(req, session, username): - options = req.get_options() +def _sslLogin(environ, session, username): + options = environ['koji.options'] client_cert = options['WebCert'] client_ca = options['ClientCA'] server_ca = options['KojiHubCA'] @@ -70,25 +124,24 @@ def _sslLogin(req, session, username): return session.ssl_login(client_cert, client_ca, server_ca, proxyuser=username) -def _assertLogin(req): - session = req._session - options = req.get_options() - if not (hasattr(req, 'currentLogin') and - hasattr(req, 'currentUser')): +def _assertLogin(environ): + session = environ['koji.session'] + options = environ['koji.options'] + if 'koji.currentLogin' not in environ or 'koji.currentUser' not in environ: raise StandardError, '_getServer() must be called before _assertLogin()' - elif req.currentLogin and req.currentUser: + elif environ['koji.currentLogin'] and environ['koji.currentUser']: if options.get('WebCert'): - if not _sslLogin(req, session, req.currentLogin): - raise koji.AuthError, 'could not login %s via SSL' % req.currentLogin + if not _sslLogin(environ, session, environ['koji.currentLogin']): + raise koji.AuthError, 'could not login %s via SSL' % environ['koji.currentLogin'] elif options.get('WebPrincipal'): - if not _krbLogin(req, req._session, req.currentLogin): - raise koji.AuthError, 'could not login using principal: %s' % req.currentLogin + if not _krbLogin(environ, environ['koji.session'], environ['koji.currentLogin']): + raise koji.AuthError, 'could not login using principal: %s' % environ['koji.currentLogin'] else: raise koji.AuthError, 'KojiWeb is incorrectly configured for authentication, contact the system administrator' # verify a valid authToken was passed in to avoid CSRF - authToken = req.form.get('a', '') - validTokens = _getValidTokens(req) + authToken = environ['koji.form'].getvalue('a', '') + validTokens = _getValidTokens(environ) if authToken and authToken in validTokens: # we have a token and it's valid pass @@ -96,51 +149,54 @@ def _assertLogin(req): # their authToken is likely expired # send them back to the page that brought them here so they # can re-click the link with a valid authToken - _redirectBack(req, page=None, forceSSL=(_getBaseURL(req).startswith('https://'))) + _redirectBack(environ, page=None, forceSSL=(_getBaseURL(environ).startswith('https://'))) assert False else: - mod_python.util.redirect(req, 'login') + _redirect(environ, 'login') assert False -def _getServer(req): - opts = req.get_options() +def _getServer(environ): + opts = environ['koji.options'] session = koji.ClientSession(opts.get('KojiHubURL', 'http://localhost/kojihub'), opts={'krbservice': opts.get('KrbService', 'host')}) - req.currentLogin = _getUserCookie(req) - if req.currentLogin: - req.currentUser = session.getUser(req.currentLogin) - if not req.currentUser: - raise koji.AuthError, 'could not get user for principal: %s' % req.currentLogin - _setUserCookie(req, req.currentLogin) + environ['koji.currentLogin'] = _getUserCookie(environ) + if environ['koji.currentLogin']: + environ['koji.currentUser'] = session.getUser(environ['koji.currentLogin']) + if not environ['koji.currentUser']: + raise koji.AuthError, 'could not get user for principal: %s' % environ['koji.currentLogin'] + _setUserCookie(environ, environ['koji.currentLogin']) else: - req.currentUser = None + environ['koji.currentUser'] = None - req._session = session + environ['koji.session'] = session return session -def _construct_url(req, page): - port = req.connection.local_addr[1] +def _construct_url(environ, page): + port = environ['SERVER_PORT'] + host = environ['SERVER_NAME'] url_scheme = 'http' - env = req.subprocess_env - if env.get('HTTPS') == 'on': + if environ.get('HTTPS') in ('on','yes','1'): url_scheme = 'https' if (url_scheme == 'https' and port == 443) or \ (url_scheme == 'http' and port == 80): - return "%s://%s%s" % (url_scheme, req.hostname, page) - return "%s://%s:%d%s" % (url_scheme, req.hostname, port, page) + return "%s://%s%s" % (url_scheme, host, page) + return "%s://%s:%s%s" % (url_scheme, host, port, page) -def _getBaseURL(req): - pieces = req.uri.split('/') - base = '/'.join(pieces[:-1]) - return _construct_url(req, base) +def _getBaseURL(environ): + base = environ['SCRIPT_NAME'] + return _construct_url(environ, base) -def _redirectBack(req, page, forceSSL): +def _redirect(environ, location): + environ['koji.redirect'] = location + raise ServerRedirect + +def _redirectBack(environ, page, forceSSL): if page: # We'll work with the page we were given pass - elif req.headers_in.get('Referer'): - page = req.headers_in.get('Referer') + elif 'HTTP_REFERER' in environ: + page = environ['HTTP_REFERER'] else: page = 'index' @@ -148,69 +204,73 @@ def _redirectBack(req, page, forceSSL): if page.startswith('http'): pass elif page.startswith('/'): - page = _construct_url(req, page) + page = _construct_url(environ, page) else: - page = _getBaseURL(req) + '/' + page + page = _getBaseURL(environ) + '/' + page if forceSSL: page = page.replace('http:', 'https:') else: page = page.replace('https:', 'http:') # and redirect to the page - mod_python.util.redirect(req, page) + _redirect(environ, page) -def login(req, page=None): - session = _getServer(req) - options = req.get_options() +def login(environ, page=None): + session = _getServer(environ) + options = environ['koji.options'] # try SSL first, fall back to Kerberos if options.get('WebCert'): - req.add_common_vars() - env = req.subprocess_env - if not env.get('HTTPS') == 'on': + if environ.get('HTTPS') not in ['on', 'yes', '1']: dest = 'login' if page: dest = dest + '?page=' + page - _redirectBack(req, dest, forceSSL=True) + _redirectBack(environ, dest, forceSSL=True) return - if env.get('SSL_CLIENT_VERIFY') != 'SUCCESS': - raise koji.AuthError, 'could not verify client: %s' % env.get('SSL_CLIENT_VERIFY') + if environ.get('SSL_CLIENT_VERIFY') != 'SUCCESS': + raise koji.AuthError, 'could not verify client: %s' % environ.get('SSL_CLIENT_VERIFY') # use the subject's common name as their username - username = env.get('SSL_CLIENT_S_DN_CN') + username = environ.get('SSL_CLIENT_S_DN_CN') if not username: raise koji.AuthError, 'unable to get user information from client certificate' - - if not _sslLogin(req, session, username): + + if not _sslLogin(environ, session, username): raise koji.AuthError, 'could not login %s using SSL certificates' % username - + + authlogger.info('Successful SSL authentication by %s', username) + elif options.get('WebPrincipal'): - principal = req.user + principal = environ.get('REMOTE_USER') if not principal: raise koji.AuthError, 'configuration error: mod_auth_kerb should have performed authentication before presenting this page' - if not _krbLogin(req, session, principal): + if not _krbLogin(environ, session, principal): raise koji.AuthError, 'could not login using principal: %s' % principal - + username = principal + authlogger.info('Successful Kerberos authentication by %s', username) else: raise koji.AuthError, 'KojiWeb is incorrectly configured for authentication, contact the system administrator' - _setUserCookie(req, username) + _setUserCookie(environ, username) # To protect the session cookie, we must forceSSL - _redirectBack(req, page, forceSSL=True) + _redirectBack(environ, page, forceSSL=True) -def logout(req, page=None): - _clearUserCookie(req) +def logout(environ, page=None): + user = _getUserCookie(environ) + _clearUserCookie(environ) + if user: + authlogger.info('Logout by %s', user) - _redirectBack(req, page, forceSSL=False) + _redirectBack(environ, page, forceSSL=False) -def index(req, packageOrder='package_name', packageStart=None): - values = _initValues(req) - server = _getServer(req) +def index(environ, packageOrder='package_name', packageStart=None): + values = _initValues(environ) + server = _getServer(environ) - user = req.currentUser + user = environ['koji.currentUser'] values['builds'] = server.listBuilds(userID=(user and user['id'] or None), queryOpts={'order': '-build_id', 'limit': 10}) @@ -239,20 +299,20 @@ def index(req, packageOrder='package_name', packageStart=None): values['notifs'] = notifs values['user'] = user - values['welcomeMessage'] = req.get_options().get('KojiGreeting', 'Welcome to Koji Web') + values['welcomeMessage'] = environ['koji.options'].get('KojiGreeting', 'Welcome to Koji Web') - return _genHTML(req, 'index.chtml') + return _genHTML(environ, 'index.chtml') -def notificationedit(req, notificationID): - server = _getServer(req) - _assertLogin(req) +def notificationedit(environ, notificationID): + server = _getServer(environ) + _assertLogin(environ) notificationID = int(notificationID) notification = server.getBuildNotification(notificationID) if notification == None: raise koji.GenericError, 'no notification with ID: %i' % notificationID - form = req.form + form = environ['koji.form'] if form.has_key('save'): package_id = form['package'] @@ -274,11 +334,11 @@ def notificationedit(req, notificationID): server.updateNotification(notification['id'], package_id, tag_id, success_only) - mod_python.util.redirect(req, 'index') + _redirect(environ, 'index') elif form.has_key('cancel'): - mod_python.util.redirect(req, 'index') + _redirect(environ, 'index') else: - values = _initValues(req, 'Edit Notification') + values = _initValues(environ, 'Edit Notification') values['notif'] = notification packages = server.listPackages() @@ -287,16 +347,16 @@ def notificationedit(req, notificationID): tags = server.listTags(queryOpts={'order': 'name'}) values['tags'] = tags - return _genHTML(req, 'notificationedit.chtml') + return _genHTML(environ, 'notificationedit.chtml') -def notificationcreate(req): - server = _getServer(req) - _assertLogin(req) +def notificationcreate(environ): + server = _getServer(environ) + _assertLogin(environ) - form = req.form + form = environ['koji.form'] if form.has_key('add'): - user = req.currentUser + user = environ['koji.currentUser'] if not user: raise koji.GenericError, 'not logged-in' @@ -319,11 +379,11 @@ def notificationcreate(req): server.createNotification(user['id'], package_id, tag_id, success_only) - mod_python.util.redirect(req, 'index') + _redirect(environ, 'index') elif form.has_key('cancel'): - mod_python.util.redirect(req, 'index') + _redirect(environ, 'index') else: - values = _initValues(req, 'Edit Notification') + values = _initValues(environ, 'Edit Notification') values['notif'] = None packages = server.listPackages() @@ -332,11 +392,11 @@ def notificationcreate(req): tags = server.listTags(queryOpts={'order': 'name'}) values['tags'] = tags - return _genHTML(req, 'notificationedit.chtml') + return _genHTML(environ, 'notificationedit.chtml') -def notificationdelete(req, notificationID): - server = _getServer(req) - _assertLogin(req) +def notificationdelete(environ, notificationID): + server = _getServer(environ) + _assertLogin(environ) notificationID = int(notificationID) notification = server.getBuildNotification(notificationID) @@ -345,13 +405,8 @@ def notificationdelete(req, notificationID): server.deleteNotification(notification['id']) - mod_python.util.redirect(req, 'index') + _redirect(environ, 'index') -def hello(req): - return _getServer(req).hello() - -def showSession(req): - return _getServer(req).showSession() # All Tasks _TASKS = ['build', @@ -377,9 +432,9 @@ _TOPLEVEL_TASKS = ['build', 'buildNotification', 'chainbuild', 'maven', 'wrapper # Tasks that can have children _PARENT_TASKS = ['build', 'chainbuild', 'maven', 'winbuild', 'newRepo', 'wrapperRPM'] -def tasks(req, owner=None, state='active', view='tree', method='all', hostID=None, channelID=None, start=None, order='-id'): - values = _initValues(req, 'Tasks', 'tasks') - server = _getServer(req) +def tasks(environ, owner=None, state='active', view='tree', method='all', hostID=None, channelID=None, start=None, order='-id'): + values = _initValues(environ, 'Tasks', 'tasks') + server = _getServer(environ) opts = {'decode': True} if owner: @@ -462,7 +517,7 @@ def tasks(req, owner=None, state='active', view='tree', method='all', hostID=Non values['channel'] = None values['channelID'] = None - loggedInUser = req.currentUser + loggedInUser = environ['koji.currentUser'] values['loggedInUser'] = loggedInUser values['order'] = order @@ -478,11 +533,11 @@ def tasks(req, owner=None, state='active', view='tree', method='all', hostID=Non for task, [descendents] in zip(tasks, descendentList): task['descendents'] = descendents - return _genHTML(req, 'tasks.chtml') + return _genHTML(environ, 'tasks.chtml') -def taskinfo(req, taskID): - server = _getServer(req) - values = _initValues(req, 'Task Info', 'tasks') +def taskinfo(environ, taskID): + server = _getServer(environ) + values = _initValues(environ, 'Task Info', 'tasks') taskID = int(taskID) task = server.getTaskInfo(taskID, request=True) @@ -601,21 +656,21 @@ def taskinfo(req, taskID): output = server.listTaskOutput(task['id']) output.sort(_sortByExtAndName) values['output'] = output - if req.currentUser: - values['perms'] = server.getUserPerms(req.currentUser['id']) + if environ['koji.currentUser']: + values['perms'] = server.getUserPerms(environ['koji.currentUser']['id']) else: values['perms'] = [] - topurl = req.get_options().get('KojiFilesURL', 'http://localhost/') + topurl = environ['koji.options'].get('KojiFilesURL', 'http://localhost/') values['pathinfo'] = koji.PathInfo(topdir=topurl) - return _genHTML(req, 'taskinfo.chtml') + return _genHTML(environ, 'taskinfo.chtml') -def imageinfo(req, imageID): +def imageinfo(environ, imageID): """Do some prep work and generate the imageinfo page for kojiweb.""" - server = _getServer(req) - values = _initValues(req, 'Image Information') - imageURL = req.get_options().get('KojiFilesURL', 'http://localhost') + '/images' + server = _getServer(environ) + values = _initValues(environ, 'Image Information') + imageURL = environ['koji.options'].get('KojiFilesURL', 'http://localhost') + '/images' imageID = int(imageID) image = server.getImageInfo(imageID=imageID, strict=True) values['image'] = image @@ -627,10 +682,10 @@ def imageinfo(req, imageID): else: values['imageBase'] = imageURL + '/' + koji.pathinfo.applianceRelPath(image['id']) - return _genHTML(req, 'imageinfo.chtml') + return _genHTML(environ, 'imageinfo.chtml') -def taskstatus(req, taskID): - server = _getServer(req) +def taskstatus(environ, taskID): + server = _getServer(environ) taskID = int(taskID) task = server.getTaskInfo(taskID) @@ -643,21 +698,21 @@ def taskstatus(req, taskID): return output -def resubmittask(req, taskID): - server = _getServer(req) - _assertLogin(req) +def resubmittask(environ, taskID): + server = _getServer(environ) + _assertLogin(environ) taskID = int(taskID) newTaskID = server.resubmitTask(taskID) - mod_python.util.redirect(req, 'taskinfo?taskID=%i' % newTaskID) + _redirect(environ, 'taskinfo?taskID=%i' % newTaskID) -def canceltask(req, taskID): - server = _getServer(req) - _assertLogin(req) +def canceltask(environ, taskID): + server = _getServer(environ) + _assertLogin(environ) taskID = int(taskID) server.cancelTask(taskID) - mod_python.util.redirect(req, 'taskinfo?taskID=%i' % taskID) + _redirect(environ, 'taskinfo?taskID=%i' % taskID) def _sortByExtAndName(a, b): """Sort two filenames, first by extension, and then by name.""" @@ -665,8 +720,8 @@ def _sortByExtAndName(a, b): bRoot, bExt = os.path.splitext(b) return cmp(aExt, bExt) or cmp(aRoot, bRoot) -def getfile(req, taskID, name, offset=None, size=None): - server = _getServer(req) +def getfile(environ, taskID, name, offset=None, size=None): + server = _getServer(environ) taskID = int(taskID) output = server.listTaskOutput(taskID, stat=True) @@ -676,14 +731,15 @@ def getfile(req, taskID, name, offset=None, size=None): mime_guess = mimetypes.guess_type(name, strict=False)[0] if mime_guess: - req.content_type = mime_guess + ctype = mime_guess else: if name.endswith('.log') or name.endswith('.ks'): - req.content_type = 'text/plain' + ctype = 'text/plain' else: - req.content_type = 'application/octet-stream' - if req.content_type != 'text/plain': - req.headers_out['Content-Disposition'] = 'attachment; filename=%s' % name + ctype = 'application/octet-stream' + if ctype != 'text/plain': + environ['koji.headers'].append('Content-Disposition', 'attachment; filename=%s' % name) + environ['koji.headers'].append(['Content-Type', ctype]) file_size = int(file_info['st_size']) if offset is None: @@ -706,30 +762,30 @@ def getfile(req, taskID, name, offset=None, size=None): if size > (file_size - offset): size = file_size - offset - req.set_content_length(size) + #environ['koji.headers'].append(['Content-Length', str(size)]) + return _chunk_file(server, environ, taskID, name, offset, size) + +def _chunk_file(server, environ, taskID, name, offset, size): remaining = size + encode_int = koji.encode_int while True: if remaining <= 0: break chunk_size = 1048576 if remaining < chunk_size: chunk_size = remaining - if offset > 2147483647: - offset = str(offset) - content = server.downloadTaskOutput(taskID, name, offset=offset, size=chunk_size) - if isinstance(offset, str): - offset = int(offset) + content = server.downloadTaskOutput(taskID, name, offset=encode_int(offset), size=chunk_size) if not content: break - req.write(content) + yield content content_length = len(content) offset += content_length remaining -= content_length -def tags(req, start=None, order=None, childID=None): - values = _initValues(req, 'Tags', 'tags') - server = _getServer(req) +def tags(environ, start=None, order=None, childID=None): + values = _initValues(environ, 'Tags', 'tags') + server = _getServer(environ) if order == None: order = 'name' @@ -738,20 +794,20 @@ def tags(req, start=None, order=None, childID=None): tags = kojiweb.util.paginateMethod(server, values, 'listTags', kw=None, start=start, dataName='tags', prefix='tag', order=order) - if req.currentUser: - values['perms'] = server.getUserPerms(req.currentUser['id']) + if environ['koji.currentUser']: + values['perms'] = server.getUserPerms(environ['koji.currentUser']['id']) else: values['perms'] = [] values['childID'] = childID - - return _genHTML(req, 'tags.chtml') + + return _genHTML(environ, 'tags.chtml') _PREFIX_CHARS = [chr(char) for char in range(48, 58) + range(97, 123)] -def packages(req, tagID=None, userID=None, order='package_name', start=None, prefix=None, inherited='1'): - values = _initValues(req, 'Packages', 'packages') - server = _getServer(req) +def packages(environ, tagID=None, userID=None, order='package_name', start=None, prefix=None, inherited='1'): + values = _initValues(environ, 'Packages', 'packages') + server = _getServer(environ) tag = None if tagID != None: if tagID.isdigit(): @@ -781,11 +837,11 @@ def packages(req, tagID=None, userID=None, order='package_name', start=None, pre values['chars'] = _PREFIX_CHARS - return _genHTML(req, 'packages.chtml') + return _genHTML(environ, 'packages.chtml') -def packageinfo(req, packageID, tagOrder='name', tagStart=None, buildOrder='-completion_time', buildStart=None): - values = _initValues(req, 'Package Info', 'packages') - server = _getServer(req) +def packageinfo(environ, packageID, tagOrder='name', tagStart=None, buildOrder='-completion_time', buildStart=None): + values = _initValues(environ, 'Package Info', 'packages') + server = _getServer(environ) if packageID.isdigit(): packageID = int(packageID) @@ -803,11 +859,11 @@ def packageinfo(req, packageID, tagOrder='name', tagStart=None, buildOrder='-com builds = kojiweb.util.paginateMethod(server, values, 'listBuilds', kw={'packageID': package['id']}, start=buildStart, dataName='builds', prefix='build', order=buildOrder) - return _genHTML(req, 'packageinfo.chtml') + return _genHTML(environ, 'packageinfo.chtml') -def taginfo(req, tagID, all='0', packageOrder='package_name', packageStart=None, buildOrder='-completion_time', buildStart=None, childID=None): - values = _initValues(req, 'Tag Info', 'tags') - server = _getServer(req) +def taginfo(environ, tagID, all='0', packageOrder='package_name', packageStart=None, buildOrder='-completion_time', buildStart=None, childID=None): + values = _initValues(environ, 'Tag Info', 'tags') + server = _getServer(environ) if tagID.isdigit(): tagID = int(tagID) @@ -850,23 +906,23 @@ def taginfo(req, tagID, all='0', packageOrder='package_name', packageStart=None, child = server.getTag(int(childID), strict=True) values['child'] = child - if req.currentUser: - values['perms'] = server.getUserPerms(req.currentUser['id']) + if environ['koji.currentUser']: + values['perms'] = server.getUserPerms(environ['koji.currentUser']['id']) else: values['perms'] = [] permList = server.getAllPerms() allPerms = dict([(perm['id'], perm['name']) for perm in permList]) values['allPerms'] = allPerms - return _genHTML(req, 'taginfo.chtml') + return _genHTML(environ, 'taginfo.chtml') -def tagcreate(req): - server = _getServer(req) - _assertLogin(req) +def tagcreate(environ): + server = _getServer(environ) + _assertLogin(environ) mavenEnabled = server.mavenEnabled() - form = req.form + form = environ['koji.form'] if form.has_key('add'): params = {} @@ -882,22 +938,22 @@ def tagcreate(req): tagID = server.createTag(name, **params) - mod_python.util.redirect(req, 'taginfo?tagID=%i' % tagID) + _redirect(environ, 'taginfo?tagID=%i' % tagID) elif form.has_key('cancel'): - mod_python.util.redirect(req, 'tags') + _redirect(environ, 'tags') else: - values = _initValues(req, 'Add Tag', 'tags') + values = _initValues(environ, 'Add Tag', 'tags') values['mavenEnabled'] = mavenEnabled values['tag'] = None values['permissions'] = server.getAllPerms() - return _genHTML(req, 'tagedit.chtml') + return _genHTML(environ, 'tagedit.chtml') -def tagedit(req, tagID): - server = _getServer(req) - _assertLogin(req) +def tagedit(environ, tagID): + server = _getServer(environ) + _assertLogin(environ) mavenEnabled = server.mavenEnabled() @@ -906,7 +962,7 @@ def tagedit(req, tagID): if tag == None: raise koji.GenericError, 'no tag with ID: %i' % tagID - form = req.form + form = environ['koji.form'] if form.has_key('save'): params = {} @@ -922,22 +978,22 @@ def tagedit(req, tagID): server.editTag2(tag['id'], **params) - mod_python.util.redirect(req, 'taginfo?tagID=%i' % tag['id']) + _redirect(environ, 'taginfo?tagID=%i' % tag['id']) elif form.has_key('cancel'): - mod_python.util.redirect(req, 'taginfo?tagID=%i' % tag['id']) + _redirect(environ, 'taginfo?tagID=%i' % tag['id']) else: - values = _initValues(req, 'Edit Tag', 'tags') + values = _initValues(environ, 'Edit Tag', 'tags') values['mavenEnabled'] = mavenEnabled values['tag'] = tag values['permissions'] = server.getAllPerms() - return _genHTML(req, 'tagedit.chtml') + return _genHTML(environ, 'tagedit.chtml') -def tagdelete(req, tagID): - server = _getServer(req) - _assertLogin(req) +def tagdelete(environ, tagID): + server = _getServer(environ) + _assertLogin(environ) tagID = int(tagID) tag = server.getTag(tagID) @@ -946,17 +1002,17 @@ def tagdelete(req, tagID): server.deleteTag(tag['id']) - mod_python.util.redirect(req, 'tags') + _redirect(environ, 'tags') -def tagparent(req, tagID, parentID, action): - server = _getServer(req) - _assertLogin(req) +def tagparent(environ, tagID, parentID, action): + server = _getServer(environ) + _assertLogin(environ) tag = server.getTag(int(tagID), strict=True) parent = server.getTag(int(parentID), strict=True) if action in ('add', 'edit'): - form = req.form + form = environ['koji.form'] if form.has_key('add') or form.has_key('save'): newDatum = {} @@ -976,7 +1032,7 @@ def tagparent(req, tagID, parentID, action): elif form.has_key('cancel'): pass else: - values = _initValues(req, action.capitalize() + ' Parent Tag', 'tags') + values = _initValues(environ, action.capitalize() + ' Parent Tag', 'tags') values['tag'] = tag values['parent'] = parent @@ -995,7 +1051,7 @@ def tagparent(req, tagID, parentID, action): else: raise koji.GenericError, 'tag %i has tag %i listed as a parent more than once' % (tag['id'], parent['id']) - return _genHTML(req, 'tagparent.chtml') + return _genHTML(environ, 'tagparent.chtml') elif action == 'remove': data = server.getInheritanceData(tag['id']) for datum in data: @@ -1009,11 +1065,11 @@ def tagparent(req, tagID, parentID, action): else: raise koji.GenericError, 'unknown action: %s' % action - mod_python.util.redirect(req, 'taginfo?tagID=%i' % tag['id']) + _redirect(environ, 'taginfo?tagID=%i' % tag['id']) -def externalrepoinfo(req, extrepoID): - values = _initValues(req, 'External Repo Info', 'tags') - server = _getServer(req) +def externalrepoinfo(environ, extrepoID): + values = _initValues(environ, 'External Repo Info', 'tags') + server = _getServer(environ) if extrepoID.isdigit(): extrepoID = int(extrepoID) @@ -1024,11 +1080,11 @@ def externalrepoinfo(req, extrepoID): values['extRepo'] = extRepo values['repoTags'] = repoTags - return _genHTML(req, 'externalrepoinfo.chtml') + return _genHTML(environ, 'externalrepoinfo.chtml') -def buildinfo(req, buildID): - values = _initValues(req, 'Build Info', 'builds') - server = _getServer(req) +def buildinfo(environ, buildID): + values = _initValues(environ, 'Build Info', 'builds') + server = _getServer(environ) buildID = int(buildID) @@ -1123,8 +1179,8 @@ def buildinfo(req, buildID): values['archivesByExt'] = archivesByExt values['noarch_log_dest'] = noarch_log_dest - if req.currentUser: - values['perms'] = server.getUserPerms(req.currentUser['id']) + if environ['koji.currentUser']: + values['perms'] = server.getUserPerms(environ['koji.currentUser']['id']) else: values['perms'] = [] for field in ['summary', 'description', 'changelog']: @@ -1140,14 +1196,14 @@ def buildinfo(req, buildID): else: values['estCompletion'] = None - topurl = req.get_options().get('KojiFilesURL', 'http://localhost/') + topurl = environ['koji.options'].get('KojiFilesURL', 'http://localhost/') values['pathinfo'] = koji.PathInfo(topdir=topurl) - return _genHTML(req, 'buildinfo.chtml') + return _genHTML(environ, 'buildinfo.chtml') -def builds(req, userID=None, tagID=None, packageID=None, state=None, order='-build_id', start=None, prefix=None, inherited='1', latest='1', type=None): - values = _initValues(req, 'Builds', 'builds') - server = _getServer(req) +def builds(environ, userID=None, tagID=None, packageID=None, state=None, order='-build_id', start=None, prefix=None, inherited='1', latest='1', type=None): + values = _initValues(environ, 'Builds', 'builds') + server = _getServer(environ) user = None if userID: @@ -1157,7 +1213,7 @@ def builds(req, userID=None, tagID=None, packageID=None, state=None, order='-bui values['userID'] = userID values['user'] = user - loggedInUser = req.currentUser + loggedInUser = environ['koji.currentUser'] values['loggedInUser'] = loggedInUser values['users'] = server.listUsers(queryOpts={'order': 'name'}) @@ -1223,11 +1279,11 @@ def builds(req, userID=None, tagID=None, packageID=None, state=None, order='-bui values['chars'] = _PREFIX_CHARS - return _genHTML(req, 'builds.chtml') + return _genHTML(environ, 'builds.chtml') -def users(req, order='name', start=None, prefix=None): - values = _initValues(req, 'Users', 'users') - server = _getServer(req) +def users(environ, order='name', start=None, prefix=None): + values = _initValues(environ, 'Users', 'users') + server = _getServer(environ) if prefix: prefix = prefix.lower()[0] @@ -1242,11 +1298,11 @@ def users(req, order='name', start=None, prefix=None): values['chars'] = _PREFIX_CHARS - return _genHTML(req, 'users.chtml') + return _genHTML(environ, 'users.chtml') -def userinfo(req, userID, packageOrder='package_name', packageStart=None, buildOrder='-completion_time', buildStart=None): - values = _initValues(req, 'User Info', 'users') - server = _getServer(req) +def userinfo(environ, userID, packageOrder='package_name', packageStart=None, buildOrder='-completion_time', buildStart=None): + values = _initValues(environ, 'User Info', 'users') + server = _getServer(environ) if userID.isdigit(): userID = int(userID) @@ -1264,11 +1320,11 @@ def userinfo(req, userID, packageOrder='package_name', packageStart=None, buildO builds = kojiweb.util.paginateMethod(server, values, 'listBuilds', kw={'userID': user['id']}, start=buildStart, dataName='builds', prefix='build', order=buildOrder, pageSize=10) - return _genHTML(req, 'userinfo.chtml') + return _genHTML(environ, 'userinfo.chtml') -def rpminfo(req, rpmID, fileOrder='name', fileStart=None, buildrootOrder='-id', buildrootStart=None): - values = _initValues(req, 'RPM Info', 'builds') - server = _getServer(req) +def rpminfo(environ, rpmID, fileOrder='name', fileStart=None, buildrootOrder='-id', buildrootStart=None): + values = _initValues(environ, 'RPM Info', 'builds') + server = _getServer(environ) rpmID = int(rpmID) rpm = server.getRPM(rpmID) @@ -1310,11 +1366,11 @@ def rpminfo(req, rpmID, fileOrder='name', fileStart=None, buildrootOrder='-id', files = kojiweb.util.paginateMethod(server, values, 'listRPMFiles', args=[rpm['id']], start=fileStart, dataName='files', prefix='file', order=fileOrder) - return _genHTML(req, 'rpminfo.chtml') + return _genHTML(environ, 'rpminfo.chtml') -def archiveinfo(req, archiveID, fileOrder='name', fileStart=None, buildrootOrder='-id', buildrootStart=None): - values = _initValues(req, 'Archive Info', 'builds') - server = _getServer(req) +def archiveinfo(environ, archiveID, fileOrder='name', fileStart=None, buildrootOrder='-id', buildrootStart=None): + values = _initValues(environ, 'Archive Info', 'builds') + server = _getServer(environ) archiveID = int(archiveID) archive = server.getArchive(archiveID) @@ -1346,11 +1402,11 @@ def archiveinfo(req, archiveID, fileOrder='name', fileStart=None, buildrootOrder values['builtInRoot'] = builtInRoot values['buildroots'] = buildroots - return _genHTML(req, 'archiveinfo.chtml') + return _genHTML(environ, 'archiveinfo.chtml') -def fileinfo(req, filename, rpmID=None, archiveID=None): - values = _initValues(req, 'File Info', 'builds') - server = _getServer(req) +def fileinfo(environ, filename, rpmID=None, archiveID=None): + values = _initValues(environ, 'File Info', 'builds') + server = _getServer(environ) values['rpm'] = None values['archive'] = None @@ -1380,11 +1436,11 @@ def fileinfo(req, filename, rpmID=None, archiveID=None): values['file'] = file - return _genHTML(req, 'fileinfo.chtml') + return _genHTML(environ, 'fileinfo.chtml') -def cancelbuild(req, buildID): - server = _getServer(req) - _assertLogin(req) +def cancelbuild(environ, buildID): + server = _getServer(environ) + _assertLogin(environ) buildID = int(buildID) build = server.getBuild(buildID) @@ -1395,11 +1451,11 @@ def cancelbuild(req, buildID): if not result: raise koji.GenericError, 'unable to cancel build' - mod_python.util.redirect(req, 'buildinfo?buildID=%i' % build['id']) + _redirect(environ, 'buildinfo?buildID=%i' % build['id']) -def hosts(req, state='enabled', start=None, order='name'): - values = _initValues(req, 'Hosts', 'hosts') - server = _getServer(req) +def hosts(environ, state='enabled', start=None, order='name'): + values = _initValues(environ, 'Hosts', 'hosts') + server = _getServer(environ) values['order'] = order @@ -1425,11 +1481,11 @@ def hosts(req, state='enabled', start=None, order='name'): # Paginate after retrieving last update info so we can sort on it kojiweb.util.paginateList(values, hosts, start, 'hosts', 'host', order) - return _genHTML(req, 'hosts.chtml') + return _genHTML(environ, 'hosts.chtml') -def hostinfo(req, hostID=None, userID=None): - values = _initValues(req, 'Host Info', 'hosts') - server = _getServer(req) +def hostinfo(environ, hostID=None, userID=None): + values = _initValues(environ, 'Host Info', 'hosts') + server = _getServer(environ) if hostID: if hostID.isdigit(): @@ -1460,23 +1516,23 @@ def hostinfo(req, hostID=None, userID=None): values['channels'] = channels values['buildroots'] = buildroots values['lastUpdate'] = server.getLastHostUpdate(host['id']) - if req.currentUser: - values['perms'] = server.getUserPerms(req.currentUser['id']) + if environ['koji.currentUser']: + values['perms'] = server.getUserPerms(environ['koji.currentUser']['id']) else: values['perms'] = [] - return _genHTML(req, 'hostinfo.chtml') + return _genHTML(environ, 'hostinfo.chtml') -def hostedit(req, hostID): - server = _getServer(req) - _assertLogin(req) +def hostedit(environ, hostID): + server = _getServer(environ) + _assertLogin(environ) hostID = int(hostID) host = server.getHost(hostID) if host == None: raise koji.GenericError, 'no host with ID: %i' % hostID - form = req.form + form = environ['koji.form'] if form.has_key('save'): arches = form['arches'].value @@ -1502,11 +1558,11 @@ def hostedit(req, hostID): if channel not in hostChannels: server.addHostToChannel(host['name'], channel) - mod_python.util.redirect(req, 'hostinfo?hostID=%i' % host['id']) + _redirect(environ, 'hostinfo?hostID=%i' % host['id']) elif form.has_key('cancel'): - mod_python.util.redirect(req, 'hostinfo?hostID=%i' % host['id']) + _redirect(environ, 'hostinfo?hostID=%i' % host['id']) else: - values = _initValues(req, 'Edit Host', 'hosts') + values = _initValues(environ, 'Edit Host', 'hosts') values['host'] = host allChannels = server.listChannels() @@ -1514,31 +1570,31 @@ def hostedit(req, hostID): values['allChannels'] = allChannels values['hostChannels'] = server.listChannels(hostID=host['id']) - return _genHTML(req, 'hostedit.chtml') + return _genHTML(environ, 'hostedit.chtml') -def disablehost(req, hostID): - server = _getServer(req) - _assertLogin(req) +def disablehost(environ, hostID): + server = _getServer(environ) + _assertLogin(environ) hostID = int(hostID) host = server.getHost(hostID, strict=True) server.disableHost(host['name']) - mod_python.util.redirect(req, 'hostinfo?hostID=%i' % host['id']) + _redirect(environ, 'hostinfo?hostID=%i' % host['id']) -def enablehost(req, hostID): - server = _getServer(req) - _assertLogin(req) +def enablehost(environ, hostID): + server = _getServer(environ) + _assertLogin(environ) hostID = int(hostID) host = server.getHost(hostID, strict=True) server.enableHost(host['name']) - mod_python.util.redirect(req, 'hostinfo?hostID=%i' % host['id']) + _redirect(environ, 'hostinfo?hostID=%i' % host['id']) -def channelinfo(req, channelID): - values = _initValues(req, 'Channel Info', 'hosts') - server = _getServer(req) +def channelinfo(environ, channelID): + values = _initValues(environ, 'Channel Info', 'hosts') + server = _getServer(environ) channelID = int(channelID) channel = server.getChannel(channelID) @@ -1558,11 +1614,11 @@ def channelinfo(req, channelID): values['channel'] = channel values['hosts'] = hosts - return _genHTML(req, 'channelinfo.chtml') + return _genHTML(environ, 'channelinfo.chtml') -def buildrootinfo(req, buildrootID, builtStart=None, builtOrder=None, componentStart=None, componentOrder=None): - values = _initValues(req, 'Buildroot Info', 'hosts') - server = _getServer(req) +def buildrootinfo(environ, buildrootID, builtStart=None, builtOrder=None, componentStart=None, componentOrder=None): + values = _initValues(environ, 'Buildroot Info', 'hosts') + server = _getServer(environ) buildrootID = int(buildrootID) buildroot = server.getBuildroot(buildrootID) @@ -1577,17 +1633,17 @@ def buildrootinfo(req, buildrootID, builtStart=None, builtOrder=None, componentS values['buildroot'] = buildroot values['task'] = task - return _genHTML(req, 'buildrootinfo.chtml') + return _genHTML(environ, 'buildrootinfo.chtml') -def rpmlist(req, type, buildrootID=None, imageID=None, start=None, order='nvr'): +def rpmlist(environ, type, buildrootID=None, imageID=None, start=None, order='nvr'): """ rpmlist requires a buildrootID OR an imageID to be passed in. From one of these values it will paginate a list of rpms included in the corresponding object. (buildroot or image) """ - values = _initValues(req, 'RPM List', 'hosts') - server = _getServer(req) + values = _initValues(environ, 'RPM List', 'hosts') + server = _getServer(environ) if buildrootID != None: buildrootID = int(buildrootID) @@ -1626,11 +1682,11 @@ def rpmlist(req, type, buildrootID=None, imageID=None, start=None, order='nvr'): values['type'] = type values['order'] = order - return _genHTML(req, 'rpmlist.chtml') + return _genHTML(environ, 'rpmlist.chtml') -def archivelist(req, buildrootID, type, start=None, order='filename'): - values = _initValues(req, 'Archive List', 'hosts') - server = _getServer(req) +def archivelist(environ, buildrootID, type, start=None, order='filename'): + values = _initValues(environ, 'Archive List', 'hosts') + server = _getServer(environ) buildrootID = int(buildrootID) buildroot = server.getBuildroot(buildrootID) @@ -1652,26 +1708,26 @@ def archivelist(req, buildrootID, type, start=None, order='filename'): values['order'] = order - return _genHTML(req, 'archivelist.chtml') + return _genHTML(environ, 'archivelist.chtml') -def buildtargets(req, start=None, order='name'): - values = _initValues(req, 'Build Targets', 'buildtargets') - server = _getServer(req) +def buildtargets(environ, start=None, order='name'): + values = _initValues(environ, 'Build Targets', 'buildtargets') + server = _getServer(environ) targets = kojiweb.util.paginateMethod(server, values, 'getBuildTargets', start=start, dataName='targets', prefix='target', order=order) values['order'] = order - if req.currentUser: - values['perms'] = server.getUserPerms(req.currentUser['id']) + if environ['koji.currentUser']: + values['perms'] = server.getUserPerms(environ['koji.currentUser']['id']) else: values['perms'] = [] - return _genHTML(req, 'buildtargets.chtml') + return _genHTML(environ, 'buildtargets.chtml') -def buildtargetinfo(req, targetID=None, name=None): - values = _initValues(req, 'Build Target Info', 'buildtargets') - server = _getServer(req) +def buildtargetinfo(environ, targetID=None, name=None): + values = _initValues(environ, 'Build Target Info', 'buildtargets') + server = _getServer(environ) target = None if targetID != None: @@ -1691,16 +1747,16 @@ def buildtargetinfo(req, targetID=None, name=None): values['target'] = target values['buildTag'] = buildTag values['destTag'] = destTag - if req.currentUser: - values['perms'] = server.getUserPerms(req.currentUser['id']) + if environ['koji.currentUser']: + values['perms'] = server.getUserPerms(environ['koji.currentUser']['id']) else: values['perms'] = [] - return _genHTML(req, 'buildtargetinfo.chtml') + return _genHTML(environ, 'buildtargetinfo.chtml') -def buildtargetedit(req, targetID): - server = _getServer(req) - _assertLogin(req) +def buildtargetedit(environ, targetID): + server = _getServer(environ) + _assertLogin(environ) targetID = int(targetID) @@ -1708,7 +1764,7 @@ def buildtargetedit(req, targetID): if target == None: raise koji.GenericError, 'invalid build target: %s' % targetID - form = req.form + form = environ['koji.form'] if form.has_key('save'): name = form['name'].value @@ -1724,24 +1780,24 @@ def buildtargetedit(req, targetID): server.editBuildTarget(target['id'], name, buildTag['id'], destTag['id']) - mod_python.util.redirect(req, 'buildtargetinfo?targetID=%i' % target['id']) + _redirect(environ, 'buildtargetinfo?targetID=%i' % target['id']) elif form.has_key('cancel'): - mod_python.util.redirect(req, 'buildtargetinfo?targetID=%i' % target['id']) + _redirect(environ, 'buildtargetinfo?targetID=%i' % target['id']) else: - values = _initValues(req, 'Edit Build Target', 'buildtargets') + values = _initValues(environ, 'Edit Build Target', 'buildtargets') tags = server.listTags() tags.sort(_sortbyname) values['target'] = target values['tags'] = tags - return _genHTML(req, 'buildtargetedit.chtml') + return _genHTML(environ, 'buildtargetedit.chtml') -def buildtargetcreate(req): - server = _getServer(req) - _assertLogin(req) +def buildtargetcreate(environ): + server = _getServer(environ) + _assertLogin(environ) - form = req.form + form = environ['koji.form'] if form.has_key('add'): # Use the str .value field of the StringField object, @@ -1757,11 +1813,11 @@ def buildtargetcreate(req): if target == None: raise koji.GenericError, 'error creating build target "%s"' % name - mod_python.util.redirect(req, 'buildtargetinfo?targetID=%i' % target['id']) + _redirect(environ, 'buildtargetinfo?targetID=%i' % target['id']) elif form.has_key('cancel'): - mod_python.util.redirect(req, 'buildtargets') + _redirect(environ, 'buildtargets') else: - values = _initValues(req, 'Add Build Target', 'builtargets') + values = _initValues(environ, 'Add Build Target', 'builtargets') tags = server.listTags() tags.sort(_sortbyname) @@ -1769,11 +1825,11 @@ def buildtargetcreate(req): values['target'] = None values['tags'] = tags - return _genHTML(req, 'buildtargetedit.chtml') + return _genHTML(environ, 'buildtargetedit.chtml') -def buildtargetdelete(req, targetID): - server = _getServer(req) - _assertLogin(req) +def buildtargetdelete(environ, targetID): + server = _getServer(environ) + _assertLogin(environ) targetID = int(targetID) @@ -1783,16 +1839,16 @@ def buildtargetdelete(req, targetID): server.deleteBuildTarget(target['id']) - mod_python.util.redirect(req, 'buildtargets') + _redirect(environ, 'buildtargets') -def reports(req): - server = _getServer(req) - values = _initValues(req, 'Reports', 'reports') - return _genHTML(req, 'reports.chtml') +def reports(environ): + server = _getServer(environ) + values = _initValues(environ, 'Reports', 'reports') + return _genHTML(environ, 'reports.chtml') -def buildsbyuser(req, start=None, order='-builds'): - values = _initValues(req, 'Builds by User', 'reports') - server = _getServer(req) +def buildsbyuser(environ, start=None, order='-builds'): + values = _initValues(environ, 'Builds by User', 'reports') + server = _getServer(environ) maxBuilds = 1 users = server.listUsers() @@ -1815,11 +1871,11 @@ def buildsbyuser(req, start=None, order='-builds'): values['increment'] = graphWidth / maxBuilds kojiweb.util.paginateList(values, users, start, 'userBuilds', 'userBuild', order) - return _genHTML(req, 'buildsbyuser.chtml') + return _genHTML(environ, 'buildsbyuser.chtml') -def rpmsbyhost(req, start=None, order=None, hostArch=None, rpmArch=None): - values = _initValues(req, 'RPMs by Host', 'reports') - server = _getServer(req) +def rpmsbyhost(environ, start=None, order=None, hostArch=None, rpmArch=None): + values = _initValues(environ, 'RPMs by Host', 'reports') + server = _getServer(environ) maxRPMs = 1 hostArchFilter = hostArch @@ -1857,11 +1913,11 @@ def rpmsbyhost(req, start=None, order=None, hostArch=None, rpmArch=None): values['increment'] = graphWidth / maxRPMs kojiweb.util.paginateList(values, hosts, start, 'hosts', 'host', order) - return _genHTML(req, 'rpmsbyhost.chtml') + return _genHTML(environ, 'rpmsbyhost.chtml') -def packagesbyuser(req, start=None, order=None): - values = _initValues(req, 'Packages by User', 'reports') - server = _getServer(req) +def packagesbyuser(environ, start=None, order=None): + values = _initValues(environ, 'Packages by User', 'reports') + server = _getServer(environ) maxPackages = 1 users = server.listUsers() @@ -1886,11 +1942,11 @@ def packagesbyuser(req, start=None, order=None): values['increment'] = graphWidth / maxPackages kojiweb.util.paginateList(values, users, start, 'users', 'user', order) - return _genHTML(req, 'packagesbyuser.chtml') + return _genHTML(environ, 'packagesbyuser.chtml') -def tasksbyhost(req, start=None, order='-tasks', hostArch=None): - values = _initValues(req, 'Tasks by Host', 'reports') - server = _getServer(req) +def tasksbyhost(environ, start=None, order='-tasks', hostArch=None): + values = _initValues(environ, 'Tasks by Host', 'reports') + server = _getServer(environ) maxTasks = 1 @@ -1923,11 +1979,11 @@ def tasksbyhost(req, start=None, order='-tasks', hostArch=None): values['increment'] = graphWidth / maxTasks kojiweb.util.paginateList(values, hosts, start, 'hosts', 'host', order) - return _genHTML(req, 'tasksbyhost.chtml') + return _genHTML(environ, 'tasksbyhost.chtml') -def tasksbyuser(req, start=None, order='-tasks'): - values = _initValues(req, 'Tasks by User', 'reports') - server = _getServer(req) +def tasksbyuser(environ, start=None, order='-tasks'): + values = _initValues(environ, 'Tasks by User', 'reports') + server = _getServer(environ) maxTasks = 1 @@ -1951,11 +2007,11 @@ def tasksbyuser(req, start=None, order='-tasks'): values['increment'] = graphWidth / maxTasks kojiweb.util.paginateList(values, users, start, 'users', 'user', order) - return _genHTML(req, 'tasksbyuser.chtml') + return _genHTML(environ, 'tasksbyuser.chtml') -def buildsbystatus(req, days='7'): - values = _initValues(req, 'Builds by Status', 'reports') - server = _getServer(req) +def buildsbystatus(environ, days='7'): + values = _initValues(environ, 'Builds by Status', 'reports') + server = _getServer(environ) days = int(days) if days != -1: @@ -1986,11 +2042,11 @@ def buildsbystatus(req, days='7'): values['maxBuilds'] = maxBuilds values['increment'] = graphWidth / maxBuilds - return _genHTML(req, 'buildsbystatus.chtml') + return _genHTML(environ, 'buildsbystatus.chtml') -def buildsbytarget(req, days='7', start=None, order='-builds'): - values = _initValues(req, 'Builds by Target', 'reports') - server = _getServer(req) +def buildsbytarget(environ, days='7', start=None, order='-builds'): + values = _initValues(environ, 'Builds by Target', 'reports') + server = _getServer(environ) days = int(days) if days != -1: @@ -2025,11 +2081,11 @@ def buildsbytarget(req, days='7', start=None, order='-builds'): values['maxBuilds'] = maxBuilds values['increment'] = graphWidth / maxBuilds - return _genHTML(req, 'buildsbytarget.chtml') - -def recentbuilds(req, user=None, tag=None, package=None): - values = _initValues(req, 'Recent Build RSS') - server = _getServer(req) + return _genHTML(environ, 'buildsbytarget.chtml') + +def recentbuilds(environ, user=None, tag=None, package=None): + values = _initValues(environ, 'Recent Build RSS') + server = _getServer(environ) tagObj = None if tag != None: @@ -2091,10 +2147,10 @@ def recentbuilds(req, user=None, tag=None, package=None): values['user'] = userObj values['package'] = packageObj values['builds'] = builds - values['weburl'] = _getBaseURL(req) + values['weburl'] = _getBaseURL(environ) - req.content_type = 'text/xml' - return _genHTML(req, 'recentbuilds.chtml') + environ['koji.headers'].append(['Content-Type', 'text/xml']) + return _genHTML(environ, 'recentbuilds.chtml') _infoURLs = {'package': 'packageinfo?packageID=%(id)i', 'build': 'buildinfo?buildID=%(id)i', @@ -2110,12 +2166,12 @@ _VALID_SEARCH_CHARS = r"""a-zA-Z0-9""" _VALID_SEARCH_SYMS = r""" @.,_/\()%+-*?|[]^$""" _VALID_SEARCH_RE = re.compile('^[' + _VALID_SEARCH_CHARS + re.escape(_VALID_SEARCH_SYMS) + ']+$') -def search(req, start=None, order='name'): - values = _initValues(req, 'Search', 'search') - server = _getServer(req) +def search(environ, start=None, order='name'): + values = _initValues(environ, 'Search', 'search') + server = _getServer(environ) values['error'] = None - form = req.form + form = environ['koji.form'] if form.has_key('terms') and form['terms']: terms = form['terms'].value type = form['type'].value @@ -2128,14 +2184,14 @@ def search(req, start=None, order='name'): values['error'] = 'Invalid search terms
' + \ 'Search terms may contain only these characters: ' + \ _VALID_SEARCH_CHARS + _VALID_SEARCH_SYMS - return _genHTML(req, 'search.chtml') + return _genHTML(environ, 'search.chtml') if match == 'regexp': try: re.compile(terms) except: values['error'] = 'Invalid regular expression' - return _genHTML(req, 'search.chtml') + return _genHTML(environ, 'search.chtml') infoURL = _infoURLs.get(type) if not infoURL: @@ -2148,7 +2204,7 @@ def search(req, start=None, order='name'): if not start and len(results) == 1: # if we found exactly one result, skip the result list and redirect to the info page # (you're feeling lucky) - mod_python.util.redirect(req, infoURL % results[0]) + _redirect(environ, infoURL % results[0]) else: if type == 'maven': typeLabel = 'Maven artifacts' @@ -2157,12 +2213,12 @@ def search(req, start=None, order='name'): else: typeLabel = '%ss' % type values['typeLabel'] = typeLabel - return _genHTML(req, 'searchresults.chtml') + return _genHTML(environ, 'searchresults.chtml') else: - return _genHTML(req, 'search.chtml') + return _genHTML(environ, 'search.chtml') -def watchlogs(req, taskID): - values = _initValues(req) +def watchlogs(environ, taskID): + values = _initValues(environ) if isinstance(taskID, list): values['tasks'] = ', '.join(taskID) else: diff --git a/www/kojiweb/wsgi_publisher.py b/www/kojiweb/wsgi_publisher.py new file mode 100644 index 00000000..6a786d85 --- /dev/null +++ b/www/kojiweb/wsgi_publisher.py @@ -0,0 +1,461 @@ +# a vaguely publisher-like dispatcher for wsgi +# +# Copyright (c) 2012 Red Hat +# +# Koji is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; +# version 2.1 of the License. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this software; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# +# Authors: +# Mike McLean + +import cgi +import inspect +import koji +import koji.util +import logging +import os.path +import pprint +import sys +import traceback + +from ConfigParser import RawConfigParser +from koji.server import WSGIWrapper, ServerError, ServerRedirect +from koji.util import dslice + + +class URLNotFound(ServerError): + """Used to generate a 404 response""" + + +class Dispatcher(object): + + def __init__(self): + #we can't do much setup until we get a request + self.firstcall = True + self.options = {} + self.startup_error = None + self.handler_index = {} + self.setup_logging1() + + def setup_logging1(self): + """Set up basic logging, before options are loaded""" + logger = logging.getLogger("koji") + logger.setLevel(logging.WARNING) + self.log_handler = logging.StreamHandler() + # Log to stderr (StreamHandler default). + # There seems to be no advantage to using wsgi.errors + log_format = '%(msecs)d [%(levelname)s] SETUP p=%(process)s %(name)s: %(message)s' + self.log_handler.setFormatter(logging.Formatter(log_format)) + self.log_handler.setLevel(logging.DEBUG) + logger.addHandler(self.log_handler) + self.formatter = None + self.logger = logging.getLogger("koji.web") + + cfgmap = [ + #option, type, default + ['SiteName', 'string', None], + ['KojiHubURL', 'string', None], + ['KojiFilesURL', 'string', None], + ['KojiTheme', 'string', None], + + ['WebPrincipal', 'string', None], + ['WebKeytab', 'string', '/etc/httpd.keytab'], + ['WebCCache', 'string', '/var/tmp/kojiweb.ccache'], + ['KrbService', 'string', 'host'], + + ['WebCert', 'string', None], + ['ClientCA', 'string', '/etc/kojiweb/clientca.crt'], + ['KojiHubCA', 'string', '/etc/kojiweb/kojihubca.crt'], + + ['PythonDebug', 'boolean', False], + + ['LoginTimeout', 'integer', 72], + + ['Secret', 'string', None], + + ['LibPath', 'string', '/usr/share/koji-web/lib'], + + ['LogLevel', 'string', 'WARNING'], + ['LogFormat', 'string', '%(msecs)d [%(levelname)s] m=%(method)s u=%(user_name)s p=%(process)s r=%(remoteaddr)s %(name)s: %(message)s'], + + ['RLIMIT_AS', 'string', None], + ['RLIMIT_CORE', 'string', None], + ['RLIMIT_CPU', 'string', None], + ['RLIMIT_DATA', 'string', None], + ['RLIMIT_FSIZE', 'string', None], + ['RLIMIT_MEMLOCK', 'string', None], + ['RLIMIT_NOFILE', 'string', None], + ['RLIMIT_NPROC', 'string', None], + ['RLIMIT_OFILE', 'string', None], + ['RLIMIT_RSS', 'string', None], + ['RLIMIT_STACK', 'string', None], + ] + + def load_config(self, environ): + """Load configuration options + + Options are read from a config file. + + 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 + """ + modpy_opts = environ.get('modpy.opts', {}) + if 'modpy.opts' in environ: + cf = modpy_opts.get('koji.web.ConfigFile', None) + else: + cf = environ.get('koji.web.ConfigFile', None) + if not cf: + cf = '/etc/kojiweb/web.conf' + if os.path.isfile(cf): + config = RawConfigParser() + config.read(cf) + elif 'modpy.opts' not in environ: + # we have no config + raise koji.GenericError, "Server configuration not found" + else: + cf = None + self.logger.warn('Warning: configuring Koji via PythonOptions is deprecated. Use web.conf') + opts = {} + for name, dtype, default in self.cfgmap: + if cf: + key = ('web', 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 + opts['Secret'] = koji.util.HiddenValue(opts['Secret']) + self.options = opts + return opts + + def setup_logging2(self, environ): + """Adjust logging based on configuration options""" + opts = self.options + #determine log level + level = opts['LogLevel'] + valid_levels = ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL') + # the config value can be a single level name or a series of + # logger:level names pairs. processed in order found + default = None + for part in level.split(): + pair = part.split(':', 1) + if len(pair) == 2: + name, level = pair + else: + name = 'koji' + level = part + default = level + if level not in valid_levels: + raise koji.GenericError, "Invalid log level: %s" % level + #all our loggers start with koji + if name == '': + name = 'koji' + default = level + elif name.startswith('.'): + name = 'koji' + name + elif not name.startswith('koji'): + name = 'koji.' + name + level_code = logging._levelNames[level] + logging.getLogger(name).setLevel(level_code) + logger = logging.getLogger("koji") + # if KojiDebug is set, force main log level to DEBUG + if opts.get('KojiDebug'): + logger.setLevel(logging.DEBUG) + elif default is None: + #LogLevel did not configure a default level + logger.setLevel(logging.WARNING) + self.formatter = HubFormatter(opts['LogFormat']) + self.formatter.environ = environ + self.log_handler.setFormatter(self.formatter) + + def find_handlers(self): + for name in vars(kojiweb_handlers): + if name.startswith('_'): + continue + try: + val = getattr(kojiweb_handlers, name, None) + if not inspect.isfunction(val): + continue + # err on the side of paranoia + args = inspect.getargspec(val) + if not args[0] or args[0][0] != 'environ': + continue + except: + tb_str = ''.join(traceback.format_exception(*sys.exc_info())) + self.logger.error(tb_str) + self.handler_index[name] = val + + def prep_handler(self, environ): + path_info = environ['PATH_INFO'] + if not path_info: + #empty path info (no trailing slash) breaks our relative urls + environ['koji.redirect'] = environ['REQUEST_URI'] + '/' + raise ServerRedirect + elif path_info == '/': + method = 'index' + else: + method = path_info.lstrip('/').split('/')[0] + environ['koji.method'] = method + self.logger.info("Method: %s", method) + func = self.handler_index.get(method) + if not func: + raise URLNotFound + #parse form args + data = {} + fs = cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ.copy(), keep_blank_values=True) + for field in fs.list: + if field.filename: + val = field + else: + val = field.value + data.setdefault(field.name, []).append(val) + # replace singleton lists with single values + # XXX - this is a bad practice, but for now we strive to emulate mod_python.publisher + for arg in data: + val = data[arg] + if isinstance(val, list) and len(val) == 1: + data[arg] = val[0] + environ['koji.form'] = fs + args, varargs, varkw, defaults = inspect.getargspec(func) + if not varkw: + # remove any unexpected args + data = dslice(data, args, strict=False) + #TODO (warning in header or something?) + return func, data + + + def _setup(self, environ): + global kojiweb_handlers + global kojiweb + options = self.load_config(environ) + if 'LibPath' in options and os.path.exists(options['LibPath']): + sys.path.insert(0, options['LibPath']) + # figure out our location and try to load index.py from same dir + scriptsdir = os.path.dirname(environ['SCRIPT_FILENAME']) + environ['koji.scriptsdir'] = scriptsdir + sys.path.insert(0, scriptsdir) + import index as kojiweb_handlers + import kojiweb + self.find_handlers() + self.setup_logging2(environ) + koji.util.setup_rlimits(options) + # TODO - plugins? + + def setup(self, environ): + try: + self._setup(environ) + except Exception: + self.startup_error = "unknown startup_error" + etype, e = sys.exc_info()[:2] + tb_short = ''.join(traceback.format_exception_only(etype, e)) + self.startup_error = "startup_error: %s" % tb_short + tb_str = ''.join(traceback.format_exception(*sys.exc_info())) + self.logger.error(tb_str) + + def simple_error_page(self, message=None, err=None): + result = ["""\ + +Error + +"""] + if message: + result.append("

%s

\n" % message) + if err: + result.append("

%s

\n" % err) + result.append("\n") + length = sum([len(x) for x in result]) + headers = [ + ('Allow', 'GET, POST, HEAD'), + ('Content-Length', str(length)), + ('Content-Type', 'text/html'), + ] + return result, headers + + def error_page(self, environ, message=None, err=True): + if err: + etype, e = sys.exc_info()[:2] + tb_short = ''.join(traceback.format_exception_only(etype, e)) + tb_long = ''.join(traceback.format_exception(*sys.exc_info())) + if isinstance(e, koji.ServerOffline): + desc = ('Outage', 'outage') + else: + desc = ('Error', 'error') + else: + etype = None + e = None + tb_short = '' + tb_long = '' + desc = ('Error', 'error') + try: + _initValues = kojiweb.util._initValues + _genHTML = kojiweb.util._genHTML + except (NameError, AttributeError): + tb_str = ''.join(traceback.format_exception(*sys.exc_info())) + self.logger.error(tb_str) + #fallback to simple error page + return self.simple_error_page(message, err=tb_short) + values = _initValues(environ, *desc) + values['etype'] = etype + values['exception'] = e + if err: + values['explanation'], values['debug_level'] = kojiweb.util.explainError(e) + if message: + values['explanation'] = message + else: + values['explanation'] = message or "Unknown error" + values['debug_level'] = 0 + values['tb_short'] = tb_short + if int(self.options.get("PythonDebug", 0)): + values['tb_long'] = tb_long + else: + values['tb_long'] = "Full tracebacks disabled" + result = _genHTML(environ, 'error.chtml') + headers = [ + ('Allow', 'GET, POST, HEAD'), + ('Content-Length', str(len(result))), + ('Content-Type', 'text/html'), + ] + return [result], headers + + def handle_request(self, environ, start_response): + if self.startup_error: + status = '200 OK' + result, headers = self.error_page(environ, message=self.startup_error) + start_response(status, headers) + return result + if environ['REQUEST_METHOD'] not in ['GET', 'POST', 'HEAD']: + status = '405 Method Not Allowed' + result, headers = self.error_page(environ, message="Method Not Allowed") + start_response(status, headers) + return result + environ['koji.options'] = self.options + try: + environ['koji.headers'] = [] + func, data = self.prep_handler(environ) + result = func(environ, **data) + status = '200 OK' + except ServerRedirect: + status = '302 Found' + location = environ['koji.redirect'] + result = '

Redirect: here

\n' % location + environ['koji.headers'].append(['Location', location]) + except URLNotFound: + status = "404 Not Found" + msg = "Not found: %s" % environ['REQUEST_URI'] + result, headers = self.error_page(environ, message=msg, err=False) + start_response(status, headers) + return result + except Exception: + tb_str = ''.join(traceback.format_exception(*sys.exc_info())) + self.logger.error(tb_str) + status = '500 Internal Server Error' + result, headers = self.error_page(environ) + start_response(status, headers) + return result + headers = { + 'allow' : ('Allow', 'GET, POST, HEAD'), + } + extra = [] + for name, value in environ.get('koji.headers', []): + key = name.lower() + if key == 'set-cookie': + extra.append((name, value)) + else: + # last one wins + headers[key] = (name, value) + if isinstance(result, basestring): + headers.setdefault('content-length', ('Content-Length', str(len(result)))) + headers.setdefault('content-type', ('Content-Type', 'text/html')) + headers = headers.values() + extra + self.logger.debug("Headers:") + self.logger.debug(koji.util.LazyString(pprint.pformat, [headers])) + start_response(status, headers) + if isinstance(result, basestring): + result = [result] + return result + + def handler(self, req): + """mod_python handler""" + wrapper = WSGIWrapper(req) + return wrapper.run(self.application) + + def application(self, environ, start_response): + """wsgi handler""" + if self.formatter: + self.formatter.environ = environ + if self.firstcall: + self.firstcall = False + self.setup(environ) + try: + result = self.handle_request(environ, start_response) + finally: + if self.formatter: + self.formatter.environ = {} + session = environ.get('koji.session') + if session: + session.logout() + return result + + +class HubFormatter(logging.Formatter): + """Support some koji specific fields in the format string""" + + def format(self, record): + # dispatcher should set environ for us + environ = self.environ + # XXX Can we avoid these data lookups if not needed? + record.method = environ.get('koji.method') + record.remoteaddr = "%s:%s" % ( + environ.get('REMOTE_ADDR', '?'), + environ.get('REMOTE_PORT', '?')) + record.user_name = environ.get('koji.currentLogin') + user = environ.get('koji.currentUser') + if user: + record.user_id = user['id'] + else: + record.user_id = None + session = environ.get('koji.session') + record.session_id = None + if session: + record.callnum = session.callnum + if session.sinfo: + record.session_id = session.sinfo.get('session.id') + else: + record.callnum = None + return logging.Formatter.format(self, record) + + +# provide necessary global handlers for mod_wsgi and mod_python +dispatcher = Dispatcher() +handler = dispatcher.handler +application = dispatcher.application diff --git a/www/lib/kojiweb/handlers.py b/www/lib/kojiweb/handlers.py deleted file mode 100644 index aaaf6b6b..00000000 --- a/www/lib/kojiweb/handlers.py +++ /dev/null @@ -1,7 +0,0 @@ -def cleanup(req): - """Perform any cleanup actions required at the end of a request. - At the moment, this logs out the webserver <-> koji session.""" - if hasattr(req, '_session') and req._session.logged_in: - req._session.logout() - - return 0 diff --git a/www/lib/kojiweb/publisher.py b/www/lib/kojiweb/publisher.py deleted file mode 100644 index 887a278d..00000000 --- a/www/lib/kojiweb/publisher.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/python - -#This is a wrapper around mod_python.publisher so that we can trap some exceptions -import koji -import mod_python.apache -import mod_python.publisher -import sys -import traceback -import util -from util import _initValues -from util import _genHTML - -old_publish_object = mod_python.publisher.publish_object - -def publish_object(req, object): - try: - return old_publish_object(req, object) - #except koji.ServerOffline: - # values = _initValues(req, 'Outage', 'outage') - # return old_publish_object(req, _genHTML(req, 'outage.chtml')) - except mod_python.apache.SERVER_RETURN: - raise - except Exception: - etype, e = sys.exc_info()[:2] - if isinstance(e, koji.ServerOffline): - values = _initValues(req, 'Outage', 'outage') - else: - values = _initValues(req, 'Error', 'error') - values['etype'] = etype - values['exception'] = e - values['explanation'], values['debug_level'] = util.explainError(e) - values['tb_short'] = ''.join(traceback.format_exception_only(etype, e)) - if int(req.get_config().get("PythonDebug", 0)): - values['tb_long'] = ''.join(traceback.format_exception(*sys.exc_info())) - else: - values['tb_long'] = "Full tracebacks disabled" - return old_publish_object(req, _genHTML(req, 'error.chtml')) - -mod_python.publisher.publish_object = publish_object - -def handler(req): - return mod_python.publisher.handler(req) diff --git a/www/lib/kojiweb/util.py b/www/lib/kojiweb/util.py index 8f9c673a..ed2b059e 100644 --- a/www/lib/kojiweb/util.py +++ b/www/lib/kojiweb/util.py @@ -1,3 +1,25 @@ +# utility functions for koji web interface +# +# Copyright (c) 2005-2012 Red Hat +# +# Koji is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; +# version 2.1 of the License. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this software; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# +# Authors: +# Mike Bonnet +# Mike McLean + import Cheetah.Template import datetime import koji @@ -24,20 +46,20 @@ except: themeInfo = {} themeCache = {} -def _initValues(req, title='Build System Info', pageID='summary'): +def _initValues(environ, title='Build System Info', pageID='summary'): global themeInfo global themeCache values = {} - values['siteName'] = req.get_options().get('SiteName', 'Koji') + values['siteName'] = environ['koji.options'].get('SiteName', 'Koji') values['title'] = title values['pageID'] = pageID values['currentDate'] = str(datetime.datetime.now()) themeCache.clear() themeInfo.clear() - themeInfo['name'] = req.get_options().get('KojiTheme', None) - themeInfo['staticdir'] = req.get_options().get('KojiStaticDir', '/usr/share/koji-web/static') + themeInfo['name'] = environ['koji.options'].get('KojiTheme', None) + themeInfo['staticdir'] = environ['koji.options'].get('KojiStaticDir', '/usr/share/koji-web/static') - req._values = values + environ['koji.values'] = values return values @@ -90,32 +112,32 @@ class XHTMLFilter(DecodeUTF8): TEMPLATES = {} -def _genHTML(req, fileName): - reqdir = os.path.dirname(req.filename) +def _genHTML(environ, fileName): + reqdir = os.path.dirname(environ['SCRIPT_FILENAME']) if os.getcwd() != reqdir: os.chdir(reqdir) - if hasattr(req, 'currentUser'): - req._values['currentUser'] = req.currentUser + if 'koji.currentUser' in environ: + environ['koji.values']['currentUser'] = environ['koji.currentUser'] else: - req._values['currentUser'] = None - req._values['authToken'] = _genToken(req) - if not req._values.has_key('mavenEnabled'): - if hasattr(req, '_session'): - req._values['mavenEnabled'] = req._session.mavenEnabled() + environ['koji.values']['currentUser'] = None + environ['koji.values']['authToken'] = _genToken(environ) + if not environ['koji.values'].has_key('mavenEnabled'): + if 'koji.session' in environ: + environ['koji.values']['mavenEnabled'] = environ['koji.session'].mavenEnabled() else: - req._values['mavenEnabled'] = False - if not req._values.has_key('winEnabled'): - if hasattr(req, '_session'): - req._values['winEnabled'] = req._session.winEnabled() + environ['koji.values']['mavenEnabled'] = False + if not environ['koji.values'].has_key('winEnabled'): + if 'koji.session' in environ: + environ['koji.values']['winEnabled'] = environ['koji.session'].winEnabled() else: - req._values['winEnabled'] = False + environ['koji.values']['winEnabled'] = False tmpl_class = TEMPLATES.get(fileName) if not tmpl_class: tmpl_class = Cheetah.Template.Template.compile(file=fileName) TEMPLATES[fileName] = tmpl_class - tmpl_inst = tmpl_class(namespaces=[req._values], filter=XHTMLFilter) + tmpl_inst = tmpl_class(namespaces=[environ['koji.values']], filter=XHTMLFilter) return tmpl_inst.respond().encode('utf-8', 'replace') def _truncTime(): @@ -123,21 +145,21 @@ def _truncTime(): # truncate to the nearest 15 minutes return now.replace(minute=(now.minute / 15 * 15), second=0, microsecond=0) -def _genToken(req, tstamp=None): - if hasattr(req, 'currentLogin') and req.currentLogin: - user = req.currentLogin +def _genToken(environ, tstamp=None): + if 'koji.currentLogin' in environ and environ['koji.currentLogin']: + user = environ['koji.currentLogin'] else: return '' if tstamp == None: tstamp = _truncTime() - return md5_constructor(user + str(tstamp) + req.get_options()['Secret']).hexdigest()[-8:] + return md5_constructor(user + str(tstamp) + environ['koji.options']['Secret'].value).hexdigest()[-8:] -def _getValidTokens(req): +def _getValidTokens(environ): tokens = [] now = _truncTime() for delta in (0, 15, 30): token_time = now - datetime.timedelta(minutes=delta) - token = _genToken(req, token_time) + token = _genToken(environ, token_time) if token: tokens.append(token) return tokens