← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/queue-api-overrides into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/queue-api-overrides/+merge/113437

== 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.
-- 
https://code.launchpad.net/~cjwatson/launchpad/queue-api-overrides/+merge/113437
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/queue-api-overrides into lp:launchpad.
=== modified file 'lib/lp/soyuz/doc/distroseriesqueue.txt'
--- lib/lp/soyuz/doc/distroseriesqueue.txt	2012-07-03 09:29:35 +0000
+++ lib/lp/soyuz/doc/distroseriesqueue.txt	2012-07-04 17:21:19 +0000
@@ -648,7 +648,7 @@
 In addition to these parameters, you must also supply
 "allowed_components", which is a sequence of IComponent.  Any overrides
 must have the existing and new component in this sequence otherwise
-QueueInconsistentStateError is raised.
+QueueAdminUnauthorizedError is raised.
 
 The alsa-utils source is already in the queue with component "main"
 and section "base".
@@ -673,7 +673,7 @@
     ...     allowed_components=(universe,))
     Traceback (most recent call last):
     ...
-    QueueInconsistentStateError: No rights to override to restricted
+    QueueAdminUnauthorizedError: No rights to override to restricted
 
 Allowing "restricted" still won't work because the original component
 is "main":
@@ -683,7 +683,7 @@
     ...     allowed_components=(restricted,))
     Traceback (most recent call last):
     ...
-    QueueInconsistentStateError: No rights to override from main
+    QueueAdminUnauthorizedError: No rights to override from main
 
 Specifying both main and restricted allows the override to restricted/web.
 overrideSource() returns True if it completed the task.
@@ -719,13 +719,13 @@
     ...     binary_changes, allowed_components=(universe,))
     Traceback (most recent call last):
     ...
-    QueueInconsistentStateError: No rights to override to restricted
+    QueueAdminUnauthorizedError: No rights to override to restricted
 
     >>> print item.overrideBinaries(
     ...     binary_changes, allowed_components=(restricted,))
     Traceback (most recent call last):
     ...
-    QueueInconsistentStateError: No rights to override from main
+    QueueAdminUnauthorizedError: No rights to override from main
 
     >>> print item.overrideBinaries(
     ...     binary_changes, allowed_components=(main,restricted))

=== modified file 'lib/lp/soyuz/interfaces/queue.py'
--- lib/lp/soyuz/interfaces/queue.py	2012-07-04 13:02:32 +0000
+++ lib/lp/soyuz/interfaces/queue.py	2012-07-04 17:21:19 +0000
@@ -16,6 +16,7 @@
     'IPackageUploadCustom',
     'IPackageUploadSet',
     'NonBuildableSourceUploadError',
+    'QueueAdminUnauthorizedError',
     'QueueBuildAcceptError',
     'QueueInconsistentStateError',
     'QueueSourceAcceptError',
@@ -26,12 +27,15 @@
 
 from lazr.enum import DBEnumeratedType
 from lazr.restful.declarations import (
+    call_with,
     error_status,
     export_as_webservice_entry,
     export_read_operation,
     export_write_operation,
     exported,
     operation_for_version,
+    operation_parameters,
+    REQUEST_USER,
     )
 from lazr.restful.fields import Reference
 from zope.interface import (
@@ -42,10 +46,12 @@
     Bool,
     Choice,
     Datetime,
+    Dict,
     Int,
     List,
     TextLine,
     )
+from zope.security.interfaces import Unauthorized
 
 from lp import _
 from lp.soyuz.enums import PackageUploadStatus
@@ -69,6 +75,10 @@
     """
 
 
+class QueueAdminUnauthorizedError(Unauthorized):
+    """User not permitted to perform a queue administration operation."""
+
+
 class NonBuildableSourceUploadError(QueueInconsistentStateError):
     """Source upload will not result in any build record.
 
