From 8d2bdac4da2c6271712f46228f976d1186906927 Mon Sep 17 00:00:00 2001 From: Dennis Gilmore Date: Mar 21 2017 19:26:14 +0000 Subject: pull in kojiweb from koji Signed-off-by: Dennis Gilmore --- diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..227c5e0 --- /dev/null +++ b/Makefile @@ -0,0 +1,126 @@ +NAME=koji +SPECFILE = $(firstword $(wildcard *.spec)) +SUBDIRS = www + +ifdef DIST +DIST_DEFINES := --define "dist $(DIST)" +endif + +ifndef VERSION +VERSION := $(shell rpm $(RPM_DEFINES) $(DIST_DEFINES) -q --qf "%{VERSION}\n" --specfile $(SPECFILE)| head -1) +endif +# the release of the package +ifndef RELEASE +RELEASE := $(shell rpm $(RPM_DEFINES) $(DIST_DEFINES) -q --qf "%{RELEASE}\n" --specfile $(SPECFILE)| head -1) +endif + +ifndef WORKDIR +WORKDIR := $(shell pwd) +endif +## Override RPM_WITH_DIRS to avoid the usage of these variables. +ifndef SRCRPMDIR +SRCRPMDIR = $(WORKDIR) +endif +ifndef BUILDDIR +BUILDDIR = $(WORKDIR) +endif +ifndef RPMDIR +RPMDIR = $(WORKDIR) +endif +## SOURCEDIR is special; it has to match the CVS checkout directory,- +## because the CVS checkout directory contains the patch files. So it basically- +## can't be overridden without breaking things. But we leave it a variable +## for consistency, and in hopes of convincing it to work sometime. +ifndef SOURCEDIR +SOURCEDIR := $(shell pwd) +endif + + +# RPM with all the overrides in place; +ifndef RPM +RPM := $(shell if test -f /usr/bin/rpmbuild ; then echo rpmbuild ; else echo rpm ; fi) +endif +ifndef RPM_WITH_DIRS +RPM_WITH_DIRS = $(RPM) --define "_sourcedir $(SOURCEDIR)" \ + --define "_builddir $(BUILDDIR)" \ + --define "_srcrpmdir $(SRCRPMDIR)" \ + --define "_rpmdir $(RPMDIR)" +endif + +# tag to export, defaulting to current tag in the spec file +ifndef TAG +TAG=$(NAME)-$(VERSION)-$(RELEASE) +endif + +_default: + @echo "read the makefile" + +clean: + rm -f *.o *.so *.pyc *~ koji*.bz2 koji*.src.rpm + rm -rf koji-$(VERSION) + for d in $(SUBDIRS); do make -s -C $$d clean; done + coverage erase + +git-clean: + @git clean -d -q -x + +test: + coverage erase + PYTHONPATH=hub/.:plugins/hub/. nosetests --with-coverage --cover-package . + coverage html + @echo Coverage report in htmlcov/index.html + +subdirs: + for d in $(SUBDIRS); do make -C $$d; [ $$? = 0 ] || exit 1; done + +test-tarball: + @rm -rf .koji-$(VERSION) + @mkdir .koji-$(VERSION) + @cp -al [A-Za-z]* .koji-$(VERSION) + @mv .koji-$(VERSION) koji-$(VERSION) + tar --bzip2 --exclude '*.tar.bz2' --exclude '*.rpm' --exclude '.#*' \ + -cpf koji-$(VERSION).tar.bz2 koji-$(VERSION) + @rm -rf koji-$(VERSION) + +tarball: clean + @git archive --format=tar --prefix=$(NAME)-$(VERSION)/ HEAD |bzip2 > $(NAME)-$(VERSION).tar.bz2 + +sources: tarball + +srpm: tarball + $(RPM_WITH_DIRS) $(DIST_DEFINES) -bs $(SPECFILE) + +rpm: tarball + $(RPM_WITH_DIRS) $(DIST_DEFINES) -bb $(SPECFILE) + +test-rpm: tarball + $(RPM_WITH_DIRS) $(DIST_DEFINES) --define "testbuild 1" -bb $(SPECFILE) + +tag:: + git tag -a $(TAG) + @echo "Tagged with: $(TAG)" + @echo + +force-tag:: + git tag -f -a $(TAG) + @echo "Tagged with: $(TAG)" + @echo + +# If and only if "make build" fails, use "make force-tag" to +# re-tag the version. +#force-tag: $(SPECFILE) +# @$(MAKE) tag TAG_OPTS="-F $(TAG_OPTS)" + +DESTDIR ?= / +TYPE = systemd +install: + @if [ "$(DESTDIR)" = "" ]; then \ + echo " "; \ + echo "ERROR: A destdir is required"; \ + exit 1; \ + fi + + mkdir -p $(DESTDIR) + + for d in $(SUBDIRS); do make DESTDIR=`cd $(DESTDIR); pwd` \ + -C $$d install TYPE=$(TYPE); [ $$? = 0 ] || exit 1; done diff --git a/www/Makefile b/www/Makefile new file mode 100644 index 0000000..25a89dd --- /dev/null +++ b/www/Makefile @@ -0,0 +1,20 @@ +SUBDIRS = kojiweb conf lib static + +_default: + @echo "nothing to make. try make install" + +clean: + rm -f *.o *.so *.pyc *~ + for d in $(SUBDIRS); do make -s -C $$d clean; done + +install: + @if [ "$(DESTDIR)" = "" ]; then \ + echo " "; \ + echo "ERROR: A destdir is required"; \ + exit 1; \ + fi + + mkdir -p $(DESTDIR)/usr/share/koji-web + + for d in $(SUBDIRS); do make DESTDIR=$(DESTDIR) \ + -C $$d install; [ $$? = 0 ] || exit 1; done diff --git a/www/conf/Makefile b/www/conf/Makefile new file mode 100644 index 0000000..f87e381 --- /dev/null +++ b/www/conf/Makefile @@ -0,0 +1,20 @@ +_default: + @echo "nothing to make. try make install" + +clean: + rm -f *.o *.so *.pyc *~ + for d in $(SUBDIRS); do make -s -C $$d clean; done + +install: + @if [ "$(DESTDIR)" = "" ]; then \ + echo " "; \ + echo "ERROR: A destdir is required"; \ + exit 1; \ + fi + + mkdir -p $(DESTDIR)/etc/httpd/conf.d + install -p -m 644 kojiweb.conf $(DESTDIR)/etc/httpd/conf.d/kojiweb.conf + + mkdir -p $(DESTDIR)/etc/kojiweb + install -p -m 644 web.conf $(DESTDIR)/etc/kojiweb/web.conf + mkdir -p $(DESTDIR)/etc/kojiweb/web.conf.d diff --git a/www/conf/kojiweb.conf b/www/conf/kojiweb.conf new file mode 100644 index 0000000..807ef23 --- /dev/null +++ b/www/conf/kojiweb.conf @@ -0,0 +1,69 @@ +#We use wsgi by default +Alias /koji "/usr/share/koji-web/scripts/wsgi_publisher.py" +#(configuration goes in /etc/kojiweb/web.conf) + + + Options ExecCGI + SetHandler wsgi-script + WSGIApplicationGroup %{GLOBAL} + # ^ works around an OpenSSL issue + # see: https://cryptography.io/en/latest/faq/#starting-cryptography-using-mod-wsgi-produces-an-internalerror-during-a-call-in-register-osrandom-engine + + Order allow,deny + Allow from all + + = 2.4> + Require all granted + + + +# Support for mod_python is DEPRECATED. If you still need mod_python support, +# then use the following directory settings instead: +# +# +# # Config for the publisher handler +# SetHandler mod_python +# # Use kojiweb's publisher (provides wsgi compat layer) +# # mod_python's publisher is no longer supported +# PythonHandler wsgi_publisher +# PythonOption koji.web.ConfigFile /etc/kojiweb/web.conf +# PythonAutoReload Off +# # Configuration via PythonOptions is DEPRECATED. Use /etc/kojiweb/web.conf +# Order allow,deny +# Allow from all +# + +# uncomment this to enable authentication via Kerberos +# +# AuthType Kerberos +# AuthName "Koji Web UI" +# KrbMethodNegotiate on +# KrbMethodK5Passwd off +# KrbServiceName HTTP +# KrbAuthRealm EXAMPLE.COM +# Krb5Keytab /etc/httpd.keytab +# KrbSaveCredentials off +# Require valid-user +# ErrorDocument 401 /koji-static/errors/unauthorized.html +# + +# uncomment this to enable authentication via SSL client certificates +# +# SSLVerifyClient require +# SSLVerifyDepth 10 +# SSLOptions +StdEnvVars +# + +Alias /koji-static/ "/usr/share/koji-web/static/" + + + Options None + AllowOverride None + + Order allow,deny + Allow from all + + = 2.4> + Require all granted + + diff --git a/www/conf/web.conf b/www/conf/web.conf new file mode 100644 index 0000000..2ec0959 --- /dev/null +++ b/www/conf/web.conf @@ -0,0 +1,44 @@ +[web] +SiteName = koji +# KojiTheme = mytheme + +# Key urls +KojiHubURL = http://hub.example.com/kojihub +KojiFilesURL = http://server.example.com/kojifiles + +# Kerberos authentication options +# WebPrincipal = koji/web@EXAMPLE.COM +# WebKeytab = /etc/httpd.keytab +# WebCCache = /var/tmp/kojiweb.ccache +# The service name of the principal being used by the hub +# KrbService = host + +# SSL authentication options +# WebCert = /etc/kojiweb/kojiweb.crt +# KojiHubCA = /etc/kojiweb/kojihubca.crt + +LoginTimeout = 72 + +# This must be changed and uncommented before deployment +# Secret = CHANGE_ME + +LibPath = /usr/share/koji-web/lib + +# If set to True, then the footer will be included literally. +# If False, then the footer will be included as another Kid Template. +# Defaults to True +LiteralFooter = True + +# This can be a space-delimited list of the numeric IDs of users that you want +# to hide from tasks listed on the front page. You might want to, for instance, +# hide the activity of an account used for continuous integration. +# HiddenUsers = 5372 1234 + +# Task types visible in pulldown menu on tasks page. +# Tasks = +# runroot plugin provided via main package could be listed as: +# Tasks = runroot +# Tasks that can exist without a parent +# ToplevelTasks = +# Tasks that can have children +# ParentTasks = diff --git a/www/docs/negotiate/index.html b/www/docs/negotiate/index.html new file mode 100644 index 0000000..ed6122e --- /dev/null +++ b/www/docs/negotiate/index.html @@ -0,0 +1,78 @@ + + + Configuring Firefox (and Mozilla) for Negotiate Authentication + + +

Configuring Firefox (and Mozilla) for Negotiate Authentication

+

+ Before Firefox and Mozilla can authenticate to a server using "Negotiate" + authentication, a couple of configuration changes must be made. +

+

+ Type about:config into the location bar, to bring + up the configuration page. Type negotiate into the Filter: box, to restrict + the listing to the configuration options we're interested in. +
+ Change network.negotiate-auth.trusted-uris to the domain you want to authenticate against, + e.g. .example.com. You can leave network.negotiate-auth.delegation-uris + blank, as it enables Kerberos ticket passing, which is not required. If you do not see those two config + options listed, your version of Firefox or Mozilla may be too old to support Negotiate authentication, and + you should consider upgrading. +
+ FC5 Update: Firefox and Mozilla on FC5 are attempting to load a library by its unversioned name, which is + not installed by default. A fix has been checked-in upstream, but in the meantime, the workaround is to set + network.negotiate-auth.gsslib to libgssapi_krb5.so.2. +
+ FC5 Update Update: If you are using the most recent Firefox or Mozilla, this workaround is + no longer necessary. +

+

+ Now, make sure you have Kerberos tickets. Typing kinit in a shell should allow you to + retrieve Kerberos tickets. klist will show you what tickets you have. +
+

+

+ Now, if you visit a Kerberos-authenticated website in the .example.com domain, you should be logged in + automatically, without having to type in your password. +

+

+

Troubleshooting

+ If you have followed the configuration steps and Negotiate authentication is not working, you can + turn on verbose logging of the authentication process, and potentially find the cause of the problem. + Exit Firefox or Mozilla. In a shell, type the following commands: +
+export NSPR_LOG_MODULES=negotiateauth:5
+export NSPR_LOG_FILE=/tmp/moz.log
+      
+ Then restart Firefox or Mozilla from that shell, and visit the website you were unable to authenticate + to earlier. Information will be logged to /tmp/moz.log, which may give a clue to the problem. + For example: +
+-1208550944[90039d0]: entering nsNegotiateAuth::GetNextToken()
+-1208550944[90039d0]: gss_init_sec_context() failed: Miscellaneous failure
+No credentials cache found
+
+      
+ means that you do not have Kerberos tickets, and need to run kinit. +
+
+ If you are able to kinit successfully from your machine but you are unable to authenticate, and you see + something like this in your log: +
+-1208994096[8d683d8]: entering nsAuthGSSAPI::GetNextToken()
+-1208994096[8d683d8]: gss_init_sec_context() failed: Miscellaneous failure
+Server not found in Kerberos database
+      
+ it generally indicates a Kerberos configuration problem. Make sure you have the following in the + [domain_realm] section of /etc/krb5.conf: +
+ .example.com = EXAMPLE.COM
+ example.com = EXAMPLE.COM
+      
+ If nothing is showing up in the log it's possible that you're behind a proxy, and that proxy is stripping off + the HTTP headers required for Negotiate authentication. As a workaround, you can try to connect to the + server via https instead, which will allow the request to pass through unmodified. Then proceed to + debug using the log, as described above. +

+ + diff --git a/www/kojiweb/Makefile b/www/kojiweb/Makefile new file mode 100644 index 0000000..4feda33 --- /dev/null +++ b/www/kojiweb/Makefile @@ -0,0 +1,24 @@ +SUBDIRS = includes + +SERVERDIR = /usr/share/koji-web/scripts +FILES = $(wildcard *.py *.chtml) + +_default: + @echo "nothing to make. try make install" + +clean: + rm -f *.o *.so *.pyc *~ + for d in $(SUBDIRS); do make -s -C $$d clean; done + +install: + @if [ "$(DESTDIR)" = "" ]; then \ + echo " "; \ + echo "ERROR: A destdir is required"; \ + exit 1; \ + fi + + mkdir -p $(DESTDIR)/$(SERVERDIR) + install -p -m 644 $(FILES) $(DESTDIR)/$(SERVERDIR) + + for d in $(SUBDIRS); do make DESTDIR=$(DESTDIR)/$(SERVERDIR) \ + -C $$d install; [ $$? = 0 ] || exit 1; done diff --git a/www/kojiweb/archiveinfo.chtml b/www/kojiweb/archiveinfo.chtml new file mode 100644 index 0000000..ee2d3f4 --- /dev/null +++ b/www/kojiweb/archiveinfo.chtml @@ -0,0 +1,158 @@ +#import koji +#from kojiweb import util +#from pprint import pformat +#import urllib + +#attr _PASSTHROUGH = ['archiveID', 'fileOrder', 'fileStart', 'buildrootOrder', 'buildrootStart'] + +#include "includes/header.chtml" +

Information for archive $archive.filename

+ + + + + + + #if $wininfo + + #else + + #end if + + #if $archive.metadata_only + + + + #end if + + + + + + + #if $maveninfo + + + + + + + + + + #end if + + + + + + + #if $wininfo + + + + + + + #end if + #if $builtInRoot + + + + #end if + #if $archive.get('extra') + + + + #end if + #if $files + + + + + #end if + + + + + #if 'rootid' in $archive and $archive.rootid + + + + #end if +
ID$archive.id
File Name$koji.pathinfo.winfile($archive)File Name$archive.filename
Metadata onlyTrue (file not imported)
File Type$archive_type.description
Build$koji.buildLabel($build)
Maven groupId$archive.group_id
Maven artifactId$archive.artifact_id
Maven version$archive.version
Size$archive.size
Checksum$archive.checksum
Platforms$archive.platforms
Flags$archive.flags
Buildroot$util.brLabel($builtInRoot)
Extra$util.escapeHTML($pformat($archive.extra))
Files + + + + + + + + + #for $file in $files + + + + #end for +
+ #if $len($filePages) > 1 +
+ Page: + +
+ #end if + #if $fileStart > 0 + <<< + #end if + #echo $fileStart + 1 # through #echo $fileStart + $fileCount # of $totalFiles + #if $fileStart + $fileCount < $totalFiles + >>> + #end if +
Name $util.sortImage($self, 'name', 'fileOrder')Size $util.sortImage($self, 'size', 'fileOrder')
$file.name$file.size
+
Component of + #if $len($buildroots) > 0 + + + + + + + + + + #for $buildroot in $buildroots + + + + + + #end for +
+ #if $len($buildrootPages) > 1 +
+ Page: + +
+ #end if + #if $buildrootStart > 0 + <<< + #end if + #echo $buildrootStart + 1 # through #echo $buildrootStart + $buildrootCount # of $totalBuildroots + #if $buildrootStart + $buildrootCount < $totalBuildroots + >>> + #end if +
Buildroot $util.sortImage($self, 'id', 'buildrootOrder')Created $util.sortImage($self, 'create_event_time', 'buildrootOrder')State $util.sortImage($self, 'state', 'buildrootOrder')
$util.brLabel($buildroot)$util.formatTime($buildroot.create_event_time)$util.imageTag($util.brStateName($buildroot.state))
+ #else + No buildroots + #end if +
Installed RPMs
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/archivelist.chtml b/www/kojiweb/archivelist.chtml new file mode 100644 index 0000000..cb5e0c1 --- /dev/null +++ b/www/kojiweb/archivelist.chtml @@ -0,0 +1,83 @@ +#from kojiweb import util + +#include "includes/header.chtml" + + #if $type == 'component' +

Component Archives of buildroot $util.brLabel($buildroot)

+ #else +

Archives built in buildroot $util.brLabel($buildroot)

+ #end if + + + + + + + + + #if $type == 'component' + + #end if + + #if $len($archives) > 0 + #for $archive in $archives + + + + #if $type == 'component' + #set $project = $archive.project and 'yes' or 'no' + + #end if + + #end for + #else + + + + #end if + + + +
+ #if $len($archivePages) > 1 +
+ Page: + +
+ #end if + #if $archiveStart > 0 + <<< + #end if + #if $totalArchives != 0 + Archives #echo $archiveStart + 1 # through #echo $archiveStart + $archiveCount # of $totalArchives + #end if + #if $archiveStart + $archiveCount < $totalArchives + >>> + #end if +
Filename $util.sortImage($self, 'filename')Type $util.sortImage($self, 'type_name')Build Dependency? $util.sortImage($self, 'project')
$archive.filename$archive.type_name$util.imageTag($project)
No Archives
+ #if $len($archivePages) > 1 +
+ Page: + +
+ #end if + #if $archiveStart > 0 + <<< + #end if + #if $totalArchives != 0 + Archives #echo $archiveStart + 1 # through #echo $archiveStart + $archiveCount # of $totalArchives + #end if + #if $archiveStart + $archiveCount < $totalArchives + >>> + #end if +
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/buildinfo.chtml b/www/kojiweb/buildinfo.chtml new file mode 100644 index 0000000..40a34c3 --- /dev/null +++ b/www/kojiweb/buildinfo.chtml @@ -0,0 +1,214 @@ +#import koji +#import koji.util +#from pprint import pformat +#from kojiweb import util + +#include "includes/header.chtml" +#set $nvrpath = $pathinfo.build($build) + +

Information for build $koji.buildLabel($build)

