← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:not-automatic-proposed into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:not-automatic-proposed into launchpad:master.

Commit message:
Add and honour DistroSeries.proposed_not_automatic

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1016776 in Launchpad itself: "Users are offered updates to packages in -proposed"
  https://bugs.launchpad.net/launchpad/+bug/1016776

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/412537

If set, this flag causes the -proposed pocket of the series to have `NotAutomatic: yes` and `ButAutomaticUpgrades: yes` written to its `Release` file, causing apt to not install versions from that suite without explicit user consent, but to apply upgrades if the user has already installed a version higher than that available from automatic suites.

See "man apt_preferences" and https://wiki.debian.org/DebianRepository/Format for detailed rules.  They're quite confusing, but this is the closest available approximation of "only upgrade a package from -proposed if you already installed an earlier version from -proposed".

This is analogous to, and largely copied from, the existing `DistroSeries.backports_not_automatic` flag.

This and https://code.launchpad.net/~cjwatson/launchpad-buildd/+git/launchpad-buildd/+merge/412535 can be landed in either order, but we must be careful not to actually enable the new flag anywhere on production before the launchpad-buildd change has been deployed to production.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:not-automatic-proposed into launchpad:master.
diff --git a/lib/lp/archivepublisher/publishing.py b/lib/lp/archivepublisher/publishing.py
index f18ca9f..a5a0cc4 100644
--- a/lib/lp/archivepublisher/publishing.py
+++ b/lib/lp/archivepublisher/publishing.py
@@ -1235,8 +1235,10 @@ class Publisher:
         release_file["Components"] = " ".join(
             reorder_components(all_components))
         release_file["Description"] = drsummary
-        if (pocket == PackagePublishingPocket.BACKPORTS and
-            distroseries.backports_not_automatic):
+        if ((pocket == PackagePublishingPocket.BACKPORTS and
+             distroseries.backports_not_automatic) or
+            (pocket == PackagePublishingPocket.PROPOSED and
+             distroseries.proposed_not_automatic)):
             release_file["NotAutomatic"] = "yes"
             release_file["ButAutomaticUpgrades"] = "yes"
 
diff --git a/lib/lp/archivepublisher/tests/test_publisher.py b/lib/lp/archivepublisher/tests/test_publisher.py
index 6ff8cb2..c0891dd 100644
--- a/lib/lp/archivepublisher/tests/test_publisher.py
+++ b/lib/lp/archivepublisher/tests/test_publisher.py
@@ -116,6 +116,7 @@ from lp.testing.matchers import FileContainsBytes
 
 
 RELEASE = PackagePublishingPocket.RELEASE
+PROPOSED = PackagePublishingPocket.PROPOSED
 BACKPORTS = PackagePublishingPocket.BACKPORTS
 
 
@@ -2064,6 +2065,45 @@ class TestPublisher(TestPublisherBase):
         self.assertIn("NotAutomatic: yes", get_release(BACKPORTS))
         self.assertIn("ButAutomaticUpgrades: yes", get_release(BACKPORTS))
 
+    def testReleaseFileForNotAutomaticProposed(self):
+        # Test Release file writing for series with NotAutomatic -proposed.
+        publisher = Publisher(
+            self.logger, self.config, self.disk_pool,
+            self.ubuntutest.main_archive)
+        self.getPubSource(filecontent=b'Hello world', pocket=RELEASE)
+        self.getPubSource(filecontent=b'Hello world', pocket=PROPOSED)
+
+        # Make everything other than breezy-autotest OBSOLETE so that they
+        # aren't republished.
+        for series in self.ubuntutest.series:
+            if series.name != "breezy-autotest":
+                series.status = SeriesStatus.OBSOLETE
+
+        publisher.A_publish(True)
+        publisher.C_writeIndexes(False)
+
+        def get_release(pocket):
+            release_path = os.path.join(
+                publisher._config.distsroot,
+                'breezy-autotest%s' % pocketsuffix[pocket], 'Release')
+            with open(release_path) as release_file:
+                return release_file.read().splitlines()
+
+        # When proposed_not_automatic is unset, no Release files have
+        # NotAutomatic: yes.
+        self.assertEqual(False, self.breezy_autotest.proposed_not_automatic)
+        publisher.D_writeReleaseFiles(False)
+        self.assertNotIn("NotAutomatic: yes", get_release(RELEASE))
+        self.assertNotIn("NotAutomatic: yes", get_release(PROPOSED))
+
+        # But with the flag set, -proposed Release files gain
+        # NotAutomatic: yes and ButAutomaticUpgrades: yes.
+        self.breezy_autotest.proposed_not_automatic = True
+        publisher.D_writeReleaseFiles(False)
+        self.assertNotIn("NotAutomatic: yes", get_release(RELEASE))
+        self.assertIn("NotAutomatic: yes", get_release(PROPOSED))
+        self.assertIn("ButAutomaticUpgrades: yes", get_release(PROPOSED))
+
     def testReleaseFileForI18n(self):
         """Test Release file writing for translated package descriptions."""
         publisher = Publisher(
diff --git a/lib/lp/registry/configure.zcml b/lib/lp/registry/configure.zcml
index 9d07930..0ccf396 100644
--- a/lib/lp/registry/configure.zcml
+++ b/lib/lp/registry/configure.zcml
@@ -1,4 +1,4 @@
-<!-- Copyright 2009-2020 Canonical Ltd.  This software is licensed under the
+<!-- Copyright 2009-2021 Canonical Ltd.  This software is licensed under the
      GNU Affero General Public License version 3 (see the file LICENSE).
 -->
 
@@ -326,6 +326,7 @@
                 description
                 driver
                 backports_not_automatic
+                proposed_not_automatic
                 include_long_descriptions
                 index_compressors
                 publish_by_hash
diff --git a/lib/lp/registry/interfaces/distroseries.py b/lib/lp/registry/interfaces/distroseries.py
index 848cf6d..da0e19e 100644
--- a/lib/lp/registry/interfaces/distroseries.py
+++ b/lib/lp/registry/interfaces/distroseries.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2020 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2021 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Interfaces including and related to IDistroSeries."""
@@ -373,6 +373,15 @@ class IDistroSeriesPublic(
             automatically upgrade within backports, but not into it.
             """))
 