@@ -394,7 +404,14 @@
         :param logger: Specify a logger object if required.  Mainly for tests.
         """
 
-    def overrideSource(new_component, new_section, allowed_components):
+    @operation_parameters(
+        new_component=TextLine(title=u"The new component name."),
+        new_section=TextLine(title=u"The new section name."))
+    @call_with(allowed_components=None, user=REQUEST_USER)
+    @export_write_operation()
+    @operation_for_version('devel')
+    def overrideSource(new_component=None, new_section=None,
+                       allowed_components=None, user=None):
         """Override the source package contained in this queue item.
 
         :param new_component: An IComponent to replace the existing one
@@ -403,6 +420,8 @@
             in the upload's source.
         :param allowed_components: A sequence of components that the
             callsite is allowed to override from and to.
+        :param user: The user requesting the override change, used if
+            allowed_components is None.
 
         :raises QueueInconsistentStateError: if either the existing
             or the new_component are not in the allowed_components
@@ -414,7 +433,21 @@
         :return: True if the source was overridden.
         """
 
-    def overrideBinaries(changes, allowed_components):
+    @operation_parameters(
+        changes=List(
+            title=u"A sequence of changes to apply.",
+            description=(
+                u"Each item may have a 'name' item which specifies the binary "
+                "package name to override; otherwise, the change applies to "
+                "all binaries in the upload. It may also have 'component', "
+                "'section', and 'priority' items which replace the "
+                "corresponding existing one in the upload's overridden "
+                "binaries."),
+            value_type=Dict(key_type=TextLine())))
+    @call_with(allowed_components=None, user=REQUEST_USER)
+    @export_write_operation()
+    @operation_for_version('devel')
+    def overrideBinaries(changes, allowed_components=None, user=None):
         """Override binary packages in a binary queue item.
 
         :param changes: A sequence of mappings of changes to apply. Each
@@ -426,6 +459,8 @@
             left unchanged.
         :param allowed_components: A sequence of components that the
             callsite is allowed to override from and to.
+        :param user: The user requesting the override change, used if
+            allowed_components is None.
 
         :raises QueueInconsistentStateError: if either the existing
             or the new_component are not in the allowed_components

=== modified file 'lib/lp/soyuz/model/queue.py'
--- lib/lp/soyuz/model/queue.py	2012-07-04 13:02:32 +0000
+++ lib/lp/soyuz/model/queue.py	2012-07-04 17:21:19 +0000
@@ -85,6 +85,7 @@
     PriorityNotFound,
     SectionNotFound,
     )
