225.1.1
by Michael Hudson-Doyle
add a script to generate a per-team view of proposed-migration and invoke it after proposed migration |
1 |
#!/usr/bin/env python3
|
2 |
# -*- coding: utf-8 -*-
|
|
3 |
||
4 |
# Copyright (C) 2018 Canonical Ltd
|
|
5 |
||
6 |
# This program is free software; you can redistribute it and/or
|
|
7 |
# modify it under the terms of the GNU General Public License
|
|
8 |
# as published by the Free Software Foundation; either version 2
|
|
9 |
# of the License, or (at your option) any later version.
|
|
10 |
#
|
|
11 |
# This program is distributed in the hope that it will be useful,
|
|
12 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14 |
# GNU General Public License for more details.
|
|
15 |
#
|
|
16 |
# A copy of the GNU General Public License version 2 is in LICENSE.
|
|
17 |
||
18 |
import argparse |
|
234.1.1
by Michael Hudson-Doyle
yaml output for generate-team-p-m |
19 |
from collections import defaultdict, OrderedDict |
269.1.2
by Sebastien Bacher
team-report: include the timezone in the update timestamp |
20 |
import datetime |
21 |
import time |
|
225.1.1
by Michael Hudson-Doyle
add a script to generate a per-team view of proposed-migration and invoke it after proposed migration |
22 |
import json |
23 |
import os |
|
24 |
import threading |
|
25 |
from urllib.request import urlopen |
|
281.1.7
by Sebastien Bacher
specify the urllib error to catch |
26 |
import urllib.error |
225.1.1
by Michael Hudson-Doyle
add a script to generate a per-team view of proposed-migration and invoke it after proposed migration |
27 |
|
28 |
import attr |
|
29 |
from jinja2 import Environment, FileSystemLoader |
|
30 |
import yaml |
|
281.1.5
by Sebastien Bacher
try reading the xz report first, if not fallback to the uncompressed one |
31 |
import lzma |
225.1.1
by Michael Hudson-Doyle
add a script to generate a per-team view of proposed-migration and invoke it after proposed migration |
32 |
|
33 |
env = Environment( |
|
34 |
loader=FileSystemLoader(os.path.dirname(os.path.abspath(__file__)) + '/templates'), |
|
35 |
autoescape=True, |
|
36 |
extensions=['jinja2.ext.i18n'], |
|
37 |
)
|
|
38 |
env.install_null_translations(True) |
|
39 |
||
40 |
_lps = {} |
|
41 |
||
42 |
def get_lp(i, anon=True): |
|
43 |
from launchpadlib.launchpad import Launchpad |
|
44 |
k = (i, anon) |
|
45 |
if k not in _lps: |
|
46 |
print(i, "connecting...") |
|
47 |
if anon: |
|
48 |
_lps[k] = Launchpad.login_anonymously('sru-team-report', 'production', version='devel') |
|
49 |
else: |
|
50 |
_lps[k] = Launchpad.login_with('sru-team-report', 'production', version='devel') |
|
51 |
return _lps[k] |
|
52 |
||
53 |
||
54 |
def get_true_ages_in_proposed(package_names, thread_count): |
|
55 |
package_names = set(package_names) |
|
56 |
r = {} |
|
57 |
def run(i): |
|
58 |
lp = get_lp(i, True) |
|
59 |
ubuntu = lp.distributions['ubuntu'] |
|
60 |
primary_archive = ubuntu.archives[0] |
|
61 |
devel_series = ubuntu.getSeries(name_or_version=ubuntu.development_series_alias) |
|
62 |
while True: |
|
63 |
try: |
|
64 |
spn = package_names.pop() |
|
65 |
except KeyError: |
|
66 |
return
|
|
67 |
print(i, "getting true age in proposed for", spn) |
|
68 |
history = primary_archive.getPublishedSources( |
|
69 |
source_name=spn, distro_series=devel_series, exact_match=True) |
|
70 |
last_proposed_spph = None |
|
71 |
for spph in history: |
|
72 |
if spph.pocket != "Proposed": |
|
73 |
break
|
|
74 |
last_proposed_spph = spph |
|
75 |
if last_proposed_spph is None: |
|
76 |
continue
|
|
77 |
age = datetime.datetime.now(tz=last_proposed_spph.date_created.tzinfo) - last_proposed_spph.date_created |
|
78 |
r[spn] = age.total_seconds() / 3600 / 24 |
|
79 |
threads = [] |
|
80 |
for i in range(thread_count): |
|
81 |
t = threading.Thread(target=run, args=(i,)) |
|
82 |
threads.append(t) |
|
83 |
t.start() |
|
84 |
for t in threads: |
|
85 |
t.join() |
|
86 |
return r |
|
87 |
||
88 |
||
89 |
def get_subscribers_lp(packages, thread_count): |
|
90 |
from lazr.restfulclient.errors import ClientError |
|
91 |
packages = set(packages) |
|
92 |
def run(i, subscribers): |
|
93 |
lp = get_lp(i, False) |
|
94 |
ubuntu = lp.distributions['ubuntu'] |
|
95 |
while True: |
|
96 |
try: |
|
97 |
spn = packages.pop() |
|
98 |
except KeyError: |
|
99 |
return
|
|
100 |
print(i, spn) |
|
101 |
distribution_source_package = ubuntu.getSourcePackage(name=spn) |
|
102 |
for subscription in distribution_source_package.getSubscriptions(): |
|
103 |
subscriber = subscription.subscriber |
|
104 |
try: |
|
105 |
if subscriber.is_team: |
|
106 |
subscribers[spn].append(subscriber.name) |
|
107 |
except ClientError: |
|
108 |
# This happens for suspended users
|
|
109 |
pass
|
|
110 |
results = [] |
|
111 |
threads = [] |
|
112 |
for i in range(thread_count): |
|
113 |
d = defaultdict(list) |
|
114 |
t = threading.Thread(target=run, args=(i, d)) |
|
115 |
results.append(d) |
|
116 |
threads.append(t) |
|
117 |
t.start() |
|
118 |
for t in threads: |
|
119 |
t.join() |
|
120 |
result = defaultdict(list) |
|
121 |
for d in results: |
|
122 |
for k, v in d.items(): |
|
123 |
result[k].extend(v) |
|
124 |
return result |
|
125 |
||
126 |
def get_subscribers_json(packages, subscribers_json): |
|
127 |
if subscribers_json is None: |
|
349
by Steve Langasek
ubuntu-archive-team is https-only |
128 |
j = urlopen("https://ubuntu-archive-team.ubuntu.com/package-team-mapping.json") |
225.1.1
by Michael Hudson-Doyle
add a script to generate a per-team view of proposed-migration and invoke it after proposed migration |
129 |
else: |
130 |
j = open(subscribers_json, 'rb') |
|
131 |
with j: |
|
132 |
team_to_packages = json.loads(j.read().decode('utf-8')) |
|
133 |
package_to_teams = {} |
|
134 |
for team, packages in team_to_packages.items(): |
|
135 |
for package in packages: |
|
136 |
package_to_teams.setdefault(package, []).append(team) |
|
137 |
return package_to_teams |
|
138 |
||
234.1.1
by Michael Hudson-Doyle
yaml output for generate-team-p-m |
139 |
def setup_yaml(): |
140 |
""" http://stackoverflow.com/a/8661021 """
|
|
141 |
represent_dict_order = ( |
|
142 |
lambda self, data: self.represent_mapping('tag:yaml.org,2002:map', |
|
143 |
data.items())) |
|
144 |
yaml.add_representer(OrderedDict, represent_dict_order) |
|
145 |
||
146 |
||
147 |
setup_yaml() |
|
148 |
||
149 |
def as_data(inst): |
|
150 |
r = OrderedDict() |
|
151 |
fields = [field.name for field in attr.fields(type(inst))] |
|
152 |
fields.extend(getattr(inst, "extra_fields", [])) |
|
153 |
for field in fields: |
|
154 |
if field.startswith('_'): |
|
155 |
continue
|
|
156 |
v = getattr( |
|
157 |
inst, |
|
158 |
'serialize_' + field, |
|
159 |
lambda: getattr(inst, field))() |
|
160 |
if v is not None: |
|
161 |
r[field] = v |
|
162 |
return r |
|
163 |
||
225.1.1
by Michael Hudson-Doyle
add a script to generate a per-team view of proposed-migration and invoke it after proposed migration |
164 |
@attr.s
|
165 |
class ArchRegression: |
|
166 |
arch = attr.ib(default=None) |
|
167 |
log_link = attr.ib(default=None) |
|
168 |
hist_link = attr.ib(default=None) |
|
169 |
||
170 |
||
171 |
@attr.s
|
|
172 |
class Regression: |
|
173 |
blocking = attr.ib(default=None) # source package name blocked |
|
174 |
package = attr.ib(default=None) # source package name doing the blocking |
|
175 |
version = attr.ib(default=None) # version that regressed |
|
176 |
arches = attr.ib(default=None) # [ArchRegression] |
|
177 |
||
234.1.1
by Michael Hudson-Doyle
yaml output for generate-team-p-m |
178 |
def serialize_arches(self): |
179 |
return [as_data(a) for a in self.arches] |
|
180 |
||
225.1.1
by Michael Hudson-Doyle
add a script to generate a per-team view of proposed-migration and invoke it after proposed migration |
181 |
@property
|
182 |
def package_version(self): |
|
183 |
return self.package + '/' + self.version |
|
184 |
||
185 |
@attr.s
|
|
186 |
class Problem: |
|
187 |
kind = attr.ib(default=None) # 'blocked-in-proposed', 'regressing-other' |
|
188 |
package_in_proposed = attr.ib(default=None) # name of package that's in proposed |
|
189 |
regressing_package = attr.ib(default=None) # name of package regressing package_in_proposed, None if blocked-in-proposed |
|
190 |
regressions = attr.ib(default=None) # [Regression] |
|
191 |
waiting = attr.ib(default=None) # [(source_package_name, arches)] |
|
192 |
data = attr.ib(default=None) # data for package_in_proposed |
|
193 |
unsatdepends = attr.ib(default=None) # [string] |
|
288.1.1
by Sebastien Bacher
Include unsatisfiable Build-Depends in the team report |
194 |
unsatbuilddep = attr.ib(default=None) # [string] |
291
by Sebastien Bacher
Displays the broken binaries information in the teams report. |
195 |
brokenbin = attr.ib(default=None) # [string] |
295.2.1
by Sebastien Bacher
Display component mismatch information in the team report |
196 |
componentmismatch = attr.ib(default=None) # [string] |
225.1.1
by Michael Hudson-Doyle
add a script to generate a per-team view of proposed-migration and invoke it after proposed migration |
197 |
|
198 |
_age = attr.ib(default=None) |
|
199 |
||
234.1.1
by Michael Hudson-Doyle
yaml output for generate-team-p-m |
200 |
extra_fields = ['age'] |
201 |
||
202 |
def serialize_regressions(self): |
|
203 |
return [as_data(r) for r in self.regressions] |
|
204 |
||
225.1.1
by Michael Hudson-Doyle
add a script to generate a per-team view of proposed-migration and invoke it after proposed migration |
205 |
@property
|
225.1.2
by Michael Hudson-Doyle
update script and template |
206 |
def late(self): |
207 |
return self.age > 3 |
|
208 |
||
209 |
@property
|
|
225.1.1
by Michael Hudson-Doyle
add a script to generate a per-team view of proposed-migration and invoke it after proposed migration |
210 |
def age(self): |
211 |
if self._age is not None: |
|
212 |
return self._age |
|
213 |
else: |
|
270.1.1
by Iain Lane
generate-team-p-m: Handle a missing policy_info |
214 |
try: |
215 |
return self.data["policy_info"]["age"]["current-age"] |
|
216 |
except KeyError: |
|
217 |
return -1 |
|
234.1.1
by Michael Hudson-Doyle
yaml output for generate-team-p-m |
218 |
|
225.1.1
by Michael Hudson-Doyle
add a script to generate a per-team view of proposed-migration and invoke it after proposed migration |
219 |
@age.setter
|
220 |
def age(self, val): |
|
221 |
self._age = val |
|
222 |
||
225.1.2
by Michael Hudson-Doyle
update script and template |
223 |
@property
|
224 |
def key_package(self): |
|
225 |
if self.regressing_package: |
|
226 |
return self.regressing_package |
|
227 |
return self.package_in_proposed |
|
228 |
||
225.1.1
by Michael Hudson-Doyle
add a script to generate a per-team view of proposed-migration and invoke it after proposed migration |
229 |
|
230 |
def main(): |
|
231 |
parser = argparse.ArgumentParser() |
|
232 |
parser.add_argument('--ppa', action='store') |
|
233 |
parser.add_argument('--components', action='store', default="main,restricted") |
|
234 |
parser.add_argument('--subscribers-from-lp', action='store_true') |
|
235 |
parser.add_argument('--subscribers-json', action='store') |
|
236 |
parser.add_argument('--true-ages', action='store_true') |
|
237 |
parser.add_argument('--excuses-yaml', action='store') |
|
238 |
parser.add_argument('output') |
|
234.1.1
by Michael Hudson-Doyle
yaml output for generate-team-p-m |
239 |
parser.add_argument('yaml_output', default=None) |
225.1.1
by Michael Hudson-Doyle
add a script to generate a per-team view of proposed-migration and invoke it after proposed migration |
240 |
args = parser.parse_args() |
241 |
||
242 |
components = args.components.split(',') |
|
243 |
||
244 |
print("fetching yaml") |
|
245 |
if args.excuses_yaml: |
|
282.1.1
by Iain Lane
generate-team-p-m, run-proposed-migration: More updates for .xz excuses |
246 |
if args.excuses_yaml.endswith('.xz'): |
247 |
yaml_text = lzma.open(args.excuses_yaml) |
|
248 |
else: |
|
249 |
yaml_text = open(args.excuses_yaml) |
|
225.1.1
by Michael Hudson-Doyle
add a script to generate a per-team view of proposed-migration and invoke it after proposed migration |
250 |
else: |
281.1.5
by Sebastien Bacher
try reading the xz report first, if not fallback to the uncompressed one |
251 |
try: |
348
by Steve Langasek
ubuntu-archive-team.ubuntu.com is now the canonical URL |
252 |
yaml_text = lzma.open(urlopen("https://ubuntu-archive-team.ubuntu.com/proposed-migration/update_excuses.yaml.xz")) |
281.1.8
by Sebastien Bacher
indicate when the fallback url is read and display the error |
253 |
except urllib.error.HTTPError as e: |
254 |
print("Reading fallback yaml (%s)" % e) |
|
348
by Steve Langasek
ubuntu-archive-team.ubuntu.com is now the canonical URL |
255 |
yaml_text = urlopen("https://ubuntu-archive-team.ubuntu.com/proposed-migration/update_excuses.yaml") |
225.1.1
by Michael Hudson-Doyle
add a script to generate a per-team view of proposed-migration and invoke it after proposed migration |
256 |
print("parsing yaml") |
257 |
# The CSafeLoader is ten times faster than the regular one
|
|
258 |
excuses = yaml.load(yaml_text, Loader=yaml.CSafeLoader) |
|
259 |
||
260 |
print("pre-processing packages") |
|
261 |
in_proposed_packages = {} |
|
262 |
for item in excuses["sources"]: |
|
263 |
source_package_name = item['item-name'] |
|
264 |
# Missing component means main
|
|
265 |
if item.get('component', 'main') not in components: |
|
266 |
continue
|
|
270.1.1
by Iain Lane
generate-team-p-m: Handle a missing policy_info |
267 |
prob = Problem(kind='package-in-proposed', data=defaultdict(dict, item), package_in_proposed=source_package_name) |
225.1.1
by Michael Hudson-Doyle
add a script to generate a per-team view of proposed-migration and invoke it after proposed migration |
268 |
in_proposed_packages[source_package_name] = prob |
269 |
prob.regressions = [] |
|
270 |
prob.waiting = [] |
|
295.2.1
by Sebastien Bacher
Display component mismatch information in the team report |
271 |
prob.componentmismatch = [] |
281.1.9
by Sebastien Bacher
don't special case verdicts entries, they are filter out now |
272 |
# The verdict entries are not items to list on the report
|
281.1.3
by Sebastien Bacher
exclude the verdit entries from the code it's simpler, thanks Laney! |
273 |
for policy in ['autopkgtest', 'update-excuse', 'block-bugs']: |
274 |
try: |
|
275 |
del item['policy_info'][policy]['verdict'] |
|
276 |
except KeyError: |
|
277 |
pass
|
|
225.1.1
by Michael Hudson-Doyle
add a script to generate a per-team view of proposed-migration and invoke it after proposed migration |
278 |
if 'autopkgtest' in item['reason']: |
279 |
for package, results in sorted(item['policy_info']['autopkgtest'].items()): |
|
280 |
regr_arches = [] |
|
281 |
wait_arches = [] |
|
282 |
for arch, result in sorted(results.items()): |
|
283 |
outcome, log, history, wtf1, wtf2 = result |
|
284 |
if outcome == "REGRESSION": |
|
285 |
regr_arches.append(ArchRegression(arch=arch, log_link=log, hist_link=history)) |
|
286 |
if outcome == "RUNNING": |
|
287 |
wait_arches.append(arch) |
|
288 |
if regr_arches: |
|
289 |
p, v = package.split('/') |
|
290 |
regr = Regression(package=p, version=v, blocking=source_package_name) |
|
291 |
regr.arches = regr_arches |
|
292 |
prob.regressions.append(regr) |
|
293 |
if wait_arches: |
|
294 |
prob.waiting.append((package + ": " + ", ".join(wait_arches))) |
|
295.2.1
by Sebastien Bacher
Display component mismatch information in the team report |
295 |
if 'depends' in item['reason']: |
296 |
for l in item['excuses']: |
|
297 |
if 'cannot depend on' in l: |
|
298 |
prob.componentmismatch.append(l) |
|
225.1.1
by Michael Hudson-Doyle
add a script to generate a per-team view of proposed-migration and invoke it after proposed migration |
299 |
if 'dependencies' in item and 'unsatisfiable-dependencies' in item['dependencies']: |
300 |
unsatd = defaultdict(list) |
|
301 |
for arch, packages in item['dependencies']['unsatisfiable-dependencies'].items(): |
|
302 |
for p in packages: |
|
303 |
unsatd[p].append(arch) |
|
304 |
prob.unsatdepends = ['{}: {}'.format(p, ', '.join(sorted(arches))) for p, arches in sorted(unsatd.items())] |
|
311.1.1
by Brian Murray
don't assume policy_info exists because this one time it didn't |
305 |
if 'policy_info' in item: |
306 |
if 'build-depends' in item['policy_info'] and 'unsatisfiable-arch-build-depends' in item['policy_info']['build-depends']: |
|
307 |
unsatdbd = defaultdict(list) |
|
308 |
for arch, packages in item['policy_info']['build-depends']['unsatisfiable-arch-build-depends'].items(): |
|
309 |
for p in packages: |
|
310 |
unsatdbd[p].append(arch) |
|
311 |
prob.unsatbuilddep = ['{}: {}'.format(p, ', '.join(sorted(arches))) for p, arches in sorted(unsatdbd.items())] |
|
225.1.1
by Michael Hudson-Doyle
add a script to generate a per-team view of proposed-migration and invoke it after proposed migration |
312 |
|
313 |
package_to_problems = defaultdict(list) |
|
314 |
||
315 |
for problem in in_proposed_packages.values(): |
|
270.1.2
by Iain Lane
generate-team-p-m: Attribute binary-only migration items to the right team |
316 |
# nautilus/riscv64 -> nautilus
|
317 |
pkg = problem.package_in_proposed.split('/')[0] |
|
318 |
package_to_problems[pkg].append(problem) |
|
225.1.1
by Michael Hudson-Doyle
add a script to generate a per-team view of proposed-migration and invoke it after proposed migration |
319 |
for regression in problem.regressions: |
320 |
if regression.blocking not in in_proposed_packages: |
|
321 |
continue
|
|
322 |
if regression.blocking == regression.package: |
|
323 |
continue
|
|
324 |
package_to_problems[regression.package].append(Problem( |
|
325 |
kind='regressing-other', package_in_proposed=regression.blocking, |
|
326 |
regressing_package=regression.package, |
|
327 |
regressions=[regression], |
|
328 |
data=in_proposed_packages[regression.blocking].data)) |
|
329 |
||
330 |
if args.true_ages: |
|
331 |
true_ages = get_true_ages_in_proposed(set(package_to_problems), 10) |
|
332 |
for package, true_age in true_ages.items(): |
|
333 |
for problem in package_to_problems[package]: |
|
334 |
problem.age = true_age |
|
335 |
||
336 |
print("getting subscribers") |
|
337 |
if args.subscribers_from_lp: |
|
338 |
subscribers = get_subscribers_lp(set(package_to_problems), 10) |
|
339 |
for p in set(package_to_problems): |
|
340 |
if p not in subscribers: |
|
341 |
subscribers[p] = ['unsubscribed'] |
|
342 |
else: |
|
343 |
subscribers = get_subscribers_json(set(package_to_problems), args.subscribers_json) |
|
344 |
for p in set(package_to_problems): |
|
270.1.2
by Iain Lane
generate-team-p-m: Attribute binary-only migration items to the right team |
345 |
pkg = p.split('/')[0] |
346 |
if pkg not in subscribers: |
|
225.1.1
by Michael Hudson-Doyle
add a script to generate a per-team view of proposed-migration and invoke it after proposed migration |
347 |
subscribers[p] = ['unknown'] |
348 |
||
349 |
all_teams = set() |
|
350 |
team_to_problems = defaultdict(list) |
|
351 |
for package, teams in subscribers.items(): |
|
352 |
all_teams |= set(teams) |
|
353 |
for team in teams: |
|
354 |
team_to_problems[team].extend(package_to_problems[package]) |
|
355 |
||
356 |
for packages in team_to_problems.values(): |
|
225.1.2
by Michael Hudson-Doyle
update script and template |
357 |
packages.sort(key=lambda prob: (-prob.age, prob.key_package)) |
358 |
||
359 |
team_to_attn_count = {} |
|
360 |
for team, problems in team_to_problems.items(): |
|
361 |
team_to_attn_count[team] = len([problem for problem in problems if problem.late]) |
|
225.1.1
by Michael Hudson-Doyle
add a script to generate a per-team view of proposed-migration and invoke it after proposed migration |
362 |
|
363 |
print("rendering") |
|
364 |
t = env.get_template('team-report.html') |
|
365 |
with open(args.output, 'w', encoding='utf-8') as fp: |
|
366 |
fp.write(t.render( |
|
367 |
all_teams=all_teams, |
|
368 |
team_to_problems=team_to_problems, |
|
225.1.2
by Michael Hudson-Doyle
update script and template |
369 |
team_to_attn_count=team_to_attn_count, |
269.1.1
by Sebastien Bacher
team-report: include the timezone in the update timestamp |
370 |
now=excuses["generated-date"].strftime("%Y.%m.%d %H:%M:%S") + ' ' + time.localtime().tm_zone)) |
234.1.1
by Michael Hudson-Doyle
yaml output for generate-team-p-m |
371 |
if args.yaml_output: |
372 |
team_to_problem_data = {} |
|
373 |
for t, ps in team_to_problems.items(): |
|
374 |
team_to_problem_data[t] = [as_data(p) for p in ps] |
|
375 |
with open(args.yaml_output, 'w', encoding='utf-8') as fp: |
|
376 |
yaml.dump(team_to_problem_data, fp) |
|
377 |
||
225.1.1
by Michael Hudson-Doyle
add a script to generate a per-team view of proposed-migration and invoke it after proposed migration |
378 |
|
379 |
if __name__ == '__main__': |
|
380 |
main() |