Merge ubuntu-debuginfod:poller into ubuntu-debuginfod:master

Proposed by Sergio Durigan Junior
Status: Merged
Merged at revision: be368a5521d47d6f32062d3a385f8197c7cfc4e0
Proposed branch: ubuntu-debuginfod:poller
Merge into: ubuntu-debuginfod:master
Diff against target: 222 lines (+210/-0)
2 files modified
ddebpoller.py (+75/-0)
debugpoller.py (+135/-0)
Reviewer Review Type Date Requested Status
Lena Voytek (community) Approve
Bryce Harrington (community) Approve
Canonical Server Core Reviewers Pending
Athos Ribeiro Pending
Canonical Server Reporter Pending
Review via email: mp+433960@code.launchpad.net

Description of the change

This MP implements the poller for debuginfod. There are 2 classes implemented here: DebugPoller, which offers the basic functionality for a poller, and DdebPoller, which extends the former and does the actual polling for newly published ddebs on LP. The intention is that, in the future, we will have more classes for other types of debug artifacts that will need to be fetched.

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

I have several suggestions for improvements but none that I feel to be critical enough to block on so set as approved, and you can take or leave the suggestions as desired. The code looks good, this will add a nice functionality!

Don't forget to also include the python3-launchpad dependency in the README.md. (Might not hurt to have some discussion in README.md or somewhere about how these new classes are used, but maybe that comes later.)

review: Approve
Revision history for this message
Sergio Durigan Junior (sergiodj) wrote :

Thanks for the review, Bryce. Comments inline.

Revision history for this message
Lena Voytek (lvoytek) wrote :

Logic looks good to me! Added some comments on formatting and some nitpicks

review: Approve
Revision history for this message
Bryce Harrington (bryce) :
Revision history for this message
Sergio Durigan Junior (sergiodj) wrote :

Thanks for the review, Lena.

