← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/archive-api-dirty into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/archive-api-dirty into lp:launchpad.

Commit message:
Add Archive.markSuiteDirty to allow requesting that a given archive/suite be published.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/archive-api-dirty/+merge/310208

My proximate reason for doing this is that it will make it easier to cause index files for new series to exist in special-purpose PPAs such as ppa:snappy-dev/ubuntu/tools.  However, we occasionally need to force a suite to be published for one reason or another and it would be nice for this not to require a manual publish-distro run.

As an additional demonstration of this being worthwhile, I used it to simplify the handling of staged files in publish-ftpmaster.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/archive-api-dirty into lp:launchpad.
=== modified file 'lib/lp/_schema_circular_imports.py'
--- lib/lp/_schema_circular_imports.py	2016-10-12 23:24:24 +0000
+++ lib/lp/_schema_circular_imports.py	2016-11-07 17:02:58 +0000
@@ -484,6 +484,10 @@
     IArchive, '_addArchiveDependency', 'pocket', PackagePublishingPocket)
 patch_entry_return_type(
     IArchive, '_addArchiveDependency', IArchiveDependency)
+patch_plain_parameter_type(
+    IArchive, 'markSuiteDirty', 'distroseries', IDistroSeries)
+patch_choice_parameter_type(
+    IArchive, 'markSuiteDirty', 'pocket', PackagePublishingPocket)
 
 # IBuildFarmJob
 patch_choice_property(IBuildFarmJob, 'status', BuildStatus)

=== modified file 'lib/lp/archivepublisher/scripts/publish_ftpmaster.py'
--- lib/lp/archivepublisher/scripts/publish_ftpmaster.py	2016-01-13 17:54:05 +0000
+++ lib/lp/archivepublisher/scripts/publish_ftpmaster.py	2016-11-07 17:02:58 +0000
@@ -434,14 +434,12 @@
         publish_distro.main()
 
     def publishDistroArchive(self, distribution, archive,
-                             security_suites=None, updated_suites=[]):
+                             security_suites=None):
         """Publish the results for an archive.
 
         :param archive: Archive to publish.
         :param security_suites: An optional list of suites to restrict
             the publishing to.
-        :param updated_suites: An optional list of archive/suite pairs that
-            have been updated out of band and should be republished.
         """
         purpose = archive.purpose
         archive_config = self.configs[distribution][purpose]
@@ -456,9 +454,6 @@
         arguments = ['-R', temporary_dists]
         if archive.purpose == ArchivePurpose.PARTNER:
             arguments.append('--partner')
-        for updated_archive, updated_suite in updated_suites:
-            if archive == updated_archive:
-                arguments.extend(['--dirty-suite', updated_suite])
 
         os.rename(get_backup_dists(archive_config), temporary_dists)
         try:
@@ -554,15 +549,14 @@
             security_suites=security_suites)
         return True
 
-    def publishDistroUploads(self, distribution, updated_suites=[]):
+    def publishDistroUploads(self, distribution):
         """Publish the distro's complete uploads."""
         self.logger.debug("Full publication.  This may take some time.")
         for archive in get_publishable_archives(distribution):
             if archive.purpose in self.configs[distribution]:
                 # This, for the main archive, is where the script spends
                 # most of its time.
-                self.publishDistroArchive(
-                    distribution, archive, updated_suites=updated_suites)
+                self.publishDistroArchive(distribution, archive)
 
     def updateStagedFilesForSuite(self, archive_config, suite):
         """Install all staged files for a single archive and suite.
@@ -602,7 +596,6 @@
 
     def updateStagedFiles(self, distribution):
         """Install all staged files for a distribution's archives."""
-        updated_suites = []
         for archive in get_publishable_archives(distribution):
             if archive.purpose not in self.configs[distribution]:
                 continue
@@ -613,8 +606,7 @@
                     if cannot_modify_suite(archive, series, pocket):
                         continue
                     if self.updateStagedFilesForSuite(archive_config, suite):
