diff --git a/modules/openstack_project/files/zuul/scoreboard.html b/modules/openstack_project/files/zuul/scoreboard.html new file mode 100644 index 0000000000..bf5817932c --- /dev/null +++ b/modules/openstack_project/files/zuul/scoreboard.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+

Bugs Causing Rechecks

+
+
+

+ + Bug ${bug.number}: ${bug.title} +

+ First seen: ${bug.first_seen.strftime("%Y-%m-%d %H:%M:%S")} UTC
+ Last seen: ${bug.last_seen.strftime("%Y-%m-%d %H:%M:%S")} UTC
+ Rechecks: ${len(bug.hits)}
+ Affecting projects: + + Affecting changes: + +
+
+ +
+
+ +
+ + + diff --git a/modules/openstack_project/manifests/zuul.pp b/modules/openstack_project/manifests/zuul.pp index f38abf7a6d..37186eb03f 100644 --- a/modules/openstack_project/manifests/zuul.pp +++ b/modules/openstack_project/manifests/zuul.pp @@ -49,4 +49,16 @@ class openstack_project::zuul( source => 'puppet:///modules/openstack_project/zuul/logging.conf', notify => Exec['zuul-reload'], } + + class { '::recheckwatch': + gerrit_server => $gerrit_server, + gerrit_user => $gerrit_user, + recheckwatch_ssh_private_key => $zuul_ssh_private_key, + } + + file { '/var/lib/recheckwatch/scoreboard.html': + ensure => present, + source => 'puppet:///modules/openstack_project/zuul/scoreboard.html', + require => File['/var/lib/recheckwatch'], + } } diff --git a/modules/recheckwatch/files/recheckwatch b/modules/recheckwatch/files/recheckwatch new file mode 100755 index 0000000000..b41ce52edb --- /dev/null +++ b/modules/recheckwatch/files/recheckwatch @@ -0,0 +1,171 @@ +#!/usr/bin/env python +# Copyright 2012 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import ConfigParser +import datetime +import re +import sys +import threading +import traceback +import cPickle as pickle +import os + +from genshi.template import TemplateLoader +from launchpadlib.launchpad import Launchpad +from launchpadlib.uris import LPNET_SERVICE_ROOT +import daemon + +try: + import daemon.pidlockfile + pid_file_module = daemon.pidlockfile +except: + # as of python-daemon 1.6 it doesn't bundle pidlockfile anymore + # instead it depends on lockfile-0.9.1 + import daemon.pidfile + pid_file_module = daemon.pidfile + + +class Hit(object): + def __init__(self, project, change): + self.project = project + self.change = change + self.ts = datetime.datetime.utcnow() + +class Bug(object): + def __init__(self, number): + self.number = number + self.hits = [] + self.projects = [] + self.changes = [] + self.last_seen = None + self.first_seen = None + launchpad = Launchpad.login_anonymously('recheckwatch', + 'production') + self.title = launchpad.bugs[number].title + + def addHit(self, hit): + self.hits.append(hit) + if not self.first_seen: + self.first_seen = hit.ts + if hit.project not in self.projects: + self.projects.append(hit.project) + if hit.change not in self.changes: + self.changes.append(hit.change) + self.last_seen = hit.ts + +class Scoreboard(threading.Thread): + def __init__(self, config): + threading.Thread.__init__(self) + self.scores = {} + + server = config.get('gerrit', 'host') + username = config.get('gerrit', 'user') + port = config.getint('gerrit', 'port') + keyfile = config.get('gerrit', 'key', None) + + self.pickle_dir = config.get('recheckwatch', 'pickle_dir') + self.pickle_file = os.path.join(self.pickle_dir, 'scoreboard.pickle') + self.template_dir = config.get('recheckwatch', 'template_dir') + self.output_file = config.get('recheckwatch', 'output_file') + self.age = config.getint('recheckwatch', 'age') + self.regex = re.compile(config.get('recheckwatch', 'regex')) + + if os.path.exists(self.pickle_file): + out = open(self.pickle_file, 'rb') + self.scores = pickle.load(out) + out.close() + + # Import here because it needs to happen after daemonization + import gerritlib.gerrit + self.gerrit = gerritlib.gerrit.Gerrit(server, username, port, keyfile) + self.update() + + def _read(self, data): + if data.get('type', '') != 'comment-added': + return + comment = data.get('comment', '') + m = self.regex.match(comment.strip()) + if not m: + return + change_record = data.get('change', {}) + change = change_record.get('number') + project = change_record.get('project') + bugno = int(m.group('bugno')) + hit = Hit(project, change) + bug = self.scores.get(bugno) + if not bug: + bug = Bug(bugno) + bug.addHit(hit) + self.scores[bugno] = bug + self.update() + + def update(self): + # Remove bugs that haven't been seen in ages + to_remove = [] + now = datetime.datetime.utcnow() + for bugno, bug in self.scores.items(): + if bug.last_seen < now-datetime.timedelta(days=self.age): + to_remove.append(bugno) + for bugno in to_remove: + del self.scores[bug] + + def impact(bug): + "This ranks more recent bugs higher" + return (len(bug.hits) * (5.0 / ((bug.last_seen-now).days))) + + # Get the bugs reverse sorted by impact + bugs = self.scores.values() + bugs.sort(lambda a,b: cmp(impact(a), impact(b))) + bugs.reverse() + loader = TemplateLoader([self.template_dir], auto_reload=True) + tmpl = loader.load('scoreboard.html') + out = open(self.output_file, 'w') + out.write(tmpl.generate(bugs = bugs).render('html', doctype='html')) + + out = open(self.pickle_file, 'wb') + pickle.dump(self.scores, out, -1) + out.close() + + def run(self): + self.gerrit.startWatching() + while True: + event = self.gerrit.getEvent() + try: + self._read(event) + except: + traceback.print_exc() + +def _main(): + config = ConfigParser.ConfigParser() + config.read(sys.argv[1]) + + s = Scoreboard(config) + s.start() + +def main(): + if len(sys.argv) < 2: + print "Usage: %s CONFIGFILE" % sys.argv[0] + sys.exit(1) + + if '-d' not in sys.argv: + pid = pid_file_module.TimeoutPIDLockFile( + "/var/run/recheckwatch/recheckwatch.pid", 10) + with daemon.DaemonContext(pidfile=pid): + _main() + else: + _main() + +if __name__ == "__main__": + main() diff --git a/modules/recheckwatch/files/recheckwatch.init b/modules/recheckwatch/files/recheckwatch.init new file mode 100644 index 0000000000..c1e45b15ff --- /dev/null +++ b/modules/recheckwatch/files/recheckwatch.init @@ -0,0 +1,146 @@ +#! /bin/sh +### BEGIN INIT INFO +# Provides: recheckwatch +# Required-Start: $remote_fs $syslog +# Required-Stop: $remote_fs $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Recheckwatch +# Description: Trunk gating system +### END INIT INFO + +# Do NOT "set -e" + +# PATH should only include /usr/* if it runs after the mountnfs.sh script +PATH=/sbin:/usr/sbin:/bin:/usr/bin +DESC="Recheckwatch" +NAME=recheckwatch +DAEMON=/usr/local/bin/recheckwatch +DAEMON_ARGS=/etc/recheckwatch/recheckwatch.conf +PIDFILE=/var/run/$NAME/$NAME.pid +SCRIPTNAME=/etc/init.d/$NAME +USER=recheckwatch + +# Exit if the package is not installed +[ -x "$DAEMON" ] || exit 0 + +# Read configuration variable file if it is present +[ -r /etc/default/$NAME ] && . /etc/default/$NAME + +# Load the VERBOSE setting and other rcS variables +. /lib/init/vars.sh + +# Define LSB log_* functions. +# Depend on lsb-base (>= 3.0-6) to ensure that this file is present. +. /lib/lsb/init-functions + +# +# Function that starts the daemon/service +# +do_start() +{ + # Return + # 0 if daemon has been started + # 1 if daemon was already running + # 2 if daemon could not be started + + mkdir -p /var/run/$NAME + chown $USER /var/run/$NAME + start-stop-daemon --start --quiet --pidfile $PIDFILE -c $USER --exec $DAEMON --test > /dev/null \ + || return 1 + start-stop-daemon --start --quiet --pidfile $PIDFILE -c $USER --exec $DAEMON -- \ + $DAEMON_ARGS \ + || return 2 + # Add code here, if necessary, that waits for the process to be ready + # to handle requests from services started subsequently which depend + # on this one. As a last resort, sleep for some time. +} + +# +# Function that stops the daemon/service +# +do_stop() +{ + # Return + # 0 if daemon has been stopped + # 1 if daemon was already stopped + # 2 if daemon could not be stopped + # other if a failure occurred + start-stop-daemon --stop --signal 9 --pidfile $PIDFILE + RETVAL="$?" + [ "$RETVAL" = 2 ] && return 2 + rm -f /var/run/$NAME/* + return "$RETVAL" +} + +# +# Function that stops the daemon/service +# +do_graceful_stop() +{ + PID=`cat $PIDFILE` + kill -USR1 $PID + + # wait until really stopped + if [ -n "${PID:-}" ]; then + i=0 + while kill -0 "${PID:-}" 2> /dev/null; do + if [ $i -eq '0' ]; then + echo -n " ... waiting " + else + echo -n "." + fi + i=$(($i+1)) + sleep 1 + done + fi + + rm -f /var/run/$NAME/* +} + +case "$1" in + start) + [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" + do_start + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + stop) + [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" + do_stop + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + status) + status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? + ;; + #reload|force-reload) + # + # If do_reload() is not implemented then leave this commented out + # and leave 'force-reload' as an alias for 'restart'. + # + #log_daemon_msg "Reloading $DESC" "$NAME" + #do_reload + #log_end_msg $? + #;; + restart|force-reload) + # + # If the "reload" option is implemented then remove the + # 'force-reload' alias + # + log_daemon_msg "Restarting $DESC" "$NAME" + do_graceful_stop + do_start + ;; + *) + #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 + echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 + exit 3 + ;; +esac + +: diff --git a/modules/recheckwatch/manifests/init.pp b/modules/recheckwatch/manifests/init.pp new file mode 100644 index 0000000000..060979e56b --- /dev/null +++ b/modules/recheckwatch/manifests/init.pp @@ -0,0 +1,118 @@ +# == Class: recheckwatch +# +class recheckwatch ( + $gerrit_server = '', + $gerrit_user = '', + $recheckwatch_ssh_private_key = '', +) { + + if ! defined(Package['python-daemon']) { + package { 'python-daemon': + ensure => present, + } + } + + if ! defined(Package['python-genshi']) { + package { 'python-genshi': + ensure => present, + } + } + + if ! defined(Package['python-launchpadlib']) { + package { 'python-launchpadlib': + ensure => present, + } + } + + if ! defined(Package['gerritlib']) { + package { 'gerritlib': + ensure => latest, + provider => pip, + require => Class['pip'], + } + } + + user { 'recheckwatch': + ensure => present, + home => '/home/recheckwatch', + shell => '/bin/bash', + gid => 'recheckwatch', + managehome => true, + require => Group['recheckwatch'], + } + + group { 'recheckwatch': + ensure => present, + } + + file { '/etc/recheckwatch': + ensure => directory, + } + + file { '/etc/recheckwatch/recheckwatch.conf': + ensure => present, + owner => 'recheckwatch', + mode => '0400', + content => template('recheckwatch/recheckwatch.conf.erb'), + require => [ + File['/etc/recheckwatch'], + User['recheckwatch'], + ], + } + + file { '/var/run/recheckwatch': + ensure => directory, + owner => 'recheckwatch', + require => User['recheckwatch'], + } + + file { '/var/www/recheckwatch': + ensure => directory, + owner => 'recheckwatch', + mode => '0755', + require => User['recheckwatch'], + } + + file { '/var/lib/recheckwatch': + ensure => directory, + owner => 'recheckwatch', + require => User['recheckwatch'], + } + + file { '/var/lib/recheckwatch/ssh': + ensure => directory, + owner => 'recheckwatch', + group => 'recheckwatch', + mode => '0500', + require => File['/var/lib/recheckwatch'], + } + + file { '/var/lib/recheckwatch/ssh/id_rsa': + owner => 'recheckwatch', + group => 'recheckwatch', + mode => '0400', + require => File['/var/lib/recheckwatch/ssh'], + content => $recheckwatch_ssh_private_key, + } + + file { '/etc/init.d/recheckwatch': + ensure => present, + owner => 'root', + group => 'root', + mode => '0555', + source => 'puppet:///modules/recheckwatch/recheckwatch.init', + } + + service { 'recheckwatch': + name => 'recheckwatch', + enable => true, + hasrestart => true, + require => File['/etc/init.d/recheckwatch'], + } + + file { '/usr/local/bin/recheckwatch': + ensure => present, + mode => '0555', + source => 'puppet:///modules/recheckwatch/recheckwatch', + } +} diff --git a/modules/recheckwatch/templates/recheckwatch.conf.erb b/modules/recheckwatch/templates/recheckwatch.conf.erb new file mode 100644 index 0000000000..89645a2d70 --- /dev/null +++ b/modules/recheckwatch/templates/recheckwatch.conf.erb @@ -0,0 +1,12 @@ +[recheckwatch] +pickle_dir=/var/lib/recheckwatch +template_dir=/var/lib/recheckwatch +output_file=/var/www/recheckwatch/rechecks.html +age=30 ; days +regex=(?i)^(?Precheck|reverify) (?:bug|lp)[\s#:]*(?P\d+)$ + +[gerrit] +server=<%= gerrit_server %> +user=<%= gerrit_user %> +sshkey=/var/lib/recheckwatch/ssh/id_rsa +port=29418 diff --git a/modules/zuul/manifests/init.pp b/modules/zuul/manifests/init.pp index 65ae1e5703..37ed97d0b3 100644 --- a/modules/zuul/manifests/init.pp +++ b/modules/zuul/manifests/init.pp @@ -18,7 +18,6 @@ class zuul ( $packages = [ 'python-webob', - 'python-daemon', 'python-lockfile', 'python-paste', ] @@ -27,19 +26,6 @@ class zuul ( ensure => present, } - user { 'zuul': - ensure => present, - home => '/home/zuul', - shell => '/bin/bash', - gid => 'zuul', - managehome => true, - require => Group['zuul'], - } - - group { 'zuul': - ensure => present, - } - # A lot of things need yaml, be conservative requiring this package to avoid # conflicts with other modules. if ! defined(Package['python-yaml']) { @@ -54,6 +40,25 @@ class zuul ( } } + if ! defined(Package['python-daemon']) { + package { 'python-daemon': + ensure => present, + } + } + + user { 'zuul': + ensure => present, + home => '/home/zuul', + shell => '/bin/bash', + gid => 'zuul', + managehome => true, + require => Group['zuul'], + } + + group { 'zuul': + ensure => present, + } + # Packages that need to be installed from pip $pip_packages = [ 'GitPython', diff --git a/modules/zuul/templates/zuul.vhost.erb b/modules/zuul/templates/zuul.vhost.erb index 2dda185652..1690f95aed 100644 --- a/modules/zuul/templates/zuul.vhost.erb +++ b/modules/zuul/templates/zuul.vhost.erb @@ -15,6 +15,8 @@ SetEnv GIT_PROJECT_ROOT /var/lib/zuul/git/ SetEnv GIT_HTTP_EXPORT_ALL + Alias /rechecks.html /var/www/recheckwatch/rechecks.html + AliasMatch ^/p/(.*/objects/[0-9a-f]{2}/[0-9a-f]{38})$ /var/lib/zuul/git/$1 AliasMatch ^/p/(.*/objects/pack/pack-[0-9a-f]{40}.(pack|idx))$ /var/lib/zuul/git/$1 ScriptAlias /p/ /usr/lib/git-core/git-http-backend/