Revision history for this message
Sergio Durigan Junior (sergiodj) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/ddebpoller.py b/ddebpoller.py
2new file mode 100644
3index 0000000..a19d92c
4--- /dev/null
5+++ b/ddebpoller.py
6@@ -0,0 +1,75 @@
7+#!/usr/bin/python3
8+
9+# Copyright (C) 2022 Canonical Ltd.
10+
11+# This program is free software: you can redistribute it and/or modify
12+# it under the terms of the GNU General Public License as published by
13+# the Free Software Foundation, either version 3 of the License, or
14+# (at your option) any later version.
15+
16+# This program is distributed in the hope that it will be useful,
17+# but WITHOUT ANY WARRANTY; without even the implied warranty of
18+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+# GNU General Public License for more details.
20+
21+# You should have received a copy of the GNU General Public License
22+# along with this program. If not, see <https://www.gnu.org/licenses/>.
23+
24+# Authors: Sergio Durigan Junior <sergio.durigan@canonical.com>
25+
26+from debugpoller import DebugPoller
27+
28+
29+class DdebPoller(DebugPoller):
30+ def __init__(
31+ self, initial_interval=None, force_initial_interval=False, dry_run=False
32+ ):
33+ """Initialize the object using 'ddeb' as its name.
34+
35+ Look at DebugPoller's docstring for an explanation about the arguments."""
36+ super().__init__(
37+ "ddeb",
38+ initial_interval=initial_interval,
39+ force_initial_interval=force_initial_interval,
40+ dry_run=dry_run,
41+ )
42+
43+ def get_ddebs(self):
44+ """Get the list of ddebs that have been published since the last
45+ timestamp from Launchpad."""
46+ timestamp = self._get_timestamp()
47+ assert timestamp is not None, f"_get_timestamp returned None"
48+ assert timestamp != "", f"_get_timestamp returned a blank timestamp"
49+ new_timestamp = self._generate_timestamp()
50+ assert new_timestamp is not None, f"_generate_timestamp returned None"
51+ assert new_timestamp != "", f"_generate_timestamp returned a blank timestamp"
52+
53+ result = []
54+
55+ self._logger.info(
56+ f"Polling ddebs created since '{timestamp}' (it's now '{new_timestamp}')"
57+ )
58+
59+ for pkg in self._main_archive.getPublishedSources(
60+ order_by_date=True, created_since_date=timestamp, status="Published"
61+ ):
62+ ddeb_urls = [url for url in pkg.binaryFileUrls() if url.endswith(".ddeb")]
63+ ddebs_len = len(ddeb_urls)
64+ if ddebs_len > 0:
65+ distro_series = self._lp.load(pkg.distro_series_link).name
66+ pkgname = pkg.source_package_name
67+ pkgver = pkg.source_package_version
68+ msg = {
69+ "source_package": pkgname,
70+ "version": pkgver,
71+ "component": pkg.component_name,
72+ "distro_series": distro_series,
73+ "ddebs": ddeb_urls,
74+ }
75+ self._logger.debug(
76+ f"For source package '{pkgname}-{pkgver}', found {ddebs_len}:\n{ddeb_urls}"
77+ )
78+ result.append(msg)
79+
80+ self._record_timestamp(new_timestamp)
81+ return result
82diff --git a/debugpoller.py b/debugpoller.py
83new file mode 100644
84index 0000000..77cb2a1
85--- /dev/null
86+++ b/debugpoller.py
87@@ -0,0 +1,135 @@
88+#!/usr/bin/python3
89+
90+# Copyright (C) 2022 Canonical Ltd.
91+
92+# This program is free software: you can redistribute it and/or modify
93+# it under the terms of the GNU General Public License as published by
94+# the Free Software Foundation, either version 3 of the License, or
95+# (at your option) any later version.
96+
97+# This program is distributed in the hope that it will be useful,
98+# but WITHOUT ANY WARRANTY; without even the implied warranty of
99+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
100+# GNU General Public License for more details.
101+
102+# You should have received a copy of the GNU General Public License
103+# along with this program. If not, see <https://www.gnu.org/licenses/>.
104+
105+# Authors: Sergio Durigan Junior <sergio.durigan@canonical.com>
106+
107+import os
108+import sys
109+from launchpadlib.launchpad import Launchpad
110+import datetime
111+import logging
112+
113+
114+class DebugPoller:
115+ """The DebugPoller class implements the basics of a poller for the
116+ debuginfod service."""
117+
118+ # The timestamp file we will save our timestamp into.
119+ TIMESTAMP_FILE = os.path.expanduser("~/.config/ubuntu-debuginfod/timestamp")
120+
121+ # The timestamp format we use. Launchpad requires this format.
122+ TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S"
123+
124+ def __init__(
125+ self,
126+ module_name,
127+ initial_interval=1,
128+ force_initial_interval=False,
129+ dry_run=False,
130+ ):
131+ """Initalize the DebugPoller object.
132+
133+ :param str module_name: Name of the DebugPoller module. This
134+ is used to compose the timestamp filename so that it is
135+ unique.
136+
137+ :param int initial_interval: The initial interval (in hours),
138+ i.e., if no timestamp file has been found then we use this
139+ value to tell Launchpad that we want files created since
140+ (NOW - INITIAL_INTERVAL). Default is 1 hour.
141+
142+ :param bool force_initial_interval: Force the poller to use
143+ the initial interval to calculate the timestamp, instead
144+ of relying on the timestamp file. Default is False.
145+
146+ :param bool dry_run: Tell the poller that it shouldn't record
147+ the timestamp in the file when the operation finishes.
148+ Default is False.
149+ """
150+ self._lp = Launchpad.login_anonymously("ubuntu-debuginfod poller", "production")
151+ self._main_archive = self._lp.distributions["ubuntu"].main_archive
152+ self.TIMESTAMP_FORMAT = f"{self.TIMESTAMP_FORMAT}-{module_name}"
153+
154+ self._logger = logging.getLogger(__name__)
155+ logging.basicConfig(
156+ level=logging.INFO,
157+ format=(
158+ "%(levelname) -10s %(asctime)s %(name) -20s %(funcName) "
159+ "-25s : %(message)s"
160+ ),
161+ )
162+
163+ self._initial_interval = initial_interval
164+ self._force_initial_interval = force_initial_interval
165+ self._dry_run = dry_run
166+
167+ def _generate_timestamp(self, interval=None):
168+ """Generate a timestamp that can be used when querying Launchpad
169+ (via getPublishedSources).
170+
171+ :param interval: Specify how long ago (in hours) the timestamp must
172+ refer to. If not specified, the timestamp is generated for
173+ the current time.
174+ :type interval: int or None
175+ """
176+ d = datetime.datetime.now(datetime.timezone.utc)
177+
178+ if interval is not None:
179+ self._logger.debug(f"Generating timestamp with interval {interval}")
180+ d = d - datetime.timedelta(hours=interval)
181+
182+ self._logger.debug(f"Generated timestamp '{d.strftime(self.TIMESTAMP_FORMAT)}'")
183+ return d.strftime(self.TIMESTAMP_FORMAT)
184+
185+ def _get_timestamp(self):
186+ """Get the timestamp from the timestamp file, or generate a
187+ new one.
188+
189+ If a timestamp file exists, returns its value. Otherwise,
190+ generate a new one with self._initial_interval.
191+ """
192+ if not os.path.exists(self.TIMESTAMP_FILE) or self._force_initial_interval:
193+ self._logger.debug(f"Timestamp file '{self.TIMESTAMP_FILE}' doesn't exist")
194+ self._logger.debug(
195+ f"Or force_initial_interval = {self._force_initial_interval}"
196+ )
197+ return self._generate_timestamp(interval=self._initial_interval)
198+
199+ with open(self.TIMESTAMP_FILE, "r", encoding="UTF-8") as f:
200+ return f.readline()
201+
202+ def _record_timestamp(self, timestamp):
203+ """Save the timestamp into the timestamp file.
204+
205+ :param str timestamp: Timestamp that should be saved into the
206+ TIMESTAMP_FILE.
207+ """
208+ if self._dry_run:
209+ self._logger.debug("dry_run enabled, not recording timestamp file")
210+ return
211+
212+ dirname = os.path.dirname(self.TIMESTAMP_FILE)
213+ os.makedirs(dirname, mode=0o755, exist_ok=True)
214+ with open(self.TIMESTAMP_FILE, "w", encoding="UTF-8") as f:
215+ f.write(timestamp)
216+
217+ def set_initial_interval(self, initial_interval):
218+ """Set the initial_interval value.
219+
220+ :param int initial_interval: The new initial_interval to be used.
221+ """
222+ self._initial_interval = initial_interval

Subscribers

People subscribed via source and target branches

to all changes: