Joe Gordon 1ee0025c0b Delete closed bugs after 5 days in recheckwatch
Instead of leaving closed bugs around for 30 days, remove them after 5
days.  If a closed bug hasn't reoccurred in 5 days there is a good chance
it has been fixed correctly.  The number of days to keep around a closed
bug is set in by closed_age in the config file.

Change-Id: I5d136173ca14a8f9e77a44e29e9499c94a417cd3
2013-09-11 18:05:48 -07:00

235 lines
7.3 KiB
Python
Executable File

#!/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
CLOSED_STATUSES = ['Fix Released', 'Invalid', 'Fix Committed']
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
self.duplicate_of = None
self.update()
def update(self):
launchpad = Launchpad.login_anonymously('recheckwatch',
'production')
lpitem = launchpad.bugs[self.number]
self.title = lpitem.title
self.status = map(lambda x: x.status,
lpitem.bug_tasks)
if lpitem.duplicate_of:
self.duplicate_of = lpitem.duplicate_of.id
def is_closed(self):
closed = True
for status in self.status:
if status not in CLOSED_STATUSES:
closed = False
return closed
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
def addHits(self, hits):
for hit in hits:
self.addHit(hit)
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.closed_age = config.getint('recheckwatch', 'closed_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_bug_format()
self.update()
def _update_bug_format(self):
for bugno, bug in self.scores.items():
if not hasattr(bug, 'duplicate_of'):
bug.duplicate_of = None
bug.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._get_bug(bugno)
bug.addHit(hit)
self.scores[bugno] = bug
self.update()
def _get_bug(self, bugno):
""""Get latest bug information and create bug if not in score."""
bug = self.scores.get(bugno)
if not bug:
bug = Bug(bugno)
else:
bug.update()
return bug
def update(self):
# Check for duplicate bugs
dupes = []
for bugno, bug in self.scores.items():
if bug.duplicate_of:
dupes.append(bugno)
for bugno in dupes:
dupno = self.scores[bugno].duplicate_of
bug = self._get_bug(dupno)
bug.addHits(self.scores[bugno].hits)
self.scores[dupno] = bug
del self.scores[bugno]
# Remove bugs that haven't been seen in ages
# Or closed bugs older then self.closed_age
to_remove = []
now = datetime.datetime.utcnow()
for bugno, bug in self.scores.items():
if (bug.last_seen < now-datetime.timedelta(days=self.age) or
(bug.is_closed() and
bug.last_seen < now-datetime.timedelta(days=self.closed_age))):
to_remove.append(bugno)
for bugno in to_remove:
del self.scores[bugno]
def impact(bug):
"""Golf rules for bugs, smaller the more urgent."""
age = (now - bug.last_seen).total_seconds()
if bug.is_closed():
age = age + (5.0 * 86400.0)
return age
# Get the bugs reverse sorted by impact
bugs = self.scores.values()
# freshen to get lp bug status
for bug in bugs:
bug.update()
bugs.sort(lambda a,b: cmp(impact(a), impact(b)))
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(daemonize=True):
config = ConfigParser.ConfigParser()
config.read(sys.argv[1])
s = Scoreboard(config)
if daemonize:
s.start()
def main():
if len(sys.argv) < 2:
print "Usage: %s CONFIGFILE" % sys.argv[0]
sys.exit(1)
if '-n' in sys.argv:
_main(daemonize=False)
elif '-d' in sys.argv:
_main()
else:
pid = pid_file_module.TimeoutPIDLockFile(
"/var/run/recheckwatch/recheckwatch.pid", 10)
with daemon.DaemonContext(pidfile=pid):
_main()
if __name__ == "__main__":
main()