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
+
+
+
+ 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/