-                        updated_suites.append((archive, suite))
-        return updated_suites
+                        archive.markSuiteDirty(series, pocket)
 
     def publish(self, distribution, security_only=False):
         """Do the main publishing work.
@@ -631,9 +623,8 @@
             if security_only:
                 has_published = self.publishSecurityUploads(distribution)
             else:
-                updated_suites = self.updateStagedFiles(distribution)
-                self.publishDistroUploads(
-                    distribution, updated_suites=updated_suites)
+                self.updateStagedFiles(distribution)
+                self.publishDistroUploads(distribution)
                 # Let's assume the main archive is always modified
                 has_published = True
 

=== modified file 'lib/lp/archivepublisher/scripts/publishdistro.py'
--- lib/lp/archivepublisher/scripts/publishdistro.py	2016-09-19 10:28:33 +0000
+++ lib/lp/archivepublisher/scripts/publishdistro.py	2016-11-07 17:02:58 +0000
@@ -235,6 +235,19 @@
             suites.add((series.name, pocket))
         return suites
 
+    def findDirtySuites(self, distribution, archive):
+        """Find the suites that have been explicitly marked as dirty."""
+        for suite in self.options.dirty_suites:
+            yield self.findSuite(distribution, suite)
+        if archive.dirty_suites is not None:
+            for suite in archive.dirty_suites:
+                try:
+                    yield distribution.getDistroSeriesAndPocket(suite)
+                except NotFoundError:
+                    self.logger.exception(
+                        "Failed to parse dirty suite '%s' for archive '%s'" %
+                        (suite, archive.reference))
+
     def getCopyArchives(self, distribution):
         """Find copy archives for the selected distribution."""
         copy_archives = list(
@@ -347,14 +360,14 @@
                     elif archive.can_be_published:
                         publisher = self.getPublisher(
                             distribution, archive, allowed_suites)
-                        for suite in self.options.dirty_suites:
-                            distroseries, pocket = self.findSuite(
-                                distribution, suite)
+                        for distroseries, pocket in self.findDirtySuites(
+                                distribution, archive):
                             if not cannot_modify_suite(
                                     archive, distroseries, pocket):
                                 publisher.markPocketDirty(
                                     distroseries, pocket)
                         self.publishArchive(archive, publisher)
+                        archive.dirty_suites = None
                         work_done = True
                     else:
                         work_done = False

=== modified file 'lib/lp/archivepublisher/tests/test_publish_ftpmaster.py'
--- lib/lp/archivepublisher/tests/test_publish_ftpmaster.py	2016-02-05 16:51:12 +0000
+++ lib/lp/archivepublisher/tests/test_publish_ftpmaster.py	2016-11-07 17:02:58 +0000
@@ -925,9 +925,9 @@
         self.assertFalse(script.updateStagedFilesForSuite(
             archive_config, distroseries.name))
 
-    def test_updateStagedFiles_updated_suites(self):
-        # updateStagedFiles returns a list of suites for which it updated
-        # staged files.
+    def test_updateStagedFiles_marks_suites_dirty(self):
+        # updateStagedFiles marks the suites for which it updated staged
+        # files as dirty.
         distro = self.makeDistroWithPublishDirectory()
         distroseries = self.factory.makeDistroSeries(distribution=distro)
         das = self.factory.makeDistroArchSeries(distroseries=distroseries)
@@ -945,9 +945,8 @@
         os.makedirs(staging_suite)
         write_marker_file(
             [staging_suite, "%s.gz" % contents_filename], "Contents")
-        self.assertEqual(
-            [(distro.main_archive, distroseries.name)],
-            script.updateStagedFiles(distro))
+        script.updateStagedFiles(distro)
+        self.assertEqual([distroseries.name], distro.main_archive.dirty_suites)
 
     def test_updateStagedFiles_considers_partner_archive(self):
         # updateStagedFiles considers the partner archive as well as the

=== modified file 'lib/lp/archivepublisher/tests/test_publishdistro.py'
--- lib/lp/archivepublisher/tests/test_publishdistro.py	2016-11-03 15:07:36 +0000
+++ lib/lp/archivepublisher/tests/test_publishdistro.py	2016-11-07 17:02:58 +0000
@@ -20,7 +20,9 @@
 
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.archivepublisher.config import getPubConfig
-from lp.archivepublisher.interfaces.archivesigningkey import IArchiveSigningKey
+from lp.archivepublisher.interfaces.archivesigningkey import (
+    IArchiveSigningKey,
+    )
 from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet
 from lp.archivepublisher.publishing import Publisher
 from lp.archivepublisher.scripts.publishdistro import PublishDistro
@@ -436,6 +438,24 @@
         # breezy-autotest has its Release files rewritten.
         self.assertThat(breezy_inrelease_path, PathExists())
 
+    def testDirtySuites(self):
+        """publish-distro can be told to publish specific suites."""
+        archive = self.factory.makeArchive(distribution=self.ubuntutest)
+        self.layer.txn.commit()
+
+        # publish-distro has nothing to publish.
+        self.runPublishDistro(['--ppa'])
+        breezy_release_path = os.path.join(
+            getPubConfig(archive).distsroot, 'breezy-autotest', 'Release')
+        self.assertThat(breezy_release_path, Not(PathExists()))
+
+        # ... but it will publish a suite anyway if it is marked as dirty.
+        archive.markSuiteDirty(
+            archive.distribution.getSeries('breezy-autotest'),
+            PackagePublishingPocket.RELEASE)
+        self.runPublishDistro(['--ppa'])
+        self.assertThat(breezy_release_path, PathExists())
+
 
 class FakeArchive:
     """A very simple fake `Archive`."""
@@ -444,6 +464,7 @@
         self.can_be_published = True
         self.purpose = purpose
         self.status = ArchiveStatus.ACTIVE
+        self.dirty_suites = []
 
 
 class FakePublisher:

=== modified file 'lib/lp/archivepublisher/tests/test_publisher.py'
--- lib/lp/archivepublisher/tests/test_publisher.py	2016-10-03 13:10:11 +0000
+++ lib/lp/archivepublisher/tests/test_publisher.py	2016-11-07 17:02:58 +0000
@@ -1318,6 +1318,16 @@
             now - timedelta(hours=12))
         self.assertIn(archive, ubuntu.getPendingPublicationPPAs())
 
+    def testDirtySuitesArchive(self):
+        # getPendingPublicationPPAs returns archives that have dirty_suites
+        # set.
+        ubuntu = getUtility(IDistributionSet)['ubuntu']
+        archive = self.factory.makeArchive()
+        self.assertNotIn(archive, ubuntu.getPendingPublicationPPAs())
+        archive.markSuiteDirty(
+            ubuntu.currentseries, PackagePublishingPocket.RELEASE)
+        self.assertIn(archive, ubuntu.getPendingPublicationPPAs())
+
     def _checkCompressedFiles(self, archive_publisher, base_file_path,
                               suffixes):
         """Assert that the various compressed versions of a file are equal.

