~ubuntu-archive/ubuntu-archive-scripts/trunk

« back to all changes in this revision

Viewing changes to sru_autosubscribe.py

  • Committer: Adam Conrad
  • Date: 2019-10-18 09:32:04 UTC
  • Revision ID: adconrad@0c3.net-20191018093204-yewoy93q72thygo2
chdist: Commit production hack to avoid perl warnings.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/python3
2
 
 
3
 
"""Subscribe uploaders to the unapproved queue to their SRU bugs
4
 
 
5
 
Author: Robie Basak <robie.basak@canonical.com>
6
 
 
7
 
Subscriptions are made on the principle that if you signed an upload, you need
8
 
to follow up on review comments on the SRU team. The SRU team have been having
9
 
trouble getting response to comments in bugs, noticed that often the sponsor
10
 
isn't subscribed, and hopes this will help.
11
 
 
12
 
This is intended to be run from a cronjob or timer. Run with --debug to see
13
 
what is going on, and additionally with --dry-run to see what would happen.
14
 
 
15
 
Principle of operation: get all key fingerprints of everyone in
16
 
~ubuntu-uploaders. Then download the .dsc file from everything in Unapproved
17
 
targetted at the proposed pockets of stable series and see who signed them.
18
 
Then subscribe this person to all bugs referenced in Launchpad-Bugs-Fixed in
19
 
the changes file of those uploads if they aren't subscribed at "Discussion"
20
 
level already.
21
 
 
22
 
To stop this script from acting on a particular bug, tag it "bot-stop-nagging".
23
 
 
24
 
It takes a while to get going; hitting the Launchpad API for all uploaders'
25
 
keys takes about a minute, then a further minute to fetch all the keys to get
26
 
their subkey fingerprints from keyserver.ubuntu.com.
27
 
 
28
 
All Ubuntu uploaders are supposed to be in ~ubuntu-uploaders but this isn't
29
 
enforced except by the correct action of the DMB. If somebody has uploaded but
30
 
is not a member of the team, this script will not find their key and therefore
31
 
will not subscribe them to their bugs.
32
 
 
33
 
The key 6309259455EFCAA8 also signs uploads and is ~ci-train-bot; it isn't a
34
 
member of ~ubuntu-uploaders and is therefore also ignored. We don't have a way
35
 
of identifying the actual sponsor in this case.
36
 
"""
37
 
 
38
 
import argparse
39
 
import itertools
40
 
import logging
41
 
import re
42
 
import subprocess
43
 
import tempfile
44
 
import urllib
45
 
 
46
 
import debian.deb822
47
 
import launchpadlib.launchpad
48
 
 
49
 
GPG_OUTPUT_PATTERN = re.compile(
50
 
    r"""gpg: Signature made.*
51
 
gpg: +using (?:RSA|DSA) key (?P<fingerprint>[0-9A-F]+)
52
 
"""
53
 
)
54
 
 
55
 
 
56
 
def find_specific_url(urls, suffix):
57
 
    for url in urls:
58
 
        if url.endswith(suffix):
59
 
            return url
60
 
    raise ValueError
61
 
 
62
 
 
63
 
def 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
 
 
92
 
def 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
 
 
110
 
def 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
 
 
120
 
def 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
 
 
136
 
def 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
 
 
143
 
def 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
 
 
185
 
def 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
 
 
228
 
if __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
 
            continue
266
 
        bug_numbers = find_changes_bugs(upload.changes_file_url)
267
 
        fingerprint = find_dsc_signing_fingerprint(dsc_url)
268
 
        try:
269
 
            signer = fingerprint_to_person[fingerprint]
270
 
        except KeyError:
271
 
            logging.debug(f"Could not find signer with fingerprint {fingerprint}")
272
 
            continue
273
 
        for bug_number in bug_numbers:
274
 
            try:
275
 
                bug = lp.bugs[bug_number]
276
 
            except KeyError:
277
 
                logging.debug(f"Could not find bug {bug_number}")
278
 
                continue
279
 
            ensure_subscribed(person=signer, bug=bug, dry_run=args.dry_run)