+ + + + + + + + + + + + + + + + + + #if $build.get('source') + + + + #end if + #if 'maven' in $typeinfo + + + + + + + + + + #end if + #if $summary + + + + #end if + #if $description + + + + #end if + + + + + #set $stateName = $util.stateName($build.state) + + + + + + + #if $build.state == $koji.BUILD_STATES.BUILDING + #if $estCompletion + + + + #end if + #else + + + + #end if + #if $task + + + + #end if + #if $build.get('extra') + + + + #end if + + + + + + + + + #for btype in $archiveIndex + #set $archivesByExt = $archiveIndex[btype] + #if not $archivesByExt + #continue + #end if + + + + + #end for + #if $changelog + + + + + #end if +
ID$build.id
Package Name$build.package_name
Version$build.version
Release$build.release
Epoch$build.epoch
Source$build['source']
Maven groupId$typeinfo.maven.group_id
Maven artifactId$typeinfo.maven.artifact_id
Maven version$typeinfo.maven.version
Summary$util.escapeHTML($summary)
Description$util.escapeHTML($description)
Built by$build.owner_name
State$stateName + #if $build.state == $koji.BUILD_STATES.BUILDING + #if $currentUser and ('admin' in $perms or $build.owner_id == $currentUser.id) + (cancel) + #end if + #end if +
Started$util.formatTimeLong($start_time)
Est. Completion$util.formatTimeLong($estCompletion)
Completed$util.formatTimeLong($build.completion_time)
Task$koji.taskLabel($task)
Extra$util.escapeHTML($pformat($build.extra))
Tags + #if $len($tags) > 0 + + #for $tag in $tags + + + + #end for +
$tag.name
+ #else + No tags + #end if +
RPMs + #if $len($rpmsByArch) > 0 + + #if $rpmsByArch.has_key('src') + + #for $rpm in $rpmsByArch['src'] + #set $rpmfile = '%(name)s-%(version)s-%(release)s.%(arch)s.rpm' % $rpm + #set $rpmpath = $pathinfo.rpm($rpm) + + + #if $rpm.metadata_only + + #else + + #end if + + #end for + #end if + #set $arches = $rpmsByArch.keys() + #silent $arches.sort() + #for $arch in $arches + #if $arch == 'src' + #silent continue + #end if + + + + + #for $rpm in $rpmsByArch[$arch] + + #set $rpmfile = '%(name)s-%(version)s-%(release)s.%(arch)s.rpm' % $rpm + #set $rpmpath = $pathinfo.rpm($rpm) + + + + #end for + #end for +
src
$rpmfile (info) (metadata only)$rpmfile (info) (download)
$arch + #if $task + #if $arch == 'noarch' + (build logs) + #else + (build logs) + #end if + #end if +
+ $rpmfile (info) (download) +
+ #else + No RPMs + #end if +
$btype.capitalize() Archives + + #set $exts = $archivesByExt.keys() + #for ext in $exts + + + + + #for $archive in $archivesByExt[$ext] + + + + #end for + #end for +
$ext + #if $task and $ext == $exts[0] + #if $btype == 'maven' + (build logs) + #elif $btype == 'win' + (build logs) + #elif $btype == 'image' + (build logs) + #else + (build logs) + #end if + #end if +
+ + #if $archive.metadata_only + $archive.display (info) + #else + $archive.display (info) (download) + #end if +
+
Changelog$util.escapeHTML($koji.util.formatChangelog($changelog))
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/buildrootinfo.chtml b/www/kojiweb/buildrootinfo.chtml new file mode 100644 index 0000000..e04781a --- /dev/null +++ b/www/kojiweb/buildrootinfo.chtml @@ -0,0 +1,62 @@ +#import koji +#from kojiweb import util +#from pprint import pformat + +#include "includes/header.chtml" + +

Information for buildroot $util.brLabel($buildroot)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #if $buildroot.get('extra') + + + + #end if + + + + + + + + + + + + +
Host$buildroot.host_name
Arch$buildroot.arch
ID$buildroot.id
Task$koji.taskLabel($task)
State$util.imageTag($util.brStateName($buildroot.state))
Created$util.formatTimeLong($buildroot.create_event_time)
Retired$util.formatTimeLong($buildroot.retire_event_time)
Repo ID$buildroot.repo_id
Repo Tag$buildroot.tag_name
Repo State$util.imageTag($util.repoStateName($buildroot.repo_state))
Repo Created$util.formatTimeLong($buildroot.repo_create_event_time)
Extra$util.escapeHTML($pformat($buildroot.extra))
Component RPMs
Built RPMs
Component Archives
Built Archives
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/buildrootinfo_cg.chtml b/www/kojiweb/buildrootinfo_cg.chtml new file mode 100644 index 0000000..8d2933d --- /dev/null +++ b/www/kojiweb/buildrootinfo_cg.chtml @@ -0,0 +1,47 @@ +#import koji +#from kojiweb import util +#from pprint import pformat + +#include "includes/header.chtml" + +

Information for external buildroot $util.brLabel($buildroot)

+ + + + + + + + + + + + + + + + + + + + + #if $buildroot.get('extra') + + + + #end if + + + + + + + + + + + + +
ID$buildroot.id
Host OS$buildroot.host_os
Host Arch$buildroot.host_arch
Content Generator$buildroot.cg_name ($buildroot.cg_version)
Container Type$buildroot.container_type
Container Arch$buildroot.container_arch
Extra$util.escapeHTML($pformat($buildroot.extra))
Component RPMs
Built RPMs
Component Archives
Built Archives
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/builds.chtml b/www/kojiweb/builds.chtml new file mode 100644 index 0000000..843955d --- /dev/null +++ b/www/kojiweb/builds.chtml @@ -0,0 +1,165 @@ +#import koji +#from kojiweb import util + +#attr _PASSTHROUGH = ['userID', 'tagID', 'packageID', 'order', 'prefix', 'state', 'inherited', 'latest', 'type'] + +#include "includes/header.chtml" + +

#if $latest then 'Latest ' else ''##if $state != None then $util.stateName($state).capitalize() + ' ' else ''##if $type then $type.capitalize() + ' ' else ''#Builds#if $package then ' of %s' % ($package.id, $package.name) else ''##if $prefix then ' starting with "%s"' % $prefix else ''##if $user then ' by %s' % ($user.id, $user.name) else ''##if $tag then ' in tag %s' % ($tag.id, $tag.name) else ''#

+ + + + + + + + + + + + + + + #if $tag + + #end if + + + + + #if $len($builds) > 0 + #for $build in $builds + + + + #if $tag + + #end if + + + #set $stateName = $util.stateName($build.state) + + + #end for + #else + + + + #end if + + + +
+ + + + + + #if $tag + + #end if + +
+ #if $tag + Latest: + + + #else + State: + + + #end if + + Built by: + + +
+ Type: + + + + Inherited: + + +
+
+ #for $char in $chars + #if $prefix == $char + $char + #else + $char + #end if + | + #end for + #if $prefix + all + #else + all + #end if +
+ #if $len($buildPages) > 1 +
+ Page: + +
+ #end if + #if $buildStart > 0 + <<< + #end if + #if $totalBuilds != 0 + Builds #echo $buildStart + 1 # through #echo $buildStart + $buildCount # of $totalBuilds + #end if + #if $buildStart + $buildCount < $totalBuilds + >>> + #end if +
ID $util.sortImage($self, 'build_id')NVR $util.sortImage($self, 'nvr')Tag $util.sortImage($self, 'tag_name')Built by $util.sortImage($self, 'owner_name')Finished $util.sortImage($self, 'completion_time')State $util.sortImage($self, 'state')
$build.build_id$koji.buildLabel($build)$build.tag_name$build.owner_name$util.formatTime($build.completion_time)$util.stateImage($build.state)
No builds
+ #if $len($buildPages) > 1 +
+ Page: + +
+ #end if + #if $buildStart > 0 + <<< + #end if + #if $totalBuilds != 0 + Builds #echo $buildStart + 1 # through #echo $buildStart + $buildCount # of $totalBuilds + #end if + #if $buildStart + $buildCount < $totalBuilds + >>> + #end if +
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/buildsbystatus.chtml b/www/kojiweb/buildsbystatus.chtml new file mode 100644 index 0000000..be7b6cb --- /dev/null +++ b/www/kojiweb/buildsbystatus.chtml @@ -0,0 +1,55 @@ +#from kojiweb import util + +#def printOption(value, label=None) +#if not $label +#set $label = $value +#end if + +#end def + +#include "includes/header.chtml" + +

Succeeded/Failed/Canceled Builds#if $days != -1 then ' in the last %i days' % $days else ''#

+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+ Show last + days +
+
TypeBuilds 
Succeededgraph row$numSucceeded
Failedgraph row$numFailed
Canceledgraph row$numCanceled
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/buildsbytarget.chtml b/www/kojiweb/buildsbytarget.chtml new file mode 100644 index 0000000..97a7e7b --- /dev/null +++ b/www/kojiweb/buildsbytarget.chtml @@ -0,0 +1,99 @@ +#from kojiweb import util + +#def printOption(value, label=None) +#if not $label +#set $label = $value +#end if + +#end def + +#include "includes/header.chtml" + +

Builds by Target#if $days != -1 then ' in the last %i days' % $days else ''#

+ + + + + + + + + + + + + #if $len($targets) > 0 + #for $target in $targets + + + + + + #end for + #else + + + + #end if + + + +
+
+ Show last + days +
+
+ #if $len($targetPages) > 1 +
+ Page: + +
+ #end if + #if $targetStart > 0 + <<< + #end if + #if $totalTargets != 0 + Build Targets #echo $targetStart + 1 # through #echo $targetStart + $targetCount # of $totalTargets + #end if + #if $targetStart + $targetCount < $totalTargets + >>> + #end if +
Name $util.sortImage($self, 'name')Builds $util.sortImage($self, 'builds') 
$target.namegraph row$target.builds
No builds
+ #if $len($targetPages) > 1 +
+ Page: + +
+ #end if + #if $targetStart > 0 + <<< + #end if + #if $totalTargets != 0 + Build Targets #echo $targetStart + 1 # through #echo $targetStart + $targetCount # of $totalTargets + #end if + #if $targetStart + $targetCount < $totalTargets + >>> + #end if +
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/buildsbyuser.chtml b/www/kojiweb/buildsbyuser.chtml new file mode 100644 index 0000000..002f645 --- /dev/null +++ b/www/kojiweb/buildsbyuser.chtml @@ -0,0 +1,73 @@ +#from kojiweb import util + +#include "includes/header.chtml" + +

Builds by User

+ + + + + + + + + + #if $len($userBuilds) > 0 + #for $userBuild in $userBuilds + + + + + + #end for + #else + + + + #end if + + + +
+ #if $len($userBuildPages) > 1 +
+ Page: + +
+ #end if + #if $userBuildStart > 0 + <<< + #end if + #if $totalUserBuilds != 0 + Users #echo $userBuildStart + 1 # through #echo $userBuildStart + $userBuildCount # of $totalUserBuilds + #end if + #if $userBuildStart + $userBuildCount < $totalUserBuilds + >>> + #end if +
Name $util.sortImage($self, 'name')Builds $util.sortImage($self, 'builds') 
$userBuild.namegraph row$userBuild.builds
No users
+ #if $len($userBuildPages) > 1 +
+ Page: + +
+ #end if + #if $userBuildStart > 0 + <<< + #end if + #if $totalUserBuilds != 0 + Users #echo $userBuildStart + 1 # through #echo $userBuildStart + $userBuildCount # of $totalUserBuilds + #end if + #if $userBuildStart + $userBuildCount < $totalUserBuilds + >>> + #end if +
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/buildtargetedit.chtml b/www/kojiweb/buildtargetedit.chtml new file mode 100644 index 0000000..647a87c --- /dev/null +++ b/www/kojiweb/buildtargetedit.chtml @@ -0,0 +1,63 @@ +#from kojiweb import util + +#include "includes/header.chtml" + + #if $target +

Edit target $target.name

+ #else +

Create build target

+ #end if + +
+ $util.authToken($self, form=True) + #if $target + + #end if + + + + + + #if $target + + + + #end if + + + + + + + + + + + + +
Name + +
ID$target.id
Build Tag + +
Destination Tag + +
+ #if $target + + #else + + #end if +
+
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/buildtargetinfo.chtml b/www/kojiweb/buildtargetinfo.chtml new file mode 100644 index 0000000..42a51f6 --- /dev/null +++ b/www/kojiweb/buildtargetinfo.chtml @@ -0,0 +1,30 @@ +#from kojiweb import util + +#include "includes/header.chtml" + +

Information for target $target.name

+ + + + + + + + + + + + + + + #if 'admin' in $perms + + + + + + + #end if +
Name$target.name
ID$target.id
Build Tag$buildTag.name
Destination Tag$destTag.name
Edit
Delete
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/buildtargets.chtml b/www/kojiweb/buildtargets.chtml new file mode 100644 index 0000000..ad02cbd --- /dev/null +++ b/www/kojiweb/buildtargets.chtml @@ -0,0 +1,76 @@ +#from kojiweb import util + +#include "includes/header.chtml" + +

Build Targets

+ + + + + + + + + #if $len($targets) > 0 + #for $target in $targets + + + + + #end for + #else + + + + #end if + + + +
+ #if $len($targetPages) > 1 +
+ Page: + +
+ #end if + #if $targetStart > 0 + <<< + #end if + #if $totalTargets != 0 + Targets #echo $targetStart + 1 # through #echo $targetStart + $targetCount # of $totalTargets + #end if + #if $targetStart + $targetCount < $totalTargets + >>> + #end if +
ID $util.sortImage($self, 'id')Name $util.sortImage($self, 'name')
$target.id$target.name
No build targets
+ #if $len($targetPages) > 1 +
+ Page: + +
+ #end if + #if $targetStart > 0 + <<< + #end if + #if $totalTargets != 0 + Targets #echo $targetStart + 1 # through #echo $targetStart + $targetCount # of $totalTargets + #end if + #if $targetStart + $targetCount < $totalTargets + >>> + #end if +
+ + #if 'admin' in $perms +
+ Create new Build Target + #end if + +#include "includes/footer.chtml" diff --git a/www/kojiweb/channelinfo.chtml b/www/kojiweb/channelinfo.chtml new file mode 100644 index 0000000..31dcdc5 --- /dev/null +++ b/www/kojiweb/channelinfo.chtml @@ -0,0 +1,31 @@ +#from kojiweb import util + +#include "includes/header.chtml" + +

Information for channel $channel.name

+ + + + + + + + + + + + + + + +
Name$channel.name
ID$channel.id
Active Tasks$taskCount
Hosts + #if $len($hosts) > 0 + #for $host in $hosts + $host.name
+ #end for + #else + No hosts + #end if +
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/error.chtml b/www/kojiweb/error.chtml new file mode 100644 index 0000000..5113805 --- /dev/null +++ b/www/kojiweb/error.chtml @@ -0,0 +1,30 @@ + +#from kojiweb import util + +#include "includes/header.chtml" + +

Error

+ +
+$util.escapeHTML($explanation) +
+ +#if $debug_level >= 1 +
+#else +
+#end if +$util.escapeHTML($tb_short) +
+ +#if $debug_level >= 2 +
+#else +
+#end if +
+#echo $util.escapeHTML($tb_long)
+
+
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/externalrepoinfo.chtml b/www/kojiweb/externalrepoinfo.chtml new file mode 100644 index 0000000..140331a --- /dev/null +++ b/www/kojiweb/externalrepoinfo.chtml @@ -0,0 +1,31 @@ +#from kojiweb import util + +#include "includes/header.chtml" + +

Information for external repo $extRepo.name

+ + + + + + + + + + + + + + + +
Name$extRepo.name
ID$extRepo.id
URL$extRepo.url
Tags using this external repo + #if $len($repoTags) + #for $tag in $repoTags + $tag.tag_name
+ #end for + #else + No tags + #end if +
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/fileinfo.chtml b/www/kojiweb/fileinfo.chtml new file mode 100644 index 0000000..7d2e1f7 --- /dev/null +++ b/www/kojiweb/fileinfo.chtml @@ -0,0 +1,64 @@ +#from kojiweb import util +#import urllib +#import datetime + +#include "includes/header.chtml" + #if $rpm +

Information for file $file.name

+ #elif $archive +

Information for file $file.name

+ #end if + + + + + + #if $rpm + + + + #end if + + + + #if $file.has_key('mtime') and $file.mtime + + + + #end if + #if $file.has_key('user') and $file.user + + + + #end if + #if $file.has_key('group') and $file.group + + + + #end if + #if $file.has_key('mode') and $file.mode + + + + #end if + #if $rpm + + + + + + #set $epoch = ($rpm.epoch != None and $str($rpm.epoch) + ':' or '') + + + #elif $archive + + + + #end if +
Name$file.name
Digest ($file.digest_algo)$file.digest
Size$file.size
Modification time$util.formatTimeLong($datetime.datetime.fromtimestamp($file.mtime))
User$file.user
Group$file.group
Mode$util.formatMode($file.mode)
Flags + #for flag in $util.formatFileFlags($file.flags) + $flag
+ #end for +
RPM$rpm.name-$epoch$rpm.version-$rpm.release.${rpm.arch}.rpm
Archive$archive.filename
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/hostedit.chtml b/www/kojiweb/hostedit.chtml new file mode 100644 index 0000000..00a64c9 --- /dev/null +++ b/www/kojiweb/hostedit.chtml @@ -0,0 +1,57 @@ +#from kojiweb import util + +#include "includes/header.chtml" + +

Edit host $host.name

+ +
+ $util.authToken($self, form=True) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name$host.name
ID + $host.id + +
Arches
Capacity
Description
Comment
Enabled? +
Channels + +
+
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/hostinfo.chtml b/www/kojiweb/hostinfo.chtml new file mode 100644 index 0000000..c7674d9 --- /dev/null +++ b/www/kojiweb/hostinfo.chtml @@ -0,0 +1,91 @@ +#from kojiweb import util + +#include "includes/header.chtml" + +

Information for host $host.name

+ + + + + + + + + + + + + + + + + + + + + + + + + #set $enabled = $host.enabled and 'yes' or 'no' + + + + + #set $ready = $host.ready and 'yes' or 'no' + + + + + + + + + + + + #if $buildroots + + #else + + #end if + + #if 'admin' in $perms + + + + #end if +
Name$host.name
ID$host.id
Arches$host.arches
Capacity$host.capacity
Task Load#echo '%.2f' % $host.task_load#
Description$util.escapeHTML($host.description)
Comment$util.escapeHTML($host.comment)
Enabled? + $util.imageTag($enabled) + #if 'admin' in $perms + #if $host.enabled + (disable) + #else + (enable) + #end if + #end if +
Ready?$util.imageTag($ready)
Last Update$util.formatTime($lastUpdate)
Channels + #for $channel in $channels + $channel.name
+ #end for + #if not $channels + No channels + #end if +
Active Buildroots + + + + + #for $buildroot in $buildroots + + + + + + #end for +
BuildrootCreatedState
$buildroot.tag_name-$buildroot.id-$buildroot.repo_id$util.formatTime($buildroot.create_event_time)$util.imageTag($util.brStateName($buildroot.state))
+
+ No buildroots +
Edit host
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/hosts.chtml b/www/kojiweb/hosts.chtml new file mode 100644 index 0000000..0750f77 --- /dev/null +++ b/www/kojiweb/hosts.chtml @@ -0,0 +1,96 @@ +#from kojiweb import util + +#attr _PASSTHROUGH = ['state', 'order'] + +#include "includes/header.chtml" + +

Hosts

+ + + + + + + + + + + + + + + + #if $len($hosts) > 0 + #for $host in $hosts + + + + + + + + + #end for + #else + + + + #end if + + + +
+ + +
+ State: + + +
+
+ #if $len($hostPages) > 1 +
+ Page: + +
+ #end if + #if $hostStart > 0 + <<< + #end if + #if $totalHosts != 0 + Hosts #echo $hostStart + 1 # through #echo $hostStart + $hostCount # of $totalHosts + #end if + #if $hostStart + $hostCount < $totalHosts + >>> + #end if +
ID $util.sortImage($self, 'id')Name $util.sortImage($self, 'name')Arches $util.sortImage($self, 'arches')Enabled? $util.sortImage($self, 'enabled')Ready? $util.sortImage($self, 'ready')Last Update $util.sortImage($self, 'last_update')
$host.id$host.name$host.arches#if $host.enabled then $util.imageTag('yes') else $util.imageTag('no')##if $host.ready then $util.imageTag('yes') else $util.imageTag('no')#$util.formatTime($host.last_update)
No hosts
+ #if $len($hostPages) > 1 +
+ Page: + +
+ #end if + #if $hostStart > 0 + <<< + #end if + #if $totalHosts != 0 + Hosts #echo $hostStart + 1 # through #echo $hostStart + $hostCount # of $totalHosts + #end if + #if $hostStart + $hostCount < $totalHosts + >>> + #end if +
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/imageinfo.chtml b/www/kojiweb/imageinfo.chtml new file mode 100644 index 0000000..69a9e50 --- /dev/null +++ b/www/kojiweb/imageinfo.chtml @@ -0,0 +1,60 @@ +#import koji +#import koji.util +#from os.path import basename +#from kojiweb import util + +#include "includes/header.chtml" + +

