Merge ~cjwatson/launchpad:oval-check-changes into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 74ab7de25966c16e9097fafede9ff3c73844ae94
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:oval-check-changes
Merge into: launchpad:master
Diff against target: 399 lines (+264/-41)
2 files modified
lib/lp/archivepublisher/scripts/publishdistro.py (+105/-37)
lib/lp/archivepublisher/tests/test_publishdistro.py (+159/-4)
Reviewer Review Type Date Requested Status
Guruprasad Approve
Review via email: mp+441468@code.launchpad.net

Commit message

Compare incoming OVAL data with already published one

Description of the change

I adopted https://code.launchpad.net/~jugmac00/launchpad/+git/launchpad/+merge/441101 and fixed up a number of issues I found while writing tests for it. Normally I'd squash all this before proposing it, but in this case I've kept the commits separate to make it clear what Jürgen did and what I did.

To post a comment you must log in.
Revision history for this message
Guruprasad (lgp171188) wrote :

LGTM 👍🏼

review: Approve
Revision history for this message
Colin Watson (cjwatson) :
Revision history for this message
Jürgen Gmach (jugmac00) wrote :

Thanks for picking this up!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/archivepublisher/scripts/publishdistro.py b/lib/lp/archivepublisher/scripts/publishdistro.py
2index 419b1ef..87e9a7d 100644
3--- a/lib/lp/archivepublisher/scripts/publishdistro.py
4+++ b/lib/lp/archivepublisher/scripts/publishdistro.py
5@@ -8,13 +8,16 @@ __all__ = [
6 ]
7
8 import os
9+from filecmp import dircmp
10 from optparse import OptionValueError
11+from pathlib import Path
12 from subprocess import CalledProcessError, check_call
13
14 from storm.store import Store
15 from zope.component import getUtility
16
17 from lp.app.errors import NotFoundError
18+from lp.archivepublisher.config import getPubConfig
19 from lp.archivepublisher.publishing import (
20 GLOBAL_PUBLISHER_LOCK,
21 cannot_modify_suite,
22@@ -46,6 +49,19 @@ def is_ppa_public(ppa):
23 return not ppa.private
24
25
26+def has_oval_data_changed(incoming_dir, published_dir):
27+ """Compare the incoming data with the already published one."""
28+ # XXX cjwatson 2023-04-19: `dircmp` in Python < 3.6 doesn't accept
29+ # path-like objects.
30+ compared = dircmp(str(incoming_dir), str(published_dir))
31+ return (
32+ bool(compared.left_only)
33+ or bool(compared.right_only)
34+ or bool(compared.diff_files)
35+ or bool(compared.funny_files)
36+ )
37+
38+
39 class PublishDistro(PublisherScript):
40 """Distro publisher."""
41
42@@ -507,56 +523,108 @@ class PublishDistro(PublisherScript):
43 # store and cause performance problems.
44 Store.of(archive).reset()
45
46- def rsync_oval_data(self):
47- if config.archivepublisher.oval_data_rsync_endpoint:
48- # Ensure that the rsync paths have a trailing slash.
49- rsync_src = os.path.join(
50- config.archivepublisher.oval_data_rsync_endpoint, ""
51- )
52- rsync_dest = os.path.join(
53- config.archivepublisher.oval_data_root, ""
54+ def rsyncOVALData(self):
55+ # Ensure that the rsync paths have a trailing slash.
56+ rsync_src = os.path.join(
57+ config.archivepublisher.oval_data_rsync_endpoint, ""
58+ )
59+ rsync_dest = os.path.join(config.archivepublisher.oval_data_root, "")
60+ rsync_command = [
61+ "/usr/bin/rsync",
62+ "-a",
63+ "-q",
64+ "--timeout={}".format(
65+ config.archivepublisher.oval_data_rsync_timeout
66+ ),
67+ "--delete",
68+ "--delete-after",
69+ rsync_src,
70+ rsync_dest,
71+ ]
72+ try:
73+ self.logger.info(
74+ "Attempting to rsync the OVAL data from '%s' to '%s'",
75+ rsync_src,
76+ rsync_dest,
77 )
78- rsync_command = [
79- "/usr/bin/rsync",
80- "-a",
81- "-q",
82- "--timeout={}".format(
83- config.archivepublisher.oval_data_rsync_timeout
84- ),
85- "--delete",
86- "--delete-after",
87+ check_call(rsync_command)
88+ except CalledProcessError:
89+ self.logger.exception(
90+ "Failed to rsync OVAL data from '%s' to '%s'",
91 rsync_src,
92 rsync_dest,
93- ]
94- try:
95- self.logger.info(
96- "Attempting to rsync the OVAL data from '%s' to '%s'",
97- rsync_src,
98- rsync_dest,
99- )
100- check_call(rsync_command)
101- except CalledProcessError:
102- self.logger.exception(
103- "Failed to rsync OVAL data from '%s' to '%s'",
104- rsync_src,
105- rsync_dest,
106- )
107- raise
108- else:
109- self.logger.info(
110- "Skipping the OVAL data sync as no rsync endpoint"
111- " has been configured."
112 )
113+ raise
114+
115+ def checkForUpdatedOVALData(self, distribution):
116+ """Compare the published OVAL files with the incoming one."""
117+ start_dir = Path(config.archivepublisher.oval_data_root)
118+ archive_set = getUtility(IArchiveSet)
119+ for owner_path in start_dir.iterdir():
120+ if not owner_path.name.startswith("~"):
121+ continue
122+ distribution_path = owner_path / distribution.name
123+ if not distribution_path.is_dir():
124+ continue
125+ for archive_path in distribution_path.iterdir():
126+ archive = archive_set.getPPAByDistributionAndOwnerName(
127+ distribution, owner_path.name[1:], archive_path.name
128+ )
129+ if archive is None:
130+ self.logger.info(
131+ "Skipping OVAL data for '~%s/%s/%s' "
132+ "(no such archive).",
133+ owner_path.name[1:],
134+ distribution.name,
135+ archive_path.name,
136+ )
137+ continue
138+ for suite_path in archive_path.iterdir():
139+ try:
140+ series, pocket = distribution.getDistroSeriesAndPocket(
141+ suite_path.name
142+ )
143+ except NotFoundError:
144+ self.logger.info(
145+ "Skipping OVAL data for '%s:%s' (no such suite).",
146+ archive.reference,
147+ suite_path.name,
148+ )
149+ continue
150+ for component in archive.getComponentsForSeries(series):
151+ incoming_dir = suite_path / component.name
152+ published_dir = (
153+ Path(getPubConfig(archive).distsroot)
154+ / series.name
155+ / component.name
156+ / "oval"
157+ )
158+ if not published_dir.is_dir() or has_oval_data_changed(
159+ incoming_dir=incoming_dir,
160+ published_dir=published_dir,
161+ ):
162+ archive.markSuiteDirty(
163+ distroseries=series, pocket=pocket
164+ )
165+ break
166
167 def main(self, reset_store_between_archives=True):
168 """See `LaunchpadScript`."""
169 self.validateOptions()
170 self.logOptions()
171
172- self.rsync_oval_data()
173+ if config.archivepublisher.oval_data_rsync_endpoint:
174+ self.rsyncOVALData()
175+ else:
176+ self.logger.info(
177+ "Skipping the OVAL data sync as no rsync endpoint"
178+ " has been configured."
179+ )
180
181 archive_ids = []
182 for distribution in self.findDistros():
183+ if config.archivepublisher.oval_data_rsync_endpoint:
184+ self.checkForUpdatedOVALData(distribution)
185 for archive in self.getTargetArchives(distribution):
186 if archive.distribution != distribution:
187 raise AssertionError(
188diff --git a/lib/lp/archivepublisher/tests/test_publishdistro.py b/lib/lp/archivepublisher/tests/test_publishdistro.py
189index 3d9e0cc..049dccb 100644
190--- a/lib/lp/archivepublisher/tests/test_publishdistro.py
191+++ b/lib/lp/archivepublisher/tests/test_publishdistro.py
192@@ -7,6 +7,7 @@ import os
193 import shutil
194 import subprocess
195 from optparse import OptionValueError
196+from pathlib import Path
197
198 from fixtures import MockPatch
199 from storm.store import Store
200@@ -33,6 +34,7 @@ from lp.registry.interfaces.pocket import PackagePublishingPocket
201 from lp.services.config import config
202 from lp.services.database.interfaces import IStore
203 from lp.services.log.logger import BufferLogger, DevNullLogger
204+from lp.services.osutils import write_file
205 from lp.services.scripts.base import LaunchpadScriptFailure
206 from lp.soyuz.enums import (
207 ArchivePublishingMethod,
208@@ -258,10 +260,11 @@ class TestPublishDistro(TestNativePublishingBase):
209 self.assertEqual(PackagePublishingStatus.PENDING, pub_source.status)
210
211 def setUpOVALDataRsync(self):
212+ self.oval_data_root = self.makeTemporaryDirectory()
213 self.pushConfig(
214 "archivepublisher",
215 oval_data_rsync_endpoint="oval.internal::oval/",
216- oval_data_root="/tmp/oval-data",
217+ oval_data_root=self.oval_data_root,
218 oval_data_rsync_timeout=90,
219 )
220
221@@ -299,7 +302,7 @@ class TestPublishDistro(TestNativePublishingBase):
222 "--delete",
223 "--delete-after",
224 "oval.internal::oval/",
225- "/tmp/oval-data/",
226+ self.oval_data_root + "/",
227 ]
228 mock_subprocess_check_call.assert_called_once_with(call_args)
229
230@@ -322,15 +325,167 @@ class TestPublishDistro(TestNativePublishingBase):
231 "--delete",
232 "--delete-after",
233 "oval.internal::oval/",
234- "/tmp/oval-data/",
235+ self.oval_data_root + "/",
236 ]
237 mock_subprocess_check_call.assert_called_once_with(call_args)
238 expected_log_line = (
239 "ERROR Failed to rsync OVAL data from "
240- "'oval.internal::oval/' to '/tmp/oval-data/'"
241+ "'oval.internal::oval/' to '%s/'" % self.oval_data_root
242 )
243 self.assertTrue(expected_log_line in self.logger.getLogBuffer())
244
245+ def test_checkForUpdatedOVALData_new(self):
246+ self.setUpOVALDataRsync()
247+ self.useFixture(
248+ MockPatch("lp.archivepublisher.scripts.publishdistro.check_call")
249+ )
250+ archive = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
251+ # Disable normal publication so that dirty_suites isn't cleared.
252+ archive.publish = False
253+ incoming_dir = (
254+ Path(self.oval_data_root)
255+ / archive.reference
256+ / "breezy-autotest"
257+ / "main"
258+ )
259+ write_file(str(incoming_dir), b"test")
260+ self.runPublishDistro(
261+ extra_args=["--ppa"], distribution=archive.distribution.name
262+ )
263+ self.assertEqual(["breezy-autotest"], archive.dirty_suites)
264+
265+ def test_checkForUpdatedOVALData_unchanged(self):
266+ self.setUpOVALDataRsync()
267+ self.useFixture(
268+ MockPatch("lp.archivepublisher.scripts.publishdistro.check_call")
269+ )
270+ archive = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
271+ # Disable normal publication so that dirty_suites isn't cleared.
272+ archive.publish = False
273+ incoming_dir = (
274+ Path(self.oval_data_root)
275+ / archive.reference
276+ / "breezy-autotest"
277+ / "main"
278+ )
279+ published_dir = (
280+ Path(getPubConfig(archive).distsroot)
281+ / "breezy-autotest"
282+ / "main"
283+ / "oval"
284+ )
285+ write_file(str(incoming_dir / "foo.oval.xml.bz2"), b"test")
286+ shutil.copytree(str(incoming_dir), str(published_dir))
287+ self.runPublishDistro(
288+ extra_args=["--ppa"], distribution=archive.distribution.name
289+ )
290+ self.assertIsNone(archive.dirty_suites)
291+
292+ def test_checkForUpdatedOVALData_updated(self):
293+ self.setUpOVALDataRsync()
294+ self.useFixture(
295+ MockPatch("lp.archivepublisher.scripts.publishdistro.check_call")
296+ )
297+ archive = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
298+ # Disable normal publication so that dirty_suites isn't cleared.
299+ archive.publish = False
300+ incoming_dir = (
301+ Path(self.oval_data_root)
302+ / archive.reference
303+ / "breezy-autotest"
304+ / "main"
305+ )
306+ published_dir = (
307+ Path(getPubConfig(archive).distsroot)
308+ / "breezy-autotest"
309+ / "main"
310+ / "oval"
311+ )
312+ write_file(str(published_dir / "foo.oval.xml.bz2"), b"old")
313+ mtime = (published_dir / "foo.oval.xml.bz2").stat().st_mtime
314+ os.utime(
315+ str(published_dir / "foo.oval.xml.bz2"), (mtime - 1, mtime - 1)
316+ )
317+ write_file(str(incoming_dir / "foo.oval.xml.bz2"), b"new")
318+ self.runPublishDistro(
319+ extra_args=["--ppa"], distribution=archive.distribution.name
320+ )
321+ self.assertEqual(["breezy-autotest"], archive.dirty_suites)
322+
323+ def test_checkForUpdatedOVALData_new_files(self):
324+ self.setUpOVALDataRsync()
325+ self.useFixture(
326+ MockPatch("lp.archivepublisher.scripts.publishdistro.check_call")
327+ )
328+ archive = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
329+ # Disable normal publication so that dirty_suites isn't cleared.
330+ archive.publish = False
331+ incoming_dir = (
332+ Path(self.oval_data_root)
333+ / archive.reference
334+ / "breezy-autotest"
335+ / "main"
336+ )
337+ published_dir = (
338+ Path(getPubConfig(archive).distsroot)
339+ / "breezy-autotest"
340+ / "main"
341+ / "oval"
342+ )
343+ write_file(str(incoming_dir / "foo.oval.xml.bz2"), b"test")
344+ shutil.copytree(str(incoming_dir), str(published_dir))
345+ write_file(str(incoming_dir / "bar.oval.xml.bz2"), b"test")
346+ self.runPublishDistro(
347+ extra_args=["--ppa"], distribution=archive.distribution.name
348+ )
349+ self.assertEqual(["breezy-autotest"], archive.dirty_suites)
350+
351+ def test_checkForUpdatedOVALData_nonexistent_archive(self):
352+ self.setUpOVALDataRsync()
353+ self.useFixture(
354+ MockPatch("lp.archivepublisher.scripts.publishdistro.check_call")
355+ )
356+ incoming_dir = (
357+ Path(self.oval_data_root)
358+ / "~nonexistent"
359+ / "ubuntutest"
360+ / "archive"
361+ / "breezy-autotest"
362+ / "main"
363+ )
364+ write_file(str(incoming_dir / "foo.oval.xml.bz2"), b"test")
365+ self.runPublishDistro(extra_args=["--ppa"], distribution="ubuntutest")
366+ self.assertIn(
367+ "INFO Skipping OVAL data for '~nonexistent/ubuntutest/archive' "
368+ "(no such archive).",
369+ self.logger.getLogBuffer(),
370+ )
371+
372+ def test_checkForUpdatedOVALData_nonexistent_suite(self):
373+ self.setUpOVALDataRsync()
374+ self.useFixture(
375+ MockPatch("lp.archivepublisher.scripts.publishdistro.check_call")
376+ )
377+ archive = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
378+ # Disable normal publication so that dirty_suites isn't cleared.
379+ archive.publish = False
380+ incoming_dir = (
381+ Path(self.oval_data_root)
382+ / archive.reference
383+ / "nonexistent"
384+ / "main"
385+ )
386+ write_file(str(incoming_dir / "foo.oval.xml.bz2"), b"test")
387+ self.runPublishDistro(
388+ extra_args=["--ppa"], distribution=archive.distribution.name
389+ )
390+ self.assertIn(
391+ "INFO Skipping OVAL data for '%s:nonexistent' (no such suite)."
392+ % archive.reference,
393+ self.logger.getLogBuffer(),
394+ )
395+ self.assertIsNone(archive.dirty_suites)
396+
397 @defer.inlineCallbacks
398 def testForPPA(self):
399 """Try to run publish-distro in PPA mode.

Subscribers

People subscribed via source and target branches

to status/vote changes: