Merge ~cjwatson/launchpad:oval-check-changes into launchpad:master
- Git
- lp:~cjwatson/launchpad
- oval-check-changes
- Merge into 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) |
Related bugs: |
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:/
To post a comment you must log in.
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
1 | diff --git a/lib/lp/archivepublisher/scripts/publishdistro.py b/lib/lp/archivepublisher/scripts/publishdistro.py |
2 | index 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( |
188 | diff --git a/lib/lp/archivepublisher/tests/test_publishdistro.py b/lib/lp/archivepublisher/tests/test_publishdistro.py |
189 | index 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. |
LGTM 👍🏼