Information for image $image.filename

+ + + + + + + + + + + + + + + + + + + #if $len($image.hash) == 32 + + #elif $len($image.hash) == 40 + + #elif $len($image.hash) == 64 + + #elif $len($image.hash) == 96 + + #elif $len($image.hash) == 128 + + #else + + #end if + + + + + + + + + + + + + + #if $image.get('xmlfile', None) + + + + #end if +
ID$image.id
File Name$image.filename
File Size$image.filesize
Arch$image.arch
Media Type$image.mediatype
Digest (md5)$image.hashDigest (sha1)$image.hashDigest (sha256)$image.hashDigest (sha384)$image.hashDigest (sha512)$image.hashHash $image.hash
Task$koji.taskLabel($task)
Buildroot/var/lib/mock/$buildroot.tag_name-$buildroot.id-$buildroot.repo_id
Included RPMs
Download Image (build logs)
Download XML Description
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/includes/Makefile b/www/kojiweb/includes/Makefile new file mode 100644 index 0000000..ab25127 --- /dev/null +++ b/www/kojiweb/includes/Makefile @@ -0,0 +1,18 @@ +SERVERDIR = /includes +FILES = $(wildcard *.chtml) + +_default: + @echo "nothing to make. try make install" + +clean: + rm -f *.o *.so *.pyc *~ + +install: + @if [ "$(DESTDIR)" = "" ]; then \ + echo " "; \ + echo "ERROR: A destdir is required"; \ + exit 1; \ + fi + + mkdir -p $(DESTDIR)/$(SERVERDIR) + install -p -m 644 $(FILES) $(DESTDIR)/$(SERVERDIR) diff --git a/www/kojiweb/includes/footer.chtml b/www/kojiweb/includes/footer.chtml new file mode 100644 index 0000000..1b197f0 --- /dev/null +++ b/www/kojiweb/includes/footer.chtml @@ -0,0 +1,27 @@ +#from kojiweb import util +
+ + + +#set $localfooterpath=$util.themePath("extra-footer.html", local=True) +#if os.path.exists($localfooterpath) +#if $literalFooter +#set $localfooter="".join(open($localfooterpath).readlines()) +$localfooter +#else +#include $localfooterpath +#end if +#end if + +
+ +#set $localbottompath=$util.themePath("extra-bottom.html", local=True) +#if os.path.exists($localbottompath) +#set $localbottom="".join(open($localbottompath).readlines()) +$localbottom +#end if + + diff --git a/www/kojiweb/includes/header.chtml b/www/kojiweb/includes/header.chtml new file mode 100644 index 0000000..e927d12 --- /dev/null +++ b/www/kojiweb/includes/header.chtml @@ -0,0 +1,106 @@ +#encoding utf-8 +#import koji +#from kojiweb import util +#import random + + +#def greeting() +#set $greetings = ('hello', 'hi', 'yo', "what's up", "g'day", 'back to work', + 'bonjour', + 'hallo', + 'ciao', + 'hola', + u'olá', + u'dobrý den', + u'zdravstvuite', + u'góðan daginn', + 'hej', + 'tervehdys', + u'grüezi', + u'céad míle fáilte', + u'hylô', + u'bună ziua', + u'jó napot', + 'dobre dan', + u'你好', + u'こんにちは', + u'नमस्कार', + u'안녕하세요') +#echo $random.choice($greetings)##slurp +#end def + + + + + $title | $siteName + + + + + + + +
+
+ + + + + + + + + $koji.formatTimeLong($currentDate) + #if not $LoginDisabled + | + #if $currentUser + $greeting(), $currentUser.name | logout + #else + login + #end if + #end if + + +
diff --git a/www/kojiweb/index.chtml b/www/kojiweb/index.chtml new file mode 100644 index 0000000..738256c --- /dev/null +++ b/www/kojiweb/index.chtml @@ -0,0 +1,161 @@ +#import koji +#from kojiweb import util + +#include "includes/header.chtml" + + + +
#if $user then 'Your ' else ''#Recent Builds
+ + + + + #if not $user + + #end if + + + + #for $build in $builds + + #set $stateName = $util.stateName($build.state) + + + #if not $user + + #end if + + + + #end for + #if not $builds + + + + #end if +
ID $util.sortImage(self, 'id')NVRBuilt byFinishedState
$build.build_id$build.nvr$build.owner_name$util.formatTime($build.completion_time)$util.stateImage($build.state)
No builds
+ +
+ +
#if $user then 'Your ' else ''#Recent Tasks
+ + + + + #if not $user + + #end if + + + + + #for $task in $tasks + #set $scratch = $util.taskScratchClass($task) + + #set $state = $util.taskState($task.state) + + + #if not $user + + #end if + + + + + #end for + #if not $tasks + + + + #end if +
ID $util.sortImage($self, 'id')TypeOwnerArchFinishedState
$task.id$koji.taskLabel($task) + #if $task.owner_type == $koji.USERTYPES['HOST'] + $task.owner_name + #else + $task.owner_name + #end if + $task.arch$util.formatTime($task.completion_time)$util.imageTag($state)
No tasks
+ + #if $user +
+ +
Your Packages
+ + + + + + + + + + #for $package in $packages + + + + #set $included = $package.blocked and 'no' or 'yes' + + + #end for + #if $totalPackages == 0 + + + + #end if +
+ #if $len($packagePages) > 1 +
+ Page: + +
+ #end if + #if $packageStart > 0 + <<< + #end if + #if $totalPackages != 0 + Package #echo $packageStart + 1 # through #echo $packageStart + $packageCount # of $totalPackages + #end if + #if $packageStart + $packageCount < $totalPackages + >>> + #end if +
Name $util.sortImage($self, 'package_name', 'packageOrder')Tag $util.sortImage($self, 'tag_name', 'packageOrder')Included? $util.sortImage($self, 'blocked', 'packageOrder')
$package.package_name$package.tag_name$util.imageTag($included)
No packages
+ +
+ +
Your Notifications
+ + + + + + + + + + + + #for $notif in $notifs + + + + + + + + #end for + #if $len($notifs) == 0 + + + + #end if +
PackageTagType
#if $notif.package then $notif.package.name else 'all'##if $notif.tag then $notif.tag.name else 'all'##if $notif.success_only then 'success only' else 'all'#editdelete
No notifications
+ +
+ Add a Notification + #end if + +#include "includes/footer.chtml" diff --git a/www/kojiweb/index.py b/www/kojiweb/index.py new file mode 100644 index 0000000..7f4e4dd --- /dev/null +++ b/www/kojiweb/index.py @@ -0,0 +1,2294 @@ +# 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 os +import os.path +import re +import sys +import mimetypes +import Cookie +import datetime +import logging +import time +import koji +import kojiweb.util +from koji.server import ServerRedirect +from kojiweb.util import _initValues +from kojiweb.util import _genHTML +from kojiweb.util import _getValidTokens +from koji.util import sha1_constructor + +# Convenience definition of a commonly-used sort function +_sortbyname = kojiweb.util.sortByKeyFunc('name') + +#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' + shasum = sha1_constructor(value) + shasum.update(options['Secret'].value) + value = "%s:%s" % (shasum.hexdigest(), value) + cookies = Cookie.SimpleCookie() + cookies['user'] = value + c = cookies['user'] #morsel instance + c['secure'] = True + c['path'] = os.path.dirname(environ['SCRIPT_NAME']) + # the Cookie module treats integer expire times as relative seconds + c['expires'] = int(options['LoginTimeout']) * 60 * 60 + out = c.OutputString() + out += '; HttpOnly' + environ['koji.headers'].append(['Set-Cookie', out]) + environ['koji.headers'].append(['Cache-Control', 'no-cache="set-cookie"']) + +def _clearUserCookie(environ): + cookies = Cookie.SimpleCookie() + cookies['user'] = '' + c = cookies['user'] #morsel instance + c['path'] = os.path.dirname(environ['SCRIPT_NAME']) + c['expires'] = 0 + out = c.OutputString() + environ['koji.headers'].append(['Set-Cookie', out]) + +def _getUserCookie(environ): + options = environ['koji.options'] + cookies = Cookie.SimpleCookie(environ.get('HTTP_COOKIE','')) + if 'user' not in cookies: + return None + value = cookies['user'].value + parts = value.split(":", 1) + if len(parts) != 2: + authlogger.warn('malformed user cookie: %s' % value) + return None + sig, value = parts + if not options['Secret'].value: + raise koji.AuthError, 'Unable to authenticate, server secret not configured' + shasum = sha1_constructor(value) + shasum.update(options['Secret'].value) + if shasum.hexdigest() != sig: + authlogger.warn('invalid user cookie: %s:%s', sig, value) + return None + parts = value.split(":", 1) + if len(parts) != 2: + authlogger.warn('invalid signed user cookie: %s:%s', sig, value) + # no embedded timestamp + return None + user, timestamp = parts + try: + timestamp = float(timestamp) + except ValueError: + authlogger.warn('invalid time in signed user cookie: %s:%s', sig, value) + return None + if (time.time() - timestamp) > (int(options['LoginTimeout']) * 60 * 60): + authlogger.info('expired user cookie: %s', value) + return None + # Otherwise, cookie is valid and current + return user + +def _krbLogin(environ, session, principal): + options = environ['koji.options'] + wprinc = options['WebPrincipal'] + keytab = options['WebKeytab'] + ccache = options['WebCCache'] + return session.krb_login(principal=wprinc, keytab=keytab, + ccache=ccache, proxyuser=principal) + +def _sslLogin(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 StandardError, '_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 _krbLogin(environ, environ['koji.session'], environ['koji.currentLogin']): + raise koji.AuthError, 'could not login using principal: %s' % environ['koji.currentLogin'] + else: + raise koji.AuthError, 'KojiWeb is incorrectly configured for authentication, contact the system administrator' + + # verify a valid authToken was passed in to avoid CSRF + authToken = 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): + opts = environ['koji.options'] + session = koji.ClientSession(opts['KojiHubURL'], + opts={'krbservice': opts['KrbService'], + 'krb_rdns': opts['KrbRDNS']}) + + 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): + if page: + # We'll work with the page we were given + pass + elif 'HTTP_REFERER' in environ: + 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_kerb should have performed authentication before presenting this page' + + if not _krbLogin(environ, session, principal): + raise koji.AuthError, 'could not login using principal: %s' % principal + + username = principal + authlogger.info('Successful Kerberos authentication by %s', username) + else: + raise koji.AuthError, 'KojiWeb is incorrectly configured for authentication, contact the system administrator' + + _setUserCookie(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(kojiweb.util.sortByKeyFunc('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 == None: + raise koji.GenericError, 'no notification with ID: %i' % notificationID + + form = environ['koji.form'] + + if form.has_key('save'): + 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 form.has_key('success_only'): + success_only = True + else: + success_only = False + + server.updateNotification(notification['id'], package_id, tag_id, success_only) + + _redirect(environ, 'index') + elif form.has_key('cancel'): + _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 form.has_key('add'): + 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 form.has_key('success_only'): + success_only = True + else: + success_only = False + + server.createNotification(user['id'], package_id, tag_id, success_only) + + _redirect(environ, 'index') + elif form.has_key('cancel'): + _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', + 'buildArch', + 'chainbuild', + 'maven', + 'buildMaven', + 'chainmaven', + 'wrapperRPM', + 'winbuild', + 'vmExec', + 'waitrepo', + 'tagBuild', + 'newRepo', + 'createrepo', + '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', 'tagBuild', 'tagNotification', 'waitrepo', 'livecd', 'appliance', 'image', 'livemedia'] +# Tasks that can have children +_PARENT_TASKS = ['build', 'chainbuild', 'maven', 'chainmaven', 'winbuild', 'newRepo', '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) + + opts = {'decode': True} + if owner: + if owner.isdigit(): + owner = int(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'] = _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: + try: + channelID = int(channelID) + except ValueError: + pass + 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 != 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'] == 'buildArch': + buildTag = server.getTag(params[1]) + values['buildTag'] = buildTag + elif task['method'] == 'buildMaven': + buildTag = params[1] + values['buildTag'] = buildTag + elif task['method'] == 'buildSRPMFromSCM': + if len(params) > 1: + buildTag = server.getTag(params[1]) + values['buildTag'] = buildTag + elif task['method'] == 'tagBuild': + destTag = server.getTag(params[0]) + build = server.getBuild(params[1]) + values['destTag'] = destTag + values['build'] = build + elif task['method'] == 'newRepo': + 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) + + if task['state'] in (koji.TASK_STATES['CLOSED'], koji.TASK_STATES['FAILED']): + try: + result = server.getTaskResult(task['id']) + values['result'] = result + values['excClass'] = None + except: + excClass, exc = sys.exc_info()[:2] + values['result'] = exc + values['excClass'] = excClass + # clear the exception, since we're just using + # it for display purposes + sys.exc_clear() + 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 + + output = server.listTaskOutput(task['id']) + output.sort(_sortByExtAndName) + values['output'] = output + if environ['koji.currentUser']: + values['perms'] = server.getUserPerms(environ['koji.currentUser']['id']) + else: + values['perms'] = [] + + topurl = environ['koji.options']['KojiFilesURL'] + values['pathinfo'] = koji.PathInfo(topdir=topurl) + + 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) + output = '%i:%s\n' % (task['id'], koji.TASK_STATES[task['state']]) + for filename, file_stats in files.items(): + output += '%s:%s\n' % (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(a, b): + """Sort two filenames, first by extension, and then by name.""" + aRoot, aExt = os.path.splitext(a) + bRoot, bExt = os.path.splitext(b) + return cmp(aExt, bExt) or cmp(aRoot, bRoot) + +def getfile(environ, taskID, name, offset=None, size=None): + server = _getServer(environ) + taskID = int(taskID) + + output = server.listTaskOutput(taskID, stat=True) + file_info = output.get(name) + if not file_info: + 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) + + +def _chunk_file(server, environ, taskID, name, offset, size): + remaining = size + encode_int = koji.encode_int + while True: + if remaining <= 0: + break + chunk_size = 1048576 + if remaining < chunk_size: + chunk_size = remaining + content = server.downloadTaskOutput(taskID, name, offset=encode_int(offset), size=chunk_size) + 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 == 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'] = [] + + values['childID'] = childID + + return _genHTML(environ, 'tags.chtml') + +_PREFIX_CHARS = [chr(char) for char in range(48, 58) + 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 != None: + if tagID.isdigit(): + tagID = int(tagID) + tag = server.getTag(tagID, strict=True) + values['tagID'] = tagID + values['tag'] = tag + user = None + if userID != None: + if userID.isdigit(): + userID = int(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) + + if packageID.isdigit(): + packageID = int(packageID) + package = server.getPackage(packageID) + if package == 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) + + if tagID.isdigit(): + tagID = int(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 not tagsByChild.has_key(child_id): + tagsByChild[child_id] = [] + tagsByChild[child_id].append(child_id) + + srcTargets = server.getBuildTargets(buildTagID=tag['id']) + srcTargets.sort(_sortbyname) + destTargets = server.getBuildTargets(destTagID=tag['id']) + destTargets.sort(_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 != 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 form.has_key('add'): + params = {} + name = form['name'].value + params['arches'] = form['arches'].value + params['locked'] = bool(form.has_key('locked')) + permission = form['permission'].value + if permission != 'none': + params['perm'] = int(permission) + if mavenEnabled: + params['maven_support'] = bool(form.has_key('maven_support')) + params['maven_include_all'] = bool(form.has_key('maven_include_all')) + + tagID = server.createTag(name, **params) + + _redirect(environ, 'taginfo?tagID=%i' % tagID) + elif form.has_key('cancel'): + _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 == None: + raise koji.GenericError, 'no tag with ID: %i' % tagID + + form = environ['koji.form'] + + if form.has_key('save'): + params = {} + params['name'] = form['name'].value + params['arches'] = form['arches'].value + params['locked'] = bool(form.has_key('locked')) + permission = form['permission'].value + if permission == 'none': + params['perm'] = None + else: + params['perm'] = int(permission) + if mavenEnabled: + params['maven_support'] = bool(form.has_key('maven_support')) + params['maven_include_all'] = bool(form.has_key('maven_include_all')) + + server.editTag2(tag['id'], **params) + + _redirect(environ, 'taginfo?tagID=%i' % tag['id']) + elif form.has_key('cancel'): + _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 == 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 form.has_key('add') or form.has_key('save'): + 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(form.has_key('intransitive')) + newDatum['noconfig'] = bool(form.has_key('noconfig')) + newDatum['pkg_filter'] = form.getfirst('pkg_filter') + + data = server.getInheritanceData(tag['id']) + data.append(newDatum) + + server.setInheritanceData(tag['id'], data) + elif form.has_key('cancel'): + 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) + + if extrepoID.isdigit(): + extrepoID = int(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) + + build = server.getBuild(buildID) + + values['title'] = koji.buildLabel(build) + ' | Build Info' + + tags = server.listTags(build['id']) + tags.sort(_sortbyname) + rpms = server.listBuildRPMs(build['id']) + rpms.sort(_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 rpmsByArch.has_key('src'): + srpm = rpmsByArch['src'][0] + headers = server.getRPMHeaders(srpm['id'], headers=['summary', 'description']) + values['summary'] = koji.fixEncoding(headers.get('summary')) + values['description'] = koji.fixEncoding(headers.get('description')) + values['changelog'] = server.getChangelogEntries(build['id']) + + noarch_log_dest = 'noarch' + if build['task_id']: + task = server.getTaskInfo(build['task_id'], request=True) + if rpmsByArch.has_key('noarch') and \ + [a for a in rpmsByArch.keys() if a not in ('noarch', 'src')]: + # This build has noarch and other-arch packages, indicating either + # noarch in extra-arches (kernel) or noarch subpackages. + # Point the log link to the arch of the buildArch task that the first + # noarch package came from. This will be correct in both the + # extra-arches case (noarch) and the subpackage case (one of the other + # arches). If noarch extra-arches and noarch subpackages are mixed in + # same build, this will become incorrect. + noarch_rpm = rpmsByArch['noarch'][0] + if noarch_rpm['buildroot_id']: + noarch_buildroot = server.getBuildroot(noarch_rpm['buildroot_id']) + if noarch_buildroot: + noarch_task = server.getTaskInfo(noarch_buildroot['task_id'], request=True) + if noarch_task: + noarch_log_dest = noarch_task['request'][2] + + # 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=['summary', 'description']) + if srpm_headers: + values['summary'] = koji.fixEncoding(srpm_headers['summary']) + values['description'] = koji.fixEncoding(srpm_headers['description']) + changelog = server.getChangelogEntries(taskID=srpm_task['id'], filepath=srpm_path) + if changelog: + values['changelog'] = changelog + else: + task = None + + values['build'] = build + values['tags'] = tags + values['rpmsByArch'] = rpmsByArch + values['task'] = task + values['typeinfo'] = typeinfo + values['archiveIndex'] = archiveIndex + + values['noarch_log_dest'] = noarch_log_dest + if environ['koji.currentUser']: + values['perms'] = server.getUserPerms(environ['koji.currentUser']['id']) + else: + values['perms'] = [] + for field in ['summary', 'description', 'changelog']: + if not values.has_key(field): + values[field] = None + + values['start_time'] = build.get('start_time') or build['creation_time'] + # 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_time'] = task['start_time'] + if build['state'] == koji.BUILD_STATES['BUILDING']: + avgDuration = server.getAverageBuildDuration(build['package_id']) + if avgDuration != 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: + if userID.isdigit(): + userID = int(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: + if tagID.isdigit(): + tagID = int(tagID) + tag = server.getTag(tagID, strict=True) + values['tagID'] = tagID + values['tag'] = tag + + package = None + if packageID: + if packageID.isdigit(): + packageID = int(packageID) + package = server.getPackage(packageID, strict=True) + values['packageID'] = packageID + values['package'] = package + + if state == 'all': + state = None + elif state != 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 = [b['name'] for b in server.listBTypes()] + btypes.sort() + 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) + + if userID.isdigit(): + userID = int(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') + +def rpminfo(environ, rpmID, fileOrder='name', fileStart=None, buildrootOrder='-id', buildrootStart=None): + values = _initValues(environ, 'RPM Info', 'builds') + server = _getServer(environ) + + rpmID = int(rpmID) + rpm = server.getRPM(rpmID) + + values['title'] = '%(name)s-%%s%(version)s-%(release)s.%(arch)s.rpm' % rpm + ' | RPM Info' + epochStr = '' + if rpm['epoch'] != None: + epochStr = '%s:' % rpm['epoch'] + values['title'] = values['title'] % epochStr + + build = None + if rpm['build_id'] != None: + build = server.getBuild(rpm['build_id']) + builtInRoot = None + if rpm['buildroot_id'] != None: + builtInRoot = server.getBuildroot(rpm['buildroot_id']) + if rpm['external_repo_id'] == 0: + values['provides'] = server.getRPMDeps(rpm['id'], koji.DEP_PROVIDE) + values['provides'].sort(_sortbyname) + values['obsoletes'] = server.getRPMDeps(rpm['id'], koji.DEP_OBSOLETE) + values['obsoletes'].sort(_sortbyname) + values['conflicts'] = server.getRPMDeps(rpm['id'], koji.DEP_CONFLICT) + values['conflicts'].sort(_sortbyname) + values['requires'] = server.getRPMDeps(rpm['id'], koji.DEP_REQUIRE) + values['requires'].sort(_sortbyname) + if koji.RPM_SUPPORTS_OPTIONAL_DEPS: + values['optional_deps'] = True + values['recommends'] = server.getRPMDeps(rpm['id'], koji.DEP_RECOMMEND) + values['recommends'].sort(_sortbyname) + values['suggests'] = server.getRPMDeps(rpm['id'], koji.DEP_SUGGEST) + values['suggests'].sort(_sortbyname) + values['supplements'] = server.getRPMDeps(rpm['id'], koji.DEP_SUPPLEMENT) + values['supplements'].sort(_sortbyname) + values['enhances'] = server.getRPMDeps(rpm['id'], koji.DEP_ENHANCE) + values['enhances'].sort(_sortbyname) + else: + values['optional_deps'] = False + headers = server.getRPMHeaders(rpm['id'], headers=['summary', 'description']) + values['summary'] = koji.fixEncoding(headers.get('summary')) + values['description'] = koji.fixEncoding(headers.get('description')) + 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'] != 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 + + 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 == 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: + if hostID.isdigit(): + hostID = int(hostID) + host = server.getHost(hostID) + if host == 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 == 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(_sortbyname) + buildroots = server.listBuildroots(hostID=host['id'], + state=[state[1] for state in koji.BR_STATES.items() if state[0] != 'EXPIRED']) + buildroots.sort(kojiweb.util.sortByKeyFunc('-create_event_time')) + + 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 == None: + raise koji.GenericError, 'no host with ID: %i' % hostID + + form = environ['koji.form'] + + if form.has_key('save'): + arches = form['arches'].value + capacity = float(form['capacity'].value) + description = form['description'].value + comment = form['comment'].value + enabled = bool(form.has_key('enabled')) + 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 form.has_key('cancel'): + _redirect(environ, 'hostinfo?hostID=%i' % host['id']) + else: + values = _initValues(environ, 'Edit Host', 'hosts') + + values['host'] = host + allChannels = server.listChannels() + allChannels.sort(_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 == 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(_sortbyname) + + values['channel'] = channel + values['hosts'] = hosts + + return _genHTML(environ, 'channelinfo.chtml') + +def buildrootinfo(environ, buildrootID, builtStart=None, builtOrder=None, componentStart=None, componentOrder=None): + values = _initValues(environ, 'Buildroot Info', 'hosts') + server = _getServer(environ) + + buildrootID = int(buildrootID) + buildroot = server.getBuildroot(buildrootID) + + if buildroot == 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 != None: + buildrootID = int(buildrootID) + buildroot = server.getBuildroot(buildrootID) + values['buildroot'] = buildroot + if buildroot == 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 != 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, buildrootID, type, start=None, order='filename'): + values = _initValues(environ, 'Archive List', 'hosts') + server = _getServer(environ) + + buildrootID = int(buildrootID) + buildroot = server.getBuildroot(buildrootID) + if buildroot == 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, 'invalid type: %s' % type + + values['buildroot'] = buildroot + 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 != None: + targetID = int(targetID) + target = server.getBuildTarget(targetID) + elif name != None: + target = server.getBuildTarget(name) + + if target == 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 == None: + raise koji.GenericError, 'invalid build target: %s' % targetID + + form = environ['koji.form'] + + if form.has_key('save'): + name = form.getfirst('name') + buildTagID = int(form.getfirst('buildTag')) + buildTag = server.getTag(buildTagID) + if buildTag == None: + raise koji.GenericError, 'invalid tag ID: %i' % buildTagID + + destTagID = int(form.getfirst('destTag')) + destTag = server.getTag(destTagID) + if destTag == 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 form.has_key('cancel'): + _redirect(environ, 'buildtargetinfo?targetID=%i' % target['id']) + else: + values = _initValues(environ, 'Edit Build Target', 'buildtargets') + tags = server.listTags() + tags.sort(_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 form.has_key('add'): + # 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 == None: + raise koji.GenericError, 'error creating build target "%s"' % name + + _redirect(environ, 'buildtargetinfo?targetID=%i' % target['id']) + elif form.has_key('cancel'): + _redirect(environ, 'buildtargets') + else: + values = _initValues(environ, 'Add Build Target', 'builtargets') + + tags = server.listTags() + tags.sort(_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 == 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) + + 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 = server.getAllArches() + hostArchList.sort() + values['hostArchList'] = hostArchList + values['rpmArch'] = rpmArch + values['rpmArchList'] = hostArchList + ['noarch', 'src'] + + if order == 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 == 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 + + 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 = server.getAllArches() + hostArchList.sort() + 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, 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 recentbuilds(environ, user=None, tag=None, package=None): + values = _initValues(environ, 'Recent Build RSS') + server = _getServer(environ) + + tagObj = None + if tag != None: + if tag.isdigit(): + tag = int(tag) + tagObj = server.getTag(tag) + + userObj = None + if user != None: + if user.isdigit(): + user = int(user) + userObj = server.getUser(user) + + packageObj = None + if package: + if package.isdigit(): + package = int(package) + packageObj = server.getPackage(package) + + if tagObj != None: + builds = server.listTagged(tagObj['id'], inherit=True, package=(packageObj and packageObj['name'] or None), + owner=(userObj and userObj['name'] or None)) + builds.sort(kojiweb.util.sortByKeyFunc('-completion_time', noneGreatest=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'} + +_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) + ']+$') +_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): + values = _initValues(environ, 'Search', 'search') + server = _getServer(environ) + values['error'] = None + + form = environ['koji.form'] + if form.has_key('terms') 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 not _VALID_SEARCH_RE.match(terms): + values['error'] = 'Invalid search terms
' + \ + 'Search terms may contain only these characters: ' + \ + _VALID_SEARCH_CHARS + _VALID_SEARCH_SYMS + return _genHTML(environ, 'search.chtml') + + if match == 'regexp': + try: + re.compile(terms) + except: + values['error'] = 'Invalid regular expression' + 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, 'searchresults.chtml') + else: + return _genHTML(environ, 'search.chtml') + +def watchlogs(environ, taskID): + values = _initValues(environ) + if isinstance(taskID, list): + values['tasks'] = ', '.join(taskID) + else: + values['tasks'] = taskID + + html = """ + + + + + Logs for task %(tasks)s | %(siteName)s + + +
+Loading logs for task %(tasks)s...
+    
+ + +""" % values + return html diff --git a/www/kojiweb/notificationedit.chtml b/www/kojiweb/notificationedit.chtml new file mode 100644 index 0000000..12aaf3d --- /dev/null +++ b/www/kojiweb/notificationedit.chtml @@ -0,0 +1,56 @@ +#from kojiweb import util + +#include "includes/header.chtml" + + #if $notif +

Edit notification

+ #else +

Create notification

+ #end if + +
+ $util.authToken($self, form=True) + #if $notif + + #end if + + + + + + + + + + + + + + + + + +
Package + +
Tag + +
Success Only?
+ #if $notif + + #else + + #end if +
+
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/packageinfo.chtml b/www/kojiweb/packageinfo.chtml new file mode 100644 index 0000000..313bc88 --- /dev/null +++ b/www/kojiweb/packageinfo.chtml @@ -0,0 +1,113 @@ +#from kojiweb import util + +#include "includes/header.chtml" + +

Information for package $package.name

+ + + + + + + + + + + + + + + + + + + +
Name$package.name
ID$package.id
Builds + #if $len($builds) > 0 + + + + + + + + + + + #for $build in $builds + + + + + #set $stateName = $util.stateName($build.state) + + + #end for +
+ #if $len($buildPages) > 1 +
+ Page: + +
+ #end if + #if $buildStart > 0 + <<< + #end if + #echo $buildStart + 1 # through #echo $buildStart + $buildCount # of $totalBuilds + #if $buildStart + $buildCount < $totalBuilds + >>> + #end if +
NVR $util.sortImage($self, 'nvr', 'buildOrder')Built by $util.sortImage($self, 'owner_name', 'buildOrder')Finished $util.sortImage($self, 'completion_time', 'buildOrder')State $util.sortImage($self, 'state', 'buildOrder')
$build.nvr$build.owner_name$util.formatTime($build.completion_time)$util.stateImage($build.state)
+ #else + No builds + #end if +
Tags + #if $len($tags) > 0 + + + + + + + + + + + #for $tag in $tags + + + + #set $included = $tag.blocked and 'no' or 'yes' + + + + #end for +
+ #if $len($tagPages) > 1 +
+ Page: + +
+ #end if + #if $tagStart > 0 + <<< + #end if + #echo $tagStart + 1 # through #echo $tagStart + $tagCount # of $totalTags + #if $tagStart + $tagCount < $totalTags + >>> + #end if +
Name $util.sortImage($self, 'name', 'tagOrder')Owner $util.sortImage($self, 'owner_name', 'tagOrder')Included? $util.sortImage($self, 'blocked', 'tagOrder')Extra Arches $util.sortImage($self, 'extra_arches', 'tagOrder')
$tag.name$tag.owner_name$util.imageTag($included)$tag.extra_arches
+ #else + No tags + #end if +
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/packages.chtml b/www/kojiweb/packages.chtml new file mode 100644 index 0000000..08cae84 --- /dev/null +++ b/www/kojiweb/packages.chtml @@ -0,0 +1,116 @@ +#from kojiweb import util + +#attr _PASSTHROUGH = ['userID', 'tagID', 'order', 'prefix', 'inherited'] + +#include "includes/header.chtml" + +

Packages#if $prefix then ' starting with "%s"' % $prefix else ''##if $tag then ' in tag %s' % ($tag.id, $tag.name) else ''##if $user then ' owned by %s' % ($user.id, $user.name) else ''#

+ + + #if $tag + + + #end if + + + + + + + + + + #if $tag or $user + + + + #end if + + #if $len($packages) > 0 + #for $package in $packages + + + + #if $tag or $user + + + + #end if + + #end for + #else + + + + #end if + + + +
+ + +
+ Inherited: + + +
+
+ #for $char in $chars + #if $prefix == $char + $char + #else + $char + #end if + | + #end for + #if $prefix + all + #else + all + #end if +
+ #if $len($packagePages) > 1 +
+ Page: + +
+ #end if + #if $packageStart > 0 + <<< + #end if + #if $totalPackages != 0 + Packages #echo $packageStart + 1 # through #echo $packageStart + $packageCount # of $totalPackages + #end if + #if $packageStart + $packageCount < $totalPackages + >>> + #end if +
ID $util.sortImage($self, 'package_id')Name $util.sortImage($self, 'package_name')Tag $util.sortImage($self, 'tag_name')Owner $util.sortImage($self, 'owner_name')Included? $util.sortImage($self, 'blocked')
$package.package_id$package.package_name$package.tag_name$package.owner_name#if $package.blocked then $util.imageTag('no') else $util.imageTag('yes')#
No packages
+ #if $len($packagePages) > 1 +
+ Page: + +
+ #end if + #if $packageStart > 0 + <<< + #end if + #if $totalPackages != 0 + Packages #echo $packageStart + 1 # through #echo $packageStart + $packageCount # of $totalPackages + #end if + #if $packageStart + $packageCount < $totalPackages + >>> + #end if +
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/packagesbyuser.chtml b/www/kojiweb/packagesbyuser.chtml new file mode 100644 index 0000000..c4e5713 --- /dev/null +++ b/www/kojiweb/packagesbyuser.chtml @@ -0,0 +1,73 @@ +#from kojiweb import util + +#include "includes/header.chtml" + +

Packages by User

+ + + + + + + + + + #if $len($users) > 0 + #for $user in $users + + + + + + #end for + #else + + + + #end if + + + +
+ #if $len($userPages) > 1 +
+ Page: + +
+ #end if + #if $userStart > 0 + <<< + #end if + #if $totalUsers != 0 + Users #echo $userStart + 1 # through #echo $userStart + $userCount # of $totalUsers + #end if + #if $userStart + $userCount < $totalUsers + >>> + #end if +
Name $util.sortImage($self, 'name')Packages $util.sortImage($self, 'packages') 
$user.namegraph row$user.packages
No users
+ #if $len($userPages) > 1 +
+ Page: + +
+ #end if + #if $userStart > 0 + <<< + #end if + #if $totalUsers != 0 + Users #echo $userStart + 1 # through #echo $userStart + $userCount # of $totalUsers + #end if + #if $userStart + $userCount < $totalUsers + >>> + #end if +
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/recentbuilds.chtml b/www/kojiweb/recentbuilds.chtml new file mode 100644 index 0000000..f1688e7 --- /dev/null +++ b/www/kojiweb/recentbuilds.chtml @@ -0,0 +1,54 @@ +#import koji +#import koji.util +#from kojiweb import util + +#def linkURL() + #set $query = [] + #if $tag + #silent $query.append('tagID=%i' % $tag.id) + #end if + #if $user + #silent $query.append('userID=%i' % $user.id) + #end if + #if $package + #silent $query.append('packageID=%i' % $package.id) + #end if + #if $query + #echo '%s/%s?%s' % ($weburl, 'builds', '&'.join($query)) + #else + #echo '%s/%s' % ($weburl, 'builds') + #end if +#end def + + + + $siteName: recent builds#if $package then ' of package ' + $package.name else ''##if $tag then ' into tag ' + $tag.name else ''##if $user then ' by user ' + $user.name else ''# + $linkURL() + + A list of the most recent builds + #if $package + of package $package.name + #end if + #if $tag + into tag $tag.name + #end if + #if $user + by user $user.name + #end if + in the $siteName Build System. The list is sorted in reverse chronological order by build completion time. + + $util.formatTimeRSS($currentDate) + #for $build in $builds + + $koji.BUILD_STATES[$build.state].lower(): $koji.buildLabel($build)#if $build.task then ', target: ' + $build.task.request[1] else ''# + $weburl/buildinfo?buildID=$build.build_id + #if $build.completion_time + $util.formatTimeRSS($build.completion_time) + #end if + #if $build.state == $koji.BUILD_STATES['COMPLETE'] and $build.changelog + <pre>$util.escapeHTML($koji.util.formatChangelog($build.changelog))</pre> + #end if + + #end for + + diff --git a/www/kojiweb/reports.chtml b/www/kojiweb/reports.chtml new file mode 100644 index 0000000..67184f9 --- /dev/null +++ b/www/kojiweb/reports.chtml @@ -0,0 +1,17 @@ +#from kojiweb import util + +#include "includes/header.chtml" + +

Reports

+ + + +#include "includes/footer.chtml" diff --git a/www/kojiweb/rpminfo.chtml b/www/kojiweb/rpminfo.chtml new file mode 100644 index 0000000..38b7a00 --- /dev/null +++ b/www/kojiweb/rpminfo.chtml @@ -0,0 +1,296 @@ +#import koji +#from kojiweb import util +#from pprint import pformat +#import time +#import urllib + +#attr _PASSTHROUGH = ['rpmID', 'fileOrder', 'fileStart', 'buildrootOrder', 'buildrootStart'] + +#include "includes/header.chtml" + #set $epoch = ($rpm.epoch != None and $str($rpm.epoch) + ':' or '') +

Information for RPM $rpm.name-$epoch$rpm.version-$rpm.release.${rpm.arch}.rpm

+ + + + + + + #if $build + + #else + + #end if + + + #if $build + + #else + + #end if + + + + + + + + + + + #if $rpm.external_repo_id == 0 + + + + + + + #end if + + + + #if $build and $build.state == $koji.BUILD_STATES.DELETED + + + + #end if + #if $rpm.external_repo_id + + + + #end if + + + + + + + #if $builtInRoot + + + + #end if + #if $rpm.get('extra') + + + + #end if + #if $rpm.external_repo_id == 0 + + + + + + + + + + + + + + + + + #if $optional_deps + + + + + + + + + + + + + + + + + #end if + + + + + #end if + + + + +
ID$rpm.id
Name$rpm.nameName$rpm.name
Version$rpm.versionVersion$rpm.version
Release$rpm.release
Epoch$rpm.epoch
Arch$rpm.arch
Summary$util.escapeHTML($summary)
Description$util.escapeHTML($description)
Build Time$time.strftime('%Y-%m-%d %H:%M:%S', $time.gmtime($rpm.buildtime)) GMT
Statedeleted
External Repository$rpm.external_repo_name
Size$rpm.size
$rpm.payloadhash
Buildroot$util.brLabel($builtInRoot)
Extra$util.escapeHTML($pformat($rpm.extra))
Provides + #if $len($provides) > 0 + + #for $dep in $provides + + + + #end for +
$util.escapeHTML($util.formatDep($dep.name, $dep.version, $dep.flags))
+ #else + No Provides + #end if +
Obsoletes + #if $len($obsoletes) > 0 + + #for $dep in $obsoletes + + + + #end for +
$util.escapeHTML($util.formatDep($dep.name, $dep.version, $dep.flags))
+ #else + No Obsoletes + #end if +
Conflicts + #if $len($conflicts) > 0 + + #for $dep in $conflicts + + + + #end for +
$util.escapeHTML($util.formatDep($dep.name, $dep.version, $dep.flags))
+ #else + No Conflicts + #end if +
Requires + #if $len($requires) > 0 + + #for $dep in $requires + + + + #end for +
$util.escapeHTML($util.formatDep($dep.name, $dep.version, $dep.flags))
+ #else + No Requires + #end if +
Recommends + #if $len($recommends) > 0 + + #for $dep in $recommends + + + + #end for +
$util.escapeHTML($util.formatDep($dep.name, $dep.version, $dep.flags))
+ #else + No Recommends + #end if +
Suggests + #if $len($suggests) > 0 + + #for $dep in $suggests + + + + #end for +
$util.escapeHTML($util.formatDep($dep.name, $dep.version, $dep.flags))
+ #else + No Suggests + #end if +
Supplements + #if $len($supplements) > 0 + + #for $dep in $supplements + + + + #end for +
$util.escapeHTML($util.formatDep($dep.name, $dep.version, $dep.flags))
+ #else + No Supplements + #end if +
Enhances + #if $len($enhances) > 0 + + #for $dep in $enhances + + + + #end for +
$util.escapeHTML($util.formatDep($dep.name, $dep.version, $dep.flags))
+ #else + No Enhances + #end if +
Files + #if $len($files) > 0 + + + + + + + + + #for $file in $files + + + + #end for +
+ #if $len($filePages) > 1 +
+ Page: + +
+ #end if + #if $fileStart > 0 + <<< + #end if + #echo $fileStart + 1 # through #echo $fileStart + $fileCount # of $totalFiles + #if $fileStart + $fileCount < $totalFiles + >>> + #end if +
Name $util.sortImage($self, 'name', 'fileOrder')Size $util.sortImage($self, 'size', 'fileOrder')
$util.escapeHTML($file.name)$file.size
+ #else + No Files + #end if +
Component of + #if $len($buildroots) > 0 + + + + + + + + + + #for $buildroot in $buildroots + + + + + + #end for +
+ #if $len($buildrootPages) > 1 +
+ Page: + +
+ #end if + #if $buildrootStart > 0 + <<< + #end if + #echo $buildrootStart + 1 # through #echo $buildrootStart + $buildrootCount # of $totalBuildroots + #if $buildrootStart + $buildrootCount < $totalBuildroots + >>> + #end if +
Buildroot $util.sortImage($self, 'id', 'buildrootOrder')Created $util.sortImage($self, 'create_event_time', 'buildrootOrder')State $util.sortImage($self, 'state', 'buildrootOrder')
$util.brLabel($buildroot)$util.formatTime($buildroot.create_event_time)$util.imageTag($util.brStateName($buildroot.state))
+ #else + No Buildroots + #end if +
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/rpmlist.chtml b/www/kojiweb/rpmlist.chtml new file mode 100644 index 0000000..0e8e554 --- /dev/null +++ b/www/kojiweb/rpmlist.chtml @@ -0,0 +1,112 @@ +#from kojiweb import util + +#include "includes/header.chtml" + +#def getID() + #if $type == 'image' +imageID=$image.id #slurp + #else +buildrootID=$buildroot.id #slurp + #end if +#end def + +#def getColspan() + #if $type == 'component' +colspan="3" #slurp + #elif $type == 'image' +colspan="2" #slurp + #else + #pass + #end if +#end def + + #if $type == 'component' +

Component RPMs of buildroot $util.brLabel($buildroot)

+ #elif $type == 'image' +

RPMs installed in $image.filename

+ #else +

RPMs built in buildroot $util.brLabel($buildroot)

+ #end if + + + + + + + + #if $type in ['component', 'image'] + + #end if + #if $type == 'component' + + #end if + + #if $len($rpms) > 0 + #for $rpm in $rpms + + #set $epoch = ($rpm.epoch != None and $str($rpm.epoch) + ':' or '') + + #if $type in ['component', 'image'] + #if $rpm.external_repo_id == 0 + + #else + + #end if + #end if + #if $type == 'component' + #set $update = $rpm.is_update and 'yes' or 'no' + + #end if + + #end for + #else + + + + #end if + + + +
+ #if $len($rpmPages) > 1 +
+ Page: + +
+ #end if + #if $rpmStart > 0 + <<< + #end if + #if $totalRpms != 0 + RPMs #echo $rpmStart + 1 # through #echo $rpmStart + $rpmCount # of $totalRpms + #end if + #if $rpmStart + $rpmCount < $totalRpms + >>> + #end if +
NVR $util.sortImage($self, 'nvr')Origin $util.sortImage($self, 'external_repo_name')Update? $util.sortImage($self, 'is_update')
$rpm.name-$epoch$rpm.version-$rpm.release.${rpm.arch}.rpminternal$rpm.external_repo_name$util.imageTag($update)
No RPMs
+ #if $len($rpmPages) > 1 +
+ Page: + +
+ #end if + #if $rpmStart > 0 + <<< + #end if + #if $totalRpms != 0 + RPMs #echo $rpmStart + 1 # through #echo $rpmStart + $rpmCount # of $totalRpms + #end if + #if $rpmStart + $rpmCount < $totalRpms + >>> + #end if +
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/rpmsbyhost.chtml b/www/kojiweb/rpmsbyhost.chtml new file mode 100644 index 0000000..2c63368 --- /dev/null +++ b/www/kojiweb/rpmsbyhost.chtml @@ -0,0 +1,105 @@ +#from kojiweb import util + +#include "includes/header.chtml" + +

#if $rpmArch then $rpmArch + ' ' else ''#RPMs by Host#if $hostArch then ' (%s)' % $hostArch else ''#

+ + + + + + + + + + + + + + + + #if $len($hosts) > 0 + #for $host in $hosts + + + + + + #end for + #else + + + + #end if + + + +
+ Host arch: #for $arch in $hostArchList + #if $arch == $hostArch + $arch | + #else + $arch | + #end if + #end for + #if $hostArch + all + #else + all + #end if +
+ RPM arch: #for $arch in $rpmArchList + #if $arch == $rpmArch + $arch | + #else + $arch | + #end if + #end for + #if $rpmArch + all + #else + all + #end if +
+ #if $len($hostPages) > 1 +
+ Page: + +
+ #end if + #if $hostStart > 0 + <<< + #end if + #if $totalHosts != 0 + Hosts #echo $hostStart + 1 # through #echo $hostStart + $hostCount # of $totalHosts + #end if + #if $hostStart + $hostCount < $totalHosts + >>> + #end if +
Name $util.sortImage($self, 'name')RPMs $util.sortImage($self, 'rpms') 
$host.namegraph row$host.rpms
No hosts
+ #if $len($hostPages) > 1 +
+ Page: + +
+ #end if + #if $hostStart > 0 + <<< + #end if + #if $totalHosts != 0 + Hosts #echo $hostStart + 1 # through #echo $hostStart + $hostCount # of $totalHosts + #end if + #if $hostStart + $hostCount < $totalHosts + >>> + #end if +
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/search.chtml b/www/kojiweb/search.chtml new file mode 100644 index 0000000..8749dad --- /dev/null +++ b/www/kojiweb/search.chtml @@ -0,0 +1,48 @@ +#from kojiweb import util + +#include "includes/header.chtml" + +

Search

+ +
+ + + #if $error + + #end if + + + + + + + + + + + + +
$error
Search + +
  + glob + regexp + exact +
 
+
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/searchresults.chtml b/www/kojiweb/searchresults.chtml new file mode 100644 index 0000000..e0c4ece --- /dev/null +++ b/www/kojiweb/searchresults.chtml @@ -0,0 +1,75 @@ +#from kojiweb import util +#import urllib + +#include "includes/header.chtml" + +

Search Results for $typeLabel matching "$terms"

+ + + + + + + + + + #if $len($results) > 0 + #for $result in $results + + + #set $quoted = $result.copy() + #silent $quoted['name'] = $urllib.quote($quoted['name']) + + + #end for + #else + + + + #end if + + + +
+ #if $len($resultPages) > 1 +
+ Page: + +
+ #end if + #if $resultStart > 0 + <<< + #end if + #if $totalResults != 0 + Results #echo $resultStart + 1 # through #echo $resultStart + $resultCount # of $totalResults + #end if + #if $resultStart + $resultCount < $totalResults + >>> + #end if +
ID $util.sortImage($self, 'id')Name $util.sortImage($self, 'name')
$result.id$result.name
No search results
+ #if $len($resultPages) > 1 +
+ Page: + +
+ #end if + #if $resultStart > 0 + <<< + #end if + #if $totalResults != 0 + Results #echo $resultStart + 1 # through #echo $resultStart + $resultCount # of $totalResults + #end if + #if $resultStart + $resultCount < $totalResults + >>> + #end if +
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/tagedit.chtml b/www/kojiweb/tagedit.chtml new file mode 100644 index 0000000..920e3a1 --- /dev/null +++ b/www/kojiweb/tagedit.chtml @@ -0,0 +1,65 @@ +#from kojiweb import util + +#include "includes/header.chtml" + + #if $tag +

Edit tag $tag.name

+ #else +

Create tag

+ #end if + +
+ $util.authToken($self, form=True) + + + + + + + + + + + + + + + + + + #if $mavenEnabled + + + + + + + #end if + + + + +
Name + + #if $tag + + #end if +
Arches
Locked
Permission + +
Maven Support? +
Include All Maven Builds? +
+ #if $tag + + #else + + #end if +
+
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/taginfo.chtml b/www/kojiweb/taginfo.chtml new file mode 100644 index 0000000..1298996 --- /dev/null +++ b/www/kojiweb/taginfo.chtml @@ -0,0 +1,170 @@ +#from kojiweb import util +#import pprint + +#include "includes/header.chtml" + +

Information for tag $tag.name

+ + + #if $child and 'admin' in $perms + + + + #end if + + + + + + + + + + + + + + + + #if $mavenEnabled + + + + + + + #end if + + + + + #if $maxDepth >= $TRUNC_DEPTH + + + + #end if + #if 'admin' in $perms + + + + #end if + #if $external_repos + + + + + #end if + + + + + + + + + + + + + + + + + + + + #if 'admin' in $perms + + + + + + + #end if + #if $tag.get('extra') + + + + #for $key in $tag['extra'] + + + + + #end for + #end if +
Add $tag.name as parent of $child.name
Name$tag.name
ID$tag.id
Arches$tag.arches
Locked#if $tag.locked then 'yes' else 'no'#
Permission#if $tag.perm_id then $allPerms[$tag.perm_id] else 'none'#
Maven Support?#if $tag.maven_support then 'yes' else 'no'#
Include All Maven Builds?#if $tag.maven_include_all then 'yes' else 'no'#
Inheritance + $tag.name + #set $numParents = $len($inheritance) + #set $iter = 0 + #set $maxDepth = 0 + #set $TRUNC_DEPTH = 7 +
    + #for $parent in $inheritance + #set $iter += 1 + #set $nextDepth = ($iter < $numParents and $inheritance[$iter].currdepth or 1) + #set $depth = $parent.currdepth + #if $depth > $maxDepth + #set $maxDepth = $depth + #end if + #if $depth == $TRUNC_DEPTH and not $all +
  • ...
  • +
  • + #else +
  • + #end if + #silent $tagsByChild[$parent.child_id].pop() + + + $parent.name + #if $depth == 1 and 'admin' in $perms + (edit) (remove) + #end if + + + #if $nextDepth > $depth +
      + #else + + #end if + #while $nextDepth < $depth +
    +
  • + #set $depth -= 1 + #end while + #end for +
+
+ #if $all + Show abbreviated tree + #else + Show full tree + #end if +
Add parent
External repos + #for $external_repo in $external_repos + $external_repo.external_repo_name + #if $external_repo.tag_id != $tag.id + (inherited from $external_repo.tag_name) + #end if +
+ #end for +
Repo created#if $repo then $util.formatTimeRSS($repo.creation_time) else ''#
Packages$numPackages
Builds$numBuilds
Targets building from this tag + #if $len($srcTargets) + #for $target in $srcTargets + $target.name
+ #end for + #else + No build targets + #end if +
Targets building to this tag + #if $len($destTargets) + #for $target in $destTargets + $target.name
+ #end for + #else + No build targets + #end if +
Edit tag
Delete tag
Extra options:
$key$pprint.pformat($tag['extra'][$key])
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/tagparent.chtml b/www/kojiweb/tagparent.chtml new file mode 100644 index 0000000..cd391f2 --- /dev/null +++ b/www/kojiweb/tagparent.chtml @@ -0,0 +1,72 @@ +#from kojiweb import util + +#include "includes/header.chtml" + + #if $inheritanceData +

Edit Parent

+ #else +

Add Parent

+ #end if + +
+ $util.authToken($self, form=True) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Tag Name + $tag.name + +
Parent Tag Name + $parent.name + +
Priority + +
Max Depth + +
Intransitive + +
Packages Only + +
Package Filter + +
+ #if $inheritanceData + + #else + + #end if +
+
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/tags.chtml b/www/kojiweb/tags.chtml new file mode 100644 index 0000000..fe39118 --- /dev/null +++ b/www/kojiweb/tags.chtml @@ -0,0 +1,76 @@ +#from kojiweb import util + +#include "includes/header.chtml" + +

Tags

+ + + + + + + + + #if $len($tags) > 0 + #for $tag in $tags + + + + + #end for + #else + + + + #end if + + + +
+ #if $len($tagPages) > 1 +
+ Page: + +
+ #end if + #if $tagStart > 0 + <<< + #end if + #if $totalTags != 0 + Tags #echo $tagStart + 1 # through #echo $tagStart + $tagCount # of $totalTags + #end if + #if $tagStart + $tagCount < $totalTags + >>> + #end if +
ID $util.sortImage($self, 'id')Name $util.sortImage($self, 'name')
$tag.id$tag.name
No tags
+ #if $len($tagPages) > 1 +
+ Page: + +
+ #end if + #if $tagStart > 0 + <<< + #end if + #if $totalTags != 0 + Tags #echo $tagStart + 1 # through #echo $tagStart + $tagCount # of $totalTags + #end if + #if $tagStart + $tagCount < $totalTags + >>> + #end if +
+ + #if 'admin' in $perms +
+ Create new Tag + #end if + +#include "includes/footer.chtml" diff --git a/www/kojiweb/taskinfo.chtml b/www/kojiweb/taskinfo.chtml new file mode 100644 index 0000000..c81405a --- /dev/null +++ b/www/kojiweb/taskinfo.chtml @@ -0,0 +1,463 @@ +#import koji +#from kojiweb import util +#import urllib +#import cgi + +#def printValue($key, $value, $sep=', ') + #if $key in ('brootid', 'buildroot_id') +$value + #elif $isinstance($value, list) +$sep.join([$str($val) for $val in $value]) + #elif $isinstance($value, dict) +$sep.join(['%s=%s' % (($n == '' and "''" or $n), $v) for $n, $v in $value.items()]) + #else +$value + #end if +#end def + +#def printProperties($props) + #echo ', '.join([$v is not None and '%s=%s' % ($n, $v) or $str($n) for $n, $v in $props.items()]) +#end def + +#def printMap($vals, $prefix='') + #for $key, $value in $vals.items() + #if $key == 'properties' + ${prefix}properties = $printProperties($value)
+ #elif $key != '__starstar' + $prefix$key = $printValue($key, $value)
+ #end if + #end for +#end def + +#def printOpts($opts) + #if $opts + Options:
+ $printMap($opts, '  ') + #end if +#end def + +#def printChildren($taskID, $childMap) + #set $iter = 0 + #set $children = $childMap[$str($taskID)] + #if $children +
    + #for $child in $children + #set $iter += 1 + #if $iter < $len($children) +
  • + #else +
  • + #end if + #set $childState = $util.taskState($child.state) + + + $koji.taskLabel($child) + + + $printChildren($child.id, $childMap) +
  • + #end for +
+ #end if +#end def + +#include "includes/header.chtml" + +

Information for task $koji.taskLabel($task)

+ + + + + + + + + + + + + + #set $state = $util.taskState($task.state) + + + + #if $taskBuild + + + + #end if + + + + #if $task.start_time + + + #end if + #if $task.state == $koji.TASK_STATES.OPEN + #if $estCompletion + + + + #end if + #elif $task.completion_time + + + + #end if + + + + + + + + + + + + + + + + #if $buildroots + + + + + #end if + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID$task.id
Method$task.method
Parameters + #if $task.method == 'buildSRPMFromSCM' + SCM URL: $params[0]
+ #if $len($params) > 1 + Build Tag:: $buildTag.name
+ #end if + #if $len($params) > 2 + $printOpts($params[2]) + #end if + #elif $task.method == 'buildSRPMFromCVS' + CVS URL: $params[0] + #elif $task.method == 'buildArch' + SRPM: $params[0]
+ Build Tag: $buildTag.name
+ Arch: $params[2]
+ Keep SRPM? #if $params[3] then 'yes' else 'no'#
+ #if $len($params) > 4 + $printOpts($params[4]) + #end if + #elif $task.method == 'tagBuild' + Destination Tag: $destTag.name
+ Build: $koji.buildLabel($build) + #elif $task.method == 'buildNotification' + #set $build = $params[1] + #set $buildTarget = $params[2] + Recipients: $printValue('', $params[0])
+ Build: $koji.buildLabel($build)
+ #if $buildTarget + Build Target: $buildTarget.name
+ #else + Build Target: (no build target)
+ #end if + Web URL: $params[3] + #elif $task.method == 'tagNotification' + Recipients: $printValue('', $params[0])
+ Successful?: #if $params[1] then 'yes' else 'no'#
+ #if $destTag + Tagged Into: $destTag.name
+ #end if + #if $srcTag + #if $destTag then 'Moved From:' else 'Untagged From:'# $srcTag.name
+ #end if + Build: $koji.buildLabel($build)
+ #if $destTag then 'Tagged By:' else 'Untagged By:'# $user.name
+ Ignore Success?: #if $params[6] then 'yes' else 'no'#
+ #if $params[7] + Failure Message: $params[7] + #end if + #elif $task.method == 'build' + Source: $params[0]
+ Build Target: $params[1]
+ $printOpts($params[2]) + #elif $task.method == 'maven' + SCM URL: $params[0]
+ Build Target: $params[1]
+ $printOpts($params[2]) + #elif $task.method == 'buildMaven' + SCM URL: $params[0]
+ Build Tag: $buildTag.name
+ #if $len($params) > 2 + $printOpts($params[2]) + #end if + #elif $task.method == 'wrapperRPM' + Spec File URL: $params[0]
+ #if 'locked' in $buildTarget + #set $buildTag = $buildTarget + Build Tag: $buildTag.name
+ #else + Build Target: $buildTarget.name
+ #end if + #if $params[2] + Build: $koji.buildLabel($params[2])
+ #end if + #if $params[3] + Task: $koji.taskLabel($wrapTask)
+ #end if + #if $len($params) > 4 + $printOpts($params[4]) + #end if + #elif $task.method == 'chainmaven' + Builds:
+ + #for $key, $val in $params[0].items() + + #end for +
$key:$printMap($val)
+ Build Target: $params[1]
+ #if $len($params) > 2 + $printOpts($params[2]) + #end if + #elif $task.method == 'livecd' or $task.method == 'appliance' or $task.method == 'livemedia' + Name: $params[0]
+ Version: $params[1]
+ Arch: $params[2]
+ Build Target: $params[3]
+ Kickstart File: $params[4]
+ $printOpts($params[5]) + #elif $task.method == 'image' + Arches: #echo ', '.join($params[2])#
+ Build Target: $params[3]
+ Installation Tree: $params[4]
+ $printOpts($params[5]) + #elif $task.method == 'createLiveCD' or $task.method == 'createAppliance' or $task.method == 'createLiveMedia' + #if $len($params) > 4: + ## new method signature + Arch: $params[3]
+ Kickstart File: $params[7]
+ #if $len($params) > 8 + $printOpts($params[8]) + #end if + #else + ## old method signature + Arch: $params[0]
+ Build Target: $params[1]
+ Kickstart File: $params[2]
+ #if $len($params) > 3 + $printOpts($params[3]) + #end if + #end if + #elif $task.method == 'createImage' + #set $target = $params[4] + Build Target: $target.name
+ Install Tree: $params[7]
+ $printOpts($params[8]) + #elif $task.method == 'winbuild' + VM: $params[0]
+ SCM URL: $params[1]
+ Build Target: $params[2]
+ #if $len($params) > 3 + $printOpts($params[3]) + #end if + #elif $task.method == 'vmExec' + VM: $params[0]
+ Exec Params:
+ #for $info in $params[1] + #if $isinstance($info, dict) + $printMap($info, '    ') + #else +   $info
+ #end if + #end for + #if $len($params) > 2 + $printOpts($params[2]) + #end if + #elif $task.method == 'newRepo' + Tag: $tag.name
+ #if $len($params) > 1 + $printOpts($params[1]) + #end if + #elif $task.method == 'prepRepo' + Tag: $params[0].name + #elif $task.method == 'createrepo' + Repo ID: $params[0]
+ Arch: $params[1]
+ #set $oldrepo = $params[2] + #if $oldrepo + Old Repo ID: $oldrepo.id
+ Old Repo Creation: $koji.formatTimeLong($oldrepo.creation_time)
+ #end if + #if $len($params) > 3 + External Repos: $printValue(None, [ext['external_repo_name'] for ext in $params[3]])
+ #end if + #elif $task.method == 'dependantTask' + Dependant Tasks:
+ #for $dep in $deps +   $koji.taskLabel($dep)
+ #end for + Subtasks:
+ #for $subtask in $params[1] +   Method: $subtask[0]
+   Parameters: #echo ', '.join([$str($subparam) for $subparam in $subtask[1]])#
+ #if $len($subtask) > 2 and $subtask[2] +   Options:
+ $printMap($subtask[2], '    ') + #end if +
+ #end for + #elif $task.method == 'chainbuild' + Build Groups:
+ #set $groupNum = 0 + #for $urls in $params[0] + #set $groupNum += 1 +   $groupNum: #echo ', '.join($urls)#
+ #end for + Build Target: $params[1]
+ $printOpts($params[2]) + #elif $task.method == 'waitrepo' + Build Target: $params[0]
+ #if $params[1] + Newer Than: $params[1]
+ #end if + #if $params[2] + NVRs: $printValue('', $params[2]) + #end if + #elif $task.method == 'restart' + Host: $params[0].name
+ #elif $task.method == 'restartVerify' + Host: $params[1].name
+ Restart Task: + $koji.taskLabel($rtask)
+ #elif $task.method == 'runroot' + Build Tag: $params[0]
+ Arch: $params[1]
+ $printOpts($params[3]) + Commands: $params[2]
+ #else + $params + #end if +
State$state + #if $currentUser and ('admin' in $perms or $task.owner == $currentUser.id) + #if $task.state in ($koji.TASK_STATES.FREE, $koji.TASK_STATES.OPEN, $koji.TASK_STATES.ASSIGNED) + (cancel) + #elif $task.state in ($koji.TASK_STATES.CANCELED, $koji.TASK_STATES.FAILED) and (not $parent) + (resubmit) + #end if + #end if +
Build$koji.buildLabel($taskBuild)
Created$util.formatTimeLong($task.create_time)
Started$util.formatTimeLong($task.start_time)
Est. Completion$util.formatTimeLong($estCompletion)
Completed$util.formatTimeLong($task.completion_time)
Owner + #if $owner + #if $owner.usertype == $koji.USERTYPES['HOST'] + $owner.name + #else + $owner.name + #end if + #end if +
Channel + #if $task.channel_id + $channelName + #end if +
Host + #if $task.host_id + $hostName + #end if +
Arch$task.arch
Buildroot#if $len($buildroots) > 1 then 's' else ''# + #for $buildroot in $buildroots + #if $task.method == 'vmExec' then '' else '/var/lib/mock/'#$buildroot.tag_name-$buildroot.id-$buildroot.repo_id
+ #end for +
Parent + #if $parent + $koji.taskLabel($parent) + #end if +
Descendants + #if $len($descendents[$str($task.id)]) > 0 + $task.method + #end if + $printChildren($task.id, $descendents) +
Waiting?#if $task.waiting then 'yes' else 'no'#
Awaited?#if $task.awaited then 'yes' else 'no'#
Priority$task.priority
Weight#echo '%.2f' % $task.weight#
Result + #if $abbr_result_text +
+ $abbr_result_text +
+
+ $full_result_text +
+ + + #else +
+ $full_result_text +
+ #end if +
Output + #for $filename in $output + $filename + #if $filename.endswith('.log') + (tail) + #end if +
+ #end for + #if $task.state not in ($koji.TASK_STATES.CLOSED, $koji.TASK_STATES.CANCELED, $koji.TASK_STATES.FAILED) and \ + $task.method in ('buildSRPMFromSCM', 'buildArch', 'createLiveMedia', 'buildMaven', 'wrapperRPM', 'vmExec', 'createrepo', 'runroot', 'createAppliance', 'createLiveCD') +
+ Watch logs + #end if +
+ +#if $abbr_result_text + +#end if +#include "includes/footer.chtml" diff --git a/www/kojiweb/tasks.chtml b/www/kojiweb/tasks.chtml new file mode 100644 index 0000000..4c1a32f --- /dev/null +++ b/www/kojiweb/tasks.chtml @@ -0,0 +1,190 @@ +#import koji +#from kojiweb import util + +#def printChildren($taskID, $childMap) + #set $iter = 0 + #set $children = $childMap[$str($taskID)] + #if $children +
    + #for $child in $children + #set $iter += 1 + #if $iter < $len($children) +
  • + #else +
  • + #end if + #set $childState = $util.taskState($child.state) + + + $koji.taskLabel($child) + + + $printChildren($child.id, $childMap) +
  • + #end for +
+ #end if +#end def + +#def headerPrefix($state) + #if $state == 'active' +Active + #elif $state == 'all' +All + #else +#echo $state.capitalize() + #end if +#end def + +#attr _PASSTHROUGH = ['owner', 'state', 'view', 'method', 'hostID', 'channelID', 'order'] + +#include "includes/header.chtml" + +

$headerPrefix($state) #if $view == 'toplevel' then 'toplevel' else ''# #if $method != 'all' then $method else ''# Tasks#if $ownerObj then ' owned by %s' % ($ownerObj.id, $ownerObj.name) else ''##if $host then ' on host %s' % ($host.id, $host.name) else ''# #if $channel then ' in channel %s' % ($channel.id, $channel.name) else ''#

+ + + + + + + + + + + + + + + + + #if $len($tasks) > 0 + #for $task in $tasks + + #set $taskState = $util.taskState($task.state) + + + #if $treeDisplay then ' ' else ''#$koji.taskLabel($task) + #if $treeDisplay + $printChildren($task.id, $task.descendents) + #end if + + + + + + + #end for + #else + + + + #end if + + + +
+
+ + + +
+ State: + + + + Owner: + + +
+ Method: + + + + View: + + +
+
+
+ #if $len($taskPages) > 1 +
+ Page: + +
+ #end if + #if $taskStart > 0 + <<< + #end if + #if $totalTasks != 0 + Tasks #echo $taskStart + 1 # through #echo $taskStart + $taskCount # of $totalTasks + #end if + #if $taskStart + $taskCount < $totalTasks + >>> + #end if +
ID $util.sortImage($self, 'id')Type $util.sortImage($self, 'method')Owner $util.sortImage($self, 'owner')Arch $util.sortImage($self, 'arch')Finished $util.sortImage($self, 'completion_time')State $util.sortImage($self, 'state')
$task.id + #if $task.owner_type == $koji.USERTYPES['HOST'] + $task.owner_name + #else + $task.owner_name + #end if + $task.arch$util.formatTime($task.completion_time)$util.imageTag($taskState)
No tasks
+ #if $len($taskPages) > 1 +
+ Page: + +
+ #end if + #if $taskStart > 0 + <<< + #end if + #if $totalTasks != 0 + Tasks #echo $taskStart + 1 # through #echo $taskStart + $taskCount # of $totalTasks + #end if + #if $taskStart + $taskCount < $totalTasks + >>> + #end if +
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/tasksbyhost.chtml b/www/kojiweb/tasksbyhost.chtml new file mode 100644 index 0000000..4efa19e --- /dev/null +++ b/www/kojiweb/tasksbyhost.chtml @@ -0,0 +1,89 @@ +#from kojiweb import util + +#include "includes/header.chtml" + +

Tasks by Host#if $hostArch then ' (%s)' % $hostArch else ''#

+ + + + + + + + + + + + + #if $len($hosts) > 0 + #for $host in $hosts + + + + + + #end for + #else + + + + #end if + + + +
+ Host arch: #for $arch in $hostArchList + #if $arch == $hostArch + $arch | + #else + $arch | + #end if + #end for + #if $hostArch + all + #else + all + #end if +
+ #if $len($hostPages) > 1 +
+ Page: + +
+ #end if + #if $hostStart > 0 + <<< + #end if + #if $totalHosts != 0 + Hosts #echo $hostStart + 1 # through #echo $hostStart + $hostCount # of $totalHosts + #end if + #if $hostStart + $hostCount < $totalHosts + >>> + #end if +
Name $util.sortImage($self, 'name')Tasks $util.sortImage($self, 'tasks') 
$host.namegraph row$host.tasks
No hosts
+ #if $len($hostPages) > 1 +
+ Page: + +
+ #end if + #if $hostStart > 0 + <<< + #end if + #if $totalHosts != 0 + Hosts #echo $hostStart + 1 # through #echo $hostStart + $hostCount # of $totalHosts + #end if + #if $hostStart + $hostCount < $totalHosts + >>> + #end if +
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/tasksbyuser.chtml b/www/kojiweb/tasksbyuser.chtml new file mode 100644 index 0000000..843ea18 --- /dev/null +++ b/www/kojiweb/tasksbyuser.chtml @@ -0,0 +1,73 @@ +#from kojiweb import util + +#include "includes/header.chtml" + +

Tasks by User

+ + + + + + + + + + #if $len($users) > 0 + #for $user in $users + + + + + + #end for + #else + + + + #end if + + + +
+ #if $len($userPages) > 1 +
+ Page: + +
+ #end if + #if $userStart > 0 + <<< + #end if + #if $totalUsers != 0 + Users #echo $userStart + 1 # through #echo $userStart + $userCount # of $totalUsers + #end if + #if $userStart + $userCount < $totalUsers + >>> + #end if +
Name $util.sortImage($self, 'name')Tasks $util.sortImage($self, 'tasks') 
$user.namegraph row$user.tasks
No users
+ #if $len($userPages) > 1 +
+ Page: + +
+ #end if + #if $userStart > 0 + <<< + #end if + #if $totalUsers != 0 + Users #echo $userStart + 1 # through #echo $userStart + $userCount # of $totalUsers + #end if + #if $userStart + $userCount < $totalUsers + >>> + #end if +
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/userinfo.chtml b/www/kojiweb/userinfo.chtml new file mode 100644 index 0000000..f7b97fb --- /dev/null +++ b/www/kojiweb/userinfo.chtml @@ -0,0 +1,108 @@ +#from kojiweb import util + +#include "includes/header.chtml" + +

Information for user $user.name

+ + + + + + + + + + + + + + + + + + + +
Name$user.name
ID$user.id
Tasks$taskCount
Packages + #if $len($packages) > 0 + + + + + + + + + + #for $package in $packages + + + + + + #end for +
+ #if $len($packagePages) > 1 +
+ Page: + +
+ #end if + #if $packageStart > 0 + <<< + #end if + #echo $packageStart + 1 # through #echo $packageStart + $packageCount # of $totalPackages + #if $packageStart + $packageCount < $totalPackages + >>> + #end if +
Name $util.sortImage($self, 'package_name', 'packageOrder')Tag $util.sortImage($self, 'tag_name', 'packageOrder')Included? $util.sortImage($self, 'blocked', 'packageOrder')
$package.package_name$package.tag_name#if $package.blocked then $util.imageTag('no') else $util.imageTag('yes')#
+ #else + No packages + #end if +
Builds + #if $len($builds) > 0 + + + + + + + + + + #for $build in $builds + + #set $stateName = $util.stateName($build.state) + + + + + #end for +
+ #if $len($buildPages) > 1 +
+ Page: + +
+ #end if + #if $buildStart > 0 + <<< + #end if + #echo $buildStart + 1 # through #echo $buildStart + $buildCount # of $totalBuilds + #if $buildStart + $buildCount < $totalBuilds + >>> + #end if +
NVR $util.sortImage($self, 'nvr', 'buildOrder')Finished $util.sortImage($self, 'completion_time', 'buildOrder')State $util.sortImage($self, 'state', 'buildOrder')
$build.nvr$util.formatTime($build.completion_time)$util.stateImage($build.state)
+ #else + No builds + #end if +
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/users.chtml b/www/kojiweb/users.chtml new file mode 100644 index 0000000..e8cafec --- /dev/null +++ b/www/kojiweb/users.chtml @@ -0,0 +1,94 @@ +#from kojiweb import util + +#include "includes/header.chtml" + +

Users#if $prefix then ' starting with "%s"' % $prefix else ''#

+ + + + + + + + + + + + + + + #if $len($users) > 0 + #for $user in $users + + + + + + + + #end for + #else + + + + #end if + + + +
+ #for $char in $chars + #if $prefix == $char + $char + #else + $char + #end if + | + #end for + #if $prefix + all + #else + all + #end if +
+ #if $len($userPages) > 1 +
+ Page: + +
+ #end if + #if $userStart > 0 + <<< + #end if + #if $totalUsers != 0 + Users #echo $userStart + 1 # through #echo $userStart + $userCount # of $totalUsers + #end if + #if $userStart + $userCount < $totalUsers + >>> + #end if +
ID $util.sortImage($self, 'id')Name $util.sortImage($self, 'name')PackagesBuildsTasks
$user.id$user.nameviewviewview
No users
+ #if $len($userPages) > 1 +
+ Page: + +
+ #end if + #if $userStart > 0 + <<< + #end if + #if $totalUsers != 0 + Users #echo $userStart + 1 # through #echo $userStart + $userCount # of $totalUsers + #end if + #if $userStart + $userCount < $totalUsers + >>> + #end if +
+ +#include "includes/footer.chtml" diff --git a/www/kojiweb/wsgi_publisher.py b/www/kojiweb/wsgi_publisher.py new file mode 100644 index 0000000..6fd7f04 --- /dev/null +++ b/www/kojiweb/wsgi_publisher.py @@ -0,0 +1,489 @@ +# a vaguely publisher-like dispatcher for wsgi +# +# Copyright (c) 2012-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 McLean + +import cgi +import inspect +import koji +import koji.util +import logging +import os.path +import pprint +import sys +import traceback + +from ConfigParser import RawConfigParser +from koji.server import WSGIWrapper, ServerError, ServerRedirect +from koji.util import dslice + + +class URLNotFound(ServerError): + """Used to generate a 404 response""" + + +class Dispatcher(object): + + def __init__(self): + #we can't do much setup until we get a request + self.firstcall = True + self.options = {} + self.startup_error = None + self.handler_index = {} + self.setup_logging1() + + def setup_logging1(self): + """Set up basic logging, before options are loaded""" + logger = logging.getLogger("koji") + logger.setLevel(logging.WARNING) + self.log_handler = logging.StreamHandler() + # Log to stderr (StreamHandler default). + # There seems to be no advantage to using wsgi.errors + log_format = '%(msecs)d [%(levelname)s] SETUP p=%(process)s %(name)s: %(message)s' + self.log_handler.setFormatter(logging.Formatter(log_format)) + self.log_handler.setLevel(logging.DEBUG) + logger.addHandler(self.log_handler) + self.formatter = None + self.logger = logging.getLogger("koji.web") + + cfgmap = [ + #option, type, default + ['SiteName', 'string', None], + ['KojiHubURL', 'string', 'http://localhost/kojihub'], + ['KojiFilesURL', 'string', 'http://localhost/kojifiles'], + ['KojiTheme', 'string', None], + ['KojiGreeting', 'string', 'Welcome to Koji Web'], + ['LiteralFooter', 'boolean', True], + + ['WebPrincipal', 'string', None], + ['WebKeytab', 'string', '/etc/httpd.keytab'], + ['WebCCache', 'string', '/var/tmp/kojiweb.ccache'], + ['KrbService', 'string', 'host'], + ['KrbRDNS', 'boolean', True], + + ['WebCert', 'string', None], + ['KojiHubCA', 'string', '/etc/kojiweb/kojihubca.crt'], + + ['PythonDebug', 'boolean', False], + + ['LoginTimeout', 'integer', 72], + + ['Secret', 'string', None], + + ['LoginDisabled', 'boolean', False], + + ['LibPath', 'string', '/usr/share/koji-web/lib'], + + ['LogLevel', 'string', 'WARNING'], + ['LogFormat', 'string', '%(msecs)d [%(levelname)s] m=%(method)s u=%(user_name)s p=%(process)s r=%(remoteaddr)s %(name)s: %(message)s'], + + ['Tasks', 'list', []], + ['ToplevelTasks', 'list', []], + ['ParentTasks', 'list', []], + + ['RLIMIT_AS', 'string', None], + ['RLIMIT_CORE', 'string', None], + ['RLIMIT_CPU', 'string', None], + ['RLIMIT_DATA', 'string', None], + ['RLIMIT_FSIZE', 'string', None], + ['RLIMIT_MEMLOCK', 'string', None], + ['RLIMIT_NOFILE', 'string', None], + ['RLIMIT_NPROC', 'string', None], + ['RLIMIT_OFILE', 'string', None], + ['RLIMIT_RSS', 'string', None], + ['RLIMIT_STACK', 'string', None], + ] + + def load_config(self, environ): + """Load configuration options + + Options are read from a config file. + + Backwards compatibility: + - if ConfigFile is not set, opts are loaded from http config + - if ConfigFile is set, then the http config must not provide Koji options + - In a future version we will load the default hub config regardless + - all PythonOptions (except koji.web.ConfigFile) are now deprecated and + support for them will disappear in a future version of Koji + """ + modpy_opts = environ.get('modpy.opts', {}) + if 'modpy.opts' in environ: + cf = modpy_opts.get('koji.web.ConfigFile', None) + cfdir = modpy_opts.get('koji.web.ConfigDir', None) + # to aid in the transition from PythonOptions to web.conf, we do + # not check the config file by default, it must be configured + if not cf and not cfdir: + self.logger.warn('Warning: configuring Koji via PythonOptions is deprecated. Use web.conf') + else: + cf = environ.get('koji.web.ConfigFile', '/etc/kojiweb/web.conf') + cfdir = environ.get('koji.web.ConfigDir', '/etc/kojiweb/web.conf.d') + if cfdir: + configs = koji.config_directory_contents(cfdir) + else: + configs = [] + if cf and os.path.isfile(cf): + configs.append(cf) + if configs: + config = RawConfigParser() + config.read(configs) + elif modpy_opts: + # presumably we are configured by modpy options + config = None + else: + raise koji.GenericError, "Configuration missing" + + opts = {} + for name, dtype, default in self.cfgmap: + if config: + key = ('web', name) + if config.has_option(*key): + if dtype == 'integer': + opts[name] = config.getint(*key) + elif dtype == 'boolean': + opts[name] = config.getboolean(*key) + elif dtype == 'list': + opts[name] = [x.strip() for x in config.get(*key).split(',')] + else: + opts[name] = config.get(*key) + else: + opts[name] = default + else: + if modpy_opts.get(name, None) is not None: + if dtype == 'integer': + opts[name] = int(modpy_opts.get(name)) + elif dtype == 'boolean': + opts[name] = modpy_opts.get(name).lower() in ('yes', 'on', 'true', '1') + else: + opts[name] = modpy_opts.get(name) + else: + opts[name] = default + if 'modpy.conf' in environ: + debug = environ['modpy.conf'].get('PythonDebug', '0').lower() + opts['PythonDebug'] = (debug in ['yes', 'on', 'true', '1']) + opts['Secret'] = koji.util.HiddenValue(opts['Secret']) + self.options = opts + return opts + + def setup_logging2(self, environ): + """Adjust logging based on configuration options""" + opts = self.options + #determine log level + level = opts['LogLevel'] + valid_levels = ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL') + # the config value can be a single level name or a series of + # logger:level names pairs. processed in order found + default = None + for part in level.split(): + pair = part.split(':', 1) + if len(pair) == 2: + name, level = pair + else: + name = 'koji' + level = part + default = level + if level not in valid_levels: + raise koji.GenericError, "Invalid log level: %s" % level + #all our loggers start with koji + if name == '': + name = 'koji' + default = level + elif name.startswith('.'): + name = 'koji' + name + elif not name.startswith('koji'): + name = 'koji.' + name + level_code = logging._levelNames[level] + logging.getLogger(name).setLevel(level_code) + logger = logging.getLogger("koji") + # if KojiDebug is set, force main log level to DEBUG + if opts.get('KojiDebug'): + logger.setLevel(logging.DEBUG) + elif default is None: + #LogLevel did not configure a default level + logger.setLevel(logging.WARNING) + self.formatter = HubFormatter(opts['LogFormat']) + self.formatter.environ = environ + self.log_handler.setFormatter(self.formatter) + + def find_handlers(self): + for name in vars(kojiweb_handlers): + if name.startswith('_'): + continue + try: + val = getattr(kojiweb_handlers, name, None) + if not inspect.isfunction(val): + continue + # err on the side of paranoia + args = inspect.getargspec(val) + if not args[0] or args[0][0] != 'environ': + continue + except: + tb_str = ''.join(traceback.format_exception(*sys.exc_info())) + self.logger.error(tb_str) + self.handler_index[name] = val + + def prep_handler(self, environ): + path_info = environ['PATH_INFO'] + if not path_info: + #empty path info (no trailing slash) breaks our relative urls + environ['koji.redirect'] = environ['REQUEST_URI'] + '/' + raise ServerRedirect + elif path_info == '/': + method = 'index' + else: + method = path_info.lstrip('/').split('/')[0] + environ['koji.method'] = method + self.logger.info("Method: %s", method) + func = self.handler_index.get(method) + if not func: + raise URLNotFound + #parse form args + data = {} + fs = cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ.copy(), keep_blank_values=True) + for field in fs.list: + if field.filename: + val = field + else: + val = field.value + data.setdefault(field.name, []).append(val) + # replace singleton lists with single values + # XXX - this is a bad practice, but for now we strive to emulate mod_python.publisher + for arg in data: + val = data[arg] + if isinstance(val, list) and len(val) == 1: + data[arg] = val[0] + environ['koji.form'] = fs + args, varargs, varkw, defaults = inspect.getargspec(func) + if not varkw: + # remove any unexpected args + data = dslice(data, args, strict=False) + #TODO (warning in header or something?) + return func, data + + + def _setup(self, environ): + global kojiweb_handlers + global kojiweb + options = self.load_config(environ) + if 'LibPath' in options and os.path.exists(options['LibPath']): + sys.path.insert(0, options['LibPath']) + # figure out our location and try to load index.py from same dir + scriptsdir = os.path.dirname(environ['SCRIPT_FILENAME']) + environ['koji.scriptsdir'] = scriptsdir + sys.path.insert(0, scriptsdir) + import index as kojiweb_handlers + import kojiweb + self.find_handlers() + self.setup_logging2(environ) + koji.util.setup_rlimits(options) + # TODO - plugins? + + def setup(self, environ): + try: + self._setup(environ) + except Exception: + self.startup_error = "unknown startup_error" + etype, e = sys.exc_info()[:2] + tb_short = ''.join(traceback.format_exception_only(etype, e)) + self.startup_error = "startup_error: %s" % tb_short + tb_str = ''.join(traceback.format_exception(*sys.exc_info())) + self.logger.error(tb_str) + + def simple_error_page(self, message=None, err=None): + result = ["""\ + +Error + +"""] + if message: + result.append("

%s

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

%s

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

Redirect: here

\n' % location + environ['koji.headers'].append(['Location', location]) + except URLNotFound: + status = "404 Not Found" + msg = "Not found: %s" % environ['REQUEST_URI'] + result, headers = self.error_page(environ, message=msg, err=False) + start_response(status, headers) + return result + except Exception: + tb_str = ''.join(traceback.format_exception(*sys.exc_info())) + self.logger.error(tb_str) + status = '500 Internal Server Error' + result, headers = self.error_page(environ) + start_response(status, headers) + return result + headers = { + 'allow' : ('Allow', 'GET, POST, HEAD'), + } + extra = [] + for name, value in environ.get('koji.headers', []): + key = name.lower() + if key == 'set-cookie': + extra.append((name, value)) + else: + # last one wins + headers[key] = (name, value) + if isinstance(result, basestring): + headers.setdefault('content-length', ('Content-Length', str(len(result)))) + headers.setdefault('content-type', ('Content-Type', 'text/html')) + headers = headers.values() + extra + self.logger.debug("Headers:") + self.logger.debug(koji.util.LazyString(pprint.pformat, [headers])) + start_response(status, headers) + if isinstance(result, basestring): + result = [result] + return result + + def handler(self, req): + """mod_python handler""" + wrapper = WSGIWrapper(req) + return wrapper.run(self.application) + + def application(self, environ, start_response): + """wsgi handler""" + if self.formatter: + self.formatter.environ = environ + if self.firstcall: + self.firstcall = False + self.setup(environ) + try: + result = self.handle_request(environ, start_response) + finally: + if self.formatter: + self.formatter.environ = {} + session = environ.get('koji.session') + if session: + session.logout() + return result + + +class HubFormatter(logging.Formatter): + """Support some koji specific fields in the format string""" + + def format(self, record): + # dispatcher should set environ for us + environ = self.environ + # XXX Can we avoid these data lookups if not needed? + record.method = environ.get('koji.method') + record.remoteaddr = "%s:%s" % ( + environ.get('REMOTE_ADDR', '?'), + environ.get('REMOTE_PORT', '?')) + record.user_name = environ.get('koji.currentLogin') + user = environ.get('koji.currentUser') + if user: + record.user_id = user['id'] + else: + record.user_id = None + session = environ.get('koji.session') + record.session_id = None + if session: + record.callnum = session.callnum + if session.sinfo: + record.session_id = session.sinfo.get('session.id') + else: + record.callnum = None + return logging.Formatter.format(self, record) + + +# provide necessary global handlers for mod_wsgi and mod_python +dispatcher = Dispatcher() +handler = dispatcher.handler +application = dispatcher.application diff --git a/www/lib/Makefile b/www/lib/Makefile new file mode 100644 index 0000000..a0e01a1 --- /dev/null +++ b/www/lib/Makefile @@ -0,0 +1,20 @@ +SUBDIRS = kojiweb + +SERVERDIR = /usr/share/koji-web/lib + +_default: + @echo "nothing to make. try make install" + +clean: + rm -f *.o *.so *.pyc *~ + for d in $(SUBDIRS); do make -s -C $$d clean; done + +install: + @if [ "$(DESTDIR)" = "" ]; then \ + echo " "; \ + echo "ERROR: A destdir is required"; \ + exit 1; \ + fi + + for d in $(SUBDIRS); do make DESTDIR=$(DESTDIR)/$(SERVERDIR) \ + -C $$d install; [ $$? = 0 ] || exit 1; done diff --git a/www/lib/kojiweb/Makefile b/www/lib/kojiweb/Makefile new file mode 100644 index 0000000..0cdcbe9 --- /dev/null +++ b/www/lib/kojiweb/Makefile @@ -0,0 +1,30 @@ +PYTHON=python +PACKAGE = $(shell basename `pwd`) +PYFILES = $(wildcard *.py) +PYVER := $(shell $(PYTHON) -c 'import sys; print "%.3s" %(sys.version)') +PYSYSDIR := $(shell $(PYTHON) -c 'import sys; print sys.prefix') +PYLIBDIR = $(PYSYSDIR)/lib/python$(PYVER) +PKGDIR = $(PYLIBDIR)/site-packages/$(PACKAGE) + +SERVERDIR = /kojiweb +FILES = $(wildcard *.py *.chtml) + +_default: + @echo "nothing to make. try make install" + +clean: + rm -f *.o *.so *.pyc *~ + for d in $(SUBDIRS); do make -s -C $$d clean; done + +install: + @if [ "$(DESTDIR)" = "" ]; then \ + echo " "; \ + echo "ERROR: A destdir is required"; \ + exit 1; \ + fi + + mkdir -p $(DESTDIR)/$(SERVERDIR) + for p in $(PYFILES) ; do \ + install -p -m 644 $$p $(DESTDIR)/$(SERVERDIR)/$$p; \ + done + $(PYTHON) -c "import compileall; compileall.compile_dir('$(DESTDIR)/$(SERVERDIR)', 1, '$(PYDIR)', 1)" diff --git a/www/lib/kojiweb/__init__.py b/www/lib/kojiweb/__init__.py new file mode 100644 index 0000000..ac095f5 --- /dev/null +++ b/www/lib/kojiweb/__init__.py @@ -0,0 +1 @@ +# identify this directory as a python module diff --git a/www/lib/kojiweb/util.py b/www/lib/kojiweb/util.py new file mode 100644 index 0000000..03d0272 --- /dev/null +++ b/www/lib/kojiweb/util.py @@ -0,0 +1,849 @@ +# utility functions for koji web interface +# +# 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 Cheetah.Template +import datetime +import koji +from koji.util import md5_constructor +import os +import stat +#a bunch of exception classes that explainError needs +from socket import error as socket_error +from socket import sslerror as socket_sslerror +from xmlrpclib import ProtocolError +from xml.parsers.expat import ExpatError +import cgi + +class NoSuchException(Exception): + pass + +try: + # pyOpenSSL might not be around + from OpenSSL.SSL import Error as SSL_Error +except: + SSL_Error = NoSuchException + + +themeInfo = {} +themeCache = {} + +def _initValues(environ, title='Build System Info', pageID='summary'): + global themeInfo + global themeCache + values = {} + values['siteName'] = environ['koji.options'].get('SiteName', 'Koji') + values['title'] = title + values['pageID'] = pageID + values['currentDate'] = str(datetime.datetime.now()) + values['literalFooter'] = environ['koji.options'].get('LiteralFooter', True) + themeCache.clear() + themeInfo.clear() + themeInfo['name'] = environ['koji.options'].get('KojiTheme', None) + themeInfo['staticdir'] = environ['koji.options'].get('KojiStaticDir', '/usr/share/koji-web/static') + + environ['koji.values'] = values + + return values + +def themePath(path, local=False): + global themeInfo + global themeCache + local = bool(local) + if (path, local) in themeCache: + return themeCache[path, local] + if not themeInfo['name']: + if local: + ret = os.path.join(themeInfo['staticdir'], path) + else: + ret = "/koji-static/%s" % path + else: + themepath = os.path.join(themeInfo['staticdir'], 'themes', themeInfo['name'], path) + if os.path.exists(themepath): + if local: + ret = themepath + else: + ret = "/koji-static/themes/%s/%s" % (themeInfo['name'], path) + else: + if local: + ret = os.path.join(themeInfo['staticdir'], path) + else: + ret = "/koji-static/%s" % path + themeCache[path, local] = ret + return ret + +class DecodeUTF8(Cheetah.Filters.Filter): + def filter(self, *args, **kw): + """Convert all strs to unicode objects""" + result = super(DecodeUTF8, self).filter(*args, **kw) + if isinstance(result, unicode): + pass + else: + result = result.decode('utf-8', 'replace') + return result + +# Escape ampersands so the output can be valid XHTML +class XHTMLFilter(DecodeUTF8): + def filter(self, *args, **kw): + result = super(XHTMLFilter, self).filter(*args, **kw) + result = result.replace('&', '&') + result = result.replace('&amp;', '&') + result = result.replace('&nbsp;', ' ') + result = result.replace('&lt;', '<') + result = result.replace('&gt;', '>') + return result + +TEMPLATES = {} + +def _genHTML(environ, fileName): + reqdir = os.path.dirname(environ['SCRIPT_FILENAME']) + if os.getcwd() != reqdir: + os.chdir(reqdir) + + if 'koji.currentUser' in environ: + environ['koji.values']['currentUser'] = environ['koji.currentUser'] + else: + environ['koji.values']['currentUser'] = None + environ['koji.values']['authToken'] = _genToken(environ) + if not environ['koji.values'].has_key('mavenEnabled'): + if 'koji.session' in environ: + environ['koji.values']['mavenEnabled'] = environ['koji.session'].mavenEnabled() + else: + environ['koji.values']['mavenEnabled'] = False + if not environ['koji.values'].has_key('winEnabled'): + if 'koji.session' in environ: + environ['koji.values']['winEnabled'] = environ['koji.session'].winEnabled() + else: + environ['koji.values']['winEnabled'] = False + if not environ['koji.values'].has_key('LoginDisabled'): + if 'koji.options' in environ: + environ['koji.values']['LoginDisabled'] = environ['koji.options']['LoginDisabled'] + else: + environ['koji.values']['LoginDisabled'] = False + + tmpl_class = TEMPLATES.get(fileName) + if not tmpl_class: + tmpl_class = Cheetah.Template.Template.compile(file=fileName) + TEMPLATES[fileName] = tmpl_class + tmpl_inst = tmpl_class(namespaces=[environ['koji.values']], filter=XHTMLFilter) + return tmpl_inst.respond().encode('utf-8', 'replace') + +def _truncTime(): + now = datetime.datetime.now() + # truncate to the nearest 15 minutes + return now.replace(minute=(now.minute / 15 * 15), second=0, microsecond=0) + +def _genToken(environ, tstamp=None): + if 'koji.currentLogin' in environ and environ['koji.currentLogin']: + user = environ['koji.currentLogin'] + else: + return '' + if tstamp == None: + tstamp = _truncTime() + return md5_constructor(user + str(tstamp) + environ['koji.options']['Secret'].value).hexdigest()[-8:] + +def _getValidTokens(environ): + tokens = [] + now = _truncTime() + for delta in (0, 15, 30): + token_time = now - datetime.timedelta(minutes=delta) + token = _genToken(environ, token_time) + if token: + tokens.append(token) + return tokens + +def toggleOrder(template, sortKey, orderVar='order'): + """ + If orderVar equals 'sortKey', return '-sortKey', else + return 'sortKey'. + """ + if template.getVar(orderVar) == sortKey: + return '-' + sortKey + else: + return sortKey + +def toggleSelected(template, var, option): + """ + If the passed in variable var equals the literal value in option, + return 'selected="selected"', otherwise return ''. + Used for setting the selected option in select boxes. + """ + if var == option: + return 'selected="selected"' + else: + return '' + +def sortImage(template, sortKey, orderVar='order'): + """ + Return an html img tag suitable for inclusion in the sortKey of a sortable table, + if the sortValue is "sortKey" or "-sortKey". + """ + orderVal = template.getVar(orderVar) + if orderVal == sortKey: + return 'ascending sort' % themePath("images/gray-triangle-up.gif") + elif orderVal == '-' + sortKey: + return 'descending sort' % themePath("images/gray-triangle-down.gif") + else: + return '' + +def passthrough(template, *vars): + """ + Construct a string suitable for use as URL + parameters. For each variable name in *vars, + if the template has a corresponding non-None value, + append that name-value pair to the string. The name-value + pairs will be separated by ampersands (&), and prefixed by + an ampersand if there are any name-value pairs. If there + are no name-value pairs, an empty string will be returned. + """ + result = [] + for var in vars: + value = template.getVar(var, default=None) + if value != None: + result.append('%s=%s' % (var, value)) + if result: + return '&' + '&'.join(result) + else: + return '' + +def passthrough_except(template, *exclude): + """ + Construct a string suitable for use as URL + parameters. The template calling this method must have + previously used + #attr _PASSTHROUGH = ... + to define the list of variable names to be passed-through. + Any variables names passed in will be excluded from the + list of variables in the output string. + """ + passvars = [] + for var in template._PASSTHROUGH: + if not var in exclude: + passvars.append(var) + return passthrough(template, *passvars) + +def sortByKeyFunc(key, noneGreatest=False): + """Return a function to sort a list of maps by the given key. + If the key starts with '-', sort in reverse order. If noneGreatest + is True, None will sort higher than all other values (instead of lower). + """ + if noneGreatest: + # Normally None evaluates to be less than every other value + # Invert the comparison so it always evaluates to greater + cmpFunc = lambda a, b: (a is None or b is None) and -(cmp(a, b)) or cmp(a, b) + else: + cmpFunc = cmp + + if key.startswith('-'): + key = key[1:] + sortFunc = lambda a, b: cmpFunc(b[key], a[key]) + else: + sortFunc = lambda a, b: cmpFunc(a[key], b[key]) + + return sortFunc + +def paginateList(values, data, start, dataName, prefix=None, order=None, noneGreatest=False, pageSize=50): + """ + Slice the 'data' list into one page worth. Start at offset + 'start' and limit the total number of pages to pageSize + (defaults to 50). 'dataName' is the name under which the + list will be added to the value map, and prefix is the name + under which a number of list-related metadata variables will + be added to the value map. + """ + if order != None: + data.sort(sortByKeyFunc(order, noneGreatest)) + + totalRows = len(data) + + if start: + start = int(start) + if not start or start < 0: + start = 0 + + data = data[start:(start + pageSize)] + count = len(data) + + _populateValues(values, dataName, prefix, data, totalRows, start, count, pageSize, order) + + return data + +def paginateMethod(server, values, methodName, args=None, kw=None, + start=None, dataName=None, prefix=None, order=None, pageSize=50): + """Paginate the results of the method with the given name when called with the given args and kws. + The method must support the queryOpts keyword parameter, and pagination is done in the database.""" + if args is None: + args = [] + if kw is None: + kw = {} + if start: + start = int(start) + if not start or start < 0: + start = 0 + if not dataName: + raise StandardError, 'dataName must be specified' + + kw['queryOpts'] = {'countOnly': True} + totalRows = getattr(server, methodName)(*args, **kw) + + kw['queryOpts'] = {'order': order, + 'offset': start, + 'limit': pageSize} + data = getattr(server, methodName)(*args, **kw) + count = len(data) + + _populateValues(values, dataName, prefix, data, totalRows, start, count, pageSize, order) + + return data + +def paginateResults(server, values, methodName, args=None, kw=None, + start=None, dataName=None, prefix=None, order=None, pageSize=50): + """Paginate the results of the method with the given name when called with the given args and kws. + This method should only be used when then method does not support the queryOpts command (because + the logic used to generate the result list prevents filtering/ordering from being done in the database). + The method must return a list of maps.""" + if args is None: + args = [] + if kw is None: + kw = {} + if start: + start = int(start) + if not start or start < 0: + start = 0 + if not dataName: + raise StandardError, 'dataName must be specified' + + kw['filterOpts'] = {'order': order, + 'offset': start, + 'limit': pageSize} + + totalRows, data = server.countAndFilterResults(methodName, *args, **kw) + count = len(data) + + _populateValues(values, dataName, prefix, data, totalRows, start, count, pageSize, order) + + return data + +def _populateValues(values, dataName, prefix, data, totalRows, start, count, pageSize, order): + """Populate the values list with the data about the list provided.""" + values[dataName] = data + # Don't use capitalize() to title() here, they mess up + # mixed-case name + values['total' + dataName[0].upper() + dataName[1:]] = totalRows + # Possibly prepend a prefix to the numeric parameters, to avoid namespace collisions + # when there is more than one list on the same page + values[(prefix and prefix + 'Start' or 'start')] = start + values[(prefix and prefix + 'Count' or 'count')] = count + values[(prefix and prefix + 'Range' or 'range')] = pageSize + values[(prefix and prefix + 'Order' or 'order')] = order + currentPage = start / pageSize + values[(prefix and prefix + 'CurrentPage' or 'currentPage')] = currentPage + totalPages = totalRows / pageSize + if totalRows % pageSize > 0: + totalPages += 1 + pages = [page for page in range(0, totalPages) if (abs(page - currentPage) < 100 or ((page + 1) % 100 == 0))] + values[(prefix and prefix + 'Pages') or 'pages'] = pages + +def stateName(stateID): + """Convert a numeric build state into a readable name.""" + return koji.BUILD_STATES[stateID].lower() + +def imageTag(name): + """Return an img tag that loads an icon with the given name""" + return '%s' \ + % (themePath("images/%s.png" % name), name, name) + +def stateImage(stateID): + """Return an IMG tag that loads an icon appropriate for + the given state""" + name = stateName(stateID) + return imageTag(name) + +def brStateName(stateID): + """Convert a numeric buildroot state into a readable name.""" + return koji.BR_STATES[stateID].lower() + + +def brLabel(brinfo): + if brinfo['br_type'] == koji.BR_TYPES['STANDARD']: + return '%(tag_name)s-%(id)i-%(repo_id)i' % brinfo + else: + return '%(cg_name)s:%(id)i' % brinfo + + +def repoStateName(stateID): + """Convert a numeric repository state into a readable name.""" + if stateID == koji.REPO_INIT: + return 'initializing' + elif stateID == koji.REPO_READY: + return 'ready' + elif stateID == koji.REPO_EXPIRED: + return 'expired' + elif stateID == koji.REPO_DELETED: + return 'deleted' + else: + return 'unknown' + +def taskState(stateID): + """Convert a numeric task state into a readable name""" + return koji.TASK_STATES[stateID].lower() + +formatTime = koji.formatTime +formatTimeRSS = koji.formatTimeLong +formatTimeLong = koji.formatTimeLong + +def formatDep(name, version, flags): + """Format dependency information into + a human-readable format. Copied from + rpmUtils/miscutils.py:formatRequires()""" + s = name + + if flags: + if flags & (koji.RPMSENSE_LESS | koji.RPMSENSE_GREATER | + koji.RPMSENSE_EQUAL): + s = s + " " + if flags & koji.RPMSENSE_LESS: + s = s + "<" + if flags & koji.RPMSENSE_GREATER: + s = s + ">" + if flags & koji.RPMSENSE_EQUAL: + s = s + "=" + if version: + s = "%s %s" %(s, version) + return s + +def formatMode(mode): + """Format a numeric mode into a ls-like string describing the access mode.""" + if stat.S_ISREG(mode): + result = '-' + elif stat.S_ISDIR(mode): + result = 'd' + elif stat.S_ISCHR(mode): + result = 'c' + elif stat.S_ISBLK(mode): + result = 'b' + elif stat.S_ISFIFO(mode): + result = 'p' + elif stat.S_ISLNK(mode): + result = 'l' + elif stat.S_ISSOCK(mode): + result = 's' + else: + # What is it? Show it like a regular file. + result = '-' + + for x in ('USR', 'GRP', 'OTH'): + for y in ('R', 'W', 'X'): + if mode & getattr(stat, 'S_I' + y + x): + result += y.lower() + else: + result += '-' + + return result + +def rowToggle(template): + """If the value of template._rowNum is even, return 'row-even'; + if it is odd, return 'row-odd'. Increment the value before checking it. + If the template does not have that value, set it to 0.""" + if not hasattr(template, '_rowNum'): + template._rowNum = 0 + template._rowNum += 1 + if template._rowNum % 2: + return 'row-odd' + else: + return 'row-even' + + +def taskScratchClass(task_object): + """ Return a css class indicating whether or not this task is a scratch + build. + """ + method = task_object['method'] + request = task_object['request'] + if method == 'build' and len(request) >= 3: + # Each task method has its own signature for what gets put in the + # request list. Builds should have an `opts` dict at index 2. + # See www/kojiweb/taskinfo.chtml for the grimoire. + opts = request[2] + if opts.get('scratch'): + return "scratch" + return "" + + +_fileFlags = {1: 'configuration', + 2: 'documentation', + 4: 'icon', + 8: 'missing ok', + 16: "don't replace", + 64: 'ghost', + 128: 'license', + 256: 'readme', + 512: 'exclude', + 1024: 'unpatched', + 2048: 'public key'} + +def formatFileFlags(flags): + """Format rpm fileflags for display. Returns + a list of human-readable strings specifying the + flags set in "flags".""" + results = [] + for flag, desc in _fileFlags.items(): + if flags & flag: + results.append(desc) + return results + +def escapeHTML(value): + """Replace special characters to the text can be displayed in + an HTML page correctly. + < : < + > : > + & : & + """ + if not value: + return value + + value = koji.fixEncoding(value) + return value.replace('&', '&').\ + replace('<', '<').\ + replace('>', '>') + +def authToken(template, first=False, form=False): + """Return the current authToken if it exists. + If form is True, return it enclosed in a hidden input field. + Otherwise, return it in a format suitable for appending to a URL. + If first is True, prefix it with ?, otherwise prefix it + with &. If no authToken exists, return an empty string.""" + token = template.getVar('authToken', default=None) + if token != None: + if form: + return '' % token + if first: + return '?a=' + token + else: + return '&a=' + token + else: + return '' + +def explainError(error): + """Explain an exception in user-consumable terms + + Some of the explanations are web-centric, which is why this call is not part + of the main koji library, at least for now... + + Returns a tuple: (str, level) + str = explanation in plain text + level = an integer indicating how much traceback data should + be shown: + 0 - no traceback data + 1 - just the exception + 2 - full traceback + """ + str = "An exception has occurred" + level = 2 + if isinstance(error, koji.ServerOffline): + str = "The server is offline. Please try again later." + level = 0 + elif isinstance(error, koji.ActionNotAllowed): + str = """\ +The web interface has tried to do something that your account is not \ +allowed to do. This is most likely a bug in the web interface.""" + elif isinstance(error, koji.FunctionDeprecated): + str = """\ +The web interface has tried to access a deprecated function. This is \ +most likely a bug in the web interface.""" + elif isinstance(error, koji.RetryError): + str = """\ +The web interface is having difficulty communicating with the main \ +server and was unable to retry an operation. Most likely this indicates \ +a network issue, but it could also be a configuration issue.""" + level = 1 + elif isinstance(error, koji.GenericError): + if getattr(error, 'fromFault', False): + str = """\ +An error has occurred on the main server. This could be a software \ +bug, a server configuration issue, or possibly something else.""" + else: + str = """\ +An error has occurred in the web interface code. This could be due to \ +a bug or a configuration issue.""" + elif isinstance(error, (socket_error, socket_sslerror)): + str = """\ +The web interface is having difficulty communicating with the main \ +server. This most likely indicates a network issue.""" + level = 1 + elif isinstance(error, (ProtocolError, ExpatError)): + str = """\ +The main server returned an invalid response. This could be caused by \ +a network issue or load issues on the server.""" + level = 1 + else: + str = "An error has occurred while processing your request." + return str, level + + +class TaskResultFragment(object): + """Represent an HTML fragment composed from texts and tags. + + This class permits us to compose HTML fragment by the default + composer method or self-defined composer function. + + Public attributes: + - text + - size + - need_escape + - begin_tag + - eng_tag + - composer + - empty_str_placeholder + """ + def __init__(self, text='', size=None, need_escape=None, begin_tag='', + end_tag='', composer=None, empty_str_placeholder=None): + self.text = text + if size is None: + self.size = len(text) + else: + self.size = size + self.need_escape = need_escape + self.begin_tag = begin_tag + self.end_tag = end_tag + if composer is None: + self.composer = self.default_composer + else: + self.composer = lambda length=None: composer(self, length) + if empty_str_placeholder is None: + self.empty_str_placeholder = '...' + else: + self.empty_str_placeholder = empty_str_placeholder + + def default_composer(self, length=None): + import cgi + if length is None: + text = self.text + else: + text = self.text[:length] + if self.need_escape: + text = cgi.escape(text) + if self.size > 0 and text == '': + text = self.empty_str_placeholder + return '%s%s%s' % (self.begin_tag, text, self.end_tag) + + +class TaskResultLine(object): + """Represent an HTML line fragment. + + This class permits us from several TaskResultFragment instances + to compose an HTML fragment that ends with a line break. You + can use the default composer method or give a self-defined version. + + Public attributes: + - fragments + - need_escape + - begin_tag + - end_tag + - composer + """ + def __init__(self, fragments=None, need_escape=None, begin_tag='', + end_tag='
', composer=None): + if fragments is None: + self.fragments = [] + else: + self.fragments = fragments + + self.need_escape = need_escape + self.begin_tag = begin_tag + self.end_tag = end_tag + if composer is None: + self.composer = self.default_composer + else: + + def composer_wrapper(length=None, postscript=None): + return composer(self, length, postscript) + + self.composer = composer_wrapper + self.size=self._size() + + def default_composer(self, length=None, postscript=None): + import cgi + line_text = '' + size = 0 + if postscript is None: + postscript = '' + + for fragment in self.fragments: + if length is None: + line_text += fragment.composer() + else: + if size >= length: break + remainder_size = length - size + line_text += fragment.composer(remainder_size) + size += fragment.size + + if self.need_escape: + line_text = cgi.escape(line_text) + + return '%s%s%s%s' % (self.begin_tag, line_text, postscript, self.end_tag) + + def _size(self): + return sum([fragment.size for fragment in self.fragments]) + + +def _parse_value(key, value, sep=', '): + _str = None + begin_tag = '' + end_tag = '' + need_escape = True + if key in ('brootid', 'buildroot_id'): + _str = str(value) + begin_tag = '' % _str + end_tag = '' + need_escape = False + elif isinstance(value, list): + _str = sep.join([str(val) for val in value]) + elif isinstance(value, dict): + _str = sep.join(['%s=%s' % ((n == '' and "''" or n), v) + for n, v in value.items()]) + else: + _str = str(value) + if _str is None: + _str = '' + + return TaskResultFragment(text=_str, need_escape=need_escape, + begin_tag=begin_tag, end_tag=end_tag) + +def task_result_to_html(result=None, exc_class=None, + max_abbr_lines=None, max_abbr_len=None, + abbr_postscript=None): + """convert the result to a mutiple lines HTML fragment + + Args: + result: task result. Default is empty string. + exc_class: Exception raised when access the task result. + max_abbr_lines: maximum abbreviated result lines. Default is 11. + max_abbr_len: maximum abbreviated result length. Default is 512. + + Returns: + Tuple of full result and abbreviated result. + """ + default_max_abbr_result_lines = 5 + default_max_abbr_result_len = 400 + + if max_abbr_lines is None: + max_abbr_lines = default_max_abbr_result_lines + if max_abbr_len is None: + max_abbr_len = default_max_abbr_result_len + + postscript_fragment = TaskResultFragment( + text='...', end_tag='', + begin_tag='' % ( + 'id="toggle-full-result"', + 'style="display: none;text-decoration:none;"')) + + if abbr_postscript is None: + abbr_postscript = postscript_fragment.composer() + elif isinstance(abbr_postscript, TaskResultFragment): + abbr_postscript = abbr_postscript.composer() + elif isinstance(abbr_postscript, str): + abbr_postscript = abbr_postscript + else: + abbr_postscript = '...' + + if not abbr_postscript.startswith(' '): + abbr_postscript = ' %s' % abbr_postscript + + full_ret_str = '' + abbr_ret_str = '' + lines = [] + + def _parse_properties(props): + return ', '.join([v is not None and '%s=%s' % (n, v) or str(n) + for n, v in props.items()]) + + if exc_class: + if hasattr(result, 'faultString'): + _str = result.faultString.strip() + else: + _str = "%s: %s" % (exc_class.__name__, str(result)) + fragment = TaskResultFragment(text=_str, need_escape=True) + line = TaskResultLine(fragments=[fragment], + begin_tag='
', end_tag='
') + lines.append(line) + elif isinstance(result, dict): + + def composer(line, length=None, postscript=None): + if postscript is None: + postscript = '' + key_fragment = line.fragments[0] + val_fragment = line.fragments[1] + if length is None: + return '%s%s = %s%s%s' % (line.begin_tag, key_fragment.composer(), + val_fragment.composer(), postscript, + line.end_tag) + first_part_len = len('%s = ') + key_fragment.size + remainder_len = length - first_part_len + if remainder_len < 0: remainder_len = 0 + + return '%s%s = %s%s%s' % ( + line.begin_tag, key_fragment.composer(), + val_fragment.composer(remainder_len), postscript, line.end_tag) + + for k, v in result.items(): + if k == 'properties': + _str = "properties = %s" % _parse_properties(v) + fragment = TaskResultFragment(text=_str) + line = TaskResultLine(fragments=[fragment], need_escape=True) + elif k != '__starstar': + val_fragment = _parse_value(k, v) + key_fragment = TaskResultFragment(text=k, need_escape=True) + line = TaskResultLine(fragments=[key_fragment, val_fragment], + need_escape=False, composer=composer) + lines.append(line) + else: + if result is not None: + fragment = _parse_value('', result) + line = TaskResultLine(fragments=[fragment]) + lines.append(line) + + if not lines: + return full_ret_str, abbr_ret_str + + total_lines = len(lines) + full_result_len = sum([line.size for line in lines]) + total_abbr_lines = 0 + total_abbr_len = 0 + + for line in lines: + line_len = line.size + full_ret_str += line.composer() + + if total_lines < max_abbr_lines and full_result_len < max_abbr_len: + continue + if total_abbr_lines >= max_abbr_lines or total_abbr_len >= max_abbr_len: + continue + + if total_abbr_len + line_len >= max_abbr_len: + remainder_abbr_len = max_abbr_len - total_abbr_len + abbr_ret_str += line.composer(remainder_abbr_len, postscript=abbr_postscript) + else: + abbr_ret_str += line.composer() + total_abbr_lines += 1 + total_abbr_len += line_len + + return full_ret_str, abbr_ret_str diff --git a/www/static/Makefile b/www/static/Makefile new file mode 100644 index 0000000..9ec4398 --- /dev/null +++ b/www/static/Makefile @@ -0,0 +1,24 @@ +SUBDIRS = images errors js themes + +SERVERDIR = /usr/share/koji-web/static +FILES = $(wildcard *.css) + +_default: + @echo "nothing to make. try make install" + +clean: + rm -f *.o *.so *.pyc *~ + for d in $(SUBDIRS); do make -s -C $$d clean; done + +install: + @if [ "$(DESTDIR)" = "" ]; then \ + echo " "; \ + echo "ERROR: A destdir is required"; \ + exit 1; \ + fi + + mkdir -p $(DESTDIR)/$(SERVERDIR) + install -p -m 644 $(FILES) $(DESTDIR)/$(SERVERDIR) + + for d in $(SUBDIRS); do make DESTDIR=$(DESTDIR)/$(SERVERDIR) \ + -C $$d install; [ $$? = 0 ] || exit 1; done diff --git a/www/static/debug.css b/www/static/debug.css new file mode 100644 index 0000000..84cbaf4 --- /dev/null +++ b/www/static/debug.css @@ -0,0 +1,9 @@ +/* for debugging purposes */ + +@import url(koji.css); + +* { + border: 1px solid black !IMPORTANT; + margin: 1px !IMPORTANT; + padding: 1px !IMPORTANT; +} diff --git a/www/static/errors/Makefile b/www/static/errors/Makefile new file mode 100644 index 0000000..9da0127 --- /dev/null +++ b/www/static/errors/Makefile @@ -0,0 +1,18 @@ +SERVERDIR = /errors +FILES = $(wildcard *.html) + +_default: + @echo "nothing to make. try make install" + +clean: + rm -f *.o *.so *.pyc *~ + +install: + @if [ "$(DESTDIR)" = "" ]; then \ + echo " "; \ + echo "ERROR: A destdir is required"; \ + exit 1; \ + fi + + mkdir -p $(DESTDIR)/$(SERVERDIR) + install -p -m 644 $(FILES) $(DESTDIR)/$(SERVERDIR) diff --git a/www/static/errors/unauthorized.html b/www/static/errors/unauthorized.html new file mode 100644 index 0000000..18fa367 --- /dev/null +++ b/www/static/errors/unauthorized.html @@ -0,0 +1,37 @@ + + + + Authentication Failed | Koji + + + + + + + + +
+
+ + + + +
+

