2
# -*- coding: utf-8 -*-
4
# Copyright (C) 2018 Canonical Ltd
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.
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.
16
# A copy of the GNU General Public License version 2 is in LICENSE.
19
from collections import defaultdict, OrderedDict
25
from urllib.request import urlopen
29
from jinja2 import Environment, FileSystemLoader
34
loader=FileSystemLoader(os.path.dirname(os.path.abspath(__file__)) + '/templates'),
36
extensions=['jinja2.ext.i18n'],
38
env.install_null_translations(True)
42
def get_lp(i, anon=True):
43
from launchpadlib.launchpad import Launchpad
46
print(i, "connecting...")
48
_lps[k] = Launchpad.login_anonymously('sru-team-report', 'production', version='devel')
50
_lps[k] = Launchpad.login_with('sru-team-report', 'production', version='devel')
54
def get_true_ages_in_proposed(package_names, thread_count):
55
package_names = set(package_names)
59
ubuntu = lp.distributions['ubuntu']
60
primary_archive = ubuntu.archives[0]
61
devel_series = ubuntu.getSeries(name_or_version=ubuntu.development_series_alias)
64
spn = package_names.pop()
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
72
if spph.pocket != "Proposed":
74
last_proposed_spph = spph
75
if last_proposed_spph is None:
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
80
for i in range(thread_count):
81
t = threading.Thread(target=run, args=(i,))
89
def get_subscribers_lp(packages, thread_count):
90
from lazr.restfulclient.errors import ClientError
91
packages = set(packages)
92
def run(i, subscribers):
94
ubuntu = lp.distributions['ubuntu']
101
distribution_source_package = ubuntu.getSourcePackage(name=spn)
102
for subscription in distribution_source_package.getSubscriptions():
103
subscriber = subscription.subscriber
105
if subscriber.is_team:
106
subscribers[spn].append(subscriber.name)
108
# This happens for suspended users
112
for i in range(thread_count):
113
d = defaultdict(list)
114
t = threading.Thread(target=run, args=(i, d))
120
result = defaultdict(list)
122
for k, v in d.items():
126
def get_subscribers_json(packages, subscribers_json):
127
if subscribers_json is None:
128
j = urlopen("http://people.canonical.com/~ubuntu-archive/package-team-mapping.json")
130
j = open(subscribers_json, 'rb')
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
140
""" http://stackoverflow.com/a/8661021 """
141
represent_dict_order = (
142
lambda self, data: self.represent_mapping('tag:yaml.org,2002:map',
144
yaml.add_representer(OrderedDict, represent_dict_order)
151
fields = [field.name for field in attr.fields(type(inst))]
152
fields.extend(getattr(inst, "extra_fields", []))
154
if field.startswith('_'):
158
'serialize_' + field,
159
lambda: getattr(inst, field))()
165
class ArchRegression:
166
arch = attr.ib(default=None)
167
log_link = attr.ib(default=None)
168
hist_link = attr.ib(default=None)
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]
178
def serialize_arches(self):
179
return [as_data(a) for a in self.arches]
182
def package_version(self):
183
return self.package + '/' + self.version
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]
194
unsatbuilddep = attr.ib(default=None) # [string]
195
brokenbin = attr.ib(default=None) # [string]
196
componentmismatch = attr.ib(default=None) # [string]
198
_age = attr.ib(default=None)
200
extra_fields = ['age']
202
def serialize_regressions(self):
203
return [as_data(r) for r in self.regressions]
211
if self._age is not None:
215
return self.data["policy_info"]["age"]["current-age"]
224
def key_package(self):
225
if self.regressing_package:
226
return self.regressing_package
227
return self.package_in_proposed
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')
239
parser.add_argument('yaml_output', default=None)
240
args = parser.parse_args()
242
components = args.components.split(',')
244
print("fetching yaml")
245
if args.excuses_yaml:
246
if args.excuses_yaml.endswith('.xz'):
247
yaml_text = lzma.open(args.excuses_yaml)
249
yaml_text = open(args.excuses_yaml)
252
yaml_text = lzma.open(urlopen("https://people.canonical.com/~ubuntu-archive/proposed-migration/update_excuses.yaml.xz"))
253
except urllib.error.HTTPError as e:
254
print("Reading fallback yaml (%s)" % e)
255
yaml_text = urlopen("https://people.canonical.com/~ubuntu-archive/proposed-migration/update_excuses.yaml")
256
print("parsing yaml")
257
# The CSafeLoader is ten times faster than the regular one
258
excuses = yaml.load(yaml_text, Loader=yaml.CSafeLoader)
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:
267
prob = Problem(kind='package-in-proposed', data=defaultdict(dict, item), package_in_proposed=source_package_name)
268
in_proposed_packages[source_package_name] = prob
269
prob.regressions = []
271
prob.componentmismatch = []
272
# The verdict entries are not items to list on the report
273
for policy in ['autopkgtest', 'update-excuse', 'block-bugs']:
275
del item['policy_info'][policy]['verdict']
278
if 'autopkgtest' in item['reason']:
279
for package, results in sorted(item['policy_info']['autopkgtest'].items()):
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)
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)
294
prob.waiting.append((package + ": " + ", ".join(wait_arches)))
295
if 'depends' in item['reason']:
296
for l in item['excuses']:
297
if 'cannot depend on' in l:
298
prob.componentmismatch.append(l)
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():
303
unsatd[p].append(arch)
304
prob.unsatdepends = ['{}: {}'.format(p, ', '.join(sorted(arches))) for p, arches in sorted(unsatd.items())]
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():
310
unsatdbd[p].append(arch)
311
prob.unsatbuilddep = ['{}: {}'.format(p, ', '.join(sorted(arches))) for p, arches in sorted(unsatdbd.items())]
312
if 'implicit-deps' in item['policy_info']['implicit-deps']:
313
prob.brokenbin = item['policy_info']['implicit-deps']['implicit-deps']['broken-binaries']
315
package_to_problems = defaultdict(list)
317
for problem in in_proposed_packages.values():
318
# nautilus/riscv64 -> nautilus
319
pkg = problem.package_in_proposed.split('/')[0]
320
package_to_problems[pkg].append(problem)
321
for regression in problem.regressions:
322
if regression.blocking not in in_proposed_packages:
324
if regression.blocking == regression.package:
326
package_to_problems[regression.package].append(Problem(
327
kind='regressing-other', package_in_proposed=regression.blocking,
328
regressing_package=regression.package,
329
regressions=[regression],
330
data=in_proposed_packages[regression.blocking].data))
333
true_ages = get_true_ages_in_proposed(set(package_to_problems), 10)
334
for package, true_age in true_ages.items():
335
for problem in package_to_problems[package]:
336
problem.age = true_age
338
print("getting subscribers")
339
if args.subscribers_from_lp:
340
subscribers = get_subscribers_lp(set(package_to_problems), 10)
341
for p in set(package_to_problems):
342
if p not in subscribers:
343
subscribers[p] = ['unsubscribed']
345
subscribers = get_subscribers_json(set(package_to_problems), args.subscribers_json)
346
for p in set(package_to_problems):
347
pkg = p.split('/')[0]
348
if pkg not in subscribers:
349
subscribers[p] = ['unknown']
352
team_to_problems = defaultdict(list)
353
for package, teams in subscribers.items():
354
all_teams |= set(teams)
356
team_to_problems[team].extend(package_to_problems[package])
358
for packages in team_to_problems.values():
359
packages.sort(key=lambda prob: (-prob.age, prob.key_package))
361
team_to_attn_count = {}
362
for team, problems in team_to_problems.items():
363
team_to_attn_count[team] = len([problem for problem in problems if problem.late])
366
t = env.get_template('team-report.html')
367
with open(args.output, 'w', encoding='utf-8') as fp:
370
team_to_problems=team_to_problems,
371
team_to_attn_count=team_to_attn_count,
372
now=excuses["generated-date"].strftime("%Y.%m.%d %H:%M:%S") + ' ' + time.localtime().tm_zone))
374
team_to_problem_data = {}
375
for t, ps in team_to_problems.items():
376
team_to_problem_data[t] = [as_data(p) for p in ps]
377
with open(args.yaml_output, 'w', encoding='utf-8') as fp:
378
yaml.dump(team_to_problem_data, fp)
381
if __name__ == '__main__':