=== modified file 'lib/lp/registry/model/distribution.py'
--- lib/lp/registry/model/distribution.py	2016-10-03 13:10:11 +0000
+++ lib/lp/registry/model/distribution.py	2016-11-07 17:02:58 +0000
@@ -1296,13 +1296,23 @@
             reapable_af_query, clauseTables=['ArchiveFile'],
             orderBy=['archive.id'], distinct=True)
 
+        dirty_suites_query = """
+        Archive.purpose = %s AND
+        Archive.distribution = %s AND
+        Archive.dirty_suites IS NOT NULL
+        """ % sqlvalues(ArchivePurpose.PPA, self)
+
+        dirty_suites_archives = Archive.select(
+            dirty_suites_query, orderBy=['archive.id'], distinct=True)
+
         deleting_archives = Archive.selectBy(
             distribution=self,
             purpose=ArchivePurpose.PPA,
             status=ArchiveStatus.DELETING).orderBy(['archive.id'])
 
         return src_archives.union(bin_archives).union(
-            reapable_af_archives).union(deleting_archives)
+            reapable_af_archives).union(dirty_suites_archives).union(
+            deleting_archives)
 
     def getArchiveByComponent(self, component_name):
         """See `IDistribution`."""

=== modified file 'lib/lp/soyuz/configure.zcml'
--- lib/lp/soyuz/configure.zcml	2016-09-24 06:22:43 +0000
+++ lib/lp/soyuz/configure.zcml	2016-11-07 17:02:58 +0000
@@ -327,8 +327,8 @@
         <require
             permission="launchpad.Edit"
             interface="lp.soyuz.interfaces.archive.IArchiveEdit"
-            set_attributes="build_debug_symbols description displayname
-                            publish publish_debug_symbols status
+            set_attributes="build_debug_symbols description dirty_suites
+                            displayname publish publish_debug_symbols status
                             suppress_subscription_notifications"/>
         <!--
            NOTE: The 'private' permission controls who can turn a public

=== modified file 'lib/lp/soyuz/interfaces/archive.py'
--- lib/lp/soyuz/interfaces/archive.py	2016-10-20 11:49:45 +0000
+++ lib/lp/soyuz/interfaces/archive.py	2016-11-07 17:02:58 +0000
@@ -671,6 +671,10 @@
             "context build.\n"
             "NOTE: This is for migration of OEM PPAs only!")))
 
