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
This commit is contained in:
parent
0a57b22886
commit
54c0ed8438
16 changed files with 1392 additions and 625 deletions
|
|
@ -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
|
||||
|
||||
<Directory /usr/share/koji-hub>
|
||||
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
|
||||
<Directory "/usr/share/koji-hub">
|
||||
Options ExecCGI
|
||||
SetHandler wsgi-script
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</Directory>
|
||||
|
||||
# Support for mod_python is DEPRECATED. If you still need mod_python support,
|
||||
# then use the following directory settings instead:
|
||||
#
|
||||
# <Directory "/usr/share/koji-hub">
|
||||
# SetHandler mod_python
|
||||
# PythonHandler kojixmlrpc
|
||||
# PythonOption ConfigFile /etc/koji-hub/hub.conf
|
||||
# PythonDebug Off
|
||||
# PythonAutoReload Off
|
||||
# </Directory>
|
||||
|
||||
# Also serve /mnt/koji
|
||||
Alias /kojifiles "/mnt/koji/"
|
||||
|
||||
<Directory "/mnt/koji">
|
||||
Options Indexes
|
||||
AllowOverride None
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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("<pre>" + strstream.getvalue() + "</pre>")
|
||||
_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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
37
koji/auth.py
37
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())
|
||||
|
||||
|
|
|
|||
182
koji/server.py
Normal file
182
koji/server.py
Normal file
|
|
@ -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 <mikem@redhat.com>
|
||||
|
||||
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()
|
||||
|
||||
47
koji/util.py
47
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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
<Directory "/usr/share/koji-web/scripts/">
|
||||
# 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
|
||||
</Directory>
|
||||
|
||||
# Support for mod_python is DEPRECATED. If you still need mod_python support,
|
||||
# then use the following directory settings instead:
|
||||
#
|
||||
# <Directory "/usr/share/koji-web/scripts/">
|
||||
# # 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
|
||||
# </Directory>
|
||||
|
||||
# uncomment this to enable authentication via Kerberos
|
||||
# <Location /koji/login>
|
||||
# AuthType Kerberos
|
||||
|
|
|
|||
24
www/conf/web.conf
Normal file
24
www/conf/web.conf
Normal file
|
|
@ -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
|
||||
File diff suppressed because it is too large
Load diff
461
www/kojiweb/wsgi_publisher.py
Normal file
461
www/kojiweb/wsgi_publisher.py
Normal file
|
|
@ -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 <mikem@redhat.com>
|
||||
|
||||
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 = ["""\
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html><head><title>Error</title></head>
|
||||
<body>
|
||||
"""]
|
||||
if message:
|
||||
result.append("<p>%s</p>\n" % message)
|
||||
if err:
|
||||
result.append("<p>%s</p>\n" % err)
|
||||
result.append("</body></html>\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 = '<p>Redirect: <a href="%s">here</a></p>\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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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 <mikeb@redhat.com>
|
||||
# Mike McLean <mikem@redhat.com>
|
||||
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue