Merge lp:~cjwatson/launchpad/queue-api-overrides into lp:launchpad

Proposed by Colin Watson
Status: Merged
Approved by: j.c.sackett
Approved revision: no longer in the source branch.
Merged at revision: 15575
Proposed branch: lp:~cjwatson/launchpad/queue-api-overrides
Merge into: lp:launchpad
Diff against target: 518 lines (+241/-20)
5 files modified
lib/lp/soyuz/browser/queue.py (+5/-2)
lib/lp/soyuz/doc/distroseriesqueue.txt (+5/-5)
lib/lp/soyuz/interfaces/queue.py (+37/-2)
lib/lp/soyuz/model/queue.py (+43/-7)
lib/lp/soyuz/tests/test_packageupload.py (+151/-4)
To merge this branch: bzr merge lp:~cjwatson/launchpad/queue-api-overrides
Reviewer Review Type Date Requested Status
j.c.sackett (community) Approve
Review via email: mp+113437@code.launchpad.net

Commit message

Export PackageUpload.overrideSource and PackageUpload.overrideBinaries.

Description of the change

== Summary ==

Replace the queue tool in Launchpad with an API client (part five).

== Proposed fix ==

This branch exports the override methods on PackageUpload. With this in addition to what's already landed, the client should be feature-complete, and the remaining work should consist of cleanups.

== Pre-implementation notes ==

I've gone round a few times with various people, particularly William Grant, on the exact way to export all of this stuff, because I gather that we want to avoid exposing the current data model in order that it can be rearranged in the future. This has led to the following design choices:

 * Everything is on devel. The only clients for this should be tools such as those in lp:ubuntu-archive-tools, which can be kept up to date if there's a need to change these interfaces.
 * There are source packages with lots of binaries that sometimes need to be overridden individually (e.g. linux) and API requests aren't especially fast. In a previous branch (currently QAed and awaiting deployment) I amended Archive.overrideBinaries to take a similar list of dicts as a "changes" parameter, allowing many override changes to be made in a single request; this exports that and improves test coverage.

I extracted this branch from https://code.launchpad.net/~cjwatson/launchpad/queue-api/+merge/108967 at Benji's request.

== Implementation details ==

The only thing I think is notable here is that I copied the new-component-requires-new-archive check from lib/lp/soyuz/scripts/queue.py into PackageUpload.overrideBinaries. This should be acceptable temporary duplication since: (a) it really belongs in the model anyway; (b) lp.soyuz.scripts.queue calls BPR.override directly rather than going through PackageUpload.overrideBinaries, so the check doesn't happen twice at run-time; (c) I will be removing lp.soyuz.scripts.queue soon so there's no point bothering to refactor it.

== LOC Rationale ==

+218. As with https://code.launchpad.net/~cjwatson/launchpad/queue-api/+merge/108967, this arc will be LoC-negative.

== Tests ==

bin/test -vvct test_packageupload

== Demo and Q/A ==

http://paste.ubuntu.com/1072964/ is the current version of my client. With this branch, it should be possible to override sources and (individual) binaries in queue items.

To post a comment you must log in.
Revision history for this message
j.c.sackett (jcsackett) wrote :