+    proposed_not_automatic = Bool(
+        title=_("Don't upgrade to proposed updates automatically"),
+        required=True,
+        description=_("""
+            Set NotAutomatic: yes and ButAutomaticUpgrades: yes in Release
+            files generated for the proposed pocket. This tells apt to
+            automatically upgrade within proposed, but not into it.
+            """))
+
     include_long_descriptions = exported(
         Bool(
             title=_(
diff --git a/lib/lp/registry/model/distroseries.py b/lib/lp/registry/model/distroseries.py
index 2ad609c..167aa8e 100644
--- a/lib/lp/registry/model/distroseries.py
+++ b/lib/lp/registry/model/distroseries.py
@@ -279,6 +279,7 @@ class DistroSeries(SQLBase, BugTargetBase, HasSpecificationsMixin,
         if "publishing_options" not in kwargs:
             kwargs["publishing_options"] = {
                 "backports_not_automatic": False,
+                "proposed_not_automatic": False,
                 "include_long_descriptions": True,
                 "index_compressors": [
                     compressor.title
@@ -828,6 +829,15 @@ class DistroSeries(SQLBase, BugTargetBase, HasSpecificationsMixin,
         self.publishing_options["backports_not_automatic"] = value
 
     @property
+    def proposed_not_automatic(self):
+        return self.publishing_options.get("proposed_not_automatic", False)
+
+    @proposed_not_automatic.setter
+    def proposed_not_automatic(self, value):
+        assert isinstance(value, bool)
+        self.publishing_options["proposed_not_automatic"] = value
+
+    @property
     def include_long_descriptions(self):
         return self.publishing_options.get("include_long_descriptions", True)
 
diff --git a/lib/lp/registry/tests/test_distroseries.py b/lib/lp/registry/tests/test_distroseries.py
index 1543eca..d67e499 100644
--- a/lib/lp/registry/tests/test_distroseries.py
+++ b/lib/lp/registry/tests/test_distroseries.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2021 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for distroseries."""
@@ -358,6 +358,16 @@ class TestDistroSeries(TestCaseWithFactory):
         self.assertTrue(
             naked_distroseries.publishing_options["backports_not_automatic"])
 
+    def test_proposed_not_automatic(self):
+        distroseries = self.factory.makeDistroSeries()
+        self.assertFalse(distroseries.proposed_not_automatic)
+        with admin_logged_in():
+            distroseries.proposed_not_automatic = True
+        self.assertTrue(distroseries.proposed_not_automatic)
+        naked_distroseries = removeSecurityProxy(distroseries)
+        self.assertTrue(
+            naked_distroseries.publishing_options["proposed_not_automatic"])
+
     def test_include_long_descriptions(self):
         distroseries = self.factory.makeDistroSeries()
         self.assertTrue(distroseries.include_long_descriptions)
diff --git a/lib/lp/soyuz/scripts/initialize_distroseries.py b/lib/lp/soyuz/scripts/initialize_distroseries.py
index 1ca534d..5a773bf 100644
--- a/lib/lp/soyuz/scripts/initialize_distroseries.py
+++ b/lib/lp/soyuz/scripts/initialize_distroseries.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2020 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2021 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Initialize a distroseries from its parent distroseries."""
@@ -383,6 +383,9 @@ class InitializeDistroSeries:
         self.distroseries.backports_not_automatic = any(
             parent.backports_not_automatic
                 for parent in self.derivation_parents)
+        self.distroseries.proposed_not_automatic = any(
+            parent.proposed_not_automatic
+                for parent in self.derivation_parents)
         self.distroseries.include_long_descriptions = any(
             parent.include_long_descriptions
                 for parent in self.derivation_parents)
diff --git a/lib/lp/soyuz/scripts/tests/test_initialize_distroseries.py b/lib/lp/soyuz/scripts/tests/test_initialize_distroseries.py
index 4079947..b3edb46 100644
--- a/lib/lp/soyuz/scripts/tests/test_initialize_distroseries.py
+++ b/lib/lp/soyuz/scripts/tests/test_initialize_distroseries.py
@@ -1,4 +1,4 @@
-# Copyright 2010-2020 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2021 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Test the initialize_distroseries script machinery."""
@@ -93,6 +93,7 @@ class InitializationHelperTestCase(TestCaseWithFactory):
         if existing_format_selection is None:
             spfss_utility.add(parent, format_selection)
         parent.backports_not_automatic = True
+        parent.proposed_not_automatic = True
         parent.include_long_descriptions = False
         parent.index_compressors = [IndexCompressionType.XZ]
         parent.publish_by_hash = True
@@ -685,6 +686,7 @@ class TestInitializeDistroSeries(InitializationHelperTestCase):
             SourcePackageFormat.FORMAT_1_0))
         # Other configuration bits are copied too.
         self.assertTrue(child.backports_not_automatic)
+        self.assertTrue(child.proposed_not_automatic)
         self.assertFalse(child.include_long_descriptions)
         self.assertEqual([IndexCompressionType.XZ], child.index_compressors)
         self.assertTrue(child.publish_by_hash)