← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/snap-auto-build-model into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/snap-auto-build-model into lp:launchpad.

Commit message:
Model new Snap columns to support auto-building.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1593359 in Launchpad itself: "Add option to trigger snap builds when top-level branch changes"
  https://bugs.launchpad.net/launchpad/+bug/1593359

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/snap-auto-build-model/+merge/297955

Model new Snap columns to support auto-building.

See https://code.launchpad.net/~cjwatson/launchpad/db-snap-auto-build/+merge/297667 for the previous database branch.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/snap-auto-build-model into lp:launchpad.
=== modified file 'lib/lp/services/config/schema-lazr.conf'
--- lib/lp/services/config/schema-lazr.conf	2016-05-25 15:31:48 +0000
+++ lib/lp/services/config/schema-lazr.conf	2016-06-20 20:33:26 +0000
@@ -1771,6 +1771,10 @@
 http_proxy: none
 
 [snappy]
+# Minimum time in minutes between dispatching automatic builds of snap
+# packages.
+auto_build_frequency: 60
+
 # Admin secret for requesting tokens from the builder proxy service.
 # For a mojo deployed proxy service, the secret is generated and will reside in
 # /srv/mojo/${MOJO_PROJECT}/${SERIES}/${MOJO_WORKSPACE}/local/admin-api-secret

=== modified file 'lib/lp/snappy/interfaces/snap.py'
--- lib/lp/snappy/interfaces/snap.py	2016-05-28 00:21:40 +0000
+++ lib/lp/snappy/interfaces/snap.py	2016-06-20 20:33:26 +0000
@@ -377,6 +377,31 @@
             "The Git branch containing a snapcraft.yaml recipe at the top "
             "level.")))
 
