Merge lp:~mwhudson/ubuntu-archive-scripts/add-team-report into lp:ubuntu-archive-scripts

Proposed by Michael Hudson-Doyle
Status: Merged
Merged at revision: 226
Proposed branch: lp:~mwhudson/ubuntu-archive-scripts/add-team-report
Merge into: lp:ubuntu-archive-scripts
Diff against target: 431 lines (+413/-0)
3 files modified
generate-team-p-m (+301/-0)
run-proposed-migration (+5/-0)
templates/team-report.html (+107/-0)
To merge this branch: bzr merge lp:~mwhudson/ubuntu-archive-scripts/add-team-report
Reviewer Review Type Date Requested Status
Steve Langasek Approve
Review via email: mp+354278@code.launchpad.net

Description of the change

Seemed the easiest approach, happy to be told to do it differently :)

This will require python3-attr and python3-jinja2 to be installed on snakefruit before deployment.

To post a comment you must log in.
227. By Michael Hudson-Doyle

update script and template

Revision history for this message
Steve Langasek (vorlon) :
review: Needs Fixing
228. By Michael Hudson-Doyle

review feedback

Revision history for this message
Steve Langasek (vorlon) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'generate-team-p-m'
--- generate-team-p-m 1970-01-01 00:00:00 +0000
+++ generate-team-p-m 2018-09-11 23:51:09 +0000
@@ -0,0 +1,301 @@
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
18import argparse
19from collections import defaultdict
20import datetime
21import json
22import os
23import threading
24from urllib.request import urlopen
25
26import attr
27from jinja2 import Environment, FileSystemLoader
28import yaml
29
30env = Environment(
31 loader=FileSystemLoader(os.path.dirname(os.path.abspath(__file__)) + '/templates'),
32 autoescape=True,
33 extensions=['jinja2.ext.i18n'],
34)
35env.install_null_translations(True)
36
37_lps = {}
38
39def get_lp(i, anon=True):
40 from launchpadlib.launchpad import Launchpad
41 k = (i, anon)
42 if k not in _lps:
43 print(i, "connecting...")
44 if anon:
45 _lps[k] = Launchpad.login_anonymously('sru-team-report', 'production', version='devel')
46 else:
47 _lps[k] = Launchpad.login_with('sru-team-report', 'production', version='devel')
48 return _lps[k]
49
50
51def get_true_ages_in_proposed(package_names, thread_count):
52 package_names = set(package_names)
53 r = {}
54 def run(i):
55 lp = get_lp(i, True)
56 ubuntu = lp.distributions['ubuntu']
57 primary_archive = ubuntu.archives[0]
58 devel_series = ubuntu.getSeries(name_or_version=ubuntu.development_series_alias)
59 while True:
60 try:
61 spn = package_names.pop()
62 except KeyError:
63 return
64 print(i, "getting true age in proposed for", spn)
65 history = primary_archive.getPublishedSources(
66 source_name=spn, distro_series=devel_series, exact_match=True)
67 last_proposed_spph = None
68 for spph in history:
69 if spph.pocket != "Proposed":
70 break
71 last_proposed_spph = spph
72 if last_proposed_spph is None:
73 continue
74 age = datetime.datetime.now(tz=last_proposed_spph.date_created.tzinfo) - last_proposed_spph.date_created
75 r[spn] = age.total_seconds() / 3600 / 24
76 threads = []
77 for i in range(thread_count):
78 t = threading.Thread(target=run, args=(i,))
79 threads.append(t)
80 t.start()
81 for t in threads:
82 t.join()
83 return r
84
85
86def get_subscribers_lp(packages, thread_count):
87 from lazr.restfulclient.errors import ClientError
88 packages = set(packages)
89 def run(i, subscribers):
90 lp = get_lp(i, False)
91 ubuntu = lp.distributions['ubuntu']
92 while True:
93 try:
94 spn = packages.pop()
95 except KeyError:
96 return
97 print(i, spn)
98 distribution_source_package = ubuntu.getSourcePackage(name=spn)
99 for subscription in distribution_source_package.getSubscriptions():
100 subscriber = subscription.subscriber
101 try:
102 if subscriber.is_team:
103 subscribers[spn].append(subscriber.name)
104 except ClientError:
105 # This happens for suspended users
106 pass
107 results = []
108 threads = []
109 for i in range(thread_count):
110 d = defaultdict(list)
111 t = threading.Thread(target=run, args=(i, d))
112 results.append(d)
113 threads.append(t)
114 t.start()
115 for t in threads:
116 t.join()
117 result = defaultdict(list)
118 for d in results:
119 for k, v in d.items():
120 result[k].extend(v)
121 return result
122
123def get_subscribers_json(packages, subscribers_json):
124 if subscribers_json is None:
125 j = urlopen("http://people.canonical.com/~ubuntu-archive/package-team-mapping.json")
126 else:
127 j = open(subscribers_json, 'rb')
128 with j:
129 team_to_packages = json.loads(j.read().decode('utf-8'))
130 package_to_teams = {}
131 for team, packages in team_to_packages.items():
132 for package in packages:
133 package_to_teams.setdefault(package, []).append(team)
134 return package_to_teams
135
136@attr.s
137class ArchRegression:
138 arch = attr.ib(default=None)
139 log_link = attr.ib(default=None)
140 hist_link = attr.ib(default=None)
141
142
143@attr.s
144class Regression:
145 blocking = attr.ib(default=None) # source package name blocked
146 package = attr.ib(default=None) # source package name doing the blocking
147 version = attr.ib(default=None) # version that regressed
148 arches = attr.ib(default=None) # [ArchRegression]
149
150 @property
151 def package_version(self):
152 return self.package + '/' + self.version
153
154@attr.s
155class Problem:
156 kind = attr.ib(default=None) # 'blocked-in-proposed', 'regressing-other'
157 package_in_proposed = attr.ib(default=None) # name of package that's in proposed
158 regressing_package = attr.ib(default=None) # name of package regressing package_in_proposed, None if blocked-in-proposed
159 regressions = attr.ib(default=None) # [Regression]
160 waiting = attr.ib(default=None) # [(source_package_name, arches)]
161 data = attr.ib(default=None) # data for package_in_proposed
162 unsatdepends = attr.ib(default=None) # [string]
163
164 _age = attr.ib(default=None)
165
166 @property
167 def late(self):
168 return self.age > 3
169
170 @property
171 def age(self):
172 if self._age is not None:
173 return self._age
174 else:
175 return self.data["policy_info"]["age"]["current-age"]
176 @age.setter
177 def age(self, val):
178 self._age = val
179
180 @property
181 def key_package(self):
182 if self.regressing_package:
183 return self.regressing_package
184 return self.package_in_proposed
185
186
187def main():
188 parser = argparse.ArgumentParser()
189 parser.add_argument('--ppa', action='store')
190 parser.add_argument('--components', action='store', default="main,restricted")
191 parser.add_argument('--subscribers-from-lp', action='store_true')
192 parser.add_argument('--subscribers-json', action='store')
193 parser.add_argument('--true-ages', action='store_true')
194 parser.add_argument('--excuses-yaml', action='store')
195 parser.add_argument('output')
196 args = parser.parse_args()
197
198 components = args.components.split(',')
199
200 print("fetching yaml")
201 if args.excuses_yaml:
202 yaml_text = open(args.excuses_yaml).read()
203 else:
204 yaml_text = urlopen("https://people.canonical.com/~ubuntu-archive/proposed-migration/update_excuses.yaml").read()
205 print("parsing yaml")
206 # The CSafeLoader is ten times faster than the regular one
207 excuses = yaml.load(yaml_text, Loader=yaml.CSafeLoader)
208
209 print("pre-processing packages")
210 in_proposed_packages = {}
211 for item in excuses["sources"]:
212 source_package_name = item['item-name']
213 # Missing component means main
214 if item.get('component', 'main') not in components:
215 continue
216 prob = Problem(kind='package-in-proposed', data=item, package_in_proposed=source_package_name)
217 in_proposed_packages[source_package_name] = prob
218 prob.regressions = []
219 prob.waiting = []
220 if 'autopkgtest' in item['reason']:
221 for package, results in sorted(item['policy_info']['autopkgtest'].items()):
222 regr_arches = []
223 wait_arches = []
224 for arch, result in sorted(results.items()):
225 outcome, log, history, wtf1, wtf2 = result
226 if outcome == "REGRESSION":
227 regr_arches.append(ArchRegression(arch=arch, log_link=log, hist_link=history))
228 if outcome == "RUNNING":
229 wait_arches.append(arch)
230 if regr_arches:
231 p, v = package.split('/')
232 regr = Regression(package=p, version=v, blocking=source_package_name)
233 regr.arches = regr_arches
234 prob.regressions.append(regr)
235 if wait_arches:
236 prob.waiting.append((package + ": " + ", ".join(wait_arches)))
237 if 'dependencies' in item and 'unsatisfiable-dependencies' in item['dependencies']:
238 unsatd = defaultdict(list)
239 for arch, packages in item['dependencies']['unsatisfiable-dependencies'].items():
240 for p in packages:
241 unsatd[p].append(arch)
242 prob.unsatdepends = ['{}: {}'.format(p, ', '.join(sorted(arches))) for p, arches in sorted(unsatd.items())]
243
244 package_to_problems = defaultdict(list)
245
246 for problem in in_proposed_packages.values():
247 package_to_problems[problem.package_in_proposed].append(problem)
248 for regression in problem.regressions:
249 if regression.blocking not in in_proposed_packages:
250 continue
251 if regression.blocking == regression.package:
252 continue
253 package_to_problems[regression.package].append(Problem(
254 kind='regressing-other', package_in_proposed=regression.blocking,
255 regressing_package=regression.package,
256 regressions=[regression],
257 data=in_proposed_packages[regression.blocking].data))
258
259 if args.true_ages:
260 true_ages = get_true_ages_in_proposed(set(package_to_problems), 10)
261 for package, true_age in true_ages.items():
262 for problem in package_to_problems[package]:
263 problem.age = true_age
264
265 print("getting subscribers")
266 if args.subscribers_from_lp:
267 subscribers = get_subscribers_lp(set(package_to_problems), 10)
268 for p in set(package_to_problems):
269 if p not in subscribers:
270 subscribers[p] = ['unsubscribed']
271 else:
272 subscribers = get_subscribers_json(set(package_to_problems), args.subscribers_json)
273 for p in set(package_to_problems):
274 if p not in subscribers:
275 subscribers[p] = ['unknown']
276
277 all_teams = set()
278 team_to_problems = defaultdict(list)
279 for package, teams in subscribers.items():
280 all_teams |= set(teams)
281 for team in teams:
282 team_to_problems[team].extend(package_to_problems[package])
283
284 for packages in team_to_problems.values():
285 packages.sort(key=lambda prob: (-prob.age, prob.key_package))
286
287 team_to_attn_count = {}
288 for team, problems in team_to_problems.items():
289 team_to_attn_count[team] = len([problem for problem in problems if problem.late])
290
291 print("rendering")
292 t = env.get_template('team-report.html')
293 with open(args.output, 'w', encoding='utf-8') as fp:
294 fp.write(t.render(
295 all_teams=all_teams,
296 team_to_problems=team_to_problems,
297 team_to_attn_count=team_to_attn_count,
298 now=excuses["generated-date"].strftime("%Y.%m.%d %H:%M:%S")))
299
300if __name__ == '__main__':
301 main()
0302
=== modified file 'run-proposed-migration'
--- run-proposed-migration 2018-05-01 17:36:23 +0000
+++ run-proposed-migration 2018-09-11 23:51:09 +0000
@@ -47,3 +47,8 @@
47mkdir -p "${logfile%/*}"47mkdir -p "${logfile%/*}"
4848
49code/b1/britney $actions >"$logfile" 2>&149code/b1/britney $actions >"$logfile" 2>&1
50if [ "$SERIES" = "$DEFAULT_SERIES" ]; then
51 generate-team-p-m --excuses-yaml $SERIES/update_excuses.yaml \
52 --subscribers-json ~/public_html/package-team-mapping.json \
53 $SERIES/update_excuses_by_team.html
54fi
5055
=== added directory 'templates'
=== added file 'templates/team-report.html'
--- templates/team-report.html 1970-01-01 00:00:00 +0000
+++ templates/team-report.html 2018-09-11 23:51:09 +0000
@@ -0,0 +1,107 @@
1<!DOCTYPE HTML>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <title>devel-proposed by team</title>
6 <style>
7 body { font-family: ubuntu }
8 .not-late { color: grey; }
9 .not-late a { color: #77f; }
10 </style>
11 </head>
12 <body lang="en">
13 <h1>devel-proposed by team</h1>
14 <p>
15 Generated: {{ now }}
16 <ul>
17 {% for team in all_teams|sort %}
18 <li><a href="#{{ team }}">{{ team }}</a> ({{ ngettext("%(num)d package", "%(num)d packages", team_to_attn_count[team]) }} needing attention)</li>
19 {% endfor %}
20 </ul>
21 {% for team in all_teams|sort %}
22 <h1 id="{{ team }}">{{ team }}</h1>
23 <p>
24 {{ ngettext("%(num)d package", "%(num)d packages", team_to_attn_count[team]) }} needing attention, {{ ngettext("%(num)d package", "%(num)d packages", team_to_problems[team]|count - team_to_attn_count[team]) }} not yet considered late
25 </p>
26 <ul>
27 {% for prob in team_to_problems[team] %}
28 {% set d = prob.data %}
29 {% set p = prob.package_in_proposed %}
30 <li {% if not prob.late %}class="not-late"{% endif %}>
31 {% if prob.kind == "regressing-other" %}
32 <b>{{ prob.regressing_package }}</b> blocking
33 <a href="http://people.canonical.com/~ubuntu-archive/proposed-migration/update_excuses.html#{{ p }}">{{ p }}</a>
34 {% else %}
35 <a href="http://people.canonical.com/~ubuntu-archive/proposed-migration/update_excuses.html#{{ p }}"><b>{{ p }}</b></a>
36 {% endif %}
37 {% set urlbase = "https://launchpad.net/ubuntu/+source/" + p + "/" %}
38 ({% if d['old-version'] != "-" %}<a href="{{ urlbase }}{{ d["old-version"] }}">{{ d["old-version"] }}{% else %}-{% endif %}</a>
39 to
40 <a href="{{ urlbase }}{{ d["new-version"] }}">{{ d["new-version"] }}</a>)
41 {% if prob.kind == "regressing-other" %}
42 for
43 {% else %}
44 in proposed for
45 {% endif %}
46 {{ ngettext("%(num)d day", "%(num)d days", prob.age|int) }}
47 <ul>
48 {% if prob.waiting %}
49 <li>
50 Waiting
51 <ul>
52 {% for p in prob.waiting %}
53 <li>{{ p }}</li>
54 {% endfor %}
55 </ul>
56 </li>
57 {% endif %}
58 {% if d.get("missing-builds") %}
59 <li>Missing builds, see excuses</li>
60 {% endif %}
61 {% if 'no-binaries' in d["reason"] %}
62 <li>No binaries</li>
63 {% endif %}
64 {% if prob.regressions %}
65 <li>
66 Regressions
67 <ul>
68 {% for regr in prob.regressions %}
69 <li>
70 {{ regr.package_version }}:
71 {% for arch_regr in regr.arches %}
72 {{ arch_regr.arch }} (<a href="{{ arch_regr.log_link }}">log</a>, <a href="{{ arch_regr.hist_link }}">history</a>)
73 {%- if not loop.last %}, {% endif %}
74 {% endfor %}
75 </li>
76 {% endfor %}
77 </ul>
78 </li>
79 {% endif %}
80 {% if prob.kind != "regressing-other" %}
81 {% if d.get('dependencies', {}).get('blocked-by', []) %}
82 <li>Depends on {{ d['dependencies']['blocked-by']|join(", ") }}</li>
83 {% endif %}
84 {% if prob.unsatdepends %}
85 <li>
86 Unsatisfiable depends:
87 <ul>
88 {% for unsatd in prob.unsatdepends %}
89 <li>{{ unsatd }}</li>
90 {% endfor %}
91 </ul>
92 </li>
93 {% endif %}
94 {% if d["policy_info"]["block-bugs"] %}
95 <li>Blocked by bug: {{ d["policy_info"]["block-bugs"]|map("int")|sort|map("string")|join(", ") }}</li>
96 {% endif %}
97 {% if d["is-candidate"] %}
98 <li><b>candidate</b></li>
99 {% endif %}
100 {% endif %}
101 </ul>
102 </li>
103 {% endfor %}
104 </ul>
105 {% endfor %}
106 </body>
107</html>

Subscribers

People subscribed via source and target branches