| 1 |
# The module which forms the heart of the MIA scripts
|
| 2 |
# Copyright (C) 2001, 2002, 2003, 2004 Martin Michlmayr <tbm@cyrius.com>
|
| 3 |
# Copyright (C) 2006 Jeroen van Wolffelaar <jeroen@wolffelaar.nl>
|
| 4 |
# Copyright (C) 2006 Christoph Berg <myon@debian.org>
|
| 5 |
# $Id$
|
| 6 |
|
| 7 |
# This program is free software; you can redistribute it and/or modify
|
| 8 |
# it under the terms of the GNU General Public License as published by
|
| 9 |
# the Free Software Foundation; either version 2 of the License, or
|
| 10 |
# (at your option) any later version.
|
| 11 |
|
| 12 |
# This program is distributed in the hope that it will be useful,
|
| 13 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
# GNU General Public License for more details.
|
| 16 |
|
| 17 |
# You should have received a copy of the GNU General Public License
|
| 18 |
# along with this program; if not, write to the Free Software
|
| 19 |
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
| 20 |
|
| 21 |
|
| 22 |
# This is a collection of software to track down developers who are
|
| 23 |
# Missing In Action (MIA).
|
| 24 |
|
| 25 |
import os, re, string, sys, time, pwd
|
| 26 |
sys.path.append("/org/qa.debian.org/carnivore")
|
| 27 |
import apt_pkg, carnivore
|
| 28 |
|
| 29 |
now = time.time()
|
| 30 |
|
| 31 |
config = "/org/qa.debian.org/mia/mia.conf"
|
| 32 |
|
| 33 |
|
| 34 |
re_summary_line = re.compile(r"(\d+)( [a-z]+ [^ \t]+)*:\s*(.*)")
|
| 35 |
|
| 36 |
apt_pkg.init()
|
| 37 |
Cnf = apt_pkg.newConfiguration()
|
| 38 |
apt_pkg.ReadConfigFileISC(Cnf, config)
|
| 39 |
info = { }
|
| 40 |
|
| 41 |
def find_status(path=Cnf["Dir::Database"]):
|
| 42 |
files = []
|
| 43 |
|
| 44 |
try:
|
| 45 |
for file in os.listdir(path):
|
| 46 |
root, ext = os.path.splitext(file)
|
| 47 |
if ext == ".summary":
|
| 48 |
files.append(root)
|
| 49 |
except OSError:
|
| 50 |
print "Cannot find directory of database: %s" % path
|
| 51 |
sys.exit(1)
|
| 52 |
|
| 53 |
return files
|
| 54 |
|
| 55 |
|
| 56 |
def read_history(id):
|
| 57 |
global info
|
| 58 |
id = id.replace('@', '=')
|
| 59 |
|
| 60 |
if info.has_key(id):
|
| 61 |
return info[id]
|
| 62 |
|
| 63 |
try:
|
| 64 |
summary = open(Cnf["Dir::Database"] + "/" + id + ".summary", "r")
|
| 65 |
except IOError:
|
| 66 |
return None
|
| 67 |
|
| 68 |
res = {}
|
| 69 |
|
| 70 |
for line in summary.readlines():
|
| 71 |
result = re_summary_line.search(line)
|
| 72 |
|
| 73 |
if result is None:
|
| 74 |
print >> sys.stderr, "Error parsing file %s: %s" % (file, line)
|
| 75 |
sys.exit(1)
|
| 76 |
|
| 77 |
text = result.group(3)
|
| 78 |
# TODO: return sensibly
|
| 79 |
if result.group(2):
|
| 80 |
extra = result.group(2).strip().split()
|
| 81 |
while extra:
|
| 82 |
if extra[0] == "nomail":
|
| 83 |
if text.find(";") == -1:
|
| 84 |
text += ";"
|
| 85 |
extra.pop(0)
|
| 86 |
text += " {command-line supplied by %s}" % extra.pop(0)
|
| 87 |
elif extra[0] == "from":
|
| 88 |
if text.find(";") == -1:
|
| 89 |
text += ";"
|
| 90 |
extra.pop(0)
|
| 91 |
text += " {from %s}" % extra.pop(0)
|
| 92 |
else:
|
| 93 |
print >> sys.stderr, "Error parsing extra info in %s: %s" % (file, line)
|
| 94 |
sys.exit(1)
|
| 95 |
|
| 96 |
t = int(result.group(1))
|
| 97 |
while res.has_key(t): # gross hack to make keys unique
|
| 98 |
t += 1
|
| 99 |
res[t] = text
|
| 100 |
|
| 101 |
summary.close()
|
| 102 |
|
| 103 |
info[id] = res
|
| 104 |
return res
|
| 105 |
|
| 106 |
regular_statusses = {
|
| 107 |
'none': 0,
|
| 108 |
'ok': 0,
|
| 109 |
'busy': 1,
|
| 110 |
'inactive': 2,
|
| 111 |
'unresponsive': 3,
|
| 112 |
'retiring': 4,
|
| 113 |
'mia': 6,
|
| 114 |
'needs-wat': 7,
|
| 115 |
'retired': 10,
|
| 116 |
'removed': 10,
|
| 117 |
'role': 99
|
| 118 |
}
|
| 119 |
suspend_statusses = {
|
| 120 |
'ok': -1,
|
| 121 |
'npa': 365*24*3600,
|
| 122 |
'willfix': 4*7*24*3600
|
| 123 |
}
|
| 124 |
prod_types = ['nice', 'prod', 'last-warning', 'wat']
|
| 125 |
|
| 126 |
def parse_status(info):
|
| 127 |
status = 'none'
|
| 128 |
status_time = -1
|
| 129 |
suspend_status = None
|
| 130 |
suspend_until = -1
|
| 131 |
prod = 'none'
|
| 132 |
prod_time = -1
|
| 133 |
times = info.keys()
|
| 134 |
times.sort()
|
| 135 |
for time in times:
|
| 136 |
value = info[time].split(';')[0]
|
| 137 |
for v in value.split(','):
|
| 138 |
v = v.strip().lower()
|
| 139 |
foo = v.split(None, 1)
|
| 140 |
word = foo[0]
|
| 141 |
rest = None
|
| 142 |
if len(foo) > 1:
|
| 143 |
rest = foo[1]
|
| 144 |
if v in regular_statusses.keys() and v != 'none':
|
| 145 |
if regular_statusses[v] == 0 \
|
| 146 |
or regular_statusses[v] > regular_statusses[status]:
|
| 147 |
status = v
|
| 148 |
status_time = time
|
| 149 |
elif word in suspend_statusses.keys():
|
| 150 |
suspend_status = word
|
| 151 |
suspend_until = time + parse_for(suspend_statusses[word], rest)
|
| 152 |
elif word in prod_types:
|
| 153 |
prod = word
|
| 154 |
prod_time = time
|
| 155 |
# suspend current status by default for 3 weeks in case of
|
| 156 |
# outgoing mail, special case: 2 weeks for nice since nice is
|
| 157 |
# to be sent twice anyways
|
| 158 |
suspend_status = 'mailed'
|
| 159 |
if prod == 'nice':
|
| 160 |
suspend_until = time + parse_for(2*7*24*3600, rest)
|
| 161 |
else:
|
| 162 |
suspend_until = time + parse_for(3*7*24*3600, rest)
|
| 163 |
elif word in ['in', 'out']:
|
| 164 |
# suspend current status by default for 2 weeks in case of
|
| 165 |
# mail contact
|
| 166 |
susp = time + parse_for(2*7*24*3600, rest)
|
| 167 |
if susp > suspend_until:
|
| 168 |
suspend_status = 'mailed'
|
| 169 |
suspend_until = susp
|
| 170 |
elif v == "-":
|
| 171 |
pass
|
| 172 |
else:
|
| 173 |
# too much stuff yet # sys.stderr.write("Warning: parse error in %s\n" % v)
|
| 174 |
if not info[time].endswith(" {parse error}"):
|
| 175 |
if info[time].find(";") == -1:
|
| 176 |
info[time] += ";"
|
| 177 |
info[time] += " {parse error}"
|
| 178 |
if status == 'none':
|
| 179 |
status = 'busy'
|
| 180 |
status_time = time
|
| 181 |
|
| 182 |
suspend = ""
|
| 183 |
res = {'status': status, 'prod': prod}
|
| 184 |
if status != 'none':
|
| 185 |
res['status_time'] = status_time
|
| 186 |
if prod != 'none':
|
| 187 |
res['prod_time'] = prod_time
|
| 188 |
if regular_statusses[status] and suspend_status:
|
| 189 |
if suspend_until > now:
|
| 190 |
res['suspend'] = suspend_status
|
| 191 |
res['suspend_until'] = suspend_until
|
| 192 |
suspend = " (but %s for another %s)" % (suspend_status,
|
| 193 |
time_passed(suspend_until, -1))
|
| 194 |
else:
|
| 195 |
suspend = " (was %s until %s ago)" % (suspend_status,
|
| 196 |
time_passed(suspend_until))
|
| 197 |
|
| 198 |
prod_for = ""
|
| 199 |
if prod_time >= 0: prod_for = " for %s" % time_passed(prod_time)
|
| 200 |
res['human'] = "Status is %s for %s%s; Prod-level is %s%s" % \
|
| 201 |
(status, time_passed(status_time), suspend, prod, prod_for)
|
| 202 |
if not info:
|
| 203 |
res['human'] = "Not in database"
|
| 204 |
|
| 205 |
return res
|
| 206 |
|
| 207 |
time_spans = {
|
| 208 |
'd': 24*3600,
|
| 209 |
'w': 7*24*3600,
|
| 210 |
'm': 30*24*3600,
|
| 211 |
'y': 365*24*3600
|
| 212 |
}
|
| 213 |
|
| 214 |
def parse_for(default, arg):
|
| 215 |
if arg == None:
|
| 216 |
return default
|
| 217 |
re_time = re.compile(r"(\d+)([dwmy])")
|
| 218 |
res = 0
|
| 219 |
for t in re_time.finditer(arg):
|
| 220 |
res += int(t.group(1)) * time_spans[t.group(2)]
|
| 221 |
return res
|
| 222 |
|
| 223 |
def time_passed(t, multiplier=1):
|
| 224 |
return "%sd" % int(multiplier*(now - t)/24/3600)
|
| 225 |
|
| 226 |
def write_status(file, id, summary, withMail=1, sender=None):
|
| 227 |
try:
|
| 228 |
status = open(Cnf["Dir::Database"] + "/" + file + ".summary", "a")
|
| 229 |
except IOError:
|
| 230 |
print "Cannot open summary file to write!"
|
| 231 |
sys.exit(1)
|
| 232 |
|
| 233 |
nomail = ""
|
| 234 |
if not withMail:
|
| 235 |
nomail = " nomail %s" % pwd.getpwuid(os.getuid())[0]
|
| 236 |
sender_str = ""
|
| 237 |
re_sender = re.compile(r"<(.+@.+)>")
|
| 238 |
if sender:
|
| 239 |
result = re_sender.search(sender)
|
| 240 |
if result:
|
| 241 |
sender_str = " from %s" % result.group(1)
|
| 242 |
|
| 243 |
string = str(int(id)) + nomail + sender_str + ": " + summary + "\n"
|
| 244 |
status.write(string)
|
| 245 |
status.close()
|
| 246 |
|
| 247 |
if info.has_key(file):
|
| 248 |
del(info[file])
|
| 249 |
|
| 250 |
|
| 251 |
def parse_history(history):
|
| 252 |
lines = [ ]
|
| 253 |
all_contacts = history.keys()
|
| 254 |
all_contacts.sort()
|
| 255 |
for when in all_contacts:
|
| 256 |
lines.append(" %s: %s" % (get_time(when, "%Y-%m-%d"), history[when]))
|
| 257 |
return lines
|
| 258 |
|
| 259 |
def get_time(when, format):
|
| 260 |
return time.strftime(format, time.gmtime(when))
|
| 261 |
|
| 262 |
def searchCarnivore(query, regex=False):
|
| 263 |
return carnivore.search(query)
|
| 264 |
|
| 265 |
def get(carnivoreId):
|
| 266 |
return miaEntry(carnivore.get(carnivoreId))
|
| 267 |
|
| 268 |
class miaEntry:
|
| 269 |
def __init__(self, carnivore):
|
| 270 |
self.ce = carnivore
|
| 271 |
self.id = carnivore.id
|
| 272 |
|
| 273 |
def __repr__(self):
|
| 274 |
return "miaEntry("+(self.ce.ldap + self.ce.email)[0]+")"
|
| 275 |
|
| 276 |
def getHistory(self):
|
| 277 |
mia_db = None
|
| 278 |
role = False
|
| 279 |
for id in self.ce.ldap + self.ce.email:
|
| 280 |
mia_status = read_history(id)
|
| 281 |
if id.find("@lists.") >= 0:
|
| 282 |
role = True
|
| 283 |
if mia_status and mia_db:
|
| 284 |
sys.stderr.write("Maintainer has duplicate entries in MIA db: %s and %s\n"
|
| 285 |
% (self.mia_path_id, id))
|
| 286 |
if mia_status:
|
| 287 |
mia_db = mia_status
|
| 288 |
self.mia_path_id = id
|
| 289 |
if role and mia_db:
|
| 290 |
sys.stderr.write("Looks like role but has MIA entry: %s \n"
|
| 291 |
% self.mia_path_id)
|
| 292 |
sys.exit(1)
|
| 293 |
if role:
|
| 294 |
return {0: 'role; Detected mailinglist address'}
|
| 295 |
if not mia_db:
|
| 296 |
return {}
|
| 297 |
return mia_db
|
| 298 |
|
| 299 |
def getStatus(self):
|
| 300 |
return parse_status(self.getHistory())
|
| 301 |
|
| 302 |
def statusIsAtLeast(self, status):
|
| 303 |
return regular_statusses[self.getStatus()['status']] \
|
| 304 |
>= regular_statusses[status]
|
| 305 |
|
| 306 |
def getPrettyprintedText(self):
|
| 307 |
text = self.ce.getPrettyprintedText()
|
| 308 |
|
| 309 |
history = self.getHistory()
|
| 310 |
text += "X-MIA: "+parse_status(history)['human']+"\n"
|
| 311 |
text += "\n".join(parse_history(history))
|
| 312 |
|
| 313 |
return text
|
| 314 |
|
| 315 |
def prettyPrint(self):
|
| 316 |
print self.getPrettyprintedText() + "\n"
|
| 317 |
|
| 318 |
# vim: ts=4:expandtab:shiftwidth=4:
|