Merge ~vorlon/ubuntu-dev-tools:pm-helper into ubuntu-dev-tools:main

Proposed by Steve Langasek
Status: Merged
Merged at revision: fd885ec2390758770128dc75954ee8c8bd316d2c
Proposed branch: ~vorlon/ubuntu-dev-tools:pm-helper
Merge into: ubuntu-dev-tools:main
Diff against target: 324 lines (+278/-0)
5 files modified
debian/control (+2/-0)
doc/pm-helper.1 (+44/-0)
pm-helper (+149/-0)
requirements.txt (+1/-0)
ubuntutools/utils.py (+82/-0)
Reviewer Review Type Date Requested Status
Simon Quigley Approve
Bryce Harrington Approve
Benjamin Drung Pending
Ubuntu Core Development Team Pending
Review via email: mp+444677@code.launchpad.net

Description of the change

As part of my +1 maintenance shift this week I scratched the itch of wanting a commandline tool to streamline identifying a package to work on in update_excuses and taking a lock on it. Initially this just takes a package name and manages update-excuse bugs for it but eventually it should be extended to scroll through the list of packages in update_excuses (oldest first) and pick one, then grab information from various sources (launchpad, autopkgtest.u.c, Debian archive, buildd.debian.org, ci.debian.net) to make suggestions on fixing.

Very raw at the moment - no tests yet.

To post a comment you must log in.
Revision history for this message
Bryce Harrington (bryce) wrote :

Cool, hopefully my suggestions inline are useful. I look forward to how this develops.

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

Thanks for the review, Bryce! I've implemented most of your suggestions, and responded to comments. Would you care to re-review?

Revision history for this message
Bryce Harrington (bryce) wrote :

Looks great Steve, the updates all look great. This'll make a solid start for the tool. The man page is a nice touch, I'm often too lazy about making those. :-)

review: Approve
Revision history for this message
Benjamin Drung (bdrung) wrote :

This script has zero test cases which makes maintaining it hard. I would expect new code to come with test cases.

Revision history for this message
Simon Quigley (tsimonq2) wrote :

I'm no longer going to block on test cases. Let's move this forward, if there is interest in a test case we can address that later.

I apologize for not looking at this sooner, given the last uploads to ubuntu-dev-tools are from me. I will take care of the upload tomorrow, along with some testing and any necessary fixes.

I noticed two potential pedantic items, I'll check them further then fix as appropriate.