Kerberos Authentication Failed

+ The Koji Web UI was unable to verify your Kerberos credentials. Please make sure that you have valid + Kerberos tickets (obtainable via kinit), and that you have + configured your browser correctly. +
+ + + +
+
+ + + diff --git a/www/static/images/1px.gif b/www/static/images/1px.gif new file mode 100644 index 0000000..41ee92f Binary files /dev/null and b/www/static/images/1px.gif differ diff --git a/www/static/images/Makefile b/www/static/images/Makefile new file mode 100644 index 0000000..fd7f2ca --- /dev/null +++ b/www/static/images/Makefile @@ -0,0 +1,18 @@ +SERVERDIR = /images +FILES = $(wildcard *.gif *.png *.ico) + +_default: + @echo "nothing to make. try make install" + +clean: + rm -f *.o *.so *.pyc *~ + +install: + @if [ "$(DESTDIR)" = "" ]; then \ + echo " "; \ + echo "ERROR: A destdir is required"; \ + exit 1; \ + fi + + mkdir -p $(DESTDIR)/$(SERVERDIR) + install -p -m 644 $(FILES) $(DESTDIR)/$(SERVERDIR) diff --git a/www/static/images/assigned.png b/www/static/images/assigned.png new file mode 100644 index 0000000..9ad3715 Binary files /dev/null and b/www/static/images/assigned.png differ diff --git a/www/static/images/bkgrnd_greydots.png b/www/static/images/bkgrnd_greydots.png new file mode 100644 index 0000000..d5e79e8 Binary files /dev/null and b/www/static/images/bkgrnd_greydots.png differ diff --git a/www/static/images/building.png b/www/static/images/building.png new file mode 100644 index 0000000..1b4710b Binary files /dev/null and b/www/static/images/building.png differ diff --git a/www/static/images/canceled.png b/www/static/images/canceled.png new file mode 100644 index 0000000..acf2036 Binary files /dev/null and b/www/static/images/canceled.png differ diff --git a/www/static/images/closed.png b/www/static/images/closed.png new file mode 100644 index 0000000..8e7a9d7 Binary files /dev/null and b/www/static/images/closed.png differ diff --git a/www/static/images/complete.png b/www/static/images/complete.png new file mode 100644 index 0000000..8e7a9d7 Binary files /dev/null and b/www/static/images/complete.png differ diff --git a/www/static/images/deleted.png b/www/static/images/deleted.png new file mode 100644 index 0000000..bbdf224 Binary files /dev/null and b/www/static/images/deleted.png differ diff --git a/www/static/images/expired.png b/www/static/images/expired.png new file mode 100644 index 0000000..dc7ed60 Binary files /dev/null and b/www/static/images/expired.png differ diff --git a/www/static/images/failed.png b/www/static/images/failed.png new file mode 100644 index 0000000..b4b29bf Binary files /dev/null and b/www/static/images/failed.png differ diff --git a/www/static/images/free.png b/www/static/images/free.png new file mode 100644 index 0000000..fa147fe Binary files /dev/null and b/www/static/images/free.png differ diff --git a/www/static/images/gray-triangle-down.gif b/www/static/images/gray-triangle-down.gif new file mode 100644 index 0000000..e5d3dac Binary files /dev/null and b/www/static/images/gray-triangle-down.gif differ diff --git a/www/static/images/gray-triangle-up.gif b/www/static/images/gray-triangle-up.gif new file mode 100644 index 0000000..09149a4 Binary files /dev/null and b/www/static/images/gray-triangle-up.gif differ diff --git a/www/static/images/init.png b/www/static/images/init.png new file mode 100644 index 0000000..8177e6b Binary files /dev/null and b/www/static/images/init.png differ diff --git a/www/static/images/initializing.png b/www/static/images/initializing.png new file mode 100644 index 0000000..8177e6b Binary files /dev/null and b/www/static/images/initializing.png differ diff --git a/www/static/images/koji.ico b/www/static/images/koji.ico new file mode 100644 index 0000000..7affb4b Binary files /dev/null and b/www/static/images/koji.ico differ diff --git a/www/static/images/koji.png b/www/static/images/koji.png new file mode 100644 index 0000000..2a7b508 Binary files /dev/null and b/www/static/images/koji.png differ diff --git a/www/static/images/no.png b/www/static/images/no.png new file mode 100644 index 0000000..b4b29bf Binary files /dev/null and b/www/static/images/no.png differ diff --git a/www/static/images/open.png b/www/static/images/open.png new file mode 100644 index 0000000..c1e6e96 Binary files /dev/null and b/www/static/images/open.png differ diff --git a/www/static/images/powered-by-koji.png b/www/static/images/powered-by-koji.png new file mode 100644 index 0000000..32b52e3 Binary files /dev/null and b/www/static/images/powered-by-koji.png differ diff --git a/www/static/images/ready.png b/www/static/images/ready.png new file mode 100644 index 0000000..8e7a9d7 Binary files /dev/null and b/www/static/images/ready.png differ diff --git a/www/static/images/unknown.png b/www/static/images/unknown.png new file mode 100644 index 0000000..5b83f4f Binary files /dev/null and b/www/static/images/unknown.png differ diff --git a/www/static/images/waiting.png b/www/static/images/waiting.png new file mode 100644 index 0000000..fa147fe Binary files /dev/null and b/www/static/images/waiting.png differ diff --git a/www/static/images/yes.png b/www/static/images/yes.png new file mode 100644 index 0000000..8e7a9d7 Binary files /dev/null and b/www/static/images/yes.png differ diff --git a/www/static/js/Makefile b/www/static/js/Makefile new file mode 100644 index 0000000..8a4627f --- /dev/null +++ b/www/static/js/Makefile @@ -0,0 +1,18 @@ +SERVERDIR = /js +FILES = $(wildcard *.js) + +_default: + @echo "nothing to make. try make install" + +clean: + rm -f *.o *.so *.pyc *~ + +install: + @if [ "$(DESTDIR)" = "" ]; then \ + echo " "; \ + echo "ERROR: A destdir is required"; \ + exit 1; \ + fi + + mkdir -p $(DESTDIR)/$(SERVERDIR) + install -p -m 644 $(FILES) $(DESTDIR)/$(SERVERDIR) diff --git a/www/static/js/watchlogs.js b/www/static/js/watchlogs.js new file mode 100644 index 0000000..2f0cdcd --- /dev/null +++ b/www/static/js/watchlogs.js @@ -0,0 +1,194 @@ +var MAX_ERRORS = 5; // errors before we just stop +var CHUNK_SIZE = 16384; + +// General globals +var baseURL = window.location.href.substring(0, window.location.href.lastIndexOf("/")); +var logElement = null; +var headerElement = null; +var errorCount = 0; +var tasks = null; +var offsets = {}; +var lastlog = ""; + +var tasksToProcess = null; +var currentTaskID = null; +var currentInfo = null; +var currentLogs = null; +var currentLog = null; + +function parseTasklist() { + var tasklist = []; + var queryStr = unescape(window.location.search.substring(1)); + var vars = queryStr.split('&'); + for (var i=0; i