Merge lp:~racb/ubuntu-archive-scripts/sru-autosubscribe into lp:ubuntu-archive-scripts

Proposed by Robie Basak
Status: Merged
Merged at revision: 351
Proposed branch: lp:~racb/ubuntu-archive-scripts/sru-autosubscribe
Merge into: lp:ubuntu-archive-scripts
Diff against target: 282 lines (+278/-0)
1 file modified
sru_autosubscribe.py (+278/-0)
To merge this branch: bzr merge lp:~racb/ubuntu-archive-scripts/sru-autosubscribe
Reviewer Review Type Date Requested Status
Steve Langasek Approve
Review via email: mp+441467@code.launchpad.net

Description of the change

Previous MP: https://code.launchpad.net/~racb/ubuntu-archive-tools/+git/ubuntu-archive-tools/+merge/429629

I couldn't use the "Resubmit" function as it's moving from git to bzr.

ubuntu-archive-tools seems like a reasonable place to put this script. Can we run it from a cronjob the same as some of the other tooling?

Test with --debug --dry-run. I've tested on Focal. Currently the Python on Focal or later is required because I used f-strings. It's easy enough to make it work with older Pythons if needed, although it'd need some testing to ensure gpg will work as expected since the way this script operates gpg is a little hacky.

To post a comment you must log in.
Revision history for this message
Robie Basak (racb) wrote :

Could an archive admin mark the previous MP at https://code.launchpad.net/~racb/ubuntu-archive-tools/+git/ubuntu-archive-tools/+merge/429629 as Rejected please? I don't have permission to do that myself.

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

On what interval do you think this cronjob should run?

Revision history for this message
Robie Basak (racb) wrote :

At the moment the script is really inefficient. So how about we start off with daily, and hopefully we'll see a significant initial benefit even though there's a race? In time I could add some caching to get it going faster.

Revision history for this message
Steve Langasek (vorlon) wrote :