Thank you all for your hard work on this! I'm certain this will improve collaboration.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/debian/control b/debian/control
2index 3f2d647..1345a0d 100644
3--- a/debian/control
4+++ b/debian/control
5@@ -20,6 +20,7 @@ Build-Depends:
6 pylint <!nocheck>,
7 python3-all,
8 python3-apt,
9+ python3-dateutil,
10 python3-debian,
11 python3-debianbts,
12 python3-distro-info,
13@@ -132,6 +133,7 @@ Package: python3-ubuntutools
14 Architecture: all
15 Section: python
16 Depends:
17+ python3-dateutil,
18 python3-debian,
19 python3-distro-info,
20 python3-httplib2,
21diff --git a/doc/pm-helper.1 b/doc/pm-helper.1
22new file mode 100644
23index 0000000..c40934b
24--- /dev/null
25+++ b/doc/pm-helper.1
26@@ -0,0 +1,44 @@
27+.\" Copyright (C) 2023, Canonical Ltd.
28+.\"
29+.\" This program is free software; you can redistribute it and/or
30+.\" modify it under the terms of the GNU General Public License, version 3.
31+.\"
32+.\" This program is distributed in the hope that it will be useful,
33+.\" but WITHOUT ANY WARRANTY; without even the implied warranty of
34+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
35+.\" General Public License for more details.
36+.\"
37+.\" You should have received a copy of the GNU General Public License
38+.\" along with this program. If not, see <http://www.gnu.org/licenses/>.
39+.TH pm\-helper 1 "June 2023" ubuntu\-dev\-tools
40+
41+.SH NAME
42+pm\-helper \- helper to guide a developer through proposed\-migration work
43+
44+.SH SYNOPSIS
45+.B pm\-helper \fR[\fIoptions\fR] [\fIpackage\fR]
46+
47+.SH DESCRIPTION
48+Claim a package from proposed\-migration to work on and get additional
49+information (such as the state of the package in Debian) that may be helpful
50+in unblocking it.
51+.PP
52+This tool is incomplete and under development.
53+
54+.SH OPTIONS
55+.TP
56+.B \-l \fIINSTANCE\fR, \fB\-\-launchpad\fR=\fIINSTANCE\fR
57+Use the specified instance of Launchpad (e.g. "staging"), instead of
58+the default of "production".
59+.TP
60+.B \-v\fR, \fB--verbose\fR
61+be more verbose
62+.TP
63+\fB\-h\fR, \fB\-\-help\fR
64+Display a help message and exit
65+
66+.SH AUTHORS
67+\fBpm\-helper\fR and this manpage were written by Steve Langasek
68+<steve.langasek@ubuntu.com>.
69+.PP
70+Both are released under the GPLv3 license.
71diff --git a/pm-helper b/pm-helper
72new file mode 100755
73index 0000000..9cdc176
74--- /dev/null
75+++ b/pm-helper
76@@ -0,0 +1,149 @@
77+#!/usr/bin/python3
78+# Find the next thing to work on for proposed-migration
79+# Copyright (C) 2023 Canonical Ltd.
80+# Author: Steve Langasek <steve.langasek@ubuntu.com>
81+
82+# This program is free software; you can redistribute it and/or
83+# modify it under the terms of the GNU General Public License, version 3.
84+
85+# This program is distributed in the hope that it will be useful,
86+# but WITHOUT ANY WARRANTY; without even the implied warranty of
87+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
88+# General Public License for more details.
89+
90+# You should have received a copy of the GNU General Public License
91+# along with this program. If not, see <http://www.gnu.org/licenses/>.
92+
93+import lzma
94+from argparse import ArgumentParser
95+import sys
96+import webbrowser
97+import yaml
98+
99+from launchpadlib.launchpad import Launchpad
100+
101+from ubuntutools.utils import get_url
102+
103+
104+# proposed-migration is only concerned with the devel series; unlike other
105+# tools, don't make this configurable
106+excuses_url = 'https://ubuntu-archive-team.ubuntu.com/proposed-migration/' \
107+ + 'update_excuses.yaml.xz'
108+
109+
110+def get_proposed_version(excuses, package):
111+ for k in excuses['sources']:
112+ if k['source'] == package:
113+ return k.get('new-version')
114+ return None
115+
116+
117+def claim_excuses_bug(launchpad, bug, package):
118+ print("LP: #%d: %s" % (bug.id, bug.title))
119+ ubuntu = launchpad.distributions['ubuntu']
120+ series = ubuntu.current_series.fullseriesname
121+
122+ for task in bug.bug_tasks:
123+ # targeting to a series doesn't make the default task disappear,
124+ # it just makes it useless
125+ if task.bug_target_name == "%s (%s)" % (package, series):
126+ our_task = task
127+ break
128+ elif task.bug_target_name == "%s (Ubuntu)" % package:
129+ our_task = task
130+
131+ if our_task.assignee == launchpad.me:
132+ print("Bug already assigned to you.")
133+ return True
134+ elif our_task.assignee:
135+ print("Currently assigned to %s" % our_task.assignee.name)
136+
137+ print('''Do you want to claim this bug? [yN] ''', end="")
138+ sys.stdout.flush()
139+ response = sys.stdin.readline()
140+ if response.strip().lower().startswith('y'):
141+ our_task.assignee = launchpad.me
142+ our_task.lp_save()
143+ return True
144+
145+ return False
146+
147+
148+def create_excuses_bug(launchpad, package, version):
149+ print("Will open a new bug")
150+ bug = launchpad.bugs.createBug(
151+ title = 'proposed-migration for %s %s' % (package, version),
152+ tags = ('update-excuse'),
153+ target = 'https://api.launchpad.net/devel/ubuntu/+source/%s' % package,
154+ description = '%s %s is stuck in -proposed.' % (package, version)
155+ )
156+
157+ task = bug.bug_tasks[0]
158+ task.assignee = launchpad.me
159+ task.lp_save()
160+
161+ print("Opening %s in browser" % bug.web_link)
162+ webbrowser.open(bug.web_link)
163+ return bug
164+
165+
166+def has_excuses_bugs(launchpad, package):
167+ ubuntu = launchpad.distributions['ubuntu']
168+ pkg = ubuntu.getSourcePackage(name=package)
169+ if not pkg:
170+ raise ValueError(f"No such source package: {package}")
171+
172+ tasks = pkg.searchTasks(tags=['update-excuse'], order_by=['id'])
173+
174+ bugs = [task.bug for task in tasks]
175+ if not bugs:
176+ return False
177+
178+ if len(bugs) == 1:
179+ print("There is 1 open update-excuse bug against %s" % package)
180+ else:
181+ print("There are %d open update-excuse bugs against %s" \
182+ % (len(bugs), package))
183+
184+ for bug in bugs:
185+ if claim_excuses_bug(launchpad, bug, package):
186+ return True
187+
188+ return True
189+
190+
191+def main():
192+ parser = ArgumentParser()
193+ parser.add_argument(
194+ "-l", "--launchpad", dest="launchpad_instance", default="production")
195+ parser.add_argument(
196+ "-v", "--verbose", default=False, action="store_true",
197+ help="be more verbose")
198+ parser.add_argument(
199+ 'package', nargs='?', help="act on this package only")
200+ args = parser.parse_args()
201+
202+ args.launchpad = Launchpad.login_with(
203+ "pm-helper", args.launchpad_instance, version="devel")
204+
205+ f = get_url(excuses_url, False)
206+ with lzma.open(f) as lzma_f:
207+ excuses = yaml.load(lzma_f, Loader=yaml.CSafeLoader)
208+
209+ if args.package:
210+ try:
211+ if not has_excuses_bugs(args.launchpad, args.package):
212+ proposed_version = get_proposed_version(excuses, args.package)
213+ if not proposed_version:
214+ print("Package %s not found in -proposed." % args.package)
215+ sys.exit(1)
216+ create_excuses_bug(args.launchpad, args.package,
217+ proposed_version)
218+ except ValueError as e:
219+ sys.stderr.write(f"{e}\n")
220+ else:
221+ pass # for now
222+
223+
224+if __name__ == '__main__':
225+ sys.exit(main())
226diff --git a/requirements.txt b/requirements.txt
227index 34b16ff..ba91ac5 100644
228--- a/requirements.txt
229+++ b/requirements.txt
230@@ -1,5 +1,6 @@
231 python-debian
232 python-debianbts
233+dateutil
234 distro-info
235 httplib2
236 launchpadlib
237diff --git a/ubuntutools/utils.py b/ubuntutools/utils.py
238new file mode 100644
239index 0000000..dcc354e
240--- /dev/null
241+++ b/ubuntutools/utils.py
242@@ -0,0 +1,82 @@
243+# Copyright (C) 2019-2023 Canonical Ltd.
244+# Author: Brian Murray <brian.murray@canonical.com> et al.
245+
246+# This program is free software: you can redistribute it and/or modify
247+# it under the terms of the GNU General Public License as published by
248+# the Free Software Foundation; version 3 of the License.
249+#
250+# This program is distributed in the hope that it will be useful,
251+# but WITHOUT ANY WARRANTY; without even the implied warranty of
252+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
253+# GNU General Public License for more details.
254+#
255+# You should have received a copy of the GNU General Public License
256+# along with this program. If not, see <http://www.gnu.org/licenses/>.
257+
258+"""Portions of archive related code that is re-used by various tools."""
259+
260+from datetime import datetime
261+import os
262+import re
263+import urllib.request
264+
265+import dateutil.parser
266+from dateutil.tz import tzutc
267+
268+
269+def get_cache_dir():
270+ cache_dir = os.environ.get('XDG_CACHE_HOME',
271+ os.path.expanduser(os.path.join('~', '.cache')))
272+ uat_cache = os.path.join(cache_dir, 'ubuntu-archive-tools')
273+ os.makedirs(uat_cache, exist_ok=True)
274+ return uat_cache
275+
276+
277+def get_url(url, force_cached):
278+ ''' Return file to the URL, possibly caching it
279+ '''
280+ cache_file = None
281+
282+ # ignore bileto urls wrt caching, they're usually too small to matter
283+ # and we don't do proper cache expiry
284+ m = re.search('ubuntu-archive-team.ubuntu.com/proposed-migration/'
285+ '([^/]*)/([^/]*)',
286+ url)
287+ if m:
288+ cache_dir = get_cache_dir()
289+ cache_file = os.path.join(cache_dir, '%s_%s' % (m.group(1), m.group(2)))
290+ else:
291+ # test logs can be cached, too
292+ m = re.search(
293+ 'https://autopkgtest.ubuntu.com/results/autopkgtest-[^/]*/([^/]*)/([^/]*)'
294+ '/[a-z0-9]*/([^/]*)/([_a-f0-9]*)@/log.gz',
295+ url)
296+ if m:
297+ cache_dir = get_cache_dir()
298+ cache_file = os.path.join(
299+ cache_dir, '%s_%s_%s_%s.gz' % (
300+ m.group(1), m.group(2), m.group(3), m.group(4)))
301+
302+ if cache_file:
303+ try:
304+ prev_mtime = os.stat(cache_file).st_mtime
305+ except FileNotFoundError:
306+ prev_mtime = 0
307+ prev_timestamp = datetime.fromtimestamp(prev_mtime, tz=tzutc())
308+ new_timestamp = datetime.now(tz=tzutc()).timestamp()
309+ if force_cached:
310+ return open(cache_file, 'rb')
311+
312+ f = urllib.request.urlopen(url)
313+
314+ if cache_file:
315+ remote_ts = dateutil.parser.parse(f.headers['last-modified'])
316+ if remote_ts > prev_timestamp:
317+ with open('%s.new' % cache_file, 'wb') as new_cache:
318+ for line in f:
319+ new_cache.write(line)
320+ os.rename('%s.new' % cache_file, cache_file)
321+ os.utime(cache_file, times=(new_timestamp, new_timestamp))
322+ f.close()
323+ f = open(cache_file, 'rb')
324+ return f

Subscribers

People subscribed via source and target branches