+    auto_build = exported(Bool(
+        title=_("Automatically build when branch changes"),
+        required=True, readonly=False,
+        description=_(
+            "Whether this snap package is built automatically when the branch "
+            "containing its snapcraft.yaml recipe changes.")))
+
+    auto_build_archive = exported(Reference(
+        IArchive, title=_("Source archive for automatic builds"),
+        required=False, readonly=False,
+        description=_(
+            "The archive from which automatic builds of this snap package "
+            "should be built.")))
+
+    auto_build_pocket = exported(Choice(
+        title=_("Pocket for automatic builds"),
+        vocabulary=PackagePublishingPocket, required=False, readonly=False,
+        description=_(
+            "The pocket for which automatic builds of this snap package "
+            "should be built.")))
+
+    is_stale = Bool(
+        title=_("Snap package is stale and is due to be rebuilt."),
+        required=True, readonly=False)
+
     store_upload = Bool(
         title=_("Automatically upload to store"),
         required=True, readonly=False,
@@ -523,6 +548,12 @@
     def preloadDataForSnaps(snaps, user):
         """Load the data related to a list of snap packages."""
 
+    def makeAutoBuilds(logger=None):
+        """Create and return automatic builds for stale snap packages.
+
+        :param logger: An optional logger.
+        """
+
     def detachFromBranch(branch):
         """Detach all snap packages from the given Bazaar branch.
 

=== modified file 'lib/lp/snappy/model/snap.py'
--- lib/lp/snappy/model/snap.py	2016-05-28 00:21:40 +0000
+++ lib/lp/snappy/model/snap.py	2016-06-20 20:33:26 +0000
@@ -6,14 +6,22 @@
     'Snap',
     ]
 
+from datetime import (
+    datetime,
+    timedelta,
+    )
 import pytz
+from storm.expr import (
+    And,
+    Desc,
+    LeftJoin,
+    Not,
+    )
 from storm.locals import (
     Bool,
     DateTime,
-    Desc,
     Int,
     JSON,
-    Not,
     Reference,
     Store,
     Storm,
@@ -56,6 +64,7 @@
     IPerson,
     IPersonSet,
     )
+from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.registry.interfaces.product import IProduct
 from lp.registry.interfaces.role import (
     IHasOwner,
@@ -67,6 +76,7 @@
     DEFAULT,
     UTC_NOW,
     )
+from lp.services.database.enumcol import DBEnum
 from lp.services.database.interfaces import (
     IMasterStore,
     IStore,
@@ -155,6 +165,15 @@
 
     git_path = Unicode(name='git_path', allow_none=True)
 
+    auto_build = Bool(name='auto_build', allow_none=False)
+
+    auto_build_archive_id = Int(name='auto_build_archive', allow_none=True)
+    auto_build_archive = Reference(auto_build_archive_id, 'Archive.id')
+
+    auto_build_pocket = DBEnum(enum=PackagePublishingPocket, allow_none=True)
+
+    is_stale = Bool(name='is_stale', allow_none=False)
+
     require_virtualized = Bool(name='require_virtualized')
 
     private = Bool(name='private')
@@ -169,7 +188,8 @@
     store_secrets = JSON('store_secrets', allow_none=True)
 
     def __init__(self, registrant, owner, distro_series, name,
-                 description=None, branch=None, git_ref=None,
+                 description=None, branch=None, git_ref=None, auto_build=False,
+                 auto_build_archive=None, auto_build_pocket=None,
                  require_virtualized=True, date_created=DEFAULT,
                  private=False, store_upload=False, store_series=None,
                  store_name=None, store_secrets=None):
@@ -185,6 +205,9 @@
         self.description = description
         self.branch = branch
         self.git_ref = git_ref
+        self.auto_build = auto_build
+        self.auto_build_archive = auto_build_archive
+        self.auto_build_pocket = auto_build_pocket
         self.require_virtualized = require_virtualized
         self.date_created = date_created
         self.date_last_modified = date_created
@@ -484,10 +507,11 @@
     """See `ISnapSet`."""
 
     def new(self, registrant, owner, distro_series, name, description=None,
-            branch=None, git_ref=None, require_virtualized=True,
-            processors=None, date_created=DEFAULT, private=False,
-            store_upload=False, store_series=None, store_name=None,
-            store_secrets=None):
+            branch=None, git_ref=None, auto_build=False,
+            auto_build_archive=None, auto_build_pocket=None,
+            require_virtualized=True, processors=None, date_created=DEFAULT,
+            private=False, store_upload=False, store_series=None,
+            store_name=None, store_secrets=None):
         """See `ISnapSet`."""
         if not registrant.inTeam(owner):
             if owner.is_team:
@@ -510,7 +534,9 @@
         store = IMasterStore(Snap)
         snap = Snap(
             registrant, owner, distro_series, name, description=description,
-            branch=branch, git_ref=git_ref,
+            branch=branch, git_ref=git_ref, auto_build=auto_build,
+            auto_build_archive=auto_build_archive,
+            auto_build_pocket=auto_build_pocket,
             require_virtualized=require_virtualized, date_created=date_created,
             private=private, store_upload=store_upload,
             store_series=store_series, store_name=store_name,
@@ -663,6 +689,55 @@
         list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
             person_ids, need_validity=True))
 
+    @staticmethod
+    def _findStaleSnaps():
+        """See `ISnapSet`."""
+        threshold_date = (
+            datetime.now(pytz.UTC) -
+            timedelta(minutes=config.snappy.auto_build_frequency))
+        origin = [
+            Snap,
+            LeftJoin(
+                SnapBuild,
+                And(
+                    SnapBuild.snap_id == Snap.id,
+                    SnapBuild.archive_id == Snap.auto_build_archive_id,
+                    SnapBuild.pocket == Snap.auto_build_pocket,
+                    # We only want Snaps that haven't had an automatic
+                    # SnapBuild dispatched for them recently.
+                    SnapBuild.date_created >= threshold_date)),
+            ]
+        return IStore(Snap).using(*origin).find(
+            Snap,
+            Snap.is_stale == True,
+            Snap.auto_build == True,
+            SnapBuild.date_created == None).config(distinct=True)
+
+    @classmethod
+    def makeAutoBuilds(cls, logger=None):
+        """See `ISnapSet`."""
+        snaps = cls._findStaleSnaps()
+        builds = []
+        for snap in snaps:
+            snap.is_stale = False
+            if logger is not None:
+                logger.debug(
+                    "Scheduling builds of snap package %s/%s",
+                    snap.owner.name, snap.name)
+            for arch in snap.getAllowedArchitectures():
+                try:
+                    build = snap.requestBuild(
+                        snap.owner, snap.auto_build_archive, arch,
+                        snap.auto_build_pocket)
+                    if logger is not None:
+                        logger.debug(
+                            " - %s: Build requested.", arch.architecturetag)
+                    builds.append(build)
+                except Exception as e:
+                    if logger is not None:
+                        logger.debug(" - %s: %s", arch.architecturetag, e)
+        return builds
+
     def detachFromBranch(self, branch):
         """See `ISnapSet`."""
         self.findByBranch(branch).set(

=== modified file 'lib/lp/snappy/tests/test_snap.py'
--- lib/lp/snappy/tests/test_snap.py	2016-05-17 12:47:34 +0000
+++ lib/lp/snappy/tests/test_snap.py	2016-06-20 20:33:26 +0000
@@ -5,12 +5,19 @@
 
 __metaclass__ = type
 
-from datetime import timedelta
+from datetime import (
+    datetime,
+    timedelta,
+    )
 
 from lazr.lifecycle.event import ObjectModifiedEvent
+import pytz
 from storm.exceptions import LostObjectError
 from storm.locals import Store
-from testtools.matchers import Equals
+from testtools.matchers import (
+    Equals,
+    MatchesStructure,
+    )
 import transaction
 from zope.component import getUtility
 from zope.event import notify
@@ -35,6 +42,7 @@
     )
 from lp.services.database.sqlbase import flush_database_caches
 from lp.services.features.testing import FeatureFixture
+from lp.services.log.logger import BufferLogger
 from lp.services.propertycache import clear_property_cache
 from lp.services.webapp.interfaces import OAuthPermission
 from lp.snappy.interfaces.snap import (
@@ -56,6 +64,7 @@
     ISnapBuild,
     ISnapBuildSet,
     )
+from lp.snappy.model.snap import SnapSet
 from lp.snappy.model.snapbuild import SnapFile
 from lp.testing import (
     admin_logged_in,
@@ -372,7 +381,7 @@
         build11 = self.factory.makeSnapBuild(snap=snap1)
         build12 = self.factory.makeSnapBuild(snap=snap1)
         build2 = self.factory.makeSnapBuild(snap=snap2)
-        build3 = self.factory.makeSnapBuild()
+        self.factory.makeSnapBuild()
         summary1 = snap1.getBuildSummariesForSnapBuildIds(
             [build11.id, build12.id])
         summary2 = snap2.getBuildSummariesForSnapBuildIds([build2.id])
@@ -391,7 +400,7 @@
         # Should not return build summaries of other snaps.
         snap1 = self.factory.makeSnap()
         snap2 = self.factory.makeSnap()
-        build1 = self.factory.makeSnapBuild(snap=snap1)
+        self.factory.makeSnapBuild(snap=snap1)
         build2 = self.factory.makeSnapBuild(snap=snap2)
         summary1 = snap1.getBuildSummariesForSnapBuildIds([build2.id])
         self.assertEqual({}, summary1)
@@ -539,6 +548,9 @@
         self.assertIsNone(snap.git_repository)
         self.assertIsNone(snap.git_path)
         self.assertIsNone(snap.git_ref)
+        self.assertFalse(snap.auto_build)
+        self.assertIsNone(snap.auto_build_archive)
+        self.assertIsNone(snap.auto_build_pocket)
         self.assertTrue(snap.require_virtualized)
         self.assertFalse(snap.private)
 
@@ -556,6 +568,9 @@
         self.assertEqual(ref.repository, snap.git_repository)
         self.assertEqual(ref.path, snap.git_path)
         self.assertEqual(ref, snap.git_ref)
+        self.assertFalse(snap.auto_build)
+        self.assertIsNone(snap.auto_build_archive)
+        self.assertIsNone(snap.auto_build_pocket)
         self.assertTrue(snap.require_virtualized)
         self.assertFalse(snap.private)
 
@@ -750,6 +765,144 @@
             BadSnapSearchContext, snap_set.findByContext,
             self.factory.makeDistribution())
 
+    def test__findStaleSnaps(self):
+        # Stale; not built automatically.
+        self.factory.makeSnap(is_stale=True)
+        # Not stale; built automatically.
+        self.factory.makeSnap(auto_build=True, is_stale=False)
+        # Stale; built automatically.
+        stale_daily = self.factory.makeSnap(auto_build=True, is_stale=True)
+        self.assertContentEqual([stale_daily], SnapSet._findStaleSnaps())
+
+    def test__findStaleSnapsDistinct(self):
+        # If a snap package has two builds due to two architectures, it only
+        # returns one recipe.
+        distroseries = self.factory.makeDistroSeries()
+        dases = [
+            self.factory.makeDistroArchSeries(distroseries=distroseries)
+            for _ in range(2)]
+        snap = self.factory.makeSnap(
+            distroseries=distroseries,
+            processors=[das.processor for das in dases],
+            auto_build=True, is_stale=True)
+        for das in dases:
+            self.factory.makeSnapBuild(
+                requester=snap.owner, snap=snap,
+                archive=snap.auto_build_archive, distroarchseries=das,
+                pocket=snap.auto_build_pocket,
+                date_created=(datetime.now(pytz.UTC) - timedelta(days=2)))
+        self.assertContentEqual([snap], SnapSet._findStaleSnaps())
+
+    def makeBuildableDistroArchSeries(self, **kwargs):
+        das = self.factory.makeDistroArchSeries(**kwargs)
+        fake_chroot = self.factory.makeLibraryFileAlias(
+            filename="fake_chroot.tar.gz", db_only=True)
+        das.addOrUpdateChroot(fake_chroot)
+        return das
+
+    def makeAutoBuildableSnap(self, **kwargs):
+        processor = self.factory.makeProcessor(supports_virtualized=True)
+        das = self.makeBuildableDistroArchSeries(processor=processor)
+        snap = self.factory.makeSnap(
+            distroseries=das.distroseries, processors=[das.processor],
+            auto_build=True, **kwargs)
+        return das, snap
+
+    def test_makeAutoBuilds(self):
+        # ISnapSet.makeAutoBuilds requests builds of
+        # appropriately-configured Snaps where possible.
+        self.assertEqual([], getUtility(ISnapSet).makeAutoBuilds())
+        das, snap = self.makeAutoBuildableSnap(is_stale=True)
+        logger = BufferLogger()
+        [build] = getUtility(ISnapSet).makeAutoBuilds(logger=logger)
+        self.assertThat(build, MatchesStructure.byEquality(
+            requester=snap.owner, snap=snap, distro_arch_series=das,
+            status=BuildStatus.NEEDSBUILD,
+            ))
+        expected_log_entries = [
+            "DEBUG Scheduling builds of snap package %s/%s" % (
+                snap.owner.name, snap.name),
+            "DEBUG  - %s: Build requested." % das.architecturetag,
+            ]
+        self.assertEqual(
+            expected_log_entries, logger.getLogBuffer().splitlines())
+        self.assertFalse(snap.is_stale)
+
+    def test_makeAutoBuilds_skips_if_built_recently(self):
+        # ISnapSet.makeAutoBuilds skips snap packages that have been built
+        # recently.
+        das, snap = self.makeAutoBuildableSnap(is_stale=True)
+        self.factory.makeSnapBuild(
+            requester=snap.owner, snap=snap, archive=snap.auto_build_archive,
+            distroarchseries=das)
+        logger = BufferLogger()
+        builds = getUtility(ISnapSet).makeAutoBuilds(logger=logger)
+        self.assertEqual([], builds)
+        self.assertEqual([], logger.getLogBuffer().splitlines())
+
+    def test_makeAutoBuilds_skips_non_stale_snaps(self):
+        # ISnapSet.makeAutoBuilds skips snap packages that are not stale.
+        das, snap = self.makeAutoBuildableSnap(is_stale=False)
+        self.assertEqual([], getUtility(ISnapSet).makeAutoBuilds())
+
+    def test_makeAutoBuilds_skips_pending(self):
+        # ISnapSet.makeAutoBuilds skips snap packages that already have
+        # pending builds.
+        das, snap = self.makeAutoBuildableSnap(is_stale=True)
+        # Simulate very long build farm queues so that this case isn't
+        # filtered out earlier.
+        self.factory.makeSnapBuild(
+            requester=snap.owner, snap=snap, archive=snap.auto_build_archive,
+            distroarchseries=das,
+            date_created=datetime.now(pytz.UTC) - timedelta(days=1))
+        logger = BufferLogger()
+        builds = getUtility(ISnapSet).makeAutoBuilds(logger=logger)
+        self.assertEqual([], builds)
+        expected_log_entries = [
+            "DEBUG Scheduling builds of snap package %s/%s" % (
+                snap.owner.name, snap.name),
+            "DEBUG  - %s: An identical build of this snap package is already "
+            "pending." % das.architecturetag,
+            ]
+        self.assertEqual(
+            expected_log_entries, logger.getLogBuffer().splitlines())
+
+    def test_makeAutoBuilds_with_older_build(self):
+        # If a previous build is not recent and the snap package is stale,
+        # ISnapSet.makeAutoBuilds requests builds.
+        das, snap = self.makeAutoBuildableSnap(is_stale=True)
+        self.factory.makeSnapBuild(
+            requester=snap.owner, snap=snap, archive=snap.auto_build_archive,
+            distroarchseries=das,
+            date_created=datetime.now(pytz.UTC) - timedelta(days=1),
+            status=BuildStatus.FULLYBUILT, duration=timedelta(minutes=1))
+        builds = getUtility(ISnapSet).makeAutoBuilds()
+        self.assertEqual(1, len(builds))
+
+    def test_makeAutoBuilds_with_older_and_newer_builds(self):
+        # If a snap package has been built twice, and the most recent build
+        # is too recent, ISnapSet.makeAutoBuilds does not request builds.
+        das, snap = self.makeAutoBuildableSnap(is_stale=True)
+        for timediff in timedelta(days=1), timedelta(minutes=30):
+            self.factory.makeSnapBuild(
+                requester=snap.owner, snap=snap,
+                archive=snap.auto_build_archive, distroarchseries=das,
+                date_created=datetime.now(pytz.UTC) - timediff,
+                status=BuildStatus.FULLYBUILT, duration=timedelta(minutes=1))
+        self.assertEqual([], getUtility(ISnapSet).makeAutoBuilds())
+
+    def test_makeAutoBuilds_with_recent_build_from_different_archive(self):
+        # If a snap package has been built recently but from an archive
+        # other than the auto_build_archive, ISnapSet.makeAutoBuilds
+        # requests builds.
+        das, snap = self.makeAutoBuildableSnap(is_stale=True)
+        self.factory.makeSnapBuild(
+            requester=snap.owner, snap=snap, distroarchseries=das,
+            date_created=datetime.now(pytz.UTC) - timedelta(minutes=30),
+            status=BuildStatus.FULLYBUILT, duration=timedelta(minutes=1))
+        builds = getUtility(ISnapSet).makeAutoBuilds()
+        self.assertEqual(1, len(builds))
+
     def test_detachFromBranch(self):
         # ISnapSet.detachFromBranch clears the given Bazaar branch from all
         # Snaps.

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2016-06-17 15:46:57 +0000
+++ lib/lp/testing/factory.py	2016-06-20 20:33:26 +0000
@@ -4630,8 +4630,9 @@
             active, secret)
 
     def makeSnap(self, registrant=None, owner=None, distroseries=None,
-                 name=None, branch=None, git_ref=None,
-                 require_virtualized=True, processors=None,
+                 name=None, branch=None, git_ref=None, auto_build=False,
+                 auto_build_archive=None, auto_build_pocket=None,
+                 is_stale=None, require_virtualized=True, processors=None,
                  date_created=DEFAULT, private=False, store_upload=False,
                  store_series=None, store_name=None, store_secrets=None):
         """Make a new Snap."""
@@ -4645,13 +4646,22 @@
             name = self.getUniqueString(u"snap-name")
         if branch is None and git_ref is None:
             branch = self.makeAnyBranch()
+        if auto_build:
+            if auto_build_archive is None:
+                auto_build_archive = self.makeArchive(
+                    distribution=distroseries.distribution, owner=owner)
+            if auto_build_pocket is None:
+                auto_build_pocket = PackagePublishingPocket.UPDATES
         snap = getUtility(ISnapSet).new(
             registrant, owner, distroseries, name,
             require_virtualized=require_virtualized, processors=processors,
             date_created=date_created, branch=branch, git_ref=git_ref,
-            private=private, store_upload=store_upload,
-            store_series=store_series, store_name=store_name,
-            store_secrets=store_secrets)
+            auto_build=auto_build, auto_build_archive=auto_build_archive,
+            auto_build_pocket=auto_build_pocket, private=private,
+            store_upload=store_upload, store_series=store_series,
+            store_name=store_name, store_secrets=store_secrets)
+        if is_stale is not None:
+            removeSecurityProxy(snap).is_stale = is_stale
         IStore(snap).flush()
         return snap
 


Follow ups