This looks good, thanks for breaking it out into smaller branches.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/soyuz/browser/queue.py'
2--- lib/lp/soyuz/browser/queue.py 2012-07-05 09:49:55 +0000
3+++ lib/lp/soyuz/browser/queue.py 2012-07-06 16:20:24 +0000
4@@ -45,6 +45,7 @@
5 from lp.soyuz.interfaces.queue import (
6 IPackageUpload,
7 IPackageUploadSet,
8+ QueueAdminUnauthorizedError,
9 QueueInconsistentStateError,
10 )
11 from lp.soyuz.interfaces.section import ISectionSet
12@@ -388,7 +389,8 @@
13 }]
14 binary_overridden = queue_item.overrideBinaries(
15 binary_changes, allowed_components)
16- except QueueInconsistentStateError as info:
17+ except (QueueAdminUnauthorizedError,
18+ QueueInconsistentStateError) as info:
19 failure.append("FAILED: %s (%s)" %
20 (queue_item.displayname, info))
21 continue
22@@ -409,7 +411,8 @@
23
24 try:
25 getattr(self, 'queue_action_' + action)(queue_item)
26- except QueueInconsistentStateError as info:
27+ except (QueueAdminUnauthorizedError,
28+ QueueInconsistentStateError) as info:
29 failure.append('FAILED: %s (%s)' %
30 (queue_item.displayname, info))
31 else:
32
33=== modified file 'lib/lp/soyuz/doc/distroseriesqueue.txt'
34--- lib/lp/soyuz/doc/distroseriesqueue.txt 2012-07-03 09:29:35 +0000
35+++ lib/lp/soyuz/doc/distroseriesqueue.txt 2012-07-06 16:20:24 +0000
36@@ -648,7 +648,7 @@
37 In addition to these parameters, you must also supply
38 "allowed_components", which is a sequence of IComponent. Any overrides
39 must have the existing and new component in this sequence otherwise
40-QueueInconsistentStateError is raised.
41+QueueAdminUnauthorizedError is raised.
42
43 The alsa-utils source is already in the queue with component "main"
44 and section "base".
45@@ -673,7 +673,7 @@
46 ... allowed_components=(universe,))
47 Traceback (most recent call last):
48 ...
49- QueueInconsistentStateError: No rights to override to restricted
50+ QueueAdminUnauthorizedError: No rights to override to restricted
51
52 Allowing "restricted" still won't work because the original component
53 is "main":
54@@ -683,7 +683,7 @@
55 ... allowed_components=(restricted,))
56 Traceback (most recent call last):
57 ...
58- QueueInconsistentStateError: No rights to override from main
59+ QueueAdminUnauthorizedError: No rights to override from main
60
61 Specifying both main and restricted allows the override to restricted/web.
62 overrideSource() returns True if it completed the task.
63@@ -719,13 +719,13 @@
64 ... binary_changes, allowed_components=(universe,))
65 Traceback (most recent call last):
66 ...
67- QueueInconsistentStateError: No rights to override to restricted
68+ QueueAdminUnauthorizedError: No rights to override to restricted
69
70 >>> print item.overrideBinaries(
71 ... binary_changes, allowed_components=(restricted,))
72 Traceback (most recent call last):
73 ...
74- QueueInconsistentStateError: No rights to override from main
75+ QueueAdminUnauthorizedError: No rights to override from main
76
77 >>> print item.overrideBinaries(
78 ... binary_changes, allowed_components=(main,restricted))
79
80=== modified file 'lib/lp/soyuz/interfaces/queue.py'
81--- lib/lp/soyuz/interfaces/queue.py 2012-07-04 13:02:32 +0000
82+++ lib/lp/soyuz/interfaces/queue.py 2012-07-06 16:20:24 +0000
83@@ -16,6 +16,7 @@
84 'IPackageUploadCustom',
85 'IPackageUploadSet',
86 'NonBuildableSourceUploadError',
87+ 'QueueAdminUnauthorizedError',
88 'QueueBuildAcceptError',
89 'QueueInconsistentStateError',
90 'QueueSourceAcceptError',
91@@ -26,12 +27,15 @@
92
93 from lazr.enum import DBEnumeratedType
94 from lazr.restful.declarations import (
95+ call_with,
96 error_status,
97 export_as_webservice_entry,
98 export_read_operation,
99 export_write_operation,
100 exported,
101 operation_for_version,
102+ operation_parameters,
103+ REQUEST_USER,
104 )
105 from lazr.restful.fields import Reference
106 from zope.interface import (
107@@ -42,10 +46,12 @@
108 Bool,
109 Choice,
110 Datetime,
111+ Dict,
112 Int,
113 List,
114 TextLine,
115 )
116+from zope.security.interfaces import Unauthorized
117
118 from lp import _
119 from lp.soyuz.enums import PackageUploadStatus
120@@ -69,6 +75,10 @@
121 """
122
123
124+class QueueAdminUnauthorizedError(Unauthorized):
125+ """User not permitted to perform a queue administration operation."""
126+
127+
128 class NonBuildableSourceUploadError(QueueInconsistentStateError):
129 """Source upload will not result in any build record.
130
131@@ -394,7 +404,14 @@
132 :param logger: Specify a logger object if required. Mainly for tests.
133 """
134
135- def overrideSource(new_component, new_section, allowed_components):
136+ @operation_parameters(
137+ new_component=TextLine(title=u"The new component name."),
138+ new_section=TextLine(title=u"The new section name."))
139+ @call_with(allowed_components=None, user=REQUEST_USER)
140+ @export_write_operation()
141+ @operation_for_version('devel')
142+ def overrideSource(new_component=None, new_section=None,
143+ allowed_components=None, user=None):
144 """Override the source package contained in this queue item.
145
146 :param new_component: An IComponent to replace the existing one
147@@ -403,6 +420,8 @@
148 in the upload's source.
149 :param allowed_components: A sequence of components that the
150 callsite is allowed to override from and to.
151+ :param user: The user requesting the override change, used if
152+ allowed_components is None.
153
154 :raises QueueInconsistentStateError: if either the existing
155 or the new_component are not in the allowed_components
156@@ -414,7 +433,21 @@
157 :return: True if the source was overridden.
158 """
159
160- def overrideBinaries(changes, allowed_components):
161+ @operation_parameters(
162+ changes=List(
163+ title=u"A sequence of changes to apply.",
164+ description=(
165+ u"Each item may have a 'name' item which specifies the binary "
166+ "package name to override; otherwise, the change applies to "
167+ "all binaries in the upload. It may also have 'component', "
168+ "'section', and 'priority' items which replace the "
169+ "corresponding existing one in the upload's overridden "
170+ "binaries."),
171+ value_type=Dict(key_type=TextLine())))
172+ @call_with(allowed_components=None, user=REQUEST_USER)
173+ @export_write_operation()
174+ @operation_for_version('devel')
175+ def overrideBinaries(changes, allowed_components=None, user=None):
176 """Override binary packages in a binary queue item.
177
178 :param changes: A sequence of mappings of changes to apply. Each
179@@ -426,6 +459,8 @@
180 left unchanged.
181 :param allowed_components: A sequence of components that the
182 callsite is allowed to override from and to.
183+ :param user: The user requesting the override change, used if
184+ allowed_components is None.
185
186 :raises QueueInconsistentStateError: if either the existing
187 or the new_component are not in the allowed_components
188
189=== modified file 'lib/lp/soyuz/model/queue.py'
190--- lib/lp/soyuz/model/queue.py 2012-07-06 13:51:34 +0000
191+++ lib/lp/soyuz/model/queue.py 2012-07-06 16:20:24 +0000
192@@ -84,6 +84,7 @@
193 PriorityNotFound,
194 SectionNotFound,
195 )
196+from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
197 from lp.soyuz.interfaces.component import IComponentSet
198 from lp.soyuz.interfaces.packagecopyjob import IPackageCopyJobSource
199 from lp.soyuz.interfaces.publishing import (
200@@ -99,6 +100,7 @@
201 IPackageUploadSet,
202 IPackageUploadSource,
203 NonBuildableSourceUploadError,
204+ QueueAdminUnauthorizedError,
205 QueueBuildAcceptError,
206 QueueInconsistentStateError,
207 QueueSourceAcceptError,
208@@ -1038,7 +1040,7 @@
209 allowed_component_names = [
210 component.name for component in allowed_components]
211 if copy_job.component_name not in allowed_component_names:
212- raise QueueInconsistentStateError(
213+ raise QueueAdminUnauthorizedError(
214 "No rights to override from %s" % copy_job.component_name)
215 copy_job.addSourceOverride(SourceOverride(
216 copy_job.package_name, new_component, new_section))
217@@ -1055,7 +1057,7 @@
218 if old_component not in allowed_components:
219 # The old component is not in the list of allowed components
220 # to override.
221- raise QueueInconsistentStateError(
222+ raise QueueAdminUnauthorizedError(
223 "No rights to override from %s" % old_component.name)
224 source.sourcepackagerelease.override(
225 component=new_component, section=new_section)
226@@ -1068,7 +1070,8 @@
227
228 return made_changes
229
230- def overrideSource(self, new_component, new_section, allowed_components):
231+ def overrideSource(self, new_component=None, new_section=None,
232+ allowed_components=None, user=None):
233 """See `IPackageUpload`."""
234 if new_component is None and new_section is None:
235 # Nothing needs overriding, bail out.
236@@ -1077,8 +1080,19 @@
237 new_component = self._nameToComponent(new_component)
238 new_section = self._nameToSection(new_section)
239
240+ if allowed_components is None and user is not None:
241+ # Get a list of components for which the user has rights to
242+ # override to or from.
243+ permission_set = getUtility(IArchivePermissionSet)
244+ permissions = permission_set.componentsForQueueAdmin(
245+ self.distroseries.main_archive, user)
246+ allowed_components = set(
247+ permission.component for permission in permissions)
248+ assert allowed_components is not None, (
249+ "Must provide allowed_components for non-webservice calls.")
250+
251 if new_component not in list(allowed_components) + [None]:
252- raise QueueInconsistentStateError(
253+ raise QueueAdminUnauthorizedError(
254 "No rights to override to %s" % new_component.name)
255
256 return (
257@@ -1113,7 +1127,7 @@
258
259 return changes_by_name, changes_for_all
260
261- def overrideBinaries(self, changes, allowed_components):
262+ def overrideBinaries(self, changes, allowed_components=None, user=None):
263 """See `IPackageUpload`."""
264 if not self.contains_build:
265 return False
266@@ -1122,6 +1136,17 @@
267 # Nothing needs overriding, bail out.
268 return False
269
270+ if allowed_components is None and user is not None:
271+ # Get a list of components for which the user has rights to
272+ # override to or from.
273+ permission_set = getUtility(IArchivePermissionSet)
274+ permissions = permission_set.componentsForQueueAdmin(
275+ self.distroseries.main_archive, user)
276+ allowed_components = set(
277+ permission.component for permission in permissions)
278+ assert allowed_components is not None, (
279+ "Must provide allowed_components for non-webservice calls.")
280+
281 changes_by_name, changes_for_all = self._filterBinaryChanges(changes)
282
283 new_components = set()
284@@ -1135,12 +1160,23 @@
285 component.name
286 for component in new_components.difference(allowed_components))
287 if disallowed_components:
288- raise QueueInconsistentStateError(
289+ raise QueueAdminUnauthorizedError(
290 "No rights to override to %s" %
291 ", ".join(disallowed_components))
292
293 made_changes = False
294 for build in self.builds:
295+ # See if the new component requires a new archive on the build.
296+ for component in new_components:
297+ distroarchseries = build.build.distro_arch_series
298+ distribution = distroarchseries.distroseries.distribution
299+ new_archive = distribution.getArchiveByComponent(
300+ component.name)
301+ if new_archive != build.build.archive:
302+ raise QueueInconsistentStateError(
303+ "Overriding component to '%s' failed because it "
304+ "would require a new archive." % component.name)
305+
306 for binarypackage in build.build.binarypackages:
307 change = changes_by_name.get(
308 binarypackage.name, changes_for_all)
309@@ -1148,7 +1184,7 @@
310 if binarypackage.component not in allowed_components:
311 # The old component is not in the list of allowed
312 # components to override.
313- raise QueueInconsistentStateError(
314+ raise QueueAdminUnauthorizedError(
315 "No rights to override from %s" %
316 binarypackage.component.name)
317 binarypackage.override(**change)
318
319=== modified file 'lib/lp/soyuz/tests/test_packageupload.py'
320--- lib/lp/soyuz/tests/test_packageupload.py 2012-07-05 13:01:48 +0000
321+++ lib/lp/soyuz/tests/test_packageupload.py 2012-07-06 16:20:24 +0000
322@@ -42,6 +42,7 @@
323 from lp.soyuz.interfaces.queue import (
324 IPackageUpload,
325 IPackageUploadSet,
326+ QueueAdminUnauthorizedError,
327 QueueInconsistentStateError,
328 )
329 from lp.soyuz.interfaces.section import ISectionSet
330@@ -444,8 +445,7 @@
331 only_allowed_component = self.factory.makeComponent()
332 section = self.factory.makeSection()
333 self.assertRaises(
334- QueueInconsistentStateError,
335- pu.overrideSource,
336+ QueueAdminUnauthorizedError, pu.overrideSource,
337 only_allowed_component, section, [only_allowed_component])
338
339 def test_overrideSource_checks_permission_for_new_component(self):
340@@ -454,8 +454,7 @@
341 disallowed_component = self.factory.makeComponent()
342 section = self.factory.makeSection()
343 self.assertRaises(
344- QueueInconsistentStateError,
345- pu.overrideSource,
346+ QueueAdminUnauthorizedError, pu.overrideSource,
347 disallowed_component, section, [current_component])
348
349 def test_overrideSource_ignores_None_component_change(self):
350@@ -971,6 +970,9 @@
351 def test_edit_permissions(self):
352 self.assertRequiresEdit("acceptFromQueue")
353 self.assertRequiresEdit("rejectFromQueue")
354+ self.assertRequiresEdit("overrideSource", new_component="main")
355+ self.assertRequiresEdit(
356+ "overrideBinaries", changes=[{"component": "main"}])
357
358 def test_acceptFromQueue_archive_admin(self):
359 # acceptFromQueue as an archive admin accepts the upload.
360@@ -1034,6 +1036,45 @@
361 for file in upload.sourcepackagerelease.files]
362 self.assertContentEqual(source_file_urls, ws_source_file_urls)
363
364+ def test_overrideSource_limited_component_permissions(self):
365+ # Overriding between two components requires queue admin of both.
366+ person = self.makeQueueAdmin([self.universe])
367+ upload, ws_upload = self.makeSourcePackageUpload(
368+ person, component=self.universe)
369+
370+ self.assertEqual("New", ws_upload.status)
371+ self.assertEqual("universe", ws_upload.component_name)
372+ self.assertRaises(Unauthorized, ws_upload.overrideSource,
373+ new_component="main")
374+
375+ with admin_logged_in():
376+ upload.overrideSource(
377+ new_component=self.main,
378+ allowed_components=[self.main, self.universe])
379+ transaction.commit()
380+ self.assertEqual("main", upload.component_name)
381+ self.assertRaises(Unauthorized, ws_upload.overrideSource,
382+ new_component="universe")
383+
384+ def test_overrideSource_changes_properties(self):
385+ # Running overrideSource changes the corresponding properties.
386+ person = self.makeQueueAdmin([self.main, self.universe])
387+ upload, ws_upload = self.makeSourcePackageUpload(
388+ person, component=self.universe)
389+ with person_logged_in(person):
390+ new_section = self.factory.makeSection()
391+ transaction.commit()
392+
393+ self.assertEqual("New", ws_upload.status)
394+ self.assertEqual("universe", ws_upload.component_name)
395+ self.assertNotEqual(new_section.name, ws_upload.section_name)
396+ ws_upload.overrideSource(
397+ new_component="main", new_section=new_section.name)
398+ self.assertEqual("main", ws_upload.component_name)
399+ self.assertEqual(new_section.name, ws_upload.section_name)
400+ ws_upload.overrideSource(new_component="universe")
401+ self.assertEqual("universe", ws_upload.component_name)
402+
403 def assertBinaryPropertiesMatch(self, arch, bpr, ws_binary):
404 expected_binary = {
405 "is_new": True,
406@@ -1078,6 +1119,112 @@
407 for file in bpr.files]
408 self.assertContentEqual(binary_file_urls, ws_binary_file_urls)
409
410+ def test_overrideBinaries_limited_component_permissions(self):
411+ # Overriding between two components requires queue admin of both.
412+ person = self.makeQueueAdmin([self.universe])
413+ upload, ws_upload = self.makeBinaryPackageUpload(
414+ person, binarypackagename="hello", component=self.universe)
415+
416+ self.assertEqual("New", ws_upload.status)
417+ self.assertEqual(
418+ set(["universe"]),
419+ set(binary["component"]
420+ for binary in ws_upload.getBinaryProperties()))
421+ self.assertRaises(
422+ Unauthorized, ws_upload.overrideBinaries,
423+ changes=[{"component": "main"}])
424+
425+ with admin_logged_in():
426+ upload.overrideBinaries(
427+ [{"component": self.main}],
428+ allowed_components=[self.main, self.universe])
429+ transaction.commit()
430+
431+ self.assertEqual(
432+ set(["main"]),
433+ set(binary["component"]
434+ for binary in ws_upload.getBinaryProperties()))
435+ self.assertRaises(
436+ Unauthorized, ws_upload.overrideBinaries,
437+ changes=[{"component": "universe"}])
438+
439+ def test_overrideBinaries_disallows_new_archive(self):
440+ # overrideBinaries refuses to override the component to something
441+ # that requires a different archive.
442+ partner = self.factory.makeComponent("partner")
443+ self.factory.makeComponentSelection(
444+ distroseries=self.distroseries, component=partner)
445+ person = self.makeQueueAdmin([self.universe, partner])
446+ upload, ws_upload = self.makeBinaryPackageUpload(
447+ person, component=self.universe)
448+
449+ self.assertEqual(
450+ "universe", ws_upload.getBinaryProperties()[0]["component"])
451+ self.assertRaises(
452+ BadRequest, ws_upload.overrideBinaries,
453+ changes=[{"component": "partner"}])
454+
455+ def test_overrideBinaries_without_name_changes_all_properties(self):
456+ # Running overrideBinaries with a change entry containing no "name"
457+ # field changes the corresponding properties of all binaries.
458+ person = self.makeQueueAdmin([self.main, self.universe])
459+ upload, ws_upload = self.makeBinaryPackageUpload(
460+ person, component=self.universe)
461+ with person_logged_in(person):
462+ new_section = self.factory.makeSection()
463+ transaction.commit()
464+
465+ self.assertEqual("New", ws_upload.status)
466+ for binary in ws_upload.getBinaryProperties():
467+ self.assertEqual("universe", binary["component"])
468+ self.assertNotEqual(new_section.name, binary["section"])
469+ self.assertEqual("OPTIONAL", binary["priority"])
470+ changes = [{
471+ "component": "main",
472+ "section": new_section.name,
473+ "priority": "extra",
474+ }]
475+ ws_upload.overrideBinaries(changes=changes)
476+ for binary in ws_upload.getBinaryProperties():
477+ self.assertEqual("main", binary["component"])
478+ self.assertEqual(new_section.name, binary["section"])
479+ self.assertEqual("EXTRA", binary["priority"])
480+
481+ def test_overrideBinaries_with_name_changes_selected_properties(self):
482+ # Running overrideBinaries with change entries containing "name"
483+ # fields changes the corresponding properties of only the selected
484+ # binaries.
485+ person = self.makeQueueAdmin([self.main, self.universe])
486+ upload, ws_upload = self.makeBinaryPackageUpload(
487+ person, component=self.universe)
488+ with person_logged_in(person):
489+ new_section = self.factory.makeSection()
490+ transaction.commit()
491+
492+ self.assertEqual("New", ws_upload.status)
493+ ws_binaries = ws_upload.getBinaryProperties()
494+ for binary in ws_binaries:
495+ self.assertEqual("universe", binary["component"])
496+ self.assertNotEqual(new_section.name, binary["section"])
497+ self.assertEqual("OPTIONAL", binary["priority"])
498+ change_one = {
499+ "name": ws_binaries[0]["name"],
500+ "component": "main",
501+ "priority": "standard",
502+ }
503+ change_two = {
504+ "name": ws_binaries[1]["name"],
505+ "section": new_section.name,
506+ }
507+ ws_upload.overrideBinaries(changes=[change_one, change_two])
508+ ws_binaries = ws_upload.getBinaryProperties()
509+ self.assertEqual("main", ws_binaries[0]["component"])
510+ self.assertNotEqual(new_section.name, ws_binaries[0]["section"])
511+ self.assertEqual("STANDARD", ws_binaries[0]["priority"])
512+ self.assertEqual("universe", ws_binaries[1]["component"])
513+ self.assertEqual(new_section.name, ws_binaries[1]["section"])
514+ self.assertEqual("OPTIONAL", ws_binaries[1]["priority"])
515+
516 def test_custom_info(self):
517 # API clients can inspect properties of custom uploads.
518 person = self.makeQueueAdmin([self.universe])