system-config/modules/recheckwatch/files/recheckwatch
Joe Gordon 1686cbb009 Make "Won't Fix" a closed status in recheckwatch
When checking if a bug is closed we go through all of its affecting
project statuses, and if a status is Won't Fix the bug will stay on the
recheckwatch list.  But if a bug is marked as won't fix its safe to
assume the root cause of the issue is fixed.  Otherwise the bug will
happen again and someone will set the status back to open.

For example: https://bugs.launchpad.net/nova/+bug/1186867

Change-Id: Iaddc4990062d5a80e188f10c14e91625b7605165
2013-09-20 14:22:09 -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', 'Won\'t Fix']
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()