+from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
 from lp.soyuz.interfaces.component import IComponentSet
 from lp.soyuz.interfaces.packagecopyjob import IPackageCopyJobSource
 from lp.soyuz.interfaces.publishing import (
@@ -100,6 +101,7 @@
     IPackageUploadSet,
     IPackageUploadSource,
     NonBuildableSourceUploadError,
+    QueueAdminUnauthorizedError,
     QueueBuildAcceptError,
     QueueInconsistentStateError,
     QueueSourceAcceptError,
@@ -1028,7 +1030,7 @@
         allowed_component_names = [
             component.name for component in allowed_components]
         if copy_job.component_name not in allowed_component_names:
-            raise QueueInconsistentStateError(
+            raise QueueAdminUnauthorizedError(
                 "No rights to override from %s" % copy_job.component_name)
         copy_job.addSourceOverride(SourceOverride(
             copy_job.package_name, new_component, new_section))
@@ -1045,7 +1047,7 @@
             if old_component not in allowed_components:
                 # The old component is not in the list of allowed components
                 # to override.
-                raise QueueInconsistentStateError(
+                raise QueueAdminUnauthorizedError(
                     "No rights to override from %s" % old_component.name)
             source.sourcepackagerelease.override(
                 component=new_component, section=new_section)
@@ -1058,7 +1060,8 @@
 
         return made_changes
 
-    def overrideSource(self, new_component, new_section, allowed_components):
+    def overrideSource(self, new_component=None, new_section=None,
+                       allowed_components=None, user=None):
         """See `IPackageUpload`."""
         if new_component is None and new_section is None:
             # Nothing needs overriding, bail out.
@@ -1067,8 +1070,19 @@
         new_component = self._nameToComponent(new_component)
         new_section = self._nameToSection(new_section)
 
+        if allowed_components is None and user is not None:
+            # Get a list of components for which the user has rights to
+            # override to or from.
+            permission_set = getUtility(IArchivePermissionSet)
+            permissions = permission_set.componentsForQueueAdmin(
+                self.distroseries.main_archive, user)
+            allowed_components = set(
+                permission.component for permission in permissions)
+        assert allowed_components is not None, (
+            "Must provide allowed_components for non-webservice calls.")
+
         if new_component not in list(allowed_components) + [None]:
-            raise QueueInconsistentStateError(
+            raise QueueAdminUnauthorizedError(
                 "No rights to override to %s" % new_component.name)
 
         return (
@@ -1103,7 +1117,7 @@
 
         return changes_by_name, changes_for_all
 
-    def overrideBinaries(self, changes, allowed_components):
+    def overrideBinaries(self, changes, allowed_components=None, user=None):
         """See `IPackageUpload`."""
         if not self.contains_build:
             return False
@@ -1112,6 +1126,17 @@
             # Nothing needs overriding, bail out.
             return False
 
+        if allowed_components is None and user is not None:
+            # Get a list of components for which the user has rights to
+            # override to or from.
+            permission_set = getUtility(IArchivePermissionSet)
+            permissions = permission_set.componentsForQueueAdmin(
+                self.distroseries.main_archive, user)
+            allowed_components = set(
+                permission.component for permission in permissions)
+        assert allowed_components is not None, (
+            "Must provide allowed_components for non-webservice calls.")
+
         changes_by_name, changes_for_all = self._filterBinaryChanges(changes)
 
         new_components = set()
@@ -1125,12 +1150,23 @@
             component.name
             for component in new_components.difference(allowed_components))
         if disallowed_components:
-            raise QueueInconsistentStateError(
+            raise QueueAdminUnauthorizedError(
                 "No rights to override to %s" %
                 ", ".join(disallowed_components))
 
         made_changes = False
         for build in self.builds:
+            # See if the new component requires a new archive on the build.
+            for component in new_components:
+                distroarchseries = build.build.distro_arch_series
+                distribution = distroarchseries.distroseries.distribution
+                new_archive = distribution.getArchiveByComponent(
+                    component.name)
+                if new_archive != build.build.archive:
+                    raise QueueInconsistentStateError(
+                        "Overriding component to '%s' failed because it "
+                        "would require a new archive." % component.name)
+
             for binarypackage in build.build.binarypackages:
                 change = changes_by_name.get(
                     binarypackage.name, changes_for_all)
@@ -1138,7 +1174,7 @@
                     if binarypackage.component not in allowed_components:
                         # The old component is not in the list of allowed
                         # components to override.
-                        raise QueueInconsistentStateError(
+                        raise QueueAdminUnauthorizedError(
                             "No rights to override from %s" %
                             binarypackage.component.name)
                     binarypackage.override(**change)

=== modified file 'lib/lp/soyuz/tests/test_packageupload.py'
--- lib/lp/soyuz/tests/test_packageupload.py	2012-07-04 13:02:32 +0000
+++ lib/lp/soyuz/tests/test_packageupload.py	2012-07-04 17:21:19 +0000
@@ -42,6 +42,7 @@
 from lp.soyuz.interfaces.queue import (
     IPackageUpload,
     IPackageUploadSet,
+    QueueAdminUnauthorizedError,
     QueueInconsistentStateError,
     )
 from lp.soyuz.interfaces.section import ISectionSet
@@ -444,8 +445,7 @@
         only_allowed_component = self.factory.makeComponent()
         section = self.factory.makeSection()
         self.assertRaises(
-            QueueInconsistentStateError,
-            pu.overrideSource,
+            QueueAdminUnauthorizedError, pu.overrideSource,
             only_allowed_component, section, [only_allowed_component])
 
     def test_overrideSource_checks_permission_for_new_component(self):
@@ -454,8 +454,7 @@
         disallowed_component = self.factory.makeComponent()
         section = self.factory.makeSection()
         self.assertRaises(
-            QueueInconsistentStateError,
-            pu.overrideSource,
+            QueueAdminUnauthorizedError, pu.overrideSource,
             disallowed_component, section, [current_component])
 
     def test_overrideSource_ignores_None_component_change(self):
@@ -971,6 +970,9 @@
     def test_edit_permissions(self):
         self.assertRequiresEdit("acceptFromQueue")
         self.assertRequiresEdit("rejectFromQueue")
+        self.assertRequiresEdit("overrideSource", new_component="main")
+        self.assertRequiresEdit(
+            "overrideBinaries", changes=[{"component": "main"}])
 
     def test_acceptFromQueue_archive_admin(self):
         # acceptFromQueue as an archive admin accepts the upload.
@@ -1034,6 +1036,45 @@
                 for file in upload.sourcepackagerelease.files]
         self.assertContentEqual(source_file_urls, ws_source_file_urls)
 
+    def test_overrideSource_limited_component_permissions(self):
+        # Overriding between two components requires queue admin of both.
+        person = self.makeQueueAdmin([self.universe])
+        upload, ws_upload = self.makeSourcePackageUpload(
+            person, component=self.universe)
+
+        self.assertEqual("New", ws_upload.status)
+        self.assertEqual("universe", ws_upload.component_name)
+        self.assertRaises(Unauthorized, ws_upload.overrideSource,
+                          new_component="main")
+
+        with admin_logged_in():
+            upload.overrideSource(
+                new_component=self.main,
+                allowed_components=[self.main, self.universe])
+        transaction.commit()
+        self.assertEqual("main", upload.component_name)
+        self.assertRaises(Unauthorized, ws_upload.overrideSource,
+                          new_component="universe")
+
+    def test_overrideSource_changes_properties(self):
+        # Running overrideSource changes the corresponding properties.
+        person = self.makeQueueAdmin([self.main, self.universe])
+        upload, ws_upload = self.makeSourcePackageUpload(
+            person, component=self.universe)
+        with person_logged_in(person):
+            new_section = self.factory.makeSection()
+        transaction.commit()
+
+        self.assertEqual("New", ws_upload.status)
+        self.assertEqual("universe", ws_upload.component_name)
+        self.assertNotEqual(new_section.name, ws_upload.section_name)
+        ws_upload.overrideSource(
+            new_component="main", new_section=new_section.name)
+        self.assertEqual("main", ws_upload.component_name)
+        self.assertEqual(new_section.name, ws_upload.section_name)
+        ws_upload.overrideSource(new_component="universe")
+        self.assertEqual("universe", ws_upload.component_name)
+
     def test_binary_info(self):
         # API clients can inspect properties of binary uploads.
         person = self.makeQueueAdmin([self.universe])
@@ -1077,6 +1118,112 @@
                 for file in bpr.files]
         self.assertContentEqual(binary_file_urls, ws_binary_file_urls)
 
+    def test_overrideBinaries_limited_component_permissions(self):
+        # Overriding between two components requires queue admin of both.
+        person = self.makeQueueAdmin([self.universe])
+        upload, ws_upload = self.makeBinaryPackageUpload(
+            person, binarypackagename="hello", component=self.universe)
+
+        self.assertEqual("New", ws_upload.status)
+        self.assertEqual(
+            set(["universe"]),
+            set(binary["component"]
+                for binary in ws_upload.getBinaryProperties()))
+        self.assertRaises(
+            Unauthorized, ws_upload.overrideBinaries,
+            changes=[{"component": "main"}])
+
+        with admin_logged_in():
+            upload.overrideBinaries(
+                [{"component": self.main}],
+                allowed_components=[self.main, self.universe])
+        transaction.commit()
+
+        self.assertEqual(
+            set(["main"]),
+            set(binary["component"]
+                for binary in ws_upload.getBinaryProperties()))
+        self.assertRaises(
+            Unauthorized, ws_upload.overrideBinaries,
+            changes=[{"component": "universe"}])
+
+    def test_overrideBinaries_disallows_new_archive(self):
+        # overrideBinaries refuses to override the component to something
+        # that requires a different archive.
+        partner = self.factory.makeComponent("partner")
+        self.factory.makeComponentSelection(
+            distroseries=self.distroseries, component=partner)
+        person = self.makeQueueAdmin([self.universe, partner])
+        upload, ws_upload = self.makeBinaryPackageUpload(
+            person, component=self.universe)
+
+        self.assertEqual(
+            "universe", ws_upload.getBinaryProperties()[0]["component"])
+        self.assertRaises(
+            BadRequest, ws_upload.overrideBinaries,
+            changes=[{"component": "partner"}])
+
+    def test_overrideBinaries_without_name_changes_all_properties(self):
+        # Running overrideBinaries with a change entry containing no "name"
+        # field changes the corresponding properties of all binaries.
+        person = self.makeQueueAdmin([self.main, self.universe])
+        upload, ws_upload = self.makeBinaryPackageUpload(
+            person, component=self.universe)
+        with person_logged_in(person):
+            new_section = self.factory.makeSection()
+        transaction.commit()
+
+        self.assertEqual("New", ws_upload.status)
+        for binary in ws_upload.getBinaryProperties():
+            self.assertEqual("universe", binary["component"])
+            self.assertNotEqual(new_section.name, binary["section"])
+            self.assertEqual("OPTIONAL", binary["priority"])
+        changes = [{
+            "component": "main",
+            "section": new_section.name,
+            "priority": "extra",
+            }]
+        ws_upload.overrideBinaries(changes=changes)
+        for binary in ws_upload.getBinaryProperties():
+            self.assertEqual("main", binary["component"])
+            self.assertEqual(new_section.name, binary["section"])
+            self.assertEqual("EXTRA", binary["priority"])
+
+    def test_overrideBinaries_with_name_changes_selected_properties(self):
+        # Running overrideBinaries with change entries containing "name"
+        # fields changes the corresponding properties of only the selected
+        # binaries.
+        person = self.makeQueueAdmin([self.main, self.universe])
+        upload, ws_upload = self.makeBinaryPackageUpload(
+            person, component=self.universe)
+        with person_logged_in(person):
+            new_section = self.factory.makeSection()
+        transaction.commit()
+
+        self.assertEqual("New", ws_upload.status)
+        ws_binaries = ws_upload.getBinaryProperties()
+        for binary in ws_binaries:
+            self.assertEqual("universe", binary["component"])
+            self.assertNotEqual(new_section.name, binary["section"])
+            self.assertEqual("OPTIONAL", binary["priority"])
+        change_one = {
+            "name": ws_binaries[0]["name"],
+            "component": "main",
+            "priority": "standard",
+            }
+        change_two = {
+            "name": ws_binaries[1]["name"],
+            "section": new_section.name,
+            }
+        ws_upload.overrideBinaries(changes=[change_one, change_two])
+        ws_binaries = ws_upload.getBinaryProperties()
+        self.assertEqual("main", ws_binaries[0]["component"])
+        self.assertNotEqual(new_section.name, ws_binaries[0]["section"])
+        self.assertEqual("STANDARD", ws_binaries[0]["priority"])
+        self.assertEqual("universe", ws_binaries[1]["component"])
+        self.assertEqual(new_section.name, ws_binaries[1]["section"])
+        self.assertEqual("OPTIONAL", ws_binaries[1]["priority"])
+
     def test_custom_info(self):
         # API clients can inspect properties of custom uploads.
         person = self.makeQueueAdmin([self.universe])


Follow ups