# 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 # Mike McLean import datetime import hashlib import http.cookies import logging import mimetypes import os import os.path import re import sys import time 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("Invalid 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): koji_opts = environ['koji.options'] opts = { 'serverca': koji_opts.get('KojiHubCA', '') } session = koji.ClientSession(koji_opts['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'] # try SSL first, fall back to Kerberos if options['WebCert']: 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') 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']: principal = environ.get('REMOTE_USER') if not principal: raise koji.AuthError( 'configuration error: mod_auth_gssapi should have performed authentication before ' 'presenting this page') if not _gssapiLogin(environ, session, principal): raise koji.AuthError('could not login using principal: %s' % principal) username = principal authlogger.info('Successful Kerberos authentication by %s', username) else: raise koji.AuthError( 'KojiWeb is incorrectly configured for authentication, contact the system ' 'administrator') _setUserCookie(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) 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('invalid 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: try: values['buildTag'] = server.getTag(params[1], strict=True) except koji.GenericError: values['buildTag'] = {'name': "%d (deleted)" % params[1], '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('invalid 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) 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("Invalid 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('invalid 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('invalid 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('invalid 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'): values = _initValues(environ, 'Hosts', 'hosts') server = _getServer(environ) values['order'] = order args = {} if state == 'enabled': args['enabled'] = True elif state == 'disabled': args['enabled'] = False else: state = 'all' values['state'] = state hosts = server.listHosts(**args) server.multicall = True for host in hosts: server.getLastHostUpdate(host['id']) updates = server.multiCall() for host, [lastUpdate] in zip(hosts, updates): host['last_update'] = lastUpdate # 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('invalid 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('invalid host 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) 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']) 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) if channel is None: raise koji.GenericError('invalid 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('invalid 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('invalid 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('invalid tag ID: %i' % buildTagID) destTagID = int(form.getfirst('destTag')) destTag = server.getTag(destTagID) if destTag is None: raise koji.GenericError('invalid 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('invalid 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'] = 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("Invalid match type: %r" % match) if not _VALID_SEARCH_RE.match(terms): values['error'] = 'Invalid search terms
' + \ '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['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 = """ Logs for task %(tasks)s | %(siteName)s
Loading logs for task %(tasks)s...
    
""" % 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')