+    dirty_suites = Attribute(
+        "Suites that the next publisher run should publish regardless of "
+        "pending publications.")
+
     processors = exported(
         CollectionField(
             title=_("Processors"),
@@ -2203,6 +2207,29 @@
         :param names: A list of token names.
         """
 
+    @operation_parameters(
+        distroseries=Reference(
+            # Really IDistroSeries.
+            Interface,
+            title=_("Distro series"), required=True),
+        pocket=Choice(
+            title=_("Pocket"),
+            # Really PackagePublishingPocket.
+            vocabulary=DBEnumeratedType,
+            required=True),
+        )
+    @export_write_operation()
+    @operation_for_version("devel")
+    def markSuiteDirty(distroseries, pocket):
+        """Mark a suite as dirty in this archive.
+
+        The next publisher run will publish this suite regardless of whether
+        it has any pending publications.
+
+        :param distroseries: An `IDistroSeries`.
+        :param pocket: A `PackagePublishingPocket`.
+        """
+
 
 class IArchiveAdmin(Interface):
     """Archive interface for operations restricted by commercial."""

=== modified file 'lib/lp/soyuz/model/archive.py'
--- lib/lp/soyuz/model/archive.py	2016-10-20 11:49:45 +0000
+++ lib/lp/soyuz/model/archive.py	2016-11-07 17:02:58 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Database class for table Archive."""
@@ -36,6 +36,7 @@
     )
 from storm.properties import (
     Int,
+    JSON,
     Unicode,
     )
 from storm.references import Reference
@@ -362,6 +363,8 @@
         dbName='suppress_subscription_notifications',
         notNull=True, default=False)
 
+    dirty_suites = JSON(name='dirty_suites', allow_none=True)
+
     def _init(self, *args, **kw):
         """Provide the right interface for URL traversal."""
         SQLBase._init(self, *args, **kw)
@@ -2414,6 +2417,14 @@
         Store.of(pcj.context).remove(pcj.context)
         job.destroySelf()
 
+    def markSuiteDirty(self, distroseries, pocket):
+        """See `IArchive`."""
+        suite = distroseries.getSuite(pocket)
+        if self.dirty_suites is None:
+            self.dirty_suites = [suite]
+        elif suite not in self.dirty_suites:
+            self.dirty_suites.append(suite)
+
 
 def validate_ppa(owner, distribution, proposed_name, private=False):
     """Can 'person' create a PPA called 'proposed_name'?

=== modified file 'lib/lp/soyuz/tests/test_archive.py'
--- lib/lp/soyuz/tests/test_archive.py	2016-07-14 16:06:01 +0000
+++ lib/lp/soyuz/tests/test_archive.py	2016-11-07 17:02:58 +0000
@@ -113,8 +113,8 @@
     login,
     login_person,
     person_logged_in,
+    RequestTimelineCollector,
     StormStatementRecorder,
-    RequestTimelineCollector,
     TestCaseWithFactory,
     )
 from lp.testing.layers import (
@@ -4136,3 +4136,51 @@
                  (existing_bpn, 'i386'): BinaryOverride(component=self.main),
                  (other_bpn, 'amd64'): BinaryOverride(component=self.main),
                 }))
+
+
+class TestMarkSuiteDirty(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_default_is_none(self):
+        archive = self.factory.makeArchive()
+        self.assertIsNone(archive.dirty_suites)
+
+    def test_requires_owner(self):
+        archive = self.factory.makeArchive()
+        self.assertRaises(Unauthorized, getattr, archive, "markSuiteDirty")
+
+    def test_first_suite(self):
+        archive = self.factory.makeArchive()
+        distroseries = self.factory.makeDistroSeries(
+            distribution=archive.distribution)
+        with person_logged_in(archive.owner):
+            archive.markSuiteDirty(
+                distroseries, PackagePublishingPocket.UPDATES)
+        self.assertEqual(
+            ["%s-updates" % distroseries.name], archive.dirty_suites)
+
+    def test_already_dirty(self):
+        archive = self.factory.makeArchive()
+        distroseries = self.factory.makeDistroSeries(
+            distribution=archive.distribution)
+        with person_logged_in(archive.owner):
+            archive.markSuiteDirty(
+                distroseries, PackagePublishingPocket.UPDATES)
+            archive.markSuiteDirty(
+                distroseries, PackagePublishingPocket.UPDATES)
+        self.assertEqual(
+            ["%s-updates" % distroseries.name], archive.dirty_suites)
+
+    def test_second_suite(self):
+        archive = self.factory.makeArchive()
+        distroseries = self.factory.makeDistroSeries(
+            distribution=archive.distribution)
+        with person_logged_in(archive.owner):
+            archive.markSuiteDirty(
+                distroseries, PackagePublishingPocket.UPDATES)
+            archive.markSuiteDirty(
+                distroseries, PackagePublishingPocket.RELEASE)
+        self.assertContentEqual(
+            ["%s-updates" % distroseries.name, distroseries.name],
+            archive.dirty_suites)


Follow ups