Have completed a test run and it's now set to run at 3:42 daily.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'sru_autosubscribe.py'
--- sru_autosubscribe.py 1970-01-01 00:00:00 +0000
+++ sru_autosubscribe.py 2023-04-19 14:29:52 +0000
@@ -0,0 +1,278 @@
1#!/usr/bin/python3
2
3"""Subscribe uploaders to the unapproved queue to their SRU bugs
4
5Author: Robie Basak <robie.basak@canonical.com>
6
7Subscriptions are made on the principle that if you signed an upload, you need
8to follow up on review comments on the SRU team. The SRU team have been having
9trouble getting response to comments in bugs, noticed that often the sponsor
10isn't subscribed, and hopes this will help.
11
12This is intended to be run from a cronjob or timer. Run with --debug to see
13what is going on, and additionally with --dry-run to see what would happen.
14
15Principle of operation: get all key fingerprints of everyone in
16~ubuntu-uploaders. Then download the .dsc file from everything in Unapproved
17targetted at the proposed pockets of stable series and see who signed them.
18Then subscribe this person to all bugs referenced in Launchpad-Bugs-Fixed in
19the changes file of those uploads if they aren't subscribed at "Discussion"
20level already.
21
22To stop this script from acting on a particular bug, tag it "bot-stop-nagging".
23
24It takes a while to get going; hitting the Launchpad API for all uploaders'
25keys takes about a minute, then a further minute to fetch all the keys to get
26their subkey fingerprints from keyserver.ubuntu.com.
27
28All Ubuntu uploaders are supposed to be in ~ubuntu-uploaders but this isn't
29enforced except by the correct action of the DMB. If somebody has uploaded but
30is not a member of the team, this script will not find their key and therefore
31will not subscribe them to their bugs.
32
33The key 6309259455EFCAA8 also signs uploads and is ~ci-train-bot; it isn't a
34member of ~ubuntu-uploaders and is therefore also ignored. We don't have a way
35of identifying the actual sponsor in this case.
36"""
37
38import argparse
39import itertools
40import logging
41import re
42import subprocess
43import tempfile
44import urllib
45
46import debian.deb822
47import launchpadlib.launchpad
48
49GPG_OUTPUT_PATTERN = re.compile(
50 r"""gpg: Signature made.*
51gpg: +using (?:RSA|DSA) key (?P<fingerprint>[0-9A-F]+)
52"""
53)
54
55
56def find_specific_url(urls, suffix):
57 for url in urls:
58 if url.endswith(suffix):
59 return url
60 raise ValueError
61
62
63def get_upload_source_urls(upload):
64 if upload.contains_source:
65 return upload.sourceFileUrls()
66 elif upload.contains_copy:
67 try:
68 copy_source_archive = upload.copy_source_archive
69 # Trigger a problem ValueError exception now rather than later
70 # This is magic launchpadlib behaviour: accessing an attribute of
71 # copy_source_archive may fail later on an access permission issue
72 # due to lazy loading.
73 getattr(copy_source_archive, "self_link")
74 except ValueError as e:
75 raise RuntimeError(
76 f"Cannot access {upload} copy_source_archive attribute: no permission?"
77 ) from e
78 return next(
79 iter(
80 upload.copy_source_archive.getPublishedSources(
81 source_name=upload.package_name,
82 version=upload.package_version,
83 exact_match=True,
84 order_by_date=True,
85 )
86 )
87 ).sourceFileUrls()
88 else:
89 raise RuntimeError(f"Cannot find source for {upload}")
90
91
92def find_dsc_signing_fingerprint(dsc_url):
93 with urllib.request.urlopen(
94 dsc_url
95 ) as dsc_fobj, tempfile.TemporaryDirectory() as tmpdir:
96 result = subprocess.run(
97 ["gpg", "--verify"],
98 input=dsc_fobj.read(),
99 capture_output=True,
100 env={"GNUPGHOME": tmpdir},
101 )
102 if result.returncode != 2:
103 raise RuntimeError("Unknown exit status from gpg")
104 m = GPG_OUTPUT_PATTERN.search(result.stderr.decode())
105 if not m:
106 raise ValueError("Signing key fingerprint not found")
107 return m.group("fingerprint")
108
109
110def find_changes_bugs(changes_url):
111 with urllib.request.urlopen(changes_url) as changes_fobj:
112 changes = debian.deb822.Changes(changes_fobj)
113 try:
114 bugs_str = changes["Launchpad-Bugs-Fixed"]
115 except KeyError:
116 return []
117 return bugs_str.split()
118
119
120def ensure_subscribed(person, bug, dry_run):
121 for subscription in bug.subscriptions:
122 if (
123 subscription.person_link == person.self_link
124 and subscription.bug_notification_level == "Discussion"
125 ):
126 logging.debug(f"{person.name} is already subscribed to {bug.id}")
127 return
128 logging.debug(f"Subscribing {person.name} to {bug.id}")
129 if "bot-stop-nagging" in bug.tags:
130 logging.debug("bot-stop-nagging detected; not subscribing")
131 return
132 if not dry_run:
133 bug.subscribe(level="Discussion", person=person)
134
135
136def parse_fingerprints(output):
137 for line in output.decode().splitlines():
138 fields = line.split(":")
139 if fields[0] == "fpr":
140 yield fields[9]
141
142
143def fetch_subkey_fingerprints(primary_fingerprints):
144 with tempfile.TemporaryDirectory() as tmpdir:
145 for primary_fingerprint in primary_fingerprints:
146 logging.debug(f"Fetching key for {primary_fingerprint}")
147 result = subprocess.run(
148 [
149 "gpg",
150 "--keyserver",
151 "keyserver.ubuntu.com",
152 "--recv-key",
153 primary_fingerprint,
154 ],
155 env={"GNUPGHOME": tmpdir},
156 capture_output=True,
157 )
158 if (
159 result.returncode == 2
160 and result.stderr == "gpg: keyserver receive failed: No data\n".encode()
161 ):
162 # Some keys cannot be fetched. See: https://irclogs.ubuntu.com/2022/09/07/%23launchpad.html#t14:03
163 continue
164 result.check_returncode()
165 result = subprocess.run(
166 [
167 "gpg",
168 "--list-keys",
169 "--with-colons",
170 "--with-fingerprint",
171 "--with-fingerprint", # duplicate to also give subkey fingerprints
172 primary_fingerprint,
173 ],
174 env={"GNUPGHOME": tmpdir},
175 capture_output=True,
176 check=True,
177 )
178 yield primary_fingerprint, (
179 fpr
180 for fpr in parse_fingerprints(result.stdout)
181 if fpr != primary_fingerprint
182 )
183
184
185def determine_fingerprint_to_person_map(lp):
186 logging.debug("Fetching uploader key fingerprints")
187 fingerprint_to_person = {
188 gpg_key.fingerprint: person
189 for person, gpg_key in itertools.chain.from_iterable(
190 ((person, gpg_key) for gpg_key in person.gpg_keys)
191 for person in lp.people["ubuntu-uploaders"].participants
192 )
193 }
194 logging.debug(f"{len(fingerprint_to_person)} fingerprints fetched")
195
196 # Add subkey fingerprints
197 logging.debug(f"Retrieving subkey fingerprints from keyserver.ubuntu.com")
198 fingerprint_to_person.update(
199 {
200 subkey_fingerprint: fingerprint_to_person[primary_fingerprint]
201 for primary_fingerprint, subkey_fingerprint in itertools.chain.from_iterable(
202 (
203 (primary_fingerprint, subkey_fingerprint)
204 for subkey_fingerprint in subkey_fingerprints
205 )
206 for primary_fingerprint, subkey_fingerprints in fetch_subkey_fingerprints(
207 fingerprint_to_person.keys()
208 )
209 )
210 }
211 )
212 logging.debug("Subkey fingerprints fetched")
213
214 # Some signers are only embedding 16 nibble keyids, so identify using these
215 # as well. It's easier to arrange collisions, but I think we can trust
216 # Ubuntu uploaders not to do this; and in the worst case scenario we'll
217 # only misidentify the uploader and give the wrong person the subscription
218 # anyway.
219 fingerprint_to_person.update(
220 {
221 fingerprint[-16:]: person
222 for fingerprint, person in fingerprint_to_person.items()
223 }
224 )
225 return fingerprint_to_person
226
227
228if __name__ == "__main__":
229 parser = argparse.ArgumentParser()
230 parser.add_argument("--dry-run", action="store_true")
231 parser.add_argument("--debug", action="store_true")
232 args = parser.parse_args()
233 if args.debug:
234 logging.basicConfig(level=logging.DEBUG)
235
236 lp = launchpadlib.launchpad.Launchpad.login_with(
237 "~racb sru_autosubscribe", "production", version="devel"
238 )
239
240 fingerprint_to_person = determine_fingerprint_to_person_map(lp)
241
242 distro_seriess = [
243 series
244 for series in lp.distributions["ubuntu"].series
245 if series.status in {"Current Stable Release", "Supported"}
246 ]
247 uploads = itertools.chain.from_iterable(
248 distro_series.getPackageUploads(pocket="Proposed", status="Unapproved")
249 for distro_series in distro_seriess
250 )
251 for upload in uploads:
252 logging.debug(f"Considering {upload}")
253 try:
254 urls = get_upload_source_urls(upload)
255 except RuntimeError:
256 logging.debug(f"Could not get source URLs for {upload}")
257 continue
258 try:
259 dsc_url = find_specific_url(urls, ".dsc")
260 except ValueError:
261 logging.debug(f"Could not find .dsc for {upload}")
262 continue
263 if not upload.changes_file_url:
264 logging.debug(f"Could not find changes file for {upload}")
265 bug_numbers = find_changes_bugs(upload.changes_file_url)
266 fingerprint = find_dsc_signing_fingerprint(dsc_url)
267 try:
268 signer = fingerprint_to_person[fingerprint]
269 except KeyError:
270 logging.debug(f"Could not find signer with fingerprint {fingerprint}")
271 continue
272 for bug_number in bug_numbers:
273 try:
274 bug = lp.bugs[bug_number]
275 except KeyError:
276 logging.debug(f"Could not find bug {bug_number}")
277 continue
278 ensure_subscribed(person=signer, bug=bug, dry_run=args.dry_run)

Subscribers

People subscribed via source and target branches