In the case that both WebPrincipal and WebCert are set, reverse the order of checking them so that WebCert is used by default.
2651 lines
90 KiB
Python
2651 lines
90 KiB
Python
# core web interface handlers for koji
|
|
#
|
|
# Copyright (c) 2005-2014 Red Hat, Inc.
|
|
#
|
|
# 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 datetime
|
|
import hashlib
|
|
import http.cookies
|
|
import logging
|
|
import mimetypes
|
|
import os
|
|
import os.path
|
|
import re
|
|
import sys
|
|
import time
|
|
import itertools
|
|
|
|
import koji
|
|
import kojiweb.util
|
|
from koji.server import ServerRedirect
|
|
from kojiweb.util import _genHTML, _getValidTokens, _initValues
|
|
|
|
|
|
# Convenience definition of a commonly-used sort function
|
|
def _sortbyname(x):
|
|
return x['name']
|
|
|
|
|
|
# regexps for input checking
|
|
_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) + ']+$')
|
|
|
|
_VALID_ARCH_RE = re.compile(r'^[\w-]+$', re.ASCII)
|
|
|
|
|
|
def _validate_arch(arch):
|
|
# archs (ASCII alnum + _ + -)
|
|
if not arch:
|
|
return None
|
|
elif _VALID_ARCH_RE.match(arch):
|
|
return arch
|
|
else:
|
|
raise koji.GenericError("No such arch: %r" % arch)
|
|
|
|
|
|
def _validate_name_or_id(value):
|
|
# integer ID or label, it is unicode alnum + search symbols (reasonable expectation?)
|
|
if value.isdigit():
|
|
return int(value)
|
|
elif _VALID_SEARCH_RE.match(value):
|
|
return value
|
|
else:
|
|
raise koji.GenericError("Invalid int/label value: %r" % value)
|
|
|
|
|
|
# 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
|
|
value = user + ':' + str(int(time.time()))
|
|
if not options['Secret'].value:
|
|
raise koji.AuthError('Unable to authenticate, server secret not configured')
|
|
digest_string = value + options['Secret'].value
|
|
digest_string = digest_string.encode('utf-8')
|
|
shasum = hashlib.sha256(digest_string)
|
|
value = "%s:%s" % (shasum.hexdigest(), value)
|
|
cookies = http.cookies.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(environ):
|
|
cookies = http.cookies.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(environ):
|
|
options = environ['koji.options']
|
|
cookies = http.cookies.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.warning('malformed user cookie: %s' % value)
|
|
return None
|
|
sig, value = parts
|
|
if not options['Secret'].value:
|
|
raise koji.AuthError('Unable to authenticate, server secret not configured')
|
|
digest_string = value + options['Secret'].value
|
|
digest_string = digest_string.encode('utf-8')
|
|
shasum = hashlib.sha256(digest_string)
|
|
if shasum.hexdigest() != sig:
|
|
authlogger.warning('invalid user cookie: %s:%s', sig, value)
|
|
return None
|
|
parts = value.split(":", 1)
|
|
if len(parts) != 2:
|
|
authlogger.warning('invalid signed user cookie: %s:%s', sig, value)
|
|
# no embedded timestamp
|
|
return None
|
|
user, timestamp = parts
|
|
try:
|
|
timestamp = float(timestamp)
|
|
except ValueError:
|
|
authlogger.warning('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
|
|
|
|
|
|
def _gssapiLogin(environ, session, principal):
|
|
options = environ['koji.options']
|
|
wprinc = options['WebPrincipal']
|
|
keytab = options['WebKeytab']
|
|
ccache = options['WebCCache']
|
|
return session.gssapi_login(principal=wprinc, keytab=keytab,
|
|
ccache=ccache, proxyuser=principal)
|
|
|
|
|
|
def _sslLogin(environ, session, username):
|
|
options = environ['koji.options']
|
|
client_cert = options['WebCert']
|
|
server_ca = options['KojiHubCA']
|
|
|
|
return session.ssl_login(client_cert, None, server_ca,
|
|
proxyuser=username)
|
|
|
|
|
|
def _assertLogin(environ):
|
|
session = environ['koji.session']
|
|
options = environ['koji.options']
|
|
if 'koji.currentLogin' not in environ or 'koji.currentUser' not in environ:
|
|
raise Exception('_getServer() must be called before _assertLogin()')
|
|
elif environ['koji.currentLogin'] and environ['koji.currentUser']:
|
|
if options['WebCert']:
|
|
if not _sslLogin(environ, session, environ['koji.currentLogin']):
|
|
raise koji.AuthError('could not login %s via SSL' % environ['koji.currentLogin'])
|
|
elif options['WebPrincipal']:
|
|
if not _gssapiLogin(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 = environ['koji.form'].getfirst('a', '')
|
|
validTokens = _getValidTokens(environ)
|
|
if authToken and authToken in validTokens:
|
|
# we have a token and it's valid
|
|
pass
|
|
else:
|
|
# 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(environ, page=None,
|
|
forceSSL=(_getBaseURL(environ).startswith('https://')))
|
|
assert False # pragma: no cover
|
|
else:
|
|
_redirect(environ, 'login')
|
|
assert False # pragma: no cover
|
|
|
|
|
|
def _getServer(environ):
|
|
options = environ['koji.options']
|
|
opts = {}
|
|
if os.path.exists(options['KojiHubCA']):
|
|
opts['serverca'] = options['KojiHubCA']
|
|
|
|
session = koji.ClientSession(options['KojiHubURL'], opts=opts)
|
|
|
|
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:
|
|
environ['koji.currentUser'] = None
|
|
|
|
environ['koji.session'] = session
|
|
return session
|
|
|
|
|
|
def _construct_url(environ, page):
|
|
port = environ['SERVER_PORT']
|
|
host = environ['SERVER_NAME']
|
|
url_scheme = environ['wsgi.url_scheme']
|
|
if (url_scheme == 'https' and port == '443') or \
|
|
(url_scheme == 'http' and port == '80'):
|
|
return "%s://%s%s" % (url_scheme, host, page)
|
|
return "%s://%s:%s%s" % (url_scheme, host, port, page)
|
|
|
|
|
|
def _getBaseURL(environ):
|
|
base = environ['SCRIPT_NAME']
|
|
return _construct_url(environ, base)
|
|
|
|
|
|
def _redirect(environ, location):
|
|
environ['koji.redirect'] = location
|
|
raise ServerRedirect
|
|
|
|
|
|
def _redirectBack(environ, page, forceSSL):
|
|
localurl = '%s://%s' % (environ['REQUEST_SCHEME'], environ['SERVER_NAME'])
|
|
if page:
|
|
# We'll work with the page we were given
|
|
pass
|
|
elif environ.get('HTTP_REFERER', '').startswith(localurl):
|
|
page = environ['HTTP_REFERER']
|
|
else:
|
|
page = 'index'
|
|
|
|
# Modify the scheme if necessary
|
|
if page.startswith('http'):
|
|
pass
|
|
elif page.startswith('/'):
|
|
page = _construct_url(environ, page)
|
|
else:
|
|
page = _getBaseURL(environ) + '/' + page
|
|
if forceSSL:
|
|
page = page.replace('http:', 'https:')
|
|
else:
|
|
page = page.replace('https:', 'http:')
|
|
|
|
# and redirect to the page
|
|
_redirect(environ, page)
|
|
|
|
|
|
def login(environ, page=None):
|
|
session = _getServer(environ)
|
|
options = environ['koji.options']
|
|
|
|
# If 'WebAuth' is not set, then default it to
|
|
# match the method of authenticating to the hub.
|
|
# This matches the original behaviour
|
|
webauth = options['WebAuth']
|
|
if not webauth:
|
|
if options['WebPrincipal']:
|
|
webauth = 'kerberos'
|
|
if options['WebCert']:
|
|
webauth = 'ssl'
|
|
|
|
if not webauth:
|
|
raise koji.AuthError(
|
|
'KojiWeb is incorrectly configured for authentication, contact the system '
|
|
'administrator')
|
|
|
|
if webauth == 'ssl':
|
|
## Clients authenticate to KojiWeb by SSL, so extract
|
|
## the username via the (verified) client certificate
|
|
if environ['wsgi.url_scheme'] != 'https':
|
|
dest = 'login'
|
|
if page:
|
|
dest = dest + '?page=' + page
|
|
_redirectBack(environ, dest, forceSSL=True)
|
|
return
|
|
|
|
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 = environ.get('SSL_CLIENT_S_DN_CN')
|
|
if not username:
|
|
raise koji.AuthError('unable to get user information from client certificate')
|
|
|
|
elif webauth == 'kerberos':
|
|
## Clients authenticate to KojiWeb by Kerberos, so extract
|
|
## the username via the REMOTE_USER which will be the
|
|
## Kerberos principal
|
|
principal = environ.get('REMOTE_USER')
|
|
if not principal:
|
|
raise koji.AuthError(
|
|
'configuration error: mod_auth_gssapi should have performed authentication before '
|
|
'presenting this page')
|
|
|
|
username = principal
|
|
else:
|
|
## It is still possible to get here if someone explicitly
|
|
## set WebAuth to an incorrect value in the configuration file
|
|
raise koji.AuthError(
|
|
'KojiWeb is incorrectly configured for authentication, contact the system '
|
|
'administrator')
|
|
|
|
## This now is how we proxy the user to the hub
|
|
if options['WebCert']:
|
|
## The username might be a principal user@REALM. Remove
|
|
## any @REALM part here.
|
|
username = username.split('@', 1)[0]
|
|
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['WebPrincipal']:
|
|
if not _gssapiLogin(environ, session, username):
|
|
raise koji.AuthError('could not login using principal: %s' % username)
|
|
|
|
authlogger.info('Successful Kerberos authentication by %s', username)
|
|
else:
|
|
raise koji.AuthError(
|
|
'KojiWeb is incorrectly configured for authentication, contact the system '
|
|
'administrator')
|
|
|
|
_setUserCookie(environ, username)
|
|
# To protect the session cookie, we must forceSSL
|
|
_redirectBack(environ, page, forceSSL=True)
|
|
|
|
|
|
def logout(environ, page=None):
|
|
user = _getUserCookie(environ)
|
|
_clearUserCookie(environ)
|
|
if user:
|
|
authlogger.info('Logout by %s', user)
|
|
|
|
_redirectBack(environ, page, forceSSL=False)
|
|
|
|
|
|
def index(environ, packageOrder='package_name', packageStart=None):
|
|
values = _initValues(environ)
|
|
server = _getServer(environ)
|
|
|
|
opts = environ['koji.options']
|
|
user = environ['koji.currentUser']
|
|
|
|
values['builds'] = server.listBuilds(
|
|
userID=(user and user['id'] or None),
|
|
queryOpts={'order': '-build_id', 'limit': 10}
|
|
)
|
|
|
|
taskOpts = {'parent': None, 'decode': True}
|
|
if user:
|
|
taskOpts['owner'] = user['id']
|
|
if opts.get('HiddenUsers'):
|
|
taskOpts['not_owner'] = [
|
|
int(userid) for userid in opts['HiddenUsers'].split()
|
|
]
|
|
values['tasks'] = server.listTasks(
|
|
opts=taskOpts,
|
|
queryOpts={'order': '-id', 'limit': 10}
|
|
)
|
|
|
|
values['order'] = '-id'
|
|
|
|
if user:
|
|
kojiweb.util.paginateResults(server, values, 'listPackages',
|
|
kw={'userID': user['id'], 'with_dups': True},
|
|
start=packageStart, dataName='packages', prefix='package',
|
|
order=packageOrder, pageSize=10)
|
|
|
|
notifs = server.getBuildNotifications(user['id'])
|
|
notifs.sort(key=lambda x: x['id'])
|
|
# XXX Make this a multicall
|
|
for notif in notifs:
|
|
notif['package'] = None
|
|
if notif['package_id']:
|
|
notif['package'] = server.getPackage(notif['package_id'])
|
|
|
|
notif['tag'] = None
|
|
if notif['tag_id']:
|
|
notif['tag'] = server.getTag(notif['tag_id'])
|
|
values['notifs'] = notifs
|
|
|
|
values['user'] = user
|
|
values['welcomeMessage'] = environ['koji.options']['KojiGreeting']
|
|
|
|
return _genHTML(environ, 'index.chtml')
|
|
|
|
|
|
def notificationedit(environ, notificationID):
|
|
server = _getServer(environ)
|
|
_assertLogin(environ)
|
|
|
|
notificationID = int(notificationID)
|
|
notification = server.getBuildNotification(notificationID)
|
|
if notification is None:
|
|
raise koji.GenericError('no notification with ID: %i' % notificationID)
|
|
|
|
form = environ['koji.form']
|
|
|
|
if 'save' in form:
|
|
package_id = form.getfirst('package')
|
|
if package_id == 'all':
|
|
package_id = None
|
|
else:
|
|
package_id = int(package_id)
|
|
|
|
tag_id = form.getfirst('tag')
|
|
if tag_id == 'all':
|
|
tag_id = None
|
|
else:
|
|
tag_id = int(tag_id)
|
|
|
|
if 'success_only' in form:
|
|
success_only = True
|
|
else:
|
|
success_only = False
|
|
|
|
server.updateNotification(notification['id'], package_id, tag_id, success_only)
|
|
|
|
_redirect(environ, 'index')
|
|
elif 'cancel' in form:
|
|
_redirect(environ, 'index')
|
|
else:
|
|
values = _initValues(environ, 'Edit Notification')
|
|
|
|
values['notif'] = notification
|
|
packages = server.listPackagesSimple(queryOpts={'order': 'package_name'})
|
|
values['packages'] = packages
|
|
tags = server.listTags(queryOpts={'order': 'name'})
|
|
values['tags'] = tags
|
|
|
|
return _genHTML(environ, 'notificationedit.chtml')
|
|
|
|
|
|
def notificationcreate(environ):
|
|
server = _getServer(environ)
|
|
_assertLogin(environ)
|
|
|
|
form = environ['koji.form']
|
|
|
|
if 'add' in form:
|
|
user = environ['koji.currentUser']
|
|
if not user:
|
|
raise koji.GenericError('not logged-in')
|
|
|
|
package_id = form.getfirst('package')
|
|
if package_id == 'all':
|
|
package_id = None
|
|
else:
|
|
package_id = int(package_id)
|
|
|
|
tag_id = form.getfirst('tag')
|
|
if tag_id == 'all':
|
|
tag_id = None
|
|
else:
|
|
tag_id = int(tag_id)
|
|
|
|
if 'success_only' in form:
|
|
success_only = True
|
|
else:
|
|
success_only = False
|
|
|
|
server.createNotification(user['id'], package_id, tag_id, success_only)
|
|
|
|
_redirect(environ, 'index')
|
|
elif 'cancel' in form:
|
|
_redirect(environ, 'index')
|
|
else:
|
|
values = _initValues(environ, 'Edit Notification')
|
|
|
|
values['notif'] = None
|
|
packages = server.listPackagesSimple(queryOpts={'order': 'package_name'})
|
|
values['packages'] = packages
|
|
tags = server.listTags(queryOpts={'order': 'name'})
|
|
values['tags'] = tags
|
|
|
|
return _genHTML(environ, 'notificationedit.chtml')
|
|
|
|
|
|
def notificationdelete(environ, notificationID):
|
|
server = _getServer(environ)
|
|
_assertLogin(environ)
|
|
|
|
notificationID = int(notificationID)
|
|
notification = server.getBuildNotification(notificationID)
|
|
if not notification:
|
|
raise koji.GenericError('no notification with ID: %i' % notificationID)
|
|
|
|
server.deleteNotification(notification['id'])
|
|
|
|
_redirect(environ, 'index')
|
|
|
|
|
|
# All Tasks
|
|
_TASKS = ['build',
|
|
'buildSRPMFromSCM',
|
|
'rebuildSRPM',
|
|
'buildArch',
|
|
'chainbuild',
|
|
'maven',
|
|
'buildMaven',
|
|
'chainmaven',
|
|
'wrapperRPM',
|
|
'winbuild',
|
|
'vmExec',
|
|
'waitrepo',
|
|
'tagBuild',
|
|
'newRepo',
|
|
'createrepo',
|
|
'distRepo',
|
|
'createdistrepo',
|
|
'buildNotification',
|
|
'tagNotification',
|
|
'dependantTask',
|
|
'livecd',
|
|
'createLiveCD',
|
|
'appliance',
|
|
'createAppliance',
|
|
'image',
|
|
'indirectionimage',
|
|
'createImage',
|
|
'livemedia',
|
|
'createLiveMedia']
|
|
# Tasks that can exist without a parent
|
|
_TOPLEVEL_TASKS = ['build', 'buildNotification', 'chainbuild', 'maven', 'chainmaven', 'wrapperRPM',
|
|
'winbuild', 'newRepo', 'distRepo', 'tagBuild', 'tagNotification', 'waitrepo',
|
|
'livecd', 'appliance', 'image', 'livemedia']
|
|
# Tasks that can have children
|
|
_PARENT_TASKS = ['build', 'chainbuild', 'maven', 'chainmaven', 'winbuild', 'newRepo', 'distRepo',
|
|
'wrapperRPM', 'livecd', 'appliance', 'image', 'livemedia']
|
|
|
|
|
|
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)
|
|
|
|
if view not in ('tree', 'toplevel', 'flat'):
|
|
raise koji.GenericError("Invalid value for view: %r" % view)
|
|
|
|
opts = {'decode': True}
|
|
if owner:
|
|
owner = _validate_name_or_id(owner)
|
|
ownerObj = server.getUser(owner, strict=True)
|
|
opts['owner'] = ownerObj['id']
|
|
values['owner'] = ownerObj['name']
|
|
values['ownerObj'] = ownerObj
|
|
else:
|
|
values['owner'] = None
|
|
values['ownerObj'] = None
|
|
|
|
values['users'] = server.listUsers(queryOpts={'order': 'name'})
|
|
|
|
if method in _TASKS + environ['koji.options']['Tasks']:
|
|
opts['method'] = method
|
|
else:
|
|
method = 'all'
|
|
values['method'] = method
|
|
values['alltasks'] = sorted(_TASKS + environ['koji.options']['Tasks'])
|
|
|
|
treeEnabled = True
|
|
if hostID or (method not in ['all'] + _PARENT_TASKS + environ['koji.options']['ParentTasks']):
|
|
# force flat view if we're filtering by a hostID or a task that never has children
|
|
if view == 'tree':
|
|
view = 'flat'
|
|
# don't let them choose tree view either
|
|
treeEnabled = False
|
|
values['treeEnabled'] = treeEnabled
|
|
|
|
toplevelEnabled = True
|
|
if method not in ['all'] + _TOPLEVEL_TASKS + environ['koji.options']['ToplevelTasks']:
|
|
# force flat view if we're viewing a task that is never a top-level task
|
|
if view == 'toplevel':
|
|
view = 'flat'
|
|
toplevelEnabled = False
|
|
values['toplevelEnabled'] = toplevelEnabled
|
|
|
|
values['view'] = view
|
|
|
|
if view == 'tree':
|
|
treeDisplay = True
|
|
else:
|
|
treeDisplay = False
|
|
values['treeDisplay'] = treeDisplay
|
|
|
|
if view in ('tree', 'toplevel'):
|
|
opts['parent'] = None
|
|
|
|
if state == 'active':
|
|
opts['state'] = [koji.TASK_STATES['FREE'],
|
|
koji.TASK_STATES['OPEN'],
|
|
koji.TASK_STATES['ASSIGNED']]
|
|
elif state == 'all':
|
|
pass
|
|
else:
|
|
# Assume they've passed in a state name
|
|
opts['state'] = [koji.TASK_STATES[state.upper()]]
|
|
values['state'] = state
|
|
|
|
if hostID:
|
|
hostID = int(hostID)
|
|
host = server.getHost(hostID, strict=True)
|
|
opts['host_id'] = host['id']
|
|
values['host'] = host
|
|
values['hostID'] = host['id']
|
|
else:
|
|
values['host'] = None
|
|
values['hostID'] = None
|
|
|
|
if channelID:
|
|
channelID = _validate_name_or_id(channelID)
|
|
channel = server.getChannel(channelID, strict=True)
|
|
opts['channel_id'] = channel['id']
|
|
values['channel'] = channel
|
|
values['channelID'] = channel['id']
|
|
else:
|
|
values['channel'] = None
|
|
values['channelID'] = None
|
|
|
|
loggedInUser = environ['koji.currentUser']
|
|
values['loggedInUser'] = loggedInUser
|
|
|
|
values['order'] = order
|
|
|
|
tasks = kojiweb.util.paginateMethod(server, values, 'listTasks', kw={'opts': opts},
|
|
start=start, dataName='tasks', prefix='task',
|
|
order=order, first_page_count=False)
|
|
|
|
if view == 'tree':
|
|
server.multicall = True
|
|
for task in tasks:
|
|
server.getTaskDescendents(task['id'], request=True)
|
|
descendentList = server.multiCall()
|
|
for task, [descendents] in zip(tasks, descendentList):
|
|
task['descendents'] = descendents
|
|
|
|
return _genHTML(environ, 'tasks.chtml')
|
|
|
|
|
|
def taskinfo(environ, taskID):
|
|
server = _getServer(environ)
|
|
values = _initValues(environ, 'Task Info', 'tasks')
|
|
|
|
taskID = int(taskID)
|
|
task = server.getTaskInfo(taskID, request=True)
|
|
if not task:
|
|
raise koji.GenericError('No such task ID: %s' % taskID)
|
|
|
|
values['title'] = koji.taskLabel(task) + ' | Task Info'
|
|
|
|
values['task'] = task
|
|
params = task['request']
|
|
values['params'] = params
|
|
|
|
if task['channel_id']:
|
|
channel = server.getChannel(task['channel_id'])
|
|
values['channelName'] = channel['name']
|
|
else:
|
|
values['channelName'] = None
|
|
if task['host_id']:
|
|
host = server.getHost(task['host_id'])
|
|
values['hostName'] = host['name']
|
|
else:
|
|
values['hostName'] = None
|
|
if task['owner']:
|
|
owner = server.getUser(task['owner'])
|
|
values['owner'] = owner
|
|
else:
|
|
values['owner'] = None
|
|
if task['parent']:
|
|
parent = server.getTaskInfo(task['parent'], request=True)
|
|
values['parent'] = parent
|
|
else:
|
|
values['parent'] = None
|
|
|
|
descendents = server.getTaskDescendents(task['id'], request=True)
|
|
values['descendents'] = descendents
|
|
|
|
builds = server.listBuilds(taskID=task['id'])
|
|
if builds:
|
|
taskBuild = builds[0]
|
|
else:
|
|
taskBuild = None
|
|
values['taskBuild'] = taskBuild
|
|
|
|
values['estCompletion'] = None
|
|
if taskBuild and taskBuild['state'] == koji.BUILD_STATES['BUILDING']:
|
|
avgDuration = server.getAverageBuildDuration(taskBuild['package_id'])
|
|
if avgDuration is not None:
|
|
avgDelta = datetime.timedelta(seconds=avgDuration)
|
|
startTime = datetime.datetime.fromtimestamp(taskBuild['creation_ts'])
|
|
values['estCompletion'] = startTime + avgDelta
|
|
|
|
buildroots = server.listBuildroots(taskID=task['id'])
|
|
values['buildroots'] = buildroots
|
|
|
|
if task['method'] in ('buildArch', 'buildMaven', 'buildSRPMFromSCM'):
|
|
if len(params) > 1:
|
|
tag_id = params[1]
|
|
if isinstance(tag_id, dict):
|
|
tag_id = tag_id.get('id')
|
|
try:
|
|
values['buildTag'] = server.getTag(tag_id, strict=True)
|
|
except koji.GenericError:
|
|
values['buildTag'] = {'name': "%d (deleted)" % tag_id, 'id': None}
|
|
elif task['method'] == 'tagBuild':
|
|
destTag = server.getTag(params[0])
|
|
build = server.getBuild(params[1])
|
|
values['destTag'] = destTag
|
|
values['build'] = build
|
|
elif task['method'] in ('newRepo', 'distRepo', 'createdistrepo'):
|
|
tag = server.getTag(params[0])
|
|
values['tag'] = tag
|
|
elif task['method'] == 'tagNotification':
|
|
destTag = None
|
|
if params[2]:
|
|
destTag = server.getTag(params[2])
|
|
srcTag = None
|
|
if params[3]:
|
|
srcTag = server.getTag(params[3])
|
|
build = server.getBuild(params[4])
|
|
user = server.getUser(params[5])
|
|
values['destTag'] = destTag
|
|
values['srcTag'] = srcTag
|
|
values['build'] = build
|
|
values['user'] = user
|
|
elif task['method'] == 'dependantTask':
|
|
deps = [server.getTaskInfo(depID, request=True) for depID in params[0]]
|
|
values['deps'] = deps
|
|
elif task['method'] == 'wrapperRPM':
|
|
buildTarget = params[1]
|
|
values['buildTarget'] = buildTarget
|
|
if params[3]:
|
|
wrapTask = server.getTaskInfo(params[3]['id'], request=True)
|
|
values['wrapTask'] = wrapTask
|
|
elif task['method'] == 'restartVerify':
|
|
values['rtask'] = server.getTaskInfo(params[0], request=True)
|
|
|
|
values['taskBuilds'] = []
|
|
if task['state'] in (koji.TASK_STATES['CLOSED'], koji.TASK_STATES['FAILED']):
|
|
try:
|
|
result = server.getTaskResult(task['id'])
|
|
except Exception:
|
|
excClass, exc = sys.exc_info()[:2]
|
|
values['result'] = exc
|
|
values['excClass'] = excClass
|
|
if not values.get('result'):
|
|
values['result'] = result
|
|
values['excClass'] = None
|
|
if task['method'] == 'buildContainer' and 'koji_builds' in result:
|
|
values['taskBuilds'] = [
|
|
server.getBuild(int(buildID)) for buildID in result['koji_builds']]
|
|
else:
|
|
values['result'] = None
|
|
values['excClass'] = None
|
|
|
|
full_result_text, abbr_result_text = kojiweb.util.task_result_to_html(
|
|
values['result'], values['excClass'], abbr_postscript='...')
|
|
values['full_result_text'] = full_result_text
|
|
values['abbr_result_text'] = abbr_result_text
|
|
|
|
topurl = environ['koji.options']['KojiFilesURL']
|
|
pathinfo = koji.PathInfo(topdir=topurl)
|
|
values['pathinfo'] = pathinfo
|
|
|
|
paths = [] # (volume, relpath) tuples
|
|
for relname, volumes in server.listTaskOutput(task['id'], all_volumes=True).items():
|
|
paths += [(volume, relname) for volume in volumes]
|
|
values['output'] = sorted(paths, key=_sortByExtAndName)
|
|
if environ['koji.currentUser']:
|
|
values['perms'] = server.getUserPerms(environ['koji.currentUser']['id'])
|
|
else:
|
|
values['perms'] = []
|
|
|
|
try:
|
|
values['params_parsed'] = _genHTML(environ, 'taskinfo_params.chtml')
|
|
except Exception:
|
|
values['params_parsed'] = None
|
|
return _genHTML(environ, 'taskinfo.chtml')
|
|
|
|
|
|
def taskstatus(environ, taskID):
|
|
server = _getServer(environ)
|
|
|
|
taskID = int(taskID)
|
|
task = server.getTaskInfo(taskID)
|
|
if not task:
|
|
return ''
|
|
files = server.listTaskOutput(taskID, stat=True, all_volumes=True)
|
|
output = '%i:%s\n' % (task['id'], koji.TASK_STATES[task['state']])
|
|
for filename, volumes_data in files.items():
|
|
for volume, file_stats in volumes_data.items():
|
|
output += '%s:%s:%s\n' % (volume, filename, file_stats['st_size'])
|
|
return output
|
|
|
|
|
|
def resubmittask(environ, taskID):
|
|
server = _getServer(environ)
|
|
_assertLogin(environ)
|
|
|
|
taskID = int(taskID)
|
|
newTaskID = server.resubmitTask(taskID)
|
|
_redirect(environ, 'taskinfo?taskID=%i' % newTaskID)
|
|
|
|
|
|
def canceltask(environ, taskID):
|
|
server = _getServer(environ)
|
|
_assertLogin(environ)
|
|
|
|
taskID = int(taskID)
|
|
server.cancelTask(taskID)
|
|
_redirect(environ, 'taskinfo?taskID=%i' % taskID)
|
|
|
|
|
|
def _sortByExtAndName(item):
|
|
"""Sort filename tuples key function, first by extension, and then by name."""
|
|
kRoot, kExt = os.path.splitext(os.path.basename(item[1]))
|
|
return (kExt, kRoot)
|
|
|
|
|
|
def getfile(environ, taskID, name, volume='DEFAULT', offset=None, size=None):
|
|
server = _getServer(environ)
|
|
taskID = int(taskID)
|
|
|
|
output = server.listTaskOutput(taskID, stat=True, all_volumes=True)
|
|
try:
|
|
file_info = output[name][volume]
|
|
except KeyError:
|
|
raise koji.GenericError('no file "%s" output by task %i' % (name, taskID))
|
|
|
|
mime_guess = mimetypes.guess_type(name, strict=False)[0]
|
|
if mime_guess:
|
|
ctype = mime_guess
|
|
else:
|
|
if name.endswith('.log') or name.endswith('.ks'):
|
|
ctype = 'text/plain'
|
|
else:
|
|
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:
|
|
offset = 0
|
|
else:
|
|
offset = int(offset)
|
|
if size is None:
|
|
size = file_size
|
|
else:
|
|
size = int(size)
|
|
if size < 0:
|
|
size = file_size
|
|
if offset < 0:
|
|
# seeking relative to the end of the file
|
|
if offset < -file_size:
|
|
offset = -file_size
|
|
if size > -offset:
|
|
size = -offset
|
|
else:
|
|
if size > (file_size - offset):
|
|
size = file_size - offset
|
|
|
|
# environ['koji.headers'].append(['Content-Length', str(size)])
|
|
return _chunk_file(server, environ, taskID, name, offset, size, volume)
|
|
|
|
|
|
def _chunk_file(server, environ, taskID, name, offset, size, volume):
|
|
remaining = size
|
|
while True:
|
|
if remaining <= 0:
|
|
break
|
|
chunk_size = 1048576
|
|
if remaining < chunk_size:
|
|
chunk_size = remaining
|
|
content = server.downloadTaskOutput(taskID, name,
|
|
offset=offset, size=chunk_size, volume=volume)
|
|
if not content:
|
|
break
|
|
yield content
|
|
content_length = len(content)
|
|
offset += content_length
|
|
remaining -= content_length
|
|
|
|
|
|
def tags(environ, start=None, order=None, childID=None):
|
|
values = _initValues(environ, 'Tags', 'tags')
|
|
server = _getServer(environ)
|
|
|
|
if order is None:
|
|
order = 'name'
|
|
values['order'] = order
|
|
|
|
kojiweb.util.paginateMethod(server, values, 'listTags', kw=None,
|
|
start=start, dataName='tags', prefix='tag', order=order)
|
|
|
|
if environ['koji.currentUser']:
|
|
values['perms'] = server.getUserPerms(environ['koji.currentUser']['id'])
|
|
else:
|
|
values['perms'] = []
|
|
|
|
if childID is None:
|
|
values['childID'] = None
|
|
else:
|
|
values['childID'] = int(childID)
|
|
|
|
return _genHTML(environ, 'tags.chtml')
|
|
|
|
|
|
_PREFIX_CHARS = [chr(char) for char in list(range(48, 58)) + list(range(97, 123))]
|
|
|
|
|
|
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 is not None:
|
|
tagID = _validate_name_or_id(tagID)
|
|
tag = server.getTag(tagID, strict=True)
|
|
values['tagID'] = tagID
|
|
values['tag'] = tag
|
|
user = None
|
|
if userID is not None:
|
|
userID = _validate_name_or_id(userID)
|
|
user = server.getUser(userID, strict=True)
|
|
values['userID'] = userID
|
|
values['user'] = user
|
|
values['order'] = order
|
|
if prefix:
|
|
prefix = prefix.lower()[0]
|
|
if prefix not in _PREFIX_CHARS:
|
|
prefix = None
|
|
values['prefix'] = prefix
|
|
inherited = int(inherited)
|
|
values['inherited'] = inherited
|
|
|
|
kojiweb.util.paginateMethod(server, values, 'listPackages',
|
|
kw={'tagID': tagID,
|
|
'userID': userID,
|
|
'prefix': prefix,
|
|
'inherited': bool(inherited)},
|
|
start=start, dataName='packages', prefix='package', order=order)
|
|
|
|
values['chars'] = _PREFIX_CHARS
|
|
|
|
return _genHTML(environ, 'packages.chtml')
|
|
|
|
|
|
def packageinfo(environ, packageID, tagOrder='name', tagStart=None, buildOrder='-completion_time',
|
|
buildStart=None):
|
|
values = _initValues(environ, 'Package Info', 'packages')
|
|
server = _getServer(environ)
|
|
|
|
packageID = _validate_name_or_id(packageID)
|
|
package = server.getPackage(packageID)
|
|
if package is None:
|
|
raise koji.GenericError('No such package ID: %s' % packageID)
|
|
|
|
values['title'] = package['name'] + ' | Package Info'
|
|
|
|
values['package'] = package
|
|
values['packageID'] = package['id']
|
|
|
|
kojiweb.util.paginateMethod(server, values, 'listTags', kw={'package': package['id']},
|
|
start=tagStart, dataName='tags', prefix='tag', order=tagOrder)
|
|
kojiweb.util.paginateMethod(server, values, 'listBuilds', kw={'packageID': package['id']},
|
|
start=buildStart, dataName='builds', prefix='build',
|
|
order=buildOrder)
|
|
|
|
return _genHTML(environ, 'packageinfo.chtml')
|
|
|
|
|
|
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)
|
|
|
|
tagID = _validate_name_or_id(tagID)
|
|
tag = server.getTag(tagID, strict=True)
|
|
|
|
values['title'] = tag['name'] + ' | Tag Info'
|
|
|
|
all = int(all)
|
|
|
|
numPackages = server.count('listPackages', tagID=tag['id'], inherited=True, with_owners=False)
|
|
numBuilds = server.count('listTagged', tag=tag['id'], inherit=True)
|
|
values['numPackages'] = numPackages
|
|
values['numBuilds'] = numBuilds
|
|
|
|
inheritance = server.getFullInheritance(tag['id'])
|
|
tagsByChild = {}
|
|
for parent in inheritance:
|
|
child_id = parent['child_id']
|
|
if child_id not in tagsByChild:
|
|
tagsByChild[child_id] = []
|
|
tagsByChild[child_id].append(child_id)
|
|
|
|
srcTargets = server.getBuildTargets(buildTagID=tag['id'])
|
|
srcTargets.sort(key=_sortbyname)
|
|
destTargets = server.getBuildTargets(destTagID=tag['id'])
|
|
destTargets.sort(key=_sortbyname)
|
|
|
|
values['tag'] = tag
|
|
values['tagID'] = tag['id']
|
|
values['inheritance'] = inheritance
|
|
values['tagsByChild'] = tagsByChild
|
|
values['srcTargets'] = srcTargets
|
|
values['destTargets'] = destTargets
|
|
values['all'] = all
|
|
values['repo'] = server.getRepo(tag['id'], state=koji.REPO_READY)
|
|
values['external_repos'] = server.getExternalRepoList(tag['id'])
|
|
|
|
child = None
|
|
if childID is not None:
|
|
child = server.getTag(int(childID), strict=True)
|
|
values['child'] = child
|
|
|
|
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(environ, 'taginfo.chtml')
|
|
|
|
|
|
def tagcreate(environ):
|
|
server = _getServer(environ)
|
|
_assertLogin(environ)
|
|
|
|
mavenEnabled = server.mavenEnabled()
|
|
|
|
form = environ['koji.form']
|
|
|
|
if 'add' in form:
|
|
params = {}
|
|
name = form['name'].value
|
|
params['arches'] = form['arches'].value
|
|
params['locked'] = 'locked' in form
|
|
permission = form['permission'].value
|
|
if permission != 'none':
|
|
params['perm'] = int(permission)
|
|
if mavenEnabled:
|
|
params['maven_support'] = bool('maven_support' in form)
|
|
params['maven_include_all'] = bool('maven_include_all' in form)
|
|
|
|
tagID = server.createTag(name, **params)
|
|
|
|
_redirect(environ, 'taginfo?tagID=%i' % tagID)
|
|
elif 'cancel' in form:
|
|
_redirect(environ, 'tags')
|
|
else:
|
|
values = _initValues(environ, 'Add Tag', 'tags')
|
|
|
|
values['mavenEnabled'] = mavenEnabled
|
|
|
|
values['tag'] = None
|
|
values['permissions'] = server.getAllPerms()
|
|
|
|
return _genHTML(environ, 'tagedit.chtml')
|
|
|
|
|
|
def tagedit(environ, tagID):
|
|
server = _getServer(environ)
|
|
_assertLogin(environ)
|
|
|
|
mavenEnabled = server.mavenEnabled()
|
|
|
|
tagID = int(tagID)
|
|
tag = server.getTag(tagID)
|
|
if tag is None:
|
|
raise koji.GenericError('no tag with ID: %i' % tagID)
|
|
|
|
form = environ['koji.form']
|
|
|
|
if 'save' in form:
|
|
params = {}
|
|
params['name'] = form['name'].value
|
|
params['arches'] = form['arches'].value
|
|
params['locked'] = bool('locked' in form)
|
|
permission = form['permission'].value
|
|
if permission == 'none':
|
|
params['perm'] = None
|
|
else:
|
|
params['perm'] = int(permission)
|
|
if mavenEnabled:
|
|
params['maven_support'] = bool('maven_support' in form)
|
|
params['maven_include_all'] = bool('maven_include_all' in form)
|
|
|
|
server.editTag2(tag['id'], **params)
|
|
|
|
_redirect(environ, 'taginfo?tagID=%i' % tag['id'])
|
|
elif 'cancel' in form:
|
|
_redirect(environ, 'taginfo?tagID=%i' % tag['id'])
|
|
else:
|
|
values = _initValues(environ, 'Edit Tag', 'tags')
|
|
|
|
values['mavenEnabled'] = mavenEnabled
|
|
|
|
values['tag'] = tag
|
|
values['permissions'] = server.getAllPerms()
|
|
|
|
return _genHTML(environ, 'tagedit.chtml')
|
|
|
|
|
|
def tagdelete(environ, tagID):
|
|
server = _getServer(environ)
|
|
_assertLogin(environ)
|
|
|
|
tagID = int(tagID)
|
|
tag = server.getTag(tagID)
|
|
if tag is None:
|
|
raise koji.GenericError('no tag with ID: %i' % tagID)
|
|
|
|
server.deleteTag(tag['id'])
|
|
|
|
_redirect(environ, 'tags')
|
|
|
|
|
|
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 = environ['koji.form']
|
|
|
|
if 'add' in form or 'save' in form:
|
|
newDatum = {}
|
|
newDatum['parent_id'] = parent['id']
|
|
newDatum['priority'] = int(form.getfirst('priority'))
|
|
maxdepth = form.getfirst('maxdepth')
|
|
maxdepth = len(maxdepth) > 0 and int(maxdepth) or None
|
|
newDatum['maxdepth'] = maxdepth
|
|
newDatum['intransitive'] = bool('intransitive' in form)
|
|
newDatum['noconfig'] = bool('noconfig' in form)
|
|
newDatum['pkg_filter'] = form.getfirst('pkg_filter')
|
|
|
|
data = server.getInheritanceData(tag['id'])
|
|
data.append(newDatum)
|
|
|
|
server.setInheritanceData(tag['id'], data)
|
|
elif 'cancel' in form:
|
|
pass
|
|
else:
|
|
values = _initValues(environ, action.capitalize() + ' Parent Tag', 'tags')
|
|
values['tag'] = tag
|
|
values['parent'] = parent
|
|
|
|
inheritanceData = server.getInheritanceData(tag['id'])
|
|
maxPriority = 0
|
|
for datum in inheritanceData:
|
|
if datum['priority'] > maxPriority:
|
|
maxPriority = datum['priority']
|
|
values['maxPriority'] = maxPriority
|
|
inheritanceData = [datum for datum in inheritanceData
|
|
if datum['parent_id'] == parent['id']]
|
|
if len(inheritanceData) == 0:
|
|
values['inheritanceData'] = None
|
|
elif len(inheritanceData) == 1:
|
|
values['inheritanceData'] = inheritanceData[0]
|
|
else:
|
|
raise koji.GenericError(
|
|
'tag %i has tag %i listed as a parent more than once' %
|
|
(tag['id'], parent['id']))
|
|
|
|
return _genHTML(environ, 'tagparent.chtml')
|
|
elif action == 'remove':
|
|
data = server.getInheritanceData(tag['id'])
|
|
for datum in data:
|
|
if datum['parent_id'] == parent['id']:
|
|
datum['delete link'] = True
|
|
break
|
|
else:
|
|
raise koji.GenericError('tag %i is not a parent of tag %i' % (parent['id'], tag['id']))
|
|
|
|
server.setInheritanceData(tag['id'], data)
|
|
else:
|
|
raise koji.GenericError('unknown action: %s' % action)
|
|
|
|
_redirect(environ, 'taginfo?tagID=%i' % tag['id'])
|
|
|
|
|
|
def externalrepoinfo(environ, extrepoID):
|
|
values = _initValues(environ, 'External Repo Info', 'tags')
|
|
server = _getServer(environ)
|
|
|
|
extrepoID = _validate_name_or_id(extrepoID)
|
|
extRepo = server.getExternalRepo(extrepoID, strict=True)
|
|
repoTags = server.getTagExternalRepos(repo_info=extRepo['id'])
|
|
|
|
values['title'] = extRepo['name'] + ' | External Repo Info'
|
|
values['extRepo'] = extRepo
|
|
values['repoTags'] = repoTags
|
|
|
|
return _genHTML(environ, 'externalrepoinfo.chtml')
|
|
|
|
|
|
def buildinfo(environ, buildID):
|
|
values = _initValues(environ, 'Build Info', 'builds')
|
|
server = _getServer(environ)
|
|
topurl = environ['koji.options']['KojiFilesURL']
|
|
pathinfo = koji.PathInfo(topdir=topurl)
|
|
|
|
buildID = int(buildID)
|
|
|
|
try:
|
|
build = server.getBuild(buildID, strict=True)
|
|
except koji.GenericError:
|
|
raise koji.GenericError("No such build ID: %i" % buildID)
|
|
|
|
values['title'] = koji.buildLabel(build) + ' | Build Info'
|
|
|
|
tags = server.listTags(build['id'])
|
|
tags.sort(key=_sortbyname)
|
|
rpms = server.listBuildRPMs(build['id'])
|
|
rpms.sort(key=_sortbyname)
|
|
typeinfo = server.getBuildType(buildID)
|
|
archiveIndex = {}
|
|
for btype in typeinfo:
|
|
archives = server.listArchives(build['id'], type=btype, queryOpts={'order': 'filename'})
|
|
idx = archiveIndex.setdefault(btype, {})
|
|
for archive in archives:
|
|
if btype == 'maven':
|
|
archive['display'] = archive['filename']
|
|
archive['dl_url'] = '/'.join([pathinfo.mavenbuild(build),
|
|
pathinfo.mavenfile(archive)])
|
|
elif btype == 'win':
|
|
archive['display'] = pathinfo.winfile(archive)
|
|
archive['dl_url'] = '/'.join([pathinfo.winbuild(build), pathinfo.winfile(archive)])
|
|
elif btype == 'image':
|
|
archive['display'] = archive['filename']
|
|
archive['dl_url'] = '/'.join([pathinfo.imagebuild(build), archive['filename']])
|
|
else:
|
|
archive['display'] = archive['filename']
|
|
archive['dl_url'] = '/'.join([pathinfo.typedir(build, btype), archive['filename']])
|
|
ext = os.path.splitext(archive['filename'])[1][1:]
|
|
idx.setdefault(ext, []).append(archive)
|
|
|
|
rpmsByArch = {}
|
|
debuginfos = []
|
|
for rpm in rpms:
|
|
if koji.is_debuginfo(rpm['name']):
|
|
debuginfos.append(rpm)
|
|
else:
|
|
rpmsByArch.setdefault(rpm['arch'], []).append(rpm)
|
|
# add debuginfos at the end
|
|
for rpm in debuginfos:
|
|
rpmsByArch.setdefault(rpm['arch'], []).append(rpm)
|
|
|
|
if 'src' in rpmsByArch:
|
|
srpm = rpmsByArch['src'][0]
|
|
result = server.getRPMHeaders(srpm['id'], headers=RPM_HEADERS)
|
|
for header in RPM_HEADERS:
|
|
values[header] = koji.fixEncoding(result.get(header))
|
|
values['changelog'] = server.getChangelogEntries(build['id'])
|
|
|
|
if build['task_id']:
|
|
task = server.getTaskInfo(build['task_id'], request=True)
|
|
# get the summary, description, and changelogs from the built srpm
|
|
# if the build is not yet complete
|
|
if build['state'] != koji.BUILD_STATES['COMPLETE']:
|
|
srpm_tasks = server.listTasks(opts={'parent': task['id'],
|
|
'method': 'buildSRPMFromSCM'})
|
|
if srpm_tasks:
|
|
srpm_task = srpm_tasks[0]
|
|
if srpm_task['state'] == koji.TASK_STATES['CLOSED']:
|
|
srpm_path = None
|
|
for output in server.listTaskOutput(srpm_task['id']):
|
|
if output.endswith('.src.rpm'):
|
|
srpm_path = output
|
|
break
|
|
if srpm_path:
|
|
srpm_headers = server.getRPMHeaders(taskID=srpm_task['id'],
|
|
filepath=srpm_path,
|
|
headers=RPM_HEADERS)
|
|
if srpm_headers:
|
|
for header in RPM_HEADERS:
|
|
values[header] = koji.fixEncoding(srpm_headers.get(header))
|
|
changelog = server.getChangelogEntries(taskID=srpm_task['id'],
|
|
filepath=srpm_path)
|
|
if changelog:
|
|
values['changelog'] = changelog
|
|
else:
|
|
task = None
|
|
|
|
# get logs
|
|
logs = server.getBuildLogs(buildID)
|
|
logs_by_dir = {}
|
|
for loginfo in logs:
|
|
loginfo['dl_url'] = "%s/%s" % (topurl, loginfo['path'])
|
|
logdir = loginfo['dir']
|
|
if logdir == '.':
|
|
logdir = ''
|
|
logs_by_dir.setdefault(logdir, []).append(loginfo)
|
|
values['logs_by_dir'] = logs_by_dir
|
|
|
|
values['build'] = build
|
|
values['tags'] = tags
|
|
values['rpmsByArch'] = rpmsByArch
|
|
values['task'] = task
|
|
values['typeinfo'] = typeinfo
|
|
values['archiveIndex'] = archiveIndex
|
|
|
|
if environ['koji.currentUser']:
|
|
values['perms'] = server.getUserPerms(environ['koji.currentUser']['id'])
|
|
else:
|
|
values['perms'] = []
|
|
for header in RPM_HEADERS + ['changelog']:
|
|
if header not in values:
|
|
values[header] = None
|
|
|
|
# We added the start_time field in 2015 as part of Koji's content
|
|
# generator feature. Builds before that point have a null value for
|
|
# start_time. Fall back to creation_ts in those cases.
|
|
# Currently new_build() has data.setdefault('start_time', 'NOW'), so all
|
|
# recent builds should have a value for the field.
|
|
values['start_ts'] = build.get('start_ts') or build['creation_ts']
|
|
# the build start time is not accurate for maven and win builds, get it from the
|
|
# task start time instead
|
|
if 'maven' in typeinfo or 'win' in typeinfo:
|
|
if task:
|
|
values['start_ts'] = task['start_ts']
|
|
if build['state'] == koji.BUILD_STATES['BUILDING']:
|
|
avgDuration = server.getAverageBuildDuration(build['package_id'])
|
|
if avgDuration is not None:
|
|
avgDelta = datetime.timedelta(seconds=avgDuration)
|
|
startTime = datetime.datetime.fromtimestamp(build['creation_ts'])
|
|
values['estCompletion'] = startTime + avgDelta
|
|
else:
|
|
values['estCompletion'] = None
|
|
|
|
values['pathinfo'] = pathinfo
|
|
return _genHTML(environ, 'buildinfo.chtml')
|
|
|
|
|
|
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:
|
|
userID = _validate_name_or_id(userID)
|
|
user = server.getUser(userID, strict=True)
|
|
values['userID'] = userID
|
|
values['user'] = user
|
|
|
|
loggedInUser = environ['koji.currentUser']
|
|
values['loggedInUser'] = loggedInUser
|
|
|
|
values['users'] = server.listUsers(queryOpts={'order': 'name'})
|
|
|
|
tag = None
|
|
if tagID:
|
|
tagID = _validate_name_or_id(tagID)
|
|
tag = server.getTag(tagID, strict=True)
|
|
values['tagID'] = tagID
|
|
values['tag'] = tag
|
|
|
|
package = None
|
|
if packageID:
|
|
packageID = _validate_name_or_id(packageID)
|
|
package = server.getPackage(packageID, strict=True)
|
|
values['packageID'] = packageID
|
|
values['package'] = package
|
|
|
|
if state == 'all':
|
|
state = None
|
|
elif state is not None:
|
|
state = int(state)
|
|
values['state'] = state
|
|
|
|
if prefix:
|
|
prefix = prefix.lower()[0]
|
|
if prefix not in _PREFIX_CHARS:
|
|
prefix = None
|
|
values['prefix'] = prefix
|
|
|
|
values['order'] = order
|
|
|
|
btypes = sorted([b['name'] for b in server.listBTypes()])
|
|
if type in btypes:
|
|
pass
|
|
elif type == 'all':
|
|
type = None
|
|
else:
|
|
type = None
|
|
values['type'] = type
|
|
values['btypes'] = btypes
|
|
|
|
if tag:
|
|
inherited = int(inherited)
|
|
values['inherited'] = inherited
|
|
latest = int(latest)
|
|
values['latest'] = latest
|
|
else:
|
|
values['inherited'] = None
|
|
values['latest'] = None
|
|
|
|
if tag:
|
|
# don't need to consider 'state' here, since only completed builds would be tagged
|
|
kojiweb.util.paginateResults(server, values, 'listTagged',
|
|
kw={'tag': tag['id'],
|
|
'package': (package and package['name'] or None),
|
|
'owner': (user and user['name'] or None),
|
|
'type': type,
|
|
'inherit': bool(inherited), 'latest': bool(latest),
|
|
'prefix': prefix},
|
|
start=start, dataName='builds', prefix='build', order=order)
|
|
else:
|
|
kojiweb.util.paginateMethod(server, values, 'listBuilds',
|
|
kw={'userID': (user and user['id'] or None),
|
|
'packageID': (package and package['id'] or None),
|
|
'type': type,
|
|
'state': state, 'prefix': prefix},
|
|
start=start, dataName='builds', prefix='build', order=order)
|
|
|
|
values['chars'] = _PREFIX_CHARS
|
|
|
|
return _genHTML(environ, 'builds.chtml')
|
|
|
|
|
|
def users(environ, order='name', start=None, prefix=None):
|
|
values = _initValues(environ, 'Users', 'users')
|
|
server = _getServer(environ)
|
|
|
|
if prefix:
|
|
prefix = prefix.lower()[0]
|
|
if prefix not in _PREFIX_CHARS:
|
|
prefix = None
|
|
values['prefix'] = prefix
|
|
|
|
values['order'] = order
|
|
|
|
kojiweb.util.paginateMethod(server, values, 'listUsers', kw={'prefix': prefix},
|
|
start=start, dataName='users', prefix='user', order=order)
|
|
|
|
values['chars'] = _PREFIX_CHARS
|
|
|
|
return _genHTML(environ, 'users.chtml')
|
|
|
|
|
|
def userinfo(environ, userID, packageOrder='package_name', packageStart=None,
|
|
buildOrder='-completion_time', buildStart=None):
|
|
values = _initValues(environ, 'User Info', 'users')
|
|
server = _getServer(environ)
|
|
|
|
userID = _validate_name_or_id(userID)
|
|
user = server.getUser(userID, strict=True)
|
|
|
|
values['title'] = user['name'] + ' | User Info'
|
|
|
|
values['user'] = user
|
|
values['userID'] = userID
|
|
values['taskCount'] = server.listTasks(opts={'owner': user['id'], 'parent': None},
|
|
queryOpts={'countOnly': True})
|
|
|
|
kojiweb.util.paginateResults(server, values, 'listPackages',
|
|
kw={'userID': user['id'], 'with_dups': True},
|
|
start=packageStart, dataName='packages', prefix='package',
|
|
order=packageOrder, pageSize=10)
|
|
|
|
kojiweb.util.paginateMethod(server, values, 'listBuilds', kw={'userID': user['id']},
|
|
start=buildStart, dataName='builds', prefix='build',
|
|
order=buildOrder, pageSize=10)
|
|
|
|
return _genHTML(environ, 'userinfo.chtml')
|
|
|
|
|
|
# headers shown in rpminfo and buildinfo pages
|
|
RPM_HEADERS = ['summary', 'description', 'license', 'disturl', 'vcs']
|
|
|
|
|
|
def rpminfo(environ, rpmID, fileOrder='name', fileStart=None, buildrootOrder='-id',
|
|
buildrootStart=None):
|
|
values = _initValues(environ, 'RPM Info', 'builds')
|
|
server = _getServer(environ)
|
|
|
|
rpmID = int(rpmID)
|
|
try:
|
|
rpm = server.getRPM(rpmID, strict=True)
|
|
except koji.GenericError:
|
|
raise koji.GenericError('No such RPM ID: %i' % rpmID)
|
|
|
|
values['title'] = '%(name)s-%%s%(version)s-%(release)s.%(arch)s.rpm' % rpm + ' | RPM Info'
|
|
epochStr = ''
|
|
if rpm['epoch'] is not None:
|
|
epochStr = '%s:' % rpm['epoch']
|
|
values['title'] = values['title'] % epochStr
|
|
|
|
build = None
|
|
if rpm['build_id'] is not None:
|
|
build = server.getBuild(rpm['build_id'])
|
|
builtInRoot = None
|
|
if rpm['buildroot_id'] is not None:
|
|
builtInRoot = server.getBuildroot(rpm['buildroot_id'])
|
|
if rpm['external_repo_id'] == 0:
|
|
dep_names = {
|
|
koji.DEP_REQUIRE: 'requires',
|
|
koji.DEP_PROVIDE: 'provides',
|
|
koji.DEP_OBSOLETE: 'obsoletes',
|
|
koji.DEP_CONFLICT: 'conflicts',
|
|
koji.DEP_SUGGEST: 'suggests',
|
|
koji.DEP_ENHANCE: 'enhances',
|
|
koji.DEP_SUPPLEMENT: 'supplements',
|
|
koji.DEP_RECOMMEND: 'recommends',
|
|
}
|
|
deps = server.getRPMDeps(rpm['id'])
|
|
for dep_type in dep_names:
|
|
values[dep_names[dep_type]] = [d for d in deps if d['type'] == dep_type]
|
|
values[dep_names[dep_type]].sort(key=_sortbyname)
|
|
result = server.getRPMHeaders(rpm['id'], headers=RPM_HEADERS)
|
|
for header in RPM_HEADERS:
|
|
values[header] = koji.fixEncoding(result.get(header))
|
|
buildroots = kojiweb.util.paginateMethod(server, values, 'listBuildroots',
|
|
kw={'rpmID': rpm['id']},
|
|
start=buildrootStart,
|
|
dataName='buildroots',
|
|
prefix='buildroot',
|
|
order=buildrootOrder)
|
|
|
|
values['rpmID'] = rpmID
|
|
values['rpm'] = rpm
|
|
values['build'] = build
|
|
values['builtInRoot'] = builtInRoot
|
|
values['buildroots'] = buildroots
|
|
|
|
kojiweb.util.paginateMethod(server, values, 'listRPMFiles', args=[rpm['id']],
|
|
start=fileStart, dataName='files', prefix='file', order=fileOrder)
|
|
|
|
return _genHTML(environ, 'rpminfo.chtml')
|
|
|
|
|
|
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)
|
|
archive_type = server.getArchiveType(type_id=archive['type_id'])
|
|
build = server.getBuild(archive['build_id'])
|
|
maveninfo = False
|
|
if 'group_id' in archive:
|
|
maveninfo = True
|
|
wininfo = False
|
|
if 'relpath' in archive:
|
|
wininfo = True
|
|
builtInRoot = None
|
|
if archive['buildroot_id'] is not None:
|
|
builtInRoot = server.getBuildroot(archive['buildroot_id'])
|
|
kojiweb.util.paginateMethod(server, values, 'listArchiveFiles', args=[archive['id']],
|
|
start=fileStart, dataName='files', prefix='file', order=fileOrder)
|
|
buildroots = kojiweb.util.paginateMethod(server, values, 'listBuildroots',
|
|
kw={'archiveID': archive['id']},
|
|
start=buildrootStart,
|
|
dataName='buildroots',
|
|
prefix='buildroot',
|
|
order=buildrootOrder)
|
|
|
|
values['title'] = archive['filename'] + ' | Archive Info'
|
|
|
|
values['archiveID'] = archive['id']
|
|
values['archive'] = archive
|
|
values['archive_type'] = archive_type
|
|
values['build'] = build
|
|
values['maveninfo'] = maveninfo
|
|
values['wininfo'] = wininfo
|
|
values['builtInRoot'] = builtInRoot
|
|
values['buildroots'] = buildroots
|
|
values['show_rpm_components'] = server.listRPMs(imageID=archive['id'], queryOpts={'limit': 1})
|
|
values['show_archive_components'] = server.listArchives(imageID=archive['id'],
|
|
queryOpts={'limit': 1})
|
|
|
|
return _genHTML(environ, 'archiveinfo.chtml')
|
|
|
|
|
|
def fileinfo(environ, filename, rpmID=None, archiveID=None):
|
|
values = _initValues(environ, 'File Info', 'builds')
|
|
server = _getServer(environ)
|
|
|
|
values['rpm'] = None
|
|
values['archive'] = None
|
|
|
|
if rpmID:
|
|
rpmID = int(rpmID)
|
|
rpm = server.getRPM(rpmID)
|
|
if not rpm:
|
|
raise koji.GenericError('No such RPM ID: %i' % rpmID)
|
|
file = server.getRPMFile(rpm['id'], filename)
|
|
if not file:
|
|
raise koji.GenericError('no file %s in RPM %i' % (filename, rpmID))
|
|
values['rpm'] = rpm
|
|
elif archiveID:
|
|
archiveID = int(archiveID)
|
|
archive = server.getArchive(archiveID)
|
|
if not archive:
|
|
raise koji.GenericError('No such archive ID: %i' % archiveID)
|
|
file = server.getArchiveFile(archive['id'], filename)
|
|
if not file:
|
|
raise koji.GenericError('no file %s in archive %i' % (filename, archiveID))
|
|
values['archive'] = archive
|
|
else:
|
|
raise koji.GenericError('either rpmID or archiveID must be specified')
|
|
|
|
values['title'] = file['name'] + ' | File Info'
|
|
|
|
values['file'] = file
|
|
|
|
return _genHTML(environ, 'fileinfo.chtml')
|
|
|
|
|
|
def cancelbuild(environ, buildID):
|
|
server = _getServer(environ)
|
|
_assertLogin(environ)
|
|
|
|
buildID = int(buildID)
|
|
build = server.getBuild(buildID)
|
|
if build is None:
|
|
raise koji.GenericError('unknown build ID: %i' % buildID)
|
|
|
|
result = server.cancelBuild(build['id'])
|
|
if not result:
|
|
raise koji.GenericError('unable to cancel build')
|
|
|
|
_redirect(environ, 'buildinfo?buildID=%i' % build['id'])
|
|
|
|
|
|
def hosts(environ, state='enabled', start=None, order='name', ready='all', channel='all',
|
|
arch='all'):
|
|
values = _initValues(environ, 'Hosts', 'hosts')
|
|
server = _getServer(environ)
|
|
|
|
values['order'] = order
|
|
|
|
hosts = server.listHosts()
|
|
values['arches'] = sorted(set(itertools.chain(*[host['arches'].split() for host in hosts])))
|
|
|
|
if state == 'enabled':
|
|
hosts = [x for x in hosts if x['enabled']]
|
|
elif state == 'disabled':
|
|
hosts = [x for x in hosts if not x['enabled']]
|
|
else:
|
|
state = 'all'
|
|
values['state'] = state
|
|
|
|
if ready == 'yes':
|
|
hosts = [x for x in hosts if x['ready']]
|
|
elif ready == 'no':
|
|
hosts = [x for x in hosts if not x['ready']]
|
|
else:
|
|
ready = 'all'
|
|
values['ready'] = ready
|
|
|
|
if arch != 'all':
|
|
arch = _validate_arch(arch)
|
|
if arch:
|
|
hosts = [x for x in hosts if arch in x['arches']]
|
|
else:
|
|
arch = 'all'
|
|
values['arch'] = arch
|
|
|
|
with server.multicall() as m:
|
|
list_channels = [m.listChannels(hostID=host['id']) for host in hosts]
|
|
for host, channels in zip(hosts, list_channels):
|
|
host['channels'] = []
|
|
host['channels_id'] = []
|
|
host['channels_enabled'] = []
|
|
for chan in channels.result:
|
|
host['channels'].append(chan['name'])
|
|
host['channels_id'].append(chan['id'])
|
|
if chan['enabled']:
|
|
host['channels_enabled'].append('enabled')
|
|
else:
|
|
host['channels_enabled'].append('disabled')
|
|
|
|
if channel != 'all':
|
|
hosts = [x for x in hosts if channel in x['channels']]
|
|
else:
|
|
channel = 'all'
|
|
values['channel'] = channel
|
|
|
|
values['channels'] = server.listChannels()
|
|
|
|
with server.multicall() as m:
|
|
updates = [m.getLastHostUpdate(host['id'], ts=True) for host in hosts]
|
|
|
|
for host, lastUpdate in zip(hosts, updates):
|
|
host['last_update'] = lastUpdate.result
|
|
|
|
# Paginate after retrieving last update info so we can sort on it
|
|
kojiweb.util.paginateList(values, hosts, start, 'hosts', 'host', order)
|
|
|
|
return _genHTML(environ, 'hosts.chtml')
|
|
|
|
|
|
def hostinfo(environ, hostID=None, userID=None):
|
|
values = _initValues(environ, 'Host Info', 'hosts')
|
|
server = _getServer(environ)
|
|
|
|
if hostID:
|
|
hostID = _validate_name_or_id(hostID)
|
|
host = server.getHost(hostID)
|
|
if host is None:
|
|
raise koji.GenericError('No such host ID: %s' % hostID)
|
|
elif userID:
|
|
userID = int(userID)
|
|
hosts = server.listHosts(userID=userID)
|
|
host = None
|
|
if hosts:
|
|
host = hosts[0]
|
|
if host is None:
|
|
raise koji.GenericError('No such host for user ID: %s' % userID)
|
|
else:
|
|
raise koji.GenericError('hostID or userID must be provided')
|
|
|
|
values['title'] = host['name'] + ' | Host Info'
|
|
|
|
channels = server.listChannels(host['id'])
|
|
channels.sort(key=_sortbyname)
|
|
for chan in channels:
|
|
if chan['enabled']:
|
|
chan['enabled'] = 'enabled'
|
|
else:
|
|
chan['enabled'] = 'disabled'
|
|
buildroots = server.listBuildroots(hostID=host['id'],
|
|
state=[state[1] for state in koji.BR_STATES.items()
|
|
if state[0] != 'EXPIRED'])
|
|
buildroots.sort(key=lambda x: x['create_event_time'], reverse=True)
|
|
|
|
values['host'] = host
|
|
values['channels'] = channels
|
|
values['buildroots'] = buildroots
|
|
values['lastUpdate'] = server.getLastHostUpdate(host['id'], ts=True)
|
|
if environ['koji.currentUser']:
|
|
values['perms'] = server.getUserPerms(environ['koji.currentUser']['id'])
|
|
else:
|
|
values['perms'] = []
|
|
|
|
return _genHTML(environ, 'hostinfo.chtml')
|
|
|
|
|
|
def hostedit(environ, hostID):
|
|
server = _getServer(environ)
|
|
_assertLogin(environ)
|
|
|
|
hostID = int(hostID)
|
|
host = server.getHost(hostID)
|
|
if host is None:
|
|
raise koji.GenericError('no host with ID: %i' % hostID)
|
|
|
|
form = environ['koji.form']
|
|
|
|
if 'save' in form:
|
|
arches = form['arches'].value
|
|
capacity = float(form['capacity'].value)
|
|
description = form['description'].value
|
|
comment = form['comment'].value
|
|
enabled = bool('enabled' in form)
|
|
channels = form.getlist('channels')
|
|
|
|
server.editHost(host['id'], arches=arches, capacity=capacity,
|
|
description=description, comment=comment)
|
|
if enabled != host['enabled']:
|
|
if enabled:
|
|
server.enableHost(host['name'])
|
|
else:
|
|
server.disableHost(host['name'])
|
|
|
|
hostChannels = [c['name'] for c in server.listChannels(hostID=host['id'])]
|
|
for channel in hostChannels:
|
|
if channel not in channels:
|
|
server.removeHostFromChannel(host['name'], channel)
|
|
for channel in channels:
|
|
if channel not in hostChannels:
|
|
server.addHostToChannel(host['name'], channel)
|
|
|
|
_redirect(environ, 'hostinfo?hostID=%i' % host['id'])
|
|
elif 'cancel' in form:
|
|
_redirect(environ, 'hostinfo?hostID=%i' % host['id'])
|
|
else:
|
|
values = _initValues(environ, 'Edit Host', 'hosts')
|
|
|
|
values['host'] = host
|
|
allChannels = server.listChannels()
|
|
allChannels.sort(key=_sortbyname)
|
|
values['allChannels'] = allChannels
|
|
values['hostChannels'] = server.listChannels(hostID=host['id'])
|
|
|
|
return _genHTML(environ, 'hostedit.chtml')
|
|
|
|
|
|
def disablehost(environ, hostID):
|
|
server = _getServer(environ)
|
|
_assertLogin(environ)
|
|
|
|
hostID = int(hostID)
|
|
host = server.getHost(hostID, strict=True)
|
|
server.disableHost(host['name'])
|
|
|
|
_redirect(environ, 'hostinfo?hostID=%i' % host['id'])
|
|
|
|
|
|
def enablehost(environ, hostID):
|
|
server = _getServer(environ)
|
|
_assertLogin(environ)
|
|
|
|
hostID = int(hostID)
|
|
host = server.getHost(hostID, strict=True)
|
|
server.enableHost(host['name'])
|
|
|
|
_redirect(environ, 'hostinfo?hostID=%i' % host['id'])
|
|
|
|
|
|
def channelinfo(environ, channelID):
|
|
values = _initValues(environ, 'Channel Info', 'hosts')
|
|
server = _getServer(environ)
|
|
|
|
channelID = int(channelID)
|
|
channel = server.getChannel(channelID)
|
|
print(channel)
|
|
if channel is None:
|
|
raise koji.GenericError('No such channel ID: %i' % channelID)
|
|
|
|
values['title'] = channel['name'] + ' | Channel Info'
|
|
|
|
states = [koji.TASK_STATES[s] for s in ('FREE', 'OPEN', 'ASSIGNED')]
|
|
values['taskCount'] = \
|
|
server.listTasks(opts={'channel_id': channelID, 'state': states},
|
|
queryOpts={'countOnly': True})
|
|
|
|
hosts = server.listHosts(channelID=channelID)
|
|
hosts.sort(key=_sortbyname)
|
|
|
|
values['channel'] = channel
|
|
values['hosts'] = hosts
|
|
values['enabled_hosts'] = len([h for h in hosts if h['enabled']])
|
|
values['ready_hosts'] = len([h for h in hosts if h['ready']])
|
|
|
|
return _genHTML(environ, 'channelinfo.chtml')
|
|
|
|
|
|
def buildrootinfo(environ, buildrootID):
|
|
values = _initValues(environ, 'Buildroot Info', 'hosts')
|
|
server = _getServer(environ)
|
|
|
|
buildrootID = int(buildrootID)
|
|
buildroot = server.getBuildroot(buildrootID)
|
|
|
|
if buildroot is None:
|
|
raise koji.GenericError('unknown buildroot ID: %i' % buildrootID)
|
|
|
|
elif buildroot['br_type'] == koji.BR_TYPES['STANDARD']:
|
|
template = 'buildrootinfo.chtml'
|
|
values['task'] = server.getTaskInfo(buildroot['task_id'], request=True)
|
|
|
|
else:
|
|
template = 'buildrootinfo_cg.chtml'
|
|
# TODO - fetch tools and extras info
|
|
|
|
values['title'] = '%s | Buildroot Info' % kojiweb.util.brLabel(buildroot)
|
|
values['buildroot'] = buildroot
|
|
|
|
return _genHTML(environ, template)
|
|
|
|
|
|
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(environ, 'RPM List', 'hosts')
|
|
server = _getServer(environ)
|
|
|
|
if buildrootID is not None:
|
|
buildrootID = int(buildrootID)
|
|
buildroot = server.getBuildroot(buildrootID)
|
|
values['buildroot'] = buildroot
|
|
if buildroot is None:
|
|
raise koji.GenericError('unknown buildroot ID: %i' % buildrootID)
|
|
|
|
if type == 'component':
|
|
kojiweb.util.paginateMethod(server, values, 'listRPMs',
|
|
kw={'componentBuildrootID': buildroot['id']},
|
|
start=start, dataName='rpms',
|
|
prefix='rpm', order=order)
|
|
elif type == 'built':
|
|
kojiweb.util.paginateMethod(server, values, 'listRPMs',
|
|
kw={'buildrootID': buildroot['id']},
|
|
start=start, dataName='rpms',
|
|
prefix='rpm', order=order)
|
|
else:
|
|
raise koji.GenericError('unrecognized type of rpmlist')
|
|
|
|
elif imageID is not None:
|
|
imageID = int(imageID)
|
|
values['image'] = server.getArchive(imageID)
|
|
# If/When future image types are supported, add elifs here if needed.
|
|
if type == 'image':
|
|
kojiweb.util.paginateMethod(server, values, 'listRPMs',
|
|
kw={'imageID': imageID},
|
|
start=start, dataName='rpms',
|
|
prefix='rpm', order=order)
|
|
else:
|
|
raise koji.GenericError('unrecognized type of image rpmlist')
|
|
|
|
else:
|
|
# It is an error if neither buildrootID and imageID are defined.
|
|
raise koji.GenericError('Both buildrootID and imageID are None')
|
|
|
|
values['type'] = type
|
|
values['order'] = order
|
|
|
|
return _genHTML(environ, 'rpmlist.chtml')
|
|
|
|
|
|
def archivelist(environ, type, buildrootID=None, imageID=None, start=None, order='filename'):
|
|
values = _initValues(environ, 'Archive List', 'hosts')
|
|
server = _getServer(environ)
|
|
|
|
if buildrootID is not None:
|
|
buildrootID = int(buildrootID)
|
|
buildroot = server.getBuildroot(buildrootID)
|
|
values['buildroot'] = buildroot
|
|
|
|
if buildroot is None:
|
|
raise koji.GenericError('unknown buildroot ID: %i' % buildrootID)
|
|
|
|
if type == 'component':
|
|
kojiweb.util.paginateMethod(server, values, 'listArchives',
|
|
kw={'componentBuildrootID': buildroot['id']},
|
|
start=start, dataName='archives', prefix='archive',
|
|
order=order)
|
|
elif type == 'built':
|
|
kojiweb.util.paginateMethod(server, values, 'listArchives',
|
|
kw={'buildrootID': buildroot['id']},
|
|
start=start, dataName='archives', prefix='archive',
|
|
order=order)
|
|
else:
|
|
raise koji.GenericError('unrecognized type of archivelist')
|
|
elif imageID is not None:
|
|
imageID = int(imageID)
|
|
values['image'] = server.getArchive(imageID)
|
|
# If/When future image types are supported, add elifs here if needed.
|
|
if type == 'image':
|
|
kojiweb.util.paginateMethod(server, values, 'listArchives', kw={'imageID': imageID},
|
|
start=start, dataName='archives', prefix='archive',
|
|
order=order)
|
|
else:
|
|
raise koji.GenericError('unrecognized type of archivelist')
|
|
else:
|
|
# It is an error if neither buildrootID and imageID are defined.
|
|
raise koji.GenericError('Both buildrootID and imageID are None')
|
|
|
|
values['type'] = type
|
|
values['order'] = order
|
|
|
|
return _genHTML(environ, 'archivelist.chtml')
|
|
|
|
|
|
def buildtargets(environ, start=None, order='name'):
|
|
values = _initValues(environ, 'Build Targets', 'buildtargets')
|
|
server = _getServer(environ)
|
|
|
|
kojiweb.util.paginateMethod(server, values, 'getBuildTargets',
|
|
start=start, dataName='targets', prefix='target', order=order)
|
|
|
|
values['order'] = order
|
|
if environ['koji.currentUser']:
|
|
values['perms'] = server.getUserPerms(environ['koji.currentUser']['id'])
|
|
else:
|
|
values['perms'] = []
|
|
|
|
return _genHTML(environ, 'buildtargets.chtml')
|
|
|
|
|
|
def buildtargetinfo(environ, targetID=None, name=None):
|
|
values = _initValues(environ, 'Build Target Info', 'buildtargets')
|
|
server = _getServer(environ)
|
|
|
|
target = None
|
|
if targetID is not None:
|
|
targetID = int(targetID)
|
|
target = server.getBuildTarget(targetID)
|
|
elif name is not None:
|
|
target = server.getBuildTarget(name)
|
|
|
|
if target is None:
|
|
raise koji.GenericError('No such build target: %s' % (targetID or name))
|
|
|
|
values['title'] = target['name'] + ' | Build Target Info'
|
|
|
|
buildTag = server.getTag(target['build_tag'])
|
|
destTag = server.getTag(target['dest_tag'])
|
|
|
|
values['target'] = target
|
|
values['buildTag'] = buildTag
|
|
values['destTag'] = destTag
|
|
if environ['koji.currentUser']:
|
|
values['perms'] = server.getUserPerms(environ['koji.currentUser']['id'])
|
|
else:
|
|
values['perms'] = []
|
|
|
|
return _genHTML(environ, 'buildtargetinfo.chtml')
|
|
|
|
|
|
def buildtargetedit(environ, targetID):
|
|
server = _getServer(environ)
|
|
_assertLogin(environ)
|
|
|
|
targetID = int(targetID)
|
|
|
|
target = server.getBuildTarget(targetID)
|
|
if target is None:
|
|
raise koji.GenericError('No such build target: %s' % targetID)
|
|
|
|
form = environ['koji.form']
|
|
|
|
if 'save' in form:
|
|
name = form.getfirst('name')
|
|
buildTagID = int(form.getfirst('buildTag'))
|
|
buildTag = server.getTag(buildTagID)
|
|
if buildTag is None:
|
|
raise koji.GenericError('No such tag ID: %i' % buildTagID)
|
|
|
|
destTagID = int(form.getfirst('destTag'))
|
|
destTag = server.getTag(destTagID)
|
|
if destTag is None:
|
|
raise koji.GenericError('No such tag ID: %i' % destTagID)
|
|
|
|
server.editBuildTarget(target['id'], name, buildTag['id'], destTag['id'])
|
|
|
|
_redirect(environ, 'buildtargetinfo?targetID=%i' % target['id'])
|
|
elif 'cancel' in form:
|
|
_redirect(environ, 'buildtargetinfo?targetID=%i' % target['id'])
|
|
else:
|
|
values = _initValues(environ, 'Edit Build Target', 'buildtargets')
|
|
tags = server.listTags()
|
|
tags.sort(key=_sortbyname)
|
|
|
|
values['target'] = target
|
|
values['tags'] = tags
|
|
|
|
return _genHTML(environ, 'buildtargetedit.chtml')
|
|
|
|
|
|
def buildtargetcreate(environ):
|
|
server = _getServer(environ)
|
|
_assertLogin(environ)
|
|
|
|
form = environ['koji.form']
|
|
|
|
if 'add' in form:
|
|
# Use the str .value field of the StringField object,
|
|
# since xmlrpclib doesn't know how to marshal the StringFields
|
|
# returned by mod_python
|
|
name = form.getfirst('name')
|
|
buildTagID = int(form.getfirst('buildTag'))
|
|
destTagID = int(form.getfirst('destTag'))
|
|
|
|
server.createBuildTarget(name, buildTagID, destTagID)
|
|
target = server.getBuildTarget(name)
|
|
|
|
if target is None:
|
|
raise koji.GenericError('error creating build target "%s"' % name)
|
|
|
|
_redirect(environ, 'buildtargetinfo?targetID=%i' % target['id'])
|
|
elif 'cancel' in form:
|
|
_redirect(environ, 'buildtargets')
|
|
else:
|
|
values = _initValues(environ, 'Add Build Target', 'builtargets')
|
|
|
|
tags = server.listTags()
|
|
tags.sort(key=_sortbyname)
|
|
|
|
values['target'] = None
|
|
values['tags'] = tags
|
|
|
|
return _genHTML(environ, 'buildtargetedit.chtml')
|
|
|
|
|
|
def buildtargetdelete(environ, targetID):
|
|
server = _getServer(environ)
|
|
_assertLogin(environ)
|
|
|
|
targetID = int(targetID)
|
|
|
|
target = server.getBuildTarget(targetID)
|
|
if target is None:
|
|
raise koji.GenericError('No such build target: %i' % targetID)
|
|
|
|
server.deleteBuildTarget(target['id'])
|
|
|
|
_redirect(environ, 'buildtargets')
|
|
|
|
|
|
def reports(environ):
|
|
_getServer(environ)
|
|
_initValues(environ, 'Reports', 'reports')
|
|
return _genHTML(environ, 'reports.chtml')
|
|
|
|
|
|
def buildsbyuser(environ, start=None, order='-builds'):
|
|
values = _initValues(environ, 'Builds by User', 'reports')
|
|
server = _getServer(environ)
|
|
|
|
maxBuilds = 1
|
|
users = server.listUsers()
|
|
|
|
server.multicall = True
|
|
for user in users:
|
|
server.listBuilds(userID=user['id'], queryOpts={'countOnly': True})
|
|
buildCounts = server.multiCall()
|
|
|
|
for user, [numBuilds] in zip(users, buildCounts):
|
|
user['builds'] = numBuilds
|
|
if numBuilds > maxBuilds:
|
|
maxBuilds = numBuilds
|
|
|
|
values['order'] = order
|
|
|
|
graphWidth = 400.0
|
|
values['graphWidth'] = graphWidth
|
|
values['maxBuilds'] = maxBuilds
|
|
values['increment'] = graphWidth / maxBuilds
|
|
kojiweb.util.paginateList(values, users, start, 'userBuilds', 'userBuild', order)
|
|
|
|
return _genHTML(environ, 'buildsbyuser.chtml')
|
|
|
|
|
|
def rpmsbyhost(environ, start=None, order=None, hostArch=None, rpmArch=None):
|
|
values = _initValues(environ, 'RPMs by Host', 'reports')
|
|
server = _getServer(environ)
|
|
|
|
hostArch = _validate_arch(hostArch)
|
|
rpmArch = _validate_arch(rpmArch)
|
|
|
|
maxRPMs = 1
|
|
hostArchFilter = hostArch
|
|
if hostArchFilter == 'ix86':
|
|
hostArchFilter = ['i386', 'i486', 'i586', 'i686']
|
|
hosts = server.listHosts(arches=hostArchFilter)
|
|
rpmArchFilter = rpmArch
|
|
if rpmArchFilter == 'ix86':
|
|
rpmArchFilter = ['i386', 'i486', 'i586', 'i686']
|
|
|
|
server.multicall = True
|
|
for host in hosts:
|
|
server.listRPMs(hostID=host['id'], arches=rpmArchFilter, queryOpts={'countOnly': True})
|
|
rpmCounts = server.multiCall()
|
|
|
|
for host, [numRPMs] in zip(hosts, rpmCounts):
|
|
host['rpms'] = numRPMs
|
|
if numRPMs > maxRPMs:
|
|
maxRPMs = numRPMs
|
|
|
|
values['hostArch'] = hostArch
|
|
hostArchList = sorted(server.getAllArches())
|
|
values['hostArchList'] = hostArchList
|
|
values['rpmArch'] = rpmArch
|
|
values['rpmArchList'] = hostArchList + ['noarch', 'src']
|
|
|
|
if order is None:
|
|
order = '-rpms'
|
|
values['order'] = order
|
|
|
|
graphWidth = 400.0
|
|
values['graphWidth'] = graphWidth
|
|
values['maxRPMs'] = maxRPMs
|
|
values['increment'] = graphWidth / maxRPMs
|
|
kojiweb.util.paginateList(values, hosts, start, 'hosts', 'host', order)
|
|
|
|
return _genHTML(environ, 'rpmsbyhost.chtml')
|
|
|
|
|
|
def packagesbyuser(environ, start=None, order=None):
|
|
values = _initValues(environ, 'Packages by User', 'reports')
|
|
server = _getServer(environ)
|
|
|
|
maxPackages = 1
|
|
users = server.listUsers()
|
|
|
|
server.multicall = True
|
|
for user in users:
|
|
server.count('listPackages', userID=user['id'], with_dups=True)
|
|
packageCounts = server.multiCall()
|
|
|
|
for user, [numPackages] in zip(users, packageCounts):
|
|
user['packages'] = numPackages
|
|
if numPackages > maxPackages:
|
|
maxPackages = numPackages
|
|
|
|
if order is None:
|
|
order = '-packages'
|
|
values['order'] = order
|
|
|
|
graphWidth = 400.0
|
|
values['graphWidth'] = graphWidth
|
|
values['maxPackages'] = maxPackages
|
|
values['increment'] = graphWidth / maxPackages
|
|
kojiweb.util.paginateList(values, users, start, 'users', 'user', order)
|
|
|
|
return _genHTML(environ, 'packagesbyuser.chtml')
|
|
|
|
|
|
def tasksbyhost(environ, start=None, order='-tasks', hostArch=None):
|
|
values = _initValues(environ, 'Tasks by Host', 'reports')
|
|
server = _getServer(environ)
|
|
|
|
maxTasks = 1
|
|
|
|
hostArch = _validate_arch(hostArch)
|
|
hostArchFilter = hostArch
|
|
if hostArchFilter == 'ix86':
|
|
hostArchFilter = ['i386', 'i486', 'i586', 'i686']
|
|
|
|
hosts = server.listHosts(arches=hostArchFilter)
|
|
|
|
server.multicall = True
|
|
for host in hosts:
|
|
server.listTasks(opts={'host_id': host['id']}, queryOpts={'countOnly': True})
|
|
taskCounts = server.multiCall()
|
|
|
|
for host, [numTasks] in zip(hosts, taskCounts):
|
|
host['tasks'] = numTasks
|
|
if numTasks > maxTasks:
|
|
maxTasks = numTasks
|
|
|
|
values['hostArch'] = hostArch
|
|
hostArchList = sorted(server.getAllArches())
|
|
values['hostArchList'] = hostArchList
|
|
|
|
values['order'] = order
|
|
|
|
graphWidth = 400.0
|
|
values['graphWidth'] = graphWidth
|
|
values['maxTasks'] = maxTasks
|
|
values['increment'] = graphWidth / maxTasks
|
|
kojiweb.util.paginateList(values, hosts, start, 'hosts', 'host', order)
|
|
|
|
return _genHTML(environ, 'tasksbyhost.chtml')
|
|
|
|
|
|
def tasksbyuser(environ, start=None, order='-tasks'):
|
|
values = _initValues(environ, 'Tasks by User', 'reports')
|
|
server = _getServer(environ)
|
|
|
|
maxTasks = 1
|
|
|
|
users = server.listUsers()
|
|
|
|
server.multicall = True
|
|
for user in users:
|
|
server.listTasks(opts={'owner': user['id']}, queryOpts={'countOnly': True})
|
|
taskCounts = server.multiCall()
|
|
|
|
for user, [numTasks] in zip(users, taskCounts):
|
|
user['tasks'] = numTasks
|
|
if numTasks > maxTasks:
|
|
maxTasks = numTasks
|
|
|
|
values['order'] = order
|
|
|
|
graphWidth = 400.0
|
|
values['graphWidth'] = graphWidth
|
|
values['maxTasks'] = maxTasks
|
|
values['increment'] = graphWidth / maxTasks
|
|
kojiweb.util.paginateList(values, users, start, 'users', 'user', order)
|
|
|
|
return _genHTML(environ, 'tasksbyuser.chtml')
|
|
|
|
|
|
def buildsbystatus(environ, days='7'):
|
|
values = _initValues(environ, 'Builds by Status', 'reports')
|
|
server = _getServer(environ)
|
|
|
|
days = int(days)
|
|
if days != -1:
|
|
seconds = 60 * 60 * 24 * days
|
|
dateAfter = time.time() - seconds
|
|
else:
|
|
dateAfter = None
|
|
values['days'] = days
|
|
|
|
server.multicall = True
|
|
# use taskID=-1 to filter out builds with a null task_id (imported rather than built in koji)
|
|
server.listBuilds(completeAfter=dateAfter, state=koji.BUILD_STATES['COMPLETE'], taskID=-1,
|
|
queryOpts={'countOnly': True})
|
|
server.listBuilds(completeAfter=dateAfter, state=koji.BUILD_STATES['FAILED'], taskID=-1,
|
|
queryOpts={'countOnly': True})
|
|
server.listBuilds(completeAfter=dateAfter, state=koji.BUILD_STATES['CANCELED'], taskID=-1,
|
|
queryOpts={'countOnly': True})
|
|
[[numSucceeded], [numFailed], [numCanceled]] = server.multiCall()
|
|
|
|
values['numSucceeded'] = numSucceeded
|
|
values['numFailed'] = numFailed
|
|
values['numCanceled'] = numCanceled
|
|
|
|
maxBuilds = 1
|
|
for value in (numSucceeded, numFailed, numCanceled):
|
|
if value > maxBuilds:
|
|
maxBuilds = value
|
|
|
|
graphWidth = 400.0
|
|
values['graphWidth'] = graphWidth
|
|
values['maxBuilds'] = maxBuilds
|
|
values['increment'] = graphWidth / maxBuilds
|
|
|
|
return _genHTML(environ, 'buildsbystatus.chtml')
|
|
|
|
|
|
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:
|
|
seconds = 60 * 60 * 24 * days
|
|
dateAfter = time.time() - seconds
|
|
else:
|
|
dateAfter = None
|
|
values['days'] = days
|
|
|
|
targets = {}
|
|
maxBuilds = 1
|
|
|
|
tasks = server.listTasks(opts={'method': 'build', 'completeAfter': dateAfter, 'decode': True})
|
|
|
|
for task in tasks:
|
|
targetName = task['request'][1]
|
|
target = targets.get(targetName)
|
|
if not target:
|
|
target = {'name': targetName}
|
|
targets[targetName] = target
|
|
builds = target.get('builds', 0) + 1
|
|
target['builds'] = builds
|
|
if builds > maxBuilds:
|
|
maxBuilds = builds
|
|
|
|
kojiweb.util.paginateList(values, list(targets.values()), start, 'targets', 'target', order)
|
|
|
|
values['order'] = order
|
|
|
|
graphWidth = 400.0
|
|
values['graphWidth'] = graphWidth
|
|
values['maxBuilds'] = maxBuilds
|
|
values['increment'] = graphWidth / maxBuilds
|
|
|
|
return _genHTML(environ, 'buildsbytarget.chtml')
|
|
|
|
|
|
def _filter_hosts_by_arch(hosts, arch):
|
|
if arch == '__all__':
|
|
return hosts
|
|
else:
|
|
return [h for h in hosts if arch in h['arches'].split()]
|
|
|
|
|
|
def clusterhealth(environ, arch='__all__'):
|
|
arch = _validate_arch(arch)
|
|
values = _initValues(environ, 'Cluster health', 'reports')
|
|
server = _getServer(environ)
|
|
channels = server.listChannels()
|
|
server.multicall = True
|
|
for channel in channels:
|
|
server.listHosts(channelID=channel['id'])
|
|
max_enabled = 0
|
|
max_capacity = 0
|
|
arches = set()
|
|
for channel, [hosts] in zip(channels, server.multiCall()):
|
|
for host in hosts:
|
|
arches |= set(host['arches'].split())
|
|
hosts = _filter_hosts_by_arch(hosts, arch)
|
|
channel['enabled_channel'] = channel['enabled']
|
|
channel['enabled'] = len([x for x in hosts if x['enabled']])
|
|
channel['disabled'] = len(hosts) - channel['enabled']
|
|
channel['ready'] = len([x for x in hosts if x['ready']])
|
|
channel['capacity'] = sum([x['capacity'] for x in hosts])
|
|
channel['load'] = sum([x['task_load'] for x in hosts])
|
|
if max_enabled < channel['enabled']:
|
|
max_enabled = channel['enabled']
|
|
if max_capacity < channel['capacity']:
|
|
max_capacity = channel['capacity']
|
|
|
|
graphWidth = 400.0
|
|
# compute values for channels
|
|
for channel in channels:
|
|
try:
|
|
channel['capacityPerc'] = channel['capacity'] / max_capacity * 100
|
|
except ZeroDivisionError:
|
|
channel['capacityPerc'] = 0
|
|
try:
|
|
channel['enabledPerc'] = channel['enabled'] / max_enabled * 100
|
|
except ZeroDivisionError:
|
|
channel['enabledPerc'] = 0
|
|
if channel['capacity']:
|
|
channel['perc_load'] = min(100, channel['load'] / channel['capacity'] * 100)
|
|
else:
|
|
channel['perc_load'] = 0.0
|
|
if channel['enabled']:
|
|
channel['perc_ready'] = min(100, channel['ready'] / channel['enabled'] * 100)
|
|
else:
|
|
channel['perc_ready'] = 0.0
|
|
|
|
values['arch'] = arch
|
|
values['arches'] = sorted(arches)
|
|
values['graphWidth'] = graphWidth
|
|
values['channels'] = sorted(channels, key=lambda x: x['name'])
|
|
return _genHTML(environ, 'clusterhealth.chtml')
|
|
|
|
|
|
def recentbuilds(environ, user=None, tag=None, package=None):
|
|
values = _initValues(environ, 'Recent Build RSS')
|
|
server = _getServer(environ)
|
|
|
|
tagObj = None
|
|
if tag is not None:
|
|
tag = _validate_name_or_id(tag)
|
|
tagObj = server.getTag(tag, strict=True)
|
|
|
|
userObj = None
|
|
if user is not None:
|
|
user = _validate_name_or_id(user)
|
|
userObj = server.getUser(user, strict=True)
|
|
|
|
packageObj = None
|
|
if package:
|
|
package = _validate_name_or_id(package)
|
|
packageObj = server.getPackage(package, strict=True)
|
|
|
|
if tagObj is not None:
|
|
builds = server.listTagged(tagObj['id'], inherit=True,
|
|
package=(packageObj and packageObj['name'] or None),
|
|
owner=(userObj and userObj['name'] or None))
|
|
builds.sort(key=kojiweb.util.sortByKeyFuncNoneGreatest('completion_time'), reverse=True)
|
|
builds = builds[:20]
|
|
else:
|
|
kwargs = {}
|
|
if userObj:
|
|
kwargs['userID'] = userObj['id']
|
|
if packageObj:
|
|
kwargs['packageID'] = packageObj['id']
|
|
builds = server.listBuilds(queryOpts={'order': '-completion_time', 'limit': 20}, **kwargs)
|
|
|
|
server.multicall = True
|
|
for build in builds:
|
|
if build['task_id']:
|
|
server.getTaskInfo(build['task_id'], request=True)
|
|
else:
|
|
server.echo(None)
|
|
tasks = server.multiCall()
|
|
|
|
server.multicall = True
|
|
queryOpts = {'limit': 3}
|
|
for build in builds:
|
|
if build['state'] == koji.BUILD_STATES['COMPLETE']:
|
|
server.getChangelogEntries(build['build_id'], queryOpts=queryOpts)
|
|
else:
|
|
server.echo(None)
|
|
clogs = server.multiCall()
|
|
|
|
for i in range(len(builds)):
|
|
task = tasks[i][0]
|
|
if isinstance(task, list):
|
|
# this is the output of server.echo(None) above
|
|
task = None
|
|
builds[i]['task'] = task
|
|
builds[i]['changelog'] = clogs[i][0]
|
|
|
|
values['tag'] = tagObj
|
|
values['user'] = userObj
|
|
values['package'] = packageObj
|
|
values['builds'] = builds
|
|
values['weburl'] = _getBaseURL(environ)
|
|
|
|
environ['koji.headers'].append(['Content-Type', 'text/xml'])
|
|
return _genHTML(environ, 'recentbuilds.chtml')
|
|
|
|
|
|
_infoURLs = {'package': 'packageinfo?packageID=%(id)i',
|
|
'build': 'buildinfo?buildID=%(id)i',
|
|
'tag': 'taginfo?tagID=%(id)i',
|
|
'target': 'buildtargetinfo?targetID=%(id)i',
|
|
'user': 'userinfo?userID=%(id)i',
|
|
'host': 'hostinfo?hostID=%(id)i',
|
|
'rpm': 'rpminfo?rpmID=%(id)i',
|
|
'maven': 'archiveinfo?archiveID=%(id)i',
|
|
'win': 'archiveinfo?archiveID=%(id)i'}
|
|
|
|
_DEFAULT_SEARCH_ORDER = {
|
|
# For searches against large tables, use '-id' to show most recent first
|
|
'build': '-id',
|
|
'rpm': '-id',
|
|
'maven': '-id',
|
|
'win': '-id',
|
|
# for other tables, ordering by name makes much more sense
|
|
'tag': 'name',
|
|
'target': 'name',
|
|
'package': 'name',
|
|
# any type not listed will default to 'name'
|
|
}
|
|
|
|
|
|
def search(environ, start=None, order=None):
|
|
if start is not None:
|
|
start = int(start)
|
|
values = _initValues(environ, 'Search', 'search')
|
|
server = _getServer(environ)
|
|
values['error'] = None
|
|
|
|
form = environ['koji.form']
|
|
if 'terms' in form and form['terms']:
|
|
terms = form['terms'].value
|
|
terms = terms.strip()
|
|
type = form['type'].value
|
|
match = form['match'].value
|
|
values['terms'] = terms
|
|
values['type'] = type
|
|
values['match'] = match
|
|
|
|
if match not in ('glob', 'regexp', 'exact'):
|
|
raise koji.GenericError("No such match type: %r" % match)
|
|
|
|
if not _VALID_SEARCH_RE.match(terms):
|
|
values['error'] = 'Invalid search terms<br/>' + \
|
|
'Search terms may contain only these characters: ' + \
|
|
_VALID_SEARCH_CHARS + _VALID_SEARCH_SYMS
|
|
values['terms'] = ''
|
|
return _genHTML(environ, 'search.chtml')
|
|
|
|
if match == 'regexp':
|
|
try:
|
|
re.compile(terms)
|
|
except Exception:
|
|
values['error'] = 'Invalid regular expression'
|
|
values['terms'] = ''
|
|
return _genHTML(environ, 'search.chtml')
|
|
|
|
infoURL = _infoURLs.get(type)
|
|
if not infoURL:
|
|
raise koji.GenericError('unknown search type: %s' % type)
|
|
values['infoURL'] = infoURL
|
|
order = order or _DEFAULT_SEARCH_ORDER.get(type, 'name')
|
|
values['order'] = order
|
|
|
|
results = kojiweb.util.paginateMethod(server, values, 'search', args=(terms, type, match),
|
|
start=start, dataName='results', prefix='result',
|
|
order=order)
|
|
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)
|
|
_redirect(environ, infoURL % results[0])
|
|
else:
|
|
if type == 'maven':
|
|
typeLabel = 'Maven artifacts'
|
|
elif type == 'win':
|
|
typeLabel = 'Windows artifacts'
|
|
else:
|
|
typeLabel = '%ss' % type
|
|
values['typeLabel'] = typeLabel
|
|
return _genHTML(environ, 'search.chtml')
|
|
else:
|
|
return _genHTML(environ, 'search.chtml')
|
|
|
|
|
|
def api(environ):
|
|
values = _initValues(environ, 'API', 'api')
|
|
server = _getServer(environ)
|
|
|
|
values['koji_hub_url'] = environ['koji.options']['KojiHubURL']
|
|
values['methods'] = sorted(server._listapi(), key=lambda x: x['name'])
|
|
try:
|
|
values['koji_version'] = server.getKojiVersion()
|
|
except koji.GenericError:
|
|
values['koji_version'] = "Can't determine (older then 1.23)"
|
|
|
|
return _genHTML(environ, 'api.chtml')
|
|
|
|
|
|
def watchlogs(environ, taskID):
|
|
values = _initValues(environ)
|
|
if isinstance(taskID, list):
|
|
values['tasks'] = ', '.join([int(x) for x in taskID])
|
|
else:
|
|
values['tasks'] = int(taskID)
|
|
|
|
html = """
|
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
|
<html>
|
|
<head>
|
|
<script type="text/javascript" src="/koji-static/js/watchlogs.js"></script>
|
|
<title>Logs for task %(tasks)s | %(siteName)s</title>
|
|
</head>
|
|
<body onload="watchLogs('logs')">
|
|
<pre id="logs">
|
|
<span>Loading logs for task %(tasks)s...</span>
|
|
</pre>
|
|
</body>
|
|
</html>
|
|
""" % values
|
|
return html
|
|
|
|
|
|
def repoinfo(environ, repoID):
|
|
values = _initValues(environ, 'Repo Info', 'tags')
|
|
server = _getServer(environ)
|
|
|
|
repoID = _validate_name_or_id(repoID)
|
|
values['repo_id'] = repoID
|
|
repo_info = server.repoInfo(repoID, strict=False)
|
|
values['repo'] = repo_info
|
|
if repo_info:
|
|
topurl = environ['koji.options']['KojiFilesURL']
|
|
pathinfo = koji.PathInfo(topdir=topurl)
|
|
if repo_info['dist']:
|
|
values['url'] = pathinfo.distrepo(repo_info['id'], repo_info['tag_name'])
|
|
else:
|
|
values['url'] = pathinfo.repo(repo_info['id'], repo_info['tag_name'])
|
|
values['repo_json'] = os.path.join(pathinfo.repo(repo_info['id'], repo_info['tag_name']),
|
|
'repo.json')
|
|
return _genHTML(environ, 'repoinfo.chtml')
|