launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #16908
Re: [Merge] lp:~cjwatson/launchpad/livefs into lp:launchpad
Review: Needs Fixing code
Diff comments:
> === modified file 'lib/lp/_schema_circular_imports.py'
> --- lib/lp/_schema_circular_imports.py 2014-04-23 14:24:15 +0000
> +++ lib/lp/_schema_circular_imports.py 2014-05-27 07:47:13 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
> +# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> """Update the interface schema values due to circular imports.
> @@ -196,6 +196,13 @@
> )
> from lp.soyuz.interfaces.buildrecords import IHasBuildRecords
> from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
> +from lp.soyuz.interfaces.livefs import (
> + ILiveFSView,
> + )
> +from lp.soyuz.interfaces.livefsbuild import (
> + ILiveFSBuild,
> + ILiveFSFile,
> + )
> from lp.soyuz.interfaces.packageset import (
> IPackageset,
> IPackagesetSet,
> @@ -576,6 +583,15 @@
> # IDistroArchSeries
> patch_reference_property(IDistroArchSeries, 'main_archive', IArchive)
>
> +# ILiveFSFile
> +patch_reference_property(ILiveFSFile, 'livefsbuild', ILiveFSBuild)
> +
> +# ILiveFSView
> +patch_entry_return_type(ILiveFSView, 'requestBuild', ILiveFSBuild)
> +ILiveFSView['builds'].value_type.schema = ILiveFSBuild
> +ILiveFSView['completed_builds'].value_type.schema = ILiveFSBuild
> +ILiveFSView['pending_builds'].value_type.schema = ILiveFSBuild
> +
> # IPackageset
> patch_collection_return_type(
> IPackageset, 'setsIncluded', IPackageset)
>
> === modified file 'lib/lp/app/browser/launchpad.py'
> --- lib/lp/app/browser/launchpad.py 2014-02-26 05:35:04 +0000
> +++ lib/lp/app/browser/launchpad.py 2014-05-27 07:47:13 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
> +# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> """Browser code for the launchpad application."""
> @@ -111,7 +111,6 @@
> from lp.registry.interfaces.role import IPersonRoles
> from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
> from lp.services.config import config
> -from lp.services.features import getFeatureFlag
> from lp.services.helpers import intOrZero
> from lp.services.identity.interfaces.account import AccountStatus
> from lp.services.propertycache import cachedproperty
> @@ -144,6 +143,7 @@
> from lp.services.worlddata.interfaces.country import ICountrySet
> from lp.services.worlddata.interfaces.language import ILanguageSet
> from lp.soyuz.interfaces.binarypackagename import IBinaryPackageNameSet
> +from lp.soyuz.interfaces.livefs import ILiveFSSet
> from lp.soyuz.interfaces.packageset import IPackagesetSet
> from lp.soyuz.interfaces.processor import IProcessorSet
> from lp.testopenid.interfaces.server import ITestOpenIDApplication
> @@ -689,6 +689,7 @@
> 'karmaaction': IKarmaActionSet,
> '+imports': ITranslationImportQueue,
> '+languages': ILanguageSet,
> + 'livefses': ILiveFSSet,
> '+nameblacklist': INameBlacklistSet,
> 'package-sets': IPackagesetSet,
> 'people': IPersonSet,
>
> === modified file 'lib/lp/app/browser/tests/test_webservice.py'
> --- lib/lp/app/browser/tests/test_webservice.py 2012-01-01 02:58:52 +0000
> +++ lib/lp/app/browser/tests/test_webservice.py 2014-05-27 07:47:13 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2011 Canonical Ltd. This software is licensed under the
> +# Copyright 2011-2014 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> """Tests for webservice features across Launchpad."""
> @@ -115,6 +115,12 @@
> object_type = 'languages'
>
>
> +class TestMissingLiveFSes(BaseMissingObjectWebService, TestCaseWithFactory):
> + """Test NotFound for webservice livefses requests."""
> +
> + object_type = 'livefses'
> +
> +
> class TestMissingPackagesets(
> BaseMissingObjectWebService, TestCaseWithFactory):
> """Test NotFound for webservice packagesets requests."""
>
> === added file 'lib/lp/archiveuploader/livefsupload.py'
> --- lib/lp/archiveuploader/livefsupload.py 1970-01-01 00:00:00 +0000
> +++ lib/lp/archiveuploader/livefsupload.py 2014-05-27 07:47:13 +0000
> @@ -0,0 +1,58 @@
> +# Copyright 2014 Canonical Ltd. This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Process a live filesystem upload."""
> +
> +__metaclass__ = type
> +
> +import os
> +
> +from zope.component import getUtility
> +
> +from lp.buildmaster.enums import BuildStatus
> +from lp.services.helpers import filenameToContentType
> +from lp.services.librarian.interfaces import ILibraryFileAliasSet
> +
> +
> +class LiveFSUpload:
> + """A live filesystem upload.
> +
> + Unlike package uploads, these have no .changes files. We simply attach
> + all the files in the upload directory to the appropriate `ILiveFSBuild`.
> + """
> +
> + def __init__(self, upload_path, logger):
> + """Create a `LiveFSUpload`.
> +
> + :param upload_path: A directory containing files to upload.
> + :param logger: The logger to be used.
> + """
> + self.upload_path = upload_path
> + self.logger = logger
> +
> + self.librarian = getUtility(ILibraryFileAliasSet)
> +
> + def process(self, build):
> + """Process this upload, loading it into the database."""
> + self.logger.debug("Beginning processing.")
> +
> + for dirpath, _, filenames in os.walk(self.upload_path):
> + if dirpath == self.upload_path:
> + # All relevant files will be in a subdirectory; this is a
> + # simple way to avoid uploading any .distro file that may
> + # exist.
> + continue
> + for livefs_file in sorted(filenames):
> + livefs_path = os.path.join(dirpath, livefs_file)
> + libraryfile = self.librarian.create(
> + livefs_file, os.stat(livefs_path).st_size,
> + open(livefs_path, "rb"),
> + filenameToContentType(livefs_path),
> + restricted=build.archive.private)
Consider build.is_private. Encapsulating this sort of rule tends to avoid security flaws later.
> + build.addFile(libraryfile)
> +
> + # The master verifies the status to confirm successful upload.
> + self.logger.debug("Updating %s" % build.title)
> + build.updateStatus(BuildStatus.FULLYBUILT)
> +
> + self.logger.debug("Finished upload.")
>
> === added file 'lib/lp/archiveuploader/tests/test_livefsupload.py'
> --- lib/lp/archiveuploader/tests/test_livefsupload.py 1970-01-01 00:00:00 +0000
> +++ lib/lp/archiveuploader/tests/test_livefsupload.py 2014-05-27 07:47:13 +0000
> @@ -0,0 +1,67 @@
> +# Copyright 2014 Canonical Ltd. This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Test uploads of LiveFSBuilds."""
> +
> +__metaclass__ = type
> +
> +import os
> +
> +from storm.store import Store
> +from zope.component import getUtility
> +
> +from lp.archiveuploader.tests.test_uploadprocessor import (
> + TestUploadProcessorBase,
> + )
> +from lp.archiveuploader.uploadprocessor import (
> + UploadHandler,
> + UploadStatusEnum,
> + )
> +from lp.buildmaster.enums import BuildStatus
> +from lp.registry.interfaces.pocket import PackagePublishingPocket
> +from lp.services.features.testing import FeatureFixture
> +from lp.services.osutils import write_file
> +from lp.soyuz.interfaces.livefs import LIVEFS_FEATURE_FLAG
> +from lp.soyuz.interfaces.livefsbuild import ILiveFSBuildSet
> +
> +
> +class TestLiveFSBuildUploads(TestUploadProcessorBase):
> + """End-to-end tests of LiveFS build uploads."""
> +
> + def setUp(self):
> + super(TestLiveFSBuildUploads, self).setUp()
> +
> + self.useFixture(FeatureFixture({LIVEFS_FEATURE_FLAG: u"on"}))
> + self.setupBreezy()
> +
> + self.switchToAdmin()
> + self.livefs = self.factory.makeLiveFS()
> + self.build = getUtility(ILiveFSBuildSet).new(
> + requester=self.livefs.owner, livefs=self.livefs,
> + archive=self.factory.makeArchive(
> + distribution=self.ubuntu, owner=self.livefs.owner),
> + distro_arch_series=self.breezy["i386"],
> + pocket=PackagePublishingPocket.RELEASE)
> + self.build.updateStatus(BuildStatus.UPLOADING)
> + Store.of(self.build).flush()
> + self.switchToUploader()
> + self.options.context = "buildd"
> +
> + self.uploadprocessor = self.getUploadProcessor(
> + self.layer.txn, builds=True)
> +
> + def test_sets_build_and_state(self):
> + # The upload processor uploads files and sets the correct status.
> + self.assertFalse(self.build.verifySuccessfulUpload())
> + upload_dir = os.path.join(
> + self.incoming_folder, "test", str(self.build.id), "ubuntu")
> + write_file(os.path.join(upload_dir, "ubuntu.squashfs"), "squashfs")
> + write_file(os.path.join(upload_dir, "ubuntu.manifest"), "manifest")
> + handler = UploadHandler.forProcessor(
> + self.uploadprocessor, self.incoming_folder, "test", self.build)
> + result = handler.processLiveFS(self.log)
> + self.assertEqual(
> + UploadStatusEnum.ACCEPTED, result,
> + "LiveFS upload failed\nGot: %s" % self.log.getLogBuffer())
> + self.assertEqual(BuildStatus.FULLYBUILT, self.build.status)
> + self.assertTrue(self.build.verifySuccessfulUpload())
>
> === modified file 'lib/lp/archiveuploader/uploadprocessor.py'
> --- lib/lp/archiveuploader/uploadprocessor.py 2013-08-01 14:09:45 +0000
> +++ lib/lp/archiveuploader/uploadprocessor.py 2014-05-27 07:47:13 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
> +# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> """Code for 'processing' 'uploads'. Also see nascentupload.py.
> @@ -57,6 +57,7 @@
> from zope.component import getUtility
>
> from lp.app.errors import NotFoundError
> +from lp.archiveuploader.livefsupload import LiveFSUpload
> from lp.archiveuploader.nascentupload import (
> EarlyReturnUploadError,
> NascentUpload,
> @@ -82,6 +83,7 @@
> IArchiveSet,
> NoSuchPPA,
> )
> +from lp.soyuz.interfaces.livefsbuild import ILiveFSBuild
>
>
> __all__ = [
> @@ -641,6 +643,28 @@
> "Unable to find %s with id %d. Skipping." %
> (job_type, job_id))
>
> + def processLiveFS(self, logger=None):
> + """Process a live filesystem upload."""
> + assert ILiveFSBuild.providedBy(self.build)
> + if logger is None:
> + logger = self.processor.log
> + try:
> + logger.info("Processing LiveFS upload %s" % self.upload_path)
> + LiveFSUpload(self.upload_path, logger).process(self.build)
> +
> + if self.processor.dry_run:
> + logger.info("Dry run, aborting transaction.")
> + self.processor.ztm.abort()
> + else:
> + logger.info(
> + "Committing the transaction and any mails associated "
> + "with this upload.")
> + self.processor.ztm.commit()
> + return UploadStatusEnum.ACCEPTED
> + except:
> + self.processor.ztm.abort()
> + raise
> +
> def process(self):
> """Process an upload that is the result of a build.
>
> @@ -660,8 +684,11 @@
> # because we want the standard cleanup to occur.
> recipe_deleted = (ISourcePackageRecipeBuild.providedBy(self.build)
> and self.build.recipe is None)
> + is_livefs = ILiveFSBuild.providedBy(self.build)
> if recipe_deleted:
> result = UploadStatusEnum.FAILED
> + elif is_livefs:
> + result = self.processLiveFS(logger)
> else:
> self.processor.log.debug("Build %s found" % self.build.id)
> [changes_file] = self.locateChangesFiles()
>
> === modified file 'lib/lp/buildmaster/browser/builder.py'
> --- lib/lp/buildmaster/browser/builder.py 2014-05-06 09:11:14 +0000
> +++ lib/lp/buildmaster/browser/builder.py 2014-05-27 07:47:13 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
> +# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> """Browser views for builders."""
> @@ -64,6 +64,7 @@
> get_build_by_id_str,
> )
> from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuildSet
> +from lp.soyuz.interfaces.livefsbuild import ILiveFSBuildSet
>
>
> class BuilderSetNavigation(GetitemNavigation):
> @@ -84,6 +85,13 @@
> return None
> return self.redirectSubTree(canonical_url(build))
>
> + @stepthrough('+livefsbuild')
> + def traverse_livefsbuild(self, name):
> + build = get_build_by_id_str(ILiveFSBuildSet, name)
> + if build is None:
> + return None
> + return self.redirectSubTree(canonical_url(build))
> +
>
> class BuilderSetBreadcrumb(Breadcrumb):
> """Builds a breadcrumb for an `IBuilderSet`."""
>
> === modified file 'lib/lp/buildmaster/enums.py'
> --- lib/lp/buildmaster/enums.py 2013-10-31 07:30:54 +0000
> +++ lib/lp/buildmaster/enums.py 2014-05-27 07:47:13 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
> +# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> """Common build interfaces."""
> @@ -149,6 +149,12 @@
> Generate translation templates from a bazaar branch.
> """)
>
> + LIVEFSBUILD = DBItem(5, """
> + Live filesystem build
> +
> + Build a live filesystem from an archive.
> + """)
> +
>
> class BuildQueueStatus(DBEnumeratedType):
> """Build queue status.
>
> === modified file 'lib/lp/buildmaster/interfaces/buildfarmjobbehaviour.py'
> --- lib/lp/buildmaster/interfaces/buildfarmjobbehaviour.py 2014-02-03 14:43:08 +0000
> +++ lib/lp/buildmaster/interfaces/buildfarmjobbehaviour.py 2014-05-27 07:47:13 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
> +# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> """Interface for build farm job behaviours."""
> @@ -41,6 +41,9 @@
> def getBuildCookie():
> """Return a string which uniquely identifies the job."""
>
> + def verifySuccessfulBuild():
> + """Check that we are allowed to collect this successful build."""
> +
> def handleStatus(bq, status, slave_status):
> """Update the build from a WAITING slave result.
>
>
> === modified file 'lib/lp/buildmaster/model/buildfarmjobbehaviour.py'
> --- lib/lp/buildmaster/model/buildfarmjobbehaviour.py 2014-05-10 18:40:36 +0000
> +++ lib/lp/buildmaster/model/buildfarmjobbehaviour.py 2014-05-27 07:47:13 +0000
> @@ -129,6 +129,17 @@
> self.build.setLog(lfa_id)
> transaction.commit()
>
> + def verifySuccessfulBuild(self):
> + """See `IBuildFarmJobBehaviour`."""
> + build = self.build
> +
> + # Explode before collecting a binary that is denied in this
> + # distroseries/pocket/archive
> + assert build.archive.canModifySuite(
> + build.distro_series, build.pocket), (
> + "%s (%s) can not be built for pocket %s in %s: illegal status"
> + % (build.title, build.id, build.pocket.name, build.archive))
> +
> # The list of build status values for which email notifications are
> # allowed to be sent. It is up to each callback as to whether it will
> # consider sending a notification but it won't do so if the status is not
> @@ -178,12 +189,7 @@
> self.build.buildqueue_record.destroySelf()
> return
>
> - # Explode before collect a binary that is denied in this
> - # distroseries/pocket/archive
> - assert build.archive.canModifySuite(
> - build.distro_series, build.pocket), (
> - "%s (%s) can not be built for pocket %s in %s: illegal status"
> - % (build.title, build.id, build.pocket.name, build.archive))
> + self.verifySuccessfulBuild()
>
> # Ensure we have the correct build root as:
> # <BUILDMASTER_ROOT>/incoming/<UPLOAD_LEAF>/<TARGET_PATH>/[FILES]
>
> === modified file 'lib/lp/buildmaster/tests/test_buildfarmjobbehaviour.py'
> --- lib/lp/buildmaster/tests/test_buildfarmjobbehaviour.py 2014-01-30 15:04:06 +0000
> +++ lib/lp/buildmaster/tests/test_buildfarmjobbehaviour.py 2014-05-27 07:47:13 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2010-2013 Canonical Ltd. This software is licensed under the
> +# Copyright 2010-2014 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> """Unit tests for BuildFarmJobBehaviourBase."""
> @@ -114,6 +114,40 @@
> (self.build.job_type.name, self.build.id), (job_type, job_id))
>
>
> +class TestVerifySuccessfulBuildMixin:
> + """Tests for `IBuildFarmJobBehaviour`'s verifySuccessfulBuild method."""
> +
> + layer = LaunchpadZopelessLayer
> +
> + def makeBuild(self):
> + """Allow classes to override the build with which the test runs."""
> + raise NotImplementedError
> +
> + def makeUnmodifiableBuild(self):
> + """Allow classes to override the build with which the test runs."""
> + raise NotImplementedError
> +
> + def setUp(self):
> + super(TestVerifySuccessfulBuildMixin, self).setUp()
> + self.factory = LaunchpadObjectFactory()
> +
> + def test_verifySuccessfulBuild_allows_modifiable_suite(self):
> + # verifySuccessfulBuild allows uploading to a suite that the archive
> + # says is modifiable.
> + build = self.makeBuild()
> + behaviour = IBuildFarmJobBehaviour(
> + build.buildqueue_record.specific_build)
> + behaviour.verifySuccessfulBuild()
> +
> + def test_verifySuccessfulBuild_denies_unmodifiable_suite(self):
> + # verifySuccessfulBuild refuses to upload to a suite that the
> + # archive says is unmodifiable.
> + build = self.makeUnmodifiableBuild()
> + behaviour = IBuildFarmJobBehaviour(
> + build.buildqueue_record.specific_build)
> + self.assertRaises(AssertionError, behaviour.verifySuccessfulBuild)
> +
> +
> class TestHandleStatusMixin:
> """Tests for `IPackageBuild`s handleStatus method.
>
>
> === modified file 'lib/lp/code/model/tests/test_recipebuilder.py'
> --- lib/lp/code/model/tests/test_recipebuilder.py 2014-01-30 15:04:06 +0000
> +++ lib/lp/code/model/tests/test_recipebuilder.py 2014-05-27 07:47:13 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2010-2013 Canonical Ltd. This software is licensed under the
> +# Copyright 2010-2014 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> """Test RecipeBuildBehaviour."""
> @@ -35,15 +35,18 @@
> from lp.buildmaster.tests.test_buildfarmjobbehaviour import (
> TestGetUploadMethodsMixin,
> TestHandleStatusMixin,
> + TestVerifySuccessfulBuildMixin,
> )
> from lp.code.model.recipebuilder import RecipeBuildBehaviour
> from lp.code.model.sourcepackagerecipebuild import SourcePackageRecipeBuild
> from lp.registry.interfaces.pocket import PackagePublishingPocket
> +from lp.registry.interfaces.series import SeriesStatus
> from lp.services.config import config
> from lp.services.log.logger import BufferLogger
> from lp.soyuz.adapters.archivedependencies import (
> get_sources_list_for_building,
> )
> +from lp.soyuz.enums import ArchivePurpose
> from lp.soyuz.interfaces.processor import IProcessorSet
> from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
> from lp.testing import (
> @@ -399,12 +402,26 @@
> build.queueBuild()
> return build
>
> + def makeUnmodifiableBuild(self):
> + archive = self.factory.makeArchive(purpose=ArchivePurpose.PRIMARY)
> + build = self.factory.makeSourcePackageRecipeBuild(
> + archive=archive, status=BuildStatus.BUILDING)
> + build.distro_series.status = SeriesStatus.CURRENT
> + build.queueBuild()
> + return build
> +
>
> class TestGetUploadMethodsForSPRecipeBuild(
> MakeSPRecipeBuildMixin, TestGetUploadMethodsMixin, TestCaseWithFactory):
> """IPackageBuild.getUpload-related methods work with SPRecipe builds."""
>
>
> +class TestVerifySuccessfulBuildForSPRBuild(
> + MakeSPRecipeBuildMixin, TestVerifySuccessfulBuildMixin,
> + TestCaseWithFactory):
> + """IBuildFarmJobBehaviour.verifySuccessfulBuild works."""
> +
> +
> class TestHandleStatusForSPRBuild(
> MakeSPRecipeBuildMixin, TestHandleStatusMixin, TrialTestCase):
> """IPackageBuild.handleStatus works with SPRecipe builds."""
>
> === modified file 'lib/lp/registry/browser/person.py'
> --- lib/lp/registry/browser/person.py 2014-03-21 03:34:57 +0000
> +++ lib/lp/registry/browser/person.py 2014-05-27 07:47:13 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
> +# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> """Person-related view classes."""
> @@ -264,6 +264,7 @@
> )
> from lp.soyuz.interfaces.archivesubscriber import IArchiveSubscriberSet
> from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuildSet
> +from lp.soyuz.interfaces.livefs import ILiveFSSet
> from lp.soyuz.interfaces.publishing import ISourcePackagePublishingHistory
> from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease
>
> @@ -494,6 +495,29 @@
> """Traverse to this person's merge queues."""
> return self.context.getMergeQueue(name)
>
> + @stepthrough('+livefs')
> + def traverse_livefs(self, distribution_name):
> + """Traverse to this person's live filesystem images."""
> + if len(self.request.stepstogo) < 2:
> + return None
> +
> + distroseries_name = self.request.stepstogo.consume()
> + livefs_name = self.request.stepstogo.consume()
> + livefs = getUtility(ILiveFSSet).interpret(
> + self.context.name, distribution_name, distroseries_name,
> + livefs_name)
> +
> + if livefs is None:
> + raise NotFoundError
> +
> + if livefs.distro_series.distribution.name != distribution_name:
> + # This live filesystem was accessed through one of its
> + # distribution's aliases, so we must redirect to its canonical
> + # URL.
> + return self.redirectSubTree(canonical_url(livefs))
> +
> + return livefs
> +
>
> class PersonSetNavigation(Navigation):
>
>
> === modified file 'lib/lp/registry/interfaces/person.py'
> --- lib/lp/registry/interfaces/person.py 2014-03-11 10:37:23 +0000
> +++ lib/lp/registry/interfaces/person.py 2014-05-27 07:47:13 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
> +# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> """Person interfaces."""
> @@ -1029,7 +1029,7 @@
>
> @call_with(requester=REQUEST_USER)
> @export_read_operation()
> - @operation_returns_collection_of(Interface) # Really IArchiveSubscriber
> + @operation_returns_collection_of(Interface) # Really IArchiveSubscriber
> @operation_for_version('devel')
> def getArchiveSubscriptions(requester):
> """Return (private) archives subscription for this person."""
>
> === modified file 'lib/lp/registry/model/person.py'
> --- lib/lp/registry/model/person.py 2014-03-11 11:34:08 +0000
> +++ lib/lp/registry/model/person.py 2014-05-27 07:47:13 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
> +# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> """Implementation classes for a Person."""
>
> === modified file 'lib/lp/security.py'
> --- lib/lp/security.py 2014-03-17 21:50:33 +0000
> +++ lib/lp/security.py 2014-05-27 07:47:13 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
> +# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> """Security policies for using content objects."""
> @@ -196,6 +196,8 @@
> IBinaryPackageReleaseDownloadCount,
> )
> from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
> +from lp.soyuz.interfaces.livefs import ILiveFS
> +from lp.soyuz.interfaces.livefsbuild import ILiveFSBuild
> from lp.soyuz.interfaces.packagecopyjob import IPlainPackageCopyJob
> from lp.soyuz.interfaces.packageset import (
> IPackageset,
> @@ -2872,3 +2874,46 @@
> sourcepackagename=self.obj.sourcepackagename,
> component=None, strict_component=False)
> return reason is None
> +
> +
> +class ViewLiveFS(DelegatedAuthorization):
> + permission = 'launchpad.View'
> + usedfor = ILiveFS
> +
> + def __init__(self, obj):
> + super(ViewLiveFS, self).__init__(obj, obj.owner, 'launchpad.View')
> +
> +
> +class EditLiveFS(EditByOwnersOrAdmins):
> + usedfor = ILiveFS
> +
> +
> +class ViewLiveFSBuild(DelegatedAuthorization):
> + permission = 'launchpad.View'
> + usedfor = ILiveFSBuild
> +
> + def iter_objects(self):
> + yield self.obj.livefs
> + yield self.obj.archive
> +
> +
> +class EditLiveFSBuild(AdminByBuilddAdmin):
> + permission = 'launchpad.Edit'
> + usedfor = ILiveFSBuild
> +
> + def checkAuthenticated(self, user):
> + """Check edit access for live filesystem builds.
> +
> + Allow admins, buildd admins, the owner of the live filesystem, and
> + the requester of the live filesystem build.
> + """
> + if user.inTeam(self.obj.requester):
> + return True
Is this still useful under the new build request permissions? The requester should always have launchpad.Edit on the LiveFS.
> + auth_livefs = EditLiveFS(self.obj.livefs)
> + if auth_livefs.checkAuthenticated(user):
> + return True
> + return super(EditLiveFSBuild, self).checkAuthenticated(user)
> +
> +
> +class AdminLiveFSBuild(AdminByBuilddAdmin):
> + usedfor = ILiveFSBuild
>
> === modified file 'lib/lp/services/helpers.py'
> --- lib/lp/services/helpers.py 2011-12-23 23:44:59 +0000
> +++ lib/lp/services/helpers.py 2014-05-27 07:47:13 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
> +# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> """Various functions and classes that are useful across different parts of
> @@ -232,6 +232,10 @@
> ".txt": "text/plain",
> # For the build master logs
> ".txt.gz": "text/plain",
> + # For live filesystem builds
> + ".manifest": "text/plain",
> + ".manifest-remove": "text/plain",
> + ".size": "text/plain",
> }
> for ending in ftmap:
> if fname.endswith(ending):
>
> === modified file 'lib/lp/soyuz/adapters/archivedependencies.py'
> --- lib/lp/soyuz/adapters/archivedependencies.py 2013-05-02 00:40:14 +0000
> +++ lib/lp/soyuz/adapters/archivedependencies.py 2014-05-27 07:47:13 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2009 Canonical Ltd. This software is licensed under the
> +# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> """Archive dependencies helper function.
> @@ -119,13 +119,16 @@
> If no ancestry could be found, default to 'universe'.
> """
> primary_archive = archive.distribution.main_archive
> - ancestries = primary_archive.getPublishedSources(
> - name=sourcepackagename,
> - distroseries=distroseries, exact_match=True)
> + if sourcepackagename is None:
> + ancestry = None
> + else:
> + ancestry = primary_archive.getPublishedSources(
> + name=sourcepackagename,
> + distroseries=distroseries, exact_match=True).first()
>
> - try:
> - return ancestries[0].component.name
> - except IndexError:
> + if ancestry is not None:
> + return ancestry.component.name
> + else:
> return 'universe'
>
>
>
> === modified file 'lib/lp/soyuz/browser/archive.py'
> --- lib/lp/soyuz/browser/archive.py 2014-05-06 09:11:14 +0000
> +++ lib/lp/soyuz/browser/archive.py 2014-05-27 07:47:13 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
> +# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> """Browser views for archive."""
> @@ -151,6 +151,7 @@
> )
> from lp.soyuz.interfaces.binarypackagename import IBinaryPackageNameSet
> from lp.soyuz.interfaces.component import IComponentSet
> +from lp.soyuz.interfaces.livefsbuild import ILiveFSBuildSet
> from lp.soyuz.interfaces.packagecopyjob import IPlainPackageCopyJobSource
> from lp.soyuz.interfaces.packagecopyrequest import IPackageCopyRequestSet
> from lp.soyuz.interfaces.packageset import IPackagesetSet
> @@ -249,6 +250,13 @@
> return None
> return build
>
> + @stepthrough('+livefsbuild')
> + def traverse_livefsbuild(self, name):
> + build = get_build_by_id_str(ILiveFSBuildSet, name)
> + if build is None or build.archive != self.context:
> + return None
> + return build
Should this still exist, now that the URL is under the LiveFS? In the unlikely event that it should still exist, it needs to redirect.
> +
> @stepthrough('+sourcepub')
> def traverse_sourcepub(self, name):
> return self._traverse_publication(name, source=True)
>
> === modified file 'lib/lp/soyuz/browser/build.py'
> --- lib/lp/soyuz/browser/build.py 2014-05-06 09:11:14 +0000
> +++ lib/lp/soyuz/browser/build.py 2014-05-27 07:47:13 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
> +# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> """Browser views for builds."""
>
> === modified file 'lib/lp/soyuz/browser/configure.zcml'
> --- lib/lp/soyuz/browser/configure.zcml 2014-04-24 07:30:36 +0000
> +++ lib/lp/soyuz/browser/configure.zcml 2014-05-27 07:47:13 +0000
> @@ -1,4 +1,4 @@
> -<!-- Copyright 2009-2013 Canonical Ltd. This software is licensed under the
> +<!-- Copyright 2009-2014 Canonical Ltd. This software is licensed under the
> GNU Affero General Public License version 3 (see the file LICENSE).
> -->
>
> @@ -728,5 +728,28 @@
> template="../templates/packagerelationship-list.pt"
> />
> </browser:pages>
> + <browser:url
> + for="lp.soyuz.interfaces.livefs.ILiveFS"
> + path_expression="string:+livefs/${distro_series/distribution/name}/${distro_series/name}/${name}"
> + attribute_to_parent="owner"
> + />
> + <browser:navigation
> + module="lp.soyuz.browser.livefs"
> + classes="LiveFSNavigation"
> + />
> + <browser:url
> + for="lp.soyuz.interfaces.livefs.ILiveFSSet"
> + path_expression="string:livefses"
> + parent_utility="lp.services.webapp.interfaces.ILaunchpadRoot"
> + />
> + <browser:url
> + for="lp.soyuz.interfaces.livefsbuild.ILiveFSBuild"
> + path_expression="string:+livefsbuild/${id}"
> + attribute_to_parent="livefs"
> + />
This could just be +build/${id}?
> + <browser:navigation
> + module="lp.soyuz.browser.livefsbuild"
> + classes="LiveFSBuildNavigation"
> + />
> </facet>
> </configure>
>
> === added file 'lib/lp/soyuz/browser/livefs.py'
> --- lib/lp/soyuz/browser/livefs.py 1970-01-01 00:00:00 +0000
> +++ lib/lp/soyuz/browser/livefs.py 2014-05-27 07:47:13 +0000
> @@ -0,0 +1,26 @@
> +# Copyright 2014 Canonical Ltd. This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +__metaclass__ = type
> +__all__ = [
> + 'LiveFSNavigation',
> + ]
> +
> +from lp.services.webapp import (
> + Navigation,
> + stepthrough,
> + )
> +from lp.soyuz.browser.build import get_build_by_id_str
> +from lp.soyuz.interfaces.livefs import ILiveFS
> +from lp.soyuz.interfaces.livefsbuild import ILiveFSBuildSet
> +
> +
> +class LiveFSNavigation(Navigation):
> + usedfor = ILiveFS
> +
> + @stepthrough('+livefsbuild')
> + def traverse_livefsbuild(self, name):
> + build = get_build_by_id_str(ILiveFSBuildSet, name)
> + if build is None or build.livefs != self.context:
> + return None
> + return build
>
> === added file 'lib/lp/soyuz/browser/livefsbuild.py'
> --- lib/lp/soyuz/browser/livefsbuild.py 1970-01-01 00:00:00 +0000
> +++ lib/lp/soyuz/browser/livefsbuild.py 2014-05-27 07:47:13 +0000
> @@ -0,0 +1,15 @@
> +# Copyright 2014 Canonical Ltd. This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +__metaclass__ = type
> +__all__ = [
> + 'LiveFSBuildNavigation',
> + ]
> +
> +from lp.services.librarian.browser import FileNavigationMixin
> +from lp.services.webapp import Navigation
> +from lp.soyuz.interfaces.livefsbuild import ILiveFSBuild
> +
> +
> +class LiveFSBuildNavigation(Navigation, FileNavigationMixin):
> + usedfor = ILiveFSBuild
>
> === added file 'lib/lp/soyuz/browser/tests/test_livefs.py'
> --- lib/lp/soyuz/browser/tests/test_livefs.py 1970-01-01 00:00:00 +0000
> +++ lib/lp/soyuz/browser/tests/test_livefs.py 2014-05-27 07:47:13 +0000
> @@ -0,0 +1,29 @@
> +# Copyright 2014 Canonical Ltd. This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Test live filesystem navigation."""
> +
> +__metaclass__ = type
> +
> +from lp.services.features.testing import FeatureFixture
> +from lp.soyuz.interfaces.livefs import LIVEFS_FEATURE_FLAG
> +from lp.testing import TestCaseWithFactory
> +from lp.testing.layers import DatabaseFunctionalLayer
> +from lp.testing.publication import test_traverse
> +
> +
> +class TestLiveFSNavigation(TestCaseWithFactory):
> +
> + layer = DatabaseFunctionalLayer
> +
> + def setUp(self):
> + super(TestLiveFSNavigation, self).setUp()
> + self.useFixture(FeatureFixture({LIVEFS_FEATURE_FLAG: u"on"}))
> +
> + def test_livefs(self):
> + livefs = self.factory.makeLiveFS()
> + obj, _, _ = test_traverse(
> + "http://api.launchpad.dev/devel/~%s/+livefs/%s/%s/%s" % (
> + livefs.owner.name, livefs.distro_series.distribution.name,
> + livefs.distro_series.name, livefs.name))
> + self.assertEqual(livefs, obj)
>
> === modified file 'lib/lp/soyuz/configure.zcml'
> --- lib/lp/soyuz/configure.zcml 2014-02-18 11:40:52 +0000
> +++ lib/lp/soyuz/configure.zcml 2014-05-27 07:47:13 +0000
> @@ -1,4 +1,4 @@
> -<!-- Copyright 2009-2013 Canonical Ltd. This software is licensed under the
> +<!-- Copyright 2009-2014 Canonical Ltd. This software is licensed under the
> GNU Affero General Public License version 3 (see the file LICENSE).
> -->
>
> @@ -975,6 +975,68 @@
> <allow
> interface=".interfaces.packagetranslationsuploadjob.IPackageTranslationsUploadJob" />
> </class>
> +
> + <!-- LiveFS -->
> + <class class=".model.livefs.LiveFS">
> + <require
> + permission="launchpad.View"
> + interface=".interfaces.livefs.ILiveFSView
> + .interfaces.livefs.ILiveFSEditableAttributes"/>
> + <require
> + permission="launchpad.Edit"
> + set_schema=".interfaces.livefs.ILiveFSEditableAttributes"
> + set_attributes="date_last_modified"/>
> + </class>
> + <subscriber
> + for=".interfaces.livefs.ILiveFS zope.lifecycleevent.interfaces.IObjectModifiedEvent"
> + handler=".model.livefs.livefs_modified"/>
> +
> + <!-- LiveFSSet -->
> + <securedutility
> + class=".model.livefs.LiveFSSet"
> + provides=".interfaces.livefs.ILiveFSSet">
> + <allow interface=".interfaces.livefs.ILiveFSSet"/>
> + </securedutility>
> +
> + <!-- LiveFSBuild -->
> + <class class=".model.livefsbuild.LiveFSBuild">
> + <require
> + permission="launchpad.View"
> + interface=".interfaces.livefsbuild.ILiveFSBuildView"/>
> + <require
> + permission="launchpad.Edit"
> + interface=".interfaces.livefsbuild.ILiveFSBuildEdit"/>
> + <require
> + permission="launchpad.Admin"
> + interface=".interfaces.livefsbuild.ILiveFSBuildAdmin"/>
> + </class>
> +
> + <!-- LiveFSBuildSet -->
> + <securedutility
> + class=".model.livefsbuild.LiveFSBuildSet"
> + provides=".interfaces.livefsbuild.ILiveFSBuildSet">
> + <allow interface=".interfaces.livefsbuild.ILiveFSBuildSet"/>
> + </securedutility>
> + <securedutility
> + class=".model.livefsbuild.LiveFSBuildSet"
> + provides="lp.buildmaster.interfaces.buildfarmjob.ISpecificBuildFarmJobSource"
> + name="LIVEFSBUILD">
> + <allow interface="lp.buildmaster.interfaces.buildfarmjob.ISpecificBuildFarmJobSource"/>
> + </securedutility>
> +
> + <!-- LiveFSBuildBehaviour -->
> + <adapter
> + for=".interfaces.livefsbuild.ILiveFSBuild"
> + provides="lp.buildmaster.interfaces.buildfarmjobbehaviour.IBuildFarmJobBehaviour"
> + factory=".model.livefsbuildbehaviour.LiveFSBuildBehaviour"
> + permission="zope.Public"/>
> +
> + <!-- LiveFSFile -->
> + <class class=".model.livefsbuild.LiveFSFile">
> + <allow
> + interface=".interfaces.livefsbuild.ILiveFSFile"/>
> + </class>
> +
> <webservice:register module="lp.soyuz.interfaces.webservice" />
>
> </configure>
>
> === added file 'lib/lp/soyuz/emailtemplates/livefsbuild-notification.txt'
> --- lib/lp/soyuz/emailtemplates/livefsbuild-notification.txt 1970-01-01 00:00:00 +0000
> +++ lib/lp/soyuz/emailtemplates/livefsbuild-notification.txt 2014-05-27 07:47:13 +0000
> @@ -0,0 +1,11 @@
> + * Live Filesystem: %(livefs_name)s
> + * Version: %(version)s
> + * Archive: %(archive_tag)s
> + * Distroseries: %(distroseries)s
> + * Architecture: %(architecturetag)s
> + * Pocket: %(pocket)s
> + * State: %(build_state)s
> + * Duration: %(build_duration)s
> + * Build Log: %(log_url)s
> + * Upload Log: %(upload_log_url)s
> + * Builder: %(builder_url)s
>
> === modified file 'lib/lp/soyuz/interfaces/binarypackagebuild.py'
> --- lib/lp/soyuz/interfaces/binarypackagebuild.py 2013-11-26 01:36:10 +0000
> +++ lib/lp/soyuz/interfaces/binarypackagebuild.py 2014-05-27 07:47:13 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
> +# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> """BinaryPackageBuild interfaces."""
> @@ -233,8 +233,8 @@
> def cancel():
> """Cancel the build if it is either pending or in progress.
>
> - Call the can_be_cancelled() method prior to this one to find out if
> - cancelling the build is possible.
> + Check the can_be_cancelled property prior to calling this method to
> + find out if cancelling the build is possible.
>
> If the build is in progress, it is marked as CANCELLING until the
> buildd manager terminates the build and marks it CANCELLED. If the
>
> === added file 'lib/lp/soyuz/interfaces/livefs.py'
> --- lib/lp/soyuz/interfaces/livefs.py 1970-01-01 00:00:00 +0000
> +++ lib/lp/soyuz/interfaces/livefs.py 2014-05-27 07:47:13 +0000
> @@ -0,0 +1,253 @@
> +# Copyright 2014 Canonical Ltd. This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Live filesystem interfaces."""
> +
> +__metaclass__ = type
> +
> +__all__ = [
> + 'DuplicateLiveFSName',
> + 'ILiveFS',
> + 'ILiveFSEditableAttributes',
> + 'ILiveFSSet',
> + 'ILiveFSView',
> + 'LIVEFS_FEATURE_FLAG',
> + 'LiveFSBuildAlreadyPending',
> + 'LiveFSFeatureDisabled',
> + 'LiveFSNotOwner',
> + 'NoSuchLiveFS',
> + ]
> +
> +import httplib
> +
> +from lazr.lifecycle.snapshot import doNotSnapshot
> +from lazr.restful.declarations import (
> + call_with,
> + collection_default_content,
> + error_status,
> + export_as_webservice_collection,
> + export_as_webservice_entry,
> + export_factory_operation,
> + export_read_operation,
> + exported,
> + operation_for_version,
> + operation_parameters,
> + operation_returns_entry,
> + REQUEST_USER,
> + )
> +from lazr.restful.fields import (
> + CollectionField,
> + Reference,
> + )
> +from zope.interface import Interface
> +from zope.schema import (
> + Choice,
> + Datetime,
> + Dict,
> + Int,
> + TextLine,
> + )
> +from zope.security.interfaces import Unauthorized
> +
> +from lp import _
> +from lp.app.errors import NameLookupFailed
> +from lp.app.validators.name import name_validator
> +from lp.registry.interfaces.distroseries import IDistroSeries
> +from lp.registry.interfaces.pocket import PackagePublishingPocket
> +from lp.registry.interfaces.person import IPerson
> +from lp.registry.interfaces.role import IHasOwner
> +from lp.services.fields import (
> + PersonChoice,
> + PublicPersonChoice,
> + )
> +from lp.soyuz.interfaces.archive import IArchive
> +from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
> +
> +
> +LIVEFS_FEATURE_FLAG = u"soyuz.livefs.allow_new"
> +
> +
> +@error_status(httplib.BAD_REQUEST)
> +class LiveFSBuildAlreadyPending(Exception):
> + """A build was requested when an identical build was already pending."""
> +
> + def __init__(self):
> + super(LiveFSBuildAlreadyPending, self).__init__(
> + "An identical build of this live filesystem image is already "
> + "pending.")
> +
> +
> +@error_status(httplib.UNAUTHORIZED)
> +class LiveFSFeatureDisabled(Unauthorized):
> + """Only certain users can create new LiveFS-related objects."""
> +
> + def __init__(self):
> + super(LiveFSFeatureDisabled, self).__init__(
> + "You do not have permission to create new live filesystems or "
> + "new live filesystem builds.")
> +
> +
> +@error_status(httplib.BAD_REQUEST)
> +class DuplicateLiveFSName(Exception):
> + """Raised for live filesystems with duplicate name/owner/distroseries."""
> +
> + def __init__(self):
> + super(DuplicateLiveFSName, self).__init__(
> + "There is already a live filesystem with the same name, owner, "
> + "and distroseries.")
> +
> +
> +@error_status(httplib.UNAUTHORIZED)
> +class LiveFSNotOwner(Unauthorized):
> + """The registrant/requester is not the owner or a member of its team."""
> +
> +
> +class NoSuchLiveFS(NameLookupFailed):
> + """The requested LiveFS does not exist."""
> + _message_prefix = "No such live filesystem with this owner/distroseries"
> +
> +
> +class ILiveFSView(Interface):
> + """`ILiveFS` attributes that require launchpad.View permission."""
> +
> + id = exported(Int(title=_("ID"), required=True, readonly=True))
> +
> + date_created = exported(Datetime(
> + title=_("Date created"), required=True, readonly=True))
> +
> + registrant = exported(PublicPersonChoice(
> + title=_("Registrant"), required=True, readonly=True,
> + vocabulary="ValidPersonOrTeam",
> + description=_(
> + "The person who registered this live filesystem image.")))
> +
> + @call_with(requester=REQUEST_USER)
> + @operation_parameters(
> + archive=Reference(schema=IArchive),
> + distro_arch_series=Reference(schema=IDistroArchSeries),
> + pocket=Choice(vocabulary=PackagePublishingPocket),
> + unique_key=TextLine(
> + title=_("A unique key for this build, if required."),
> + required=False),
> + metadata_override=Dict(
> + title=_("A dict of data about the image."),
> + key_type=TextLine(), required=False))
> + # Really ILiveFSBuild, patched in _schema_circular_imports.py.
> + @export_factory_operation(Interface, [])
> + @operation_for_version("devel")
> + def requestBuild(requester, archive, distro_arch_series, pocket,
> + unique_key=None, metadata_override=None):
> + """Request that the live filesystem be built.
> +
> + :param requester: The person requesting the build.
> + :param archive: The IArchive to associate the build with.
> + :param distro_arch_series: The architecture to build for.
> + :param pocket: The pocket that should be targeted.
> + :param unique_key: An optional unique key for this build; if set,
> + this identifies a class of builds for this live filesystem.
> + :param metadata_override: An optional JSON string with a dict of
> + data about the image; this will be merged into the metadata dict
> + for the live filesystem.
> + :return: `ILiveFSBuild`.
> + """
> +
> + builds = exported(doNotSnapshot(CollectionField(
> + title=_("All builds of this live filesystem."),
> + description=_(
> + "All builds of this live filesystem, sorted in descending order "
> + "of finishing (or starting if not completed successfully)."),
> + # Really ILiveFSBuild, patched in _schema_circular_imports.py.
> + value_type=Reference(schema=Interface), readonly=True)))
> +
> + completed_builds = exported(doNotSnapshot(CollectionField(
> + title=_("Completed builds of this live filesystem."),
> + description=_(
> + "Completed builds of this live filesystem, sorted in descending "
> + "order of finishing."),
> + # Really ILiveFSBuild, patched in _schema_circular_imports.py.
> + value_type=Reference(schema=Interface), readonly=True)))
> +
> + pending_builds = exported(doNotSnapshot(CollectionField(
> + title=_("Pending builds of this live filesystem."),
> + description=_(
> + "Pending builds of this live filesystem, sorted in descending "
> + "order of creation."),
> + # Really ILiveFSBuild, patched in _schema_circular_imports.py.
> + value_type=Reference(schema=Interface), readonly=True)))
> +
> +
> +class ILiveFSEditableAttributes(IHasOwner):
> + """`ILiveFS` attributes that can be edited.
> +
> + These attributes need launchpad.View to see, and launchpad.Edit to change.
> + """
> + date_last_modified = exported(Datetime(
> + title=_("Date last modified"), required=True, readonly=True))
> +
> + owner = exported(PersonChoice(
> + title=_("Owner"), required=True, readonly=False,
> + vocabulary="AllUserTeamsParticipationPlusSelf",
> + description=_("The owner of this live filesystem image.")))
> +
> + distro_series = exported(Reference(
> + IDistroSeries, title=_("Distro Series"), required=True, readonly=False,
> + description=_("The series for which the image should be built.")))
> +
> + name = exported(TextLine(
> + title=_("Name"), required=True, readonly=False,
> + constraint=name_validator,
> + description=_("The name of the live filesystem image.")))
> +
> + metadata = exported(Dict(
> + title=_(
> + "A dict of data about the image. Entries here will be passed to "
> + "the builder slave."),
> + key_type=TextLine(), required=True, readonly=False))
> +
> +
> +class ILiveFS(ILiveFSView, ILiveFSEditableAttributes):
> + """A buildable live filesystem image."""
> +
> + # XXX cjwatson 2014-05-06 bug=760849: "beta" is a lie to get WADL
> + # generation working. Individual attributes must set their version to
> + # "devel".
> + export_as_webservice_entry(
> + singular_name="livefs", plural_name="livefses", as_of="beta")
> +
> +
> +class ILiveFSSet(Interface):
> + """A utility to create and access live filesystems."""
> +
> + export_as_webservice_collection(ILiveFS)
> +
> + @call_with(registrant=REQUEST_USER)
> + @export_factory_operation(
> + ILiveFS, ["owner", "distro_series", "name", "metadata"])
> + @operation_for_version("devel")
> + def new(registrant, owner, distro_series, name, metadata,
> + date_created=None):
> + """Create an `ILiveFS`."""
> +
> + def exists(owner, distro_series, name):
> + """Check to see if a matching live filesystem exists."""
> +
> + @operation_parameters(
> + owner=Reference(IPerson, title=_("Owner"), required=True),
> + distro_series=Reference(
> + IDistroSeries, title=_("Distroseries"), required=True),
> + name=TextLine(title=_("Live filesystem name"), required=True))
> + @operation_returns_entry(ILiveFS)
> + @export_read_operation()
> + @operation_for_version("devel")
> + def getByName(owner, distro_series, name):
> + """Return the appropriate `ILiveFS` for the given objects."""
> +
> + def interpret(owner_name, distribution_name, distro_series_name, name):
> + """Like `getByName`, but takes names of objects."""
> +
> + @collection_default_content()
> + def getAll():
> + """Return all of the live filesystems in Launchpad.
> +
> + :return: A (potentially empty) sequence of `ILiveFS` instances.
> + """
>
> === added file 'lib/lp/soyuz/interfaces/livefsbuild.py'
> --- lib/lp/soyuz/interfaces/livefsbuild.py 1970-01-01 00:00:00 +0000
> +++ lib/lp/soyuz/interfaces/livefsbuild.py 2014-05-27 07:47:13 +0000
> @@ -0,0 +1,199 @@
> +# Copyright 2014 Canonical Ltd. This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Live filesystem build interfaces."""
> +
> +__metaclass__ = type
> +
> +__all__ = [
> + 'ILiveFSBuild',
> + 'ILiveFSBuildSet',
> + 'ILiveFSFile',
> + ]
> +
> +from lazr.restful.declarations import (
> + export_as_webservice_entry,
> + export_read_operation,
> + export_write_operation,
> + exported,
> + operation_for_version,
> + operation_parameters,
> + )
> +from lazr.restful.fields import Reference
> +from zope.interface import Interface
> +from zope.schema import (
> + Bool,
> + Choice,
> + Dict,
> + Int,
> + TextLine,
> + )
> +
> +from lp import _
> +from lp.buildmaster.interfaces.buildfarmjob import ISpecificBuildFarmJobSource
> +from lp.buildmaster.interfaces.packagebuild import IPackageBuild
> +from lp.registry.interfaces.person import IPerson
> +from lp.registry.interfaces.pocket import PackagePublishingPocket
> +from lp.services.database.constants import DEFAULT
> +from lp.services.librarian.interfaces import ILibraryFileAlias
> +from lp.soyuz.interfaces.archive import IArchive
> +from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
> +from lp.soyuz.interfaces.livefs import ILiveFS
> +
> +
> +class ILiveFSFile(Interface):
> + """A file produced by a live filesystem build."""
> +
> + livefsbuild = Reference(
> + # Really ILiveFSBuild, patched in _schema_circular_imports.py.
> + Interface,
> + title=_("The live filesystem build producing this file."),
> + required=True, readonly=True)
> + libraryfile = Reference(
> + ILibraryFileAlias, title=_("The library file alias for this file."),
> + required=True, readonly=True)
> +
> +
> +class ILiveFSBuildView(IPackageBuild):
> + """`ILiveFSBuild` attributes that require launchpad.View permission."""
> +
> + requester = exported(Reference(
> + IPerson,
> + title=_("The person who requested this build."),
> + required=True, readonly=True))
> +
> + livefs = exported(Reference(
> + ILiveFS,
> + title=_("The live filesystem to build."),
> + required=True, readonly=True))
> +
> + archive = exported(Reference(
> + IArchive,
> + title=_("The archive from which to build the live filesystem."),
> + required=True, readonly=True))
> +
> + distro_arch_series = exported(Reference(
> + IDistroArchSeries,
> + title=_("The series and architecture for which to build."),
> + required=True, readonly=True))
> +
> + pocket = exported(Choice(
> + title=_("The pocket for which to build."),
> + vocabulary=PackagePublishingPocket, required=True, readonly=True))
> +
> + unique_key = exported(TextLine(
> + title=_(
> + "An optional unique key; if set, this identifies a class of "
> + "builds for this live filesystem."),
> + required=False, readonly=True))
> +
> + metadata_override = exported(Dict(
> + title=_(
> + "A dict of data about the image; this will be merged into the "
> + "metadata dict for the live filesystem."),
> + key_type=TextLine(), required=False, readonly=True))
> +
> + is_virtualized = Bool(
> + title=_("If True, this build is virtualized."), readonly=True)
> +
> + version = exported(TextLine(
> + title=_("A timestamp-based version identifying this build."),
> + required=True, readonly=True))
> +
> + score = exported(Int(
> + title=_("Score of the related build farm job (if any)."),
> + required=False, readonly=True))
> +
> + can_be_rescored = exported(Bool(
> + title=_("Can be rescored"),
> + required=True, readonly=True,
> + description=_("Whether this build record can be rescored manually.")))
> +
> + can_be_cancelled = exported(Bool(
> + title=_("Can be cancelled"),
> + required=True, readonly=True,
> + description=_("Whether this build record can be cancelled.")))
> +
> + def getFiles():
> + """Retrieve the build's `ILiveFSFile` records.
> +
> + :return: A result set of (`ILiveFSFile`, `ILibraryFileAlias`,
> + `ILibraryFileContent`).
> + """
> +
> + def getFileByName(filename):
> + """Return the corresponding `ILibraryFileAlias` in this context.
> +
> + The following file types (and extension) can be looked up:
> +
> + * Build log: '.txt.gz'
> + * Upload log: '_log.txt'
> +
> + Any filename not matching one of these extensions is looked up as a
> + live filesystem output file.
> +
> + :param filename: The filename to look up.
> + :raises NotFoundError: if no file exists with the given name.
> + :return: The corresponding `ILibraryFileAlias`.
> + """
> +
> + @export_read_operation()
> + @operation_for_version("devel")
> + def getFileUrls():
> + """URLs for all the files produced by this build.
> +
> + :return: A collection of URLs for this build."""
> +
> +
> +class ILiveFSBuildEdit(Interface):
> + """`ILiveFSBuild` attributes that require launchpad.Edit."""
> +
> + def addFile(lfa):
> + """Add a file to this build.
> +
> + :param lfa: An `ILibraryFileAlias`.
> + :return: An `ILiveFSFile`.
> + """
> +
> + @export_write_operation()
> + @operation_for_version("devel")
> + def cancel():
> + """Cancel the build if it is either pending or in progress.
> +
> + Check the can_be_cancelled property prior to calling this method to
> + find out if cancelling the build is possible.
> +
> + If the build is in progress, it is marked as CANCELLING until the
> + buildd manager terminates the build and marks it CANCELLED. If the
> + build is not in progress, it is marked CANCELLED immediately and is
> + removed from the build queue.
> +
> + If the build is not in a cancellable state, this method is a no-op.
> + """
> +
> +
> +class ILiveFSBuildAdmin(Interface):
> + """`ILiveFSBuild` attributes that require launchpad.Admin."""
> +
> + @operation_parameters(score=Int(title=_("Score"), required=True))
> + @export_write_operation()
> + @operation_for_version("devel")
> + def rescore(score):
> + """Change the build's score."""
> +
> +
> +class ILiveFSBuild(ILiveFSBuildView, ILiveFSBuildEdit, ILiveFSBuildAdmin):
> + """Build information for live filesystem builds."""
> +
> + # XXX cjwatson 2014-05-06 bug=760849: "beta" is a lie to get WADL
> + # generation working. Individual attributes must set their version to
> + # "devel".
> + export_as_webservice_entry(singular_name="livefs_build", as_of="beta")
> +
> +
> +class ILiveFSBuildSet(ISpecificBuildFarmJobSource):
> + """Utility for `ILiveFSBuild`."""
> +
> + def new(requester, livefs, archive, distro_arch_series, pocket,
> + unique_key=None, metadata_override=None, date_created=DEFAULT):
> + """Create an `ILiveFSBuild`."""
>
> === modified file 'lib/lp/soyuz/interfaces/webservice.py'
> --- lib/lp/soyuz/interfaces/webservice.py 2013-09-13 07:09:55 +0000
> +++ lib/lp/soyuz/interfaces/webservice.py 2014-05-27 07:47:13 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
> +# Copyright 2010-2014 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> """All the interfaces that are exposed through the webservice.
> @@ -29,6 +29,9 @@
> 'IBinaryPackagePublishingHistory',
> 'IBinaryPackageReleaseDownloadCount',
> 'IDistroArchSeries',
> + 'ILiveFS',
> + 'ILiveFSBuild',
> + 'ILiveFSSet',
> 'IPackageUpload',
> 'IPackageset',
> 'IPackagesetSet',
> @@ -86,6 +89,11 @@
> )
> from lp.soyuz.interfaces.buildrecords import IncompatibleArguments
> from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
> +from lp.soyuz.interfaces.livefs import (
> + ILiveFS,
> + ILiveFSSet,
> + )
> +from lp.soyuz.interfaces.livefsbuild import ILiveFSBuild
> from lp.soyuz.interfaces.packageset import (
> DuplicatePackagesetName,
> IPackageset,
>
> === added file 'lib/lp/soyuz/mail/livefsbuild.py'
> --- lib/lp/soyuz/mail/livefsbuild.py 1970-01-01 00:00:00 +0000
> +++ lib/lp/soyuz/mail/livefsbuild.py 2014-05-27 07:47:13 +0000
> @@ -0,0 +1,89 @@
> +# Copyright 2014 Canonical Ltd. This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +__metaclass__ = type
> +__all__ = [
> + 'LiveFSBuildMailer',
> + ]
> +
> +from lp.app.browser.tales import DurationFormatterAPI
> +from lp.archivepublisher.utils import get_ppa_reference
> +from lp.services.config import config
> +from lp.services.mail.basemailer import (
> + BaseMailer,
> + RecipientReason,
> + )
> +from lp.services.webapp import canonical_url
> +
> +
> +class LiveFSBuildMailer(BaseMailer):
> +
> + app = 'soyuz'
> +
> + @classmethod
> + def forStatus(cls, build):
> + """Create a mailer for notifying about live filesystem build status.
> +
> + :param build: The relevant build.
> + """
> + requester = build.requester
> + recipients = {requester: RecipientReason.forBuildRequester(requester)}
> + return cls(
> + "[LiveFS build #%(build_id)d] %(build_title)s",
> + "livefsbuild-notification.txt", recipients,
> + config.canonical.noreply_from_address, build)
> +
> + def __init__(self, subject, template_name, recipients, from_address,
> + build):
> + super(LiveFSBuildMailer, self).__init__(
> + subject, template_name, recipients, from_address,
> + notification_type="livefs-build-status")
> + self.build = build
> +
> + def _getHeaders(self, email):
> + """See `BaseMailer`."""
> + headers = super(LiveFSBuildMailer, self)._getHeaders(email)
> + headers["X-Launchpad-Build-State"] = self.build.status.name
> + return headers
> +
> + def _getTemplateParams(self, email, recipient):
> + """See `BaseMailer`."""
> + build = self.build
> + params = super(LiveFSBuildMailer, self)._getTemplateParams(
> + email, recipient)
> + params.update({
> + "build_id": build.id,
> + "build_title": build.title,
> + "livefs_name": build.livefs.name,
> + "version": build.version,
> + "distroseries": build.livefs.distro_series,
> + "architecturetag": build.distro_arch_series.architecturetag,
> + "pocket": build.pocket.name,
> + "build_state": build.status.title,
> + "build_duration": "",
> + "log_url": "",
> + "upload_log_url": "",
> + "builder_url": "",
> + "build_url": canonical_url(self.build),
> + })
> + if build.archive.is_ppa:
> + archive_tag = "%s PPA" % get_ppa_reference(build.archive)
> + else:
> + archive_tag = "%s primary archive" % (
> + build.archive.distribution.name)
> + params["archive_tag"] = archive_tag
> + if build.duration is not None:
> + duration_formatter = DurationFormatterAPI(build.duration)
> + params["build_duration"] = duration_formatter.approximateduration()
> + if build.log is not None:
> + params["log_url"] = build.log_url
> + if build.upload_log is not None:
> + params["upload_log_url"] = build.upload_log_url
> + if build.builder is not None:
> + params["builder_url"] = canonical_url(build.builder)
> + return params
> +
> + def _getFooter(self, params):
> + """See `BaseMailer`."""
> + return ("%(build_url)s\n"
> + "%(reason)s\n" % params)
>
> === added file 'lib/lp/soyuz/model/livefs.py'
> --- lib/lp/soyuz/model/livefs.py 1970-01-01 00:00:00 +0000
> +++ lib/lp/soyuz/model/livefs.py 2014-05-27 07:47:13 +0000
> @@ -0,0 +1,256 @@
> +# Copyright 2014 Canonical Ltd. This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +__metaclass__ = type
> +__all__ = [
> + 'LiveFS',
> + ]
> +
> +import pytz
> +from storm.exceptions import IntegrityError
> +from storm.locals import (
> + DateTime,
> + Desc,
> + Int,
> + JSON,
> + Reference,
> + Store,
> + Storm,
> + Unicode,
> + )
> +from zope.component import getUtility
> +from zope.interface import implements
> +
> +from lp.buildmaster.enums import BuildStatus
> +from lp.registry.errors import NoSuchDistroSeries
> +from lp.registry.interfaces.distribution import (
> + IDistributionSet,
> + NoSuchDistribution,
> + )
> +from lp.registry.interfaces.distroseries import IDistroSeriesSet
> +from lp.registry.interfaces.person import (
> + IPersonSet,
> + NoSuchPerson,
> + )
> +from lp.registry.interfaces.role import IHasOwner
> +from lp.services.database.constants import (
> + DEFAULT,
> + UTC_NOW,
> + )
> +from lp.services.database.interfaces import (
> + IMasterStore,
> + IStore,
> + )
> +from lp.services.database.stormexpr import Greatest
> +from lp.services.features import getFeatureFlag
> +from lp.soyuz.interfaces.livefs import (
> + DuplicateLiveFSName,
> + ILiveFS,
> + ILiveFSSet,
> + LIVEFS_FEATURE_FLAG,
> + LiveFSBuildAlreadyPending,
> + LiveFSFeatureDisabled,
> + LiveFSNotOwner,
> + NoSuchLiveFS,
> + )
> +from lp.soyuz.interfaces.livefsbuild import ILiveFSBuildSet
> +from lp.soyuz.model.archive import Archive
> +from lp.soyuz.model.livefsbuild import LiveFSBuild
> +
> +
> +def livefs_modified(livefs, event):
> + """Update the date_last_modified property when a LiveFS is modified.
> +
> + This method is registered as a subscriber to `IObjectModifiedEvent`
> + events on live filesystems.
> + """
> + livefs.date_last_modified = UTC_NOW
> +
> +
> +class LiveFS(Storm):
> + """See `ILiveFS`."""
> +
> + __storm_table__ = 'LiveFS'
> +
> + implements(ILiveFS, IHasOwner)
> +
> + id = Int(primary=True)
> +
> + date_created = DateTime(
> + name='date_created', tzinfo=pytz.UTC, allow_none=False)
> + date_last_modified = DateTime(
> + name='date_last_modified', tzinfo=pytz.UTC, allow_none=False)
> +
> + registrant_id = Int(name='registrant', allow_none=False)
> + registrant = Reference(registrant_id, 'Person.id')
> +
> + owner_id = Int(name='owner', allow_none=False)
> + owner = Reference(owner_id, 'Person.id')
> +
> + distro_series_id = Int(name='distro_series', allow_none=False)
> + distro_series = Reference(distro_series_id, 'DistroSeries.id')
> +
> + name = Unicode(name='name', allow_none=False)
> +
> + metadata = JSON('json_data')
> +
> + def __init__(self, registrant, owner, distro_series, name, metadata,
> + date_created):
> + """Construct a `LiveFS`."""
> + if not getFeatureFlag(LIVEFS_FEATURE_FLAG):
> + raise LiveFSFeatureDisabled
> + super(LiveFS, self).__init__()
> + self.registrant = registrant
> + self.owner = owner
> + self.distro_series = distro_series
> + self.name = name
> + self.metadata = metadata
> + self.date_created = date_created
> + self.date_last_modified = date_created
> +
> + def requestBuild(self, requester, archive, distro_arch_series, pocket,
> + unique_key=None, metadata_override=None):
> + """See `ILiveFS`."""
> + if not requester.inTeam(self.owner):
> + raise LiveFSNotOwner(
> + "%s cannot create live filesystem builds owned by %s." %
> + (requester.displayname, self.owner.displayname))
This needs to perform some kind of check that somebody relevant can see the Archive. Archive.addArchiveDependency does a similar check (using check_permission in the model 😱; separately we probably want to replace that with a build-time Archive.owner membership check).
It might want to just verify that LiveFS.owner can see the Archive. But we probably want to rerun that check during dispatch as well.
This also has interesting interactions with private ArchiveDependencies, as being able to see a dependent archive would let you run a LiveFSBuild with the dependency in ources.list and thus obtain its buildd_secret.
> +
> + pending = IStore(self).find(
> + LiveFSBuild,
> + LiveFSBuild.livefs_id == self.id,
> + LiveFSBuild.archive_id == archive.id,
> + LiveFSBuild.distro_arch_series_id == distro_arch_series.id,
> + LiveFSBuild.pocket == pocket,
> + LiveFSBuild.unique_key == unique_key,
> + LiveFSBuild.status == BuildStatus.NEEDSBUILD)
> + if pending.any() is not None:
> + raise LiveFSBuildAlreadyPending
> +
> + build = getUtility(ILiveFSBuildSet).new(
> + requester, self, archive, distro_arch_series, pocket,
> + unique_key=unique_key, metadata_override=metadata_override)
> + build.queueBuild()
> + return build
> +
> + def _getBuilds(self, filter_term, order_by):
> + """The actual query to get the builds."""
> + query_args = [
> + LiveFSBuild.livefs == self,
> + LiveFSBuild.archive_id == Archive.id,
> + Archive._enabled == True,
> + ]
> + if filter_term is not None:
> + query_args.append(filter_term)
> + result = Store.of(self).find(LiveFSBuild, *query_args)
> + result.order_by(order_by)
> + return result
This probably wants to do security filtering on the archive. get_archive_privacy_filter or get_enabled_archive_filter might be your friends.
> +
> + @property
> + def builds(self):
> + """See `ILiveFS`."""
> + order_by = (
> + Desc(Greatest(
> + LiveFSBuild.date_started,
> + LiveFSBuild.date_finished)),
> + Desc(LiveFSBuild.date_created),
> + Desc(LiveFSBuild.id))
> + return self._getBuilds(None, order_by)
> +
> + @property
> + def completed_builds(self):
> + """See `ILiveFS`."""
> + filter_term = (LiveFSBuild.status != BuildStatus.NEEDSBUILD)
I see that SourcePackageRecipe.completed_builds does this as well, but status != NEEDSBUILD hardly implies completion.
> + order_by = (
> + Desc(Greatest(
> + LiveFSBuild.date_started,
> + LiveFSBuild.date_finished)),
> + Desc(LiveFSBuild.id))
> + return self._getBuilds(filter_term, order_by)
> +
> + @property
> + def pending_builds(self):
> + """See `ILiveFS`."""
> + filter_term = (LiveFSBuild.status == BuildStatus.NEEDSBUILD)
> + # We want to order by date_created but this is the same as ordering
> + # by id (since id increases monotonically) and is less expensive.
> + order_by = Desc(LiveFSBuild.id)
> + return self._getBuilds(filter_term, order_by)
> +
> +
> +class LiveFSSet:
> + """See `ILiveFSSet`."""
> +
> + implements(ILiveFSSet)
> +
> + def new(self, registrant, owner, distro_series, name, metadata,
> + date_created=DEFAULT):
> + """See `ILiveFSSet`."""
> + if not registrant.inTeam(owner):
> + if owner.is_team:
> + raise LiveFSNotOwner(
> + "%s is not a member of %s." %
> + (registrant.displayname, owner.displayname))
> + else:
> + raise LiveFSNotOwner(
> + "%s cannot create live filesystems owned by %s." %
> + (registrant.displayname, owner.displayname))
> +
> + store = IMasterStore(LiveFS)
> + livefs = LiveFS(
> + registrant, owner, distro_series, name, metadata, date_created)
> + store.add(livefs)
> +
> + try:
> + store.flush()
> + except IntegrityError:
> + raise DuplicateLiveFSName
> +
> + return livefs
> +
> + def _getByName(self, owner, distro_series, name):
> + store = IStore(LiveFS)
> + return store.find(
> + LiveFS,
> + LiveFS.owner == owner,
> + LiveFS.distro_series == distro_series,
> + LiveFS.name == name).one()
> +
> + def exists(self, owner, distro_series, name):
> + """See `ILiveFSSet`."""
> + return self._getByName(owner, distro_series, name) is not None
> +
> + def getByName(self, owner, distro_series, name):
> + """See `ILiveFSSet`."""
> + livefs = self._getByName(owner, distro_series, name)
> + if livefs is None:
> + raise NoSuchLiveFS(name)
> + return livefs
> +
> + def _findOrRaise(self, error, name, finder, *args):
> + if name is None:
> + return None
> + args = list(args)
> + args.append(name)
> + result = finder(*args)
> + if result is None:
> + raise error(name)
> + return result
> +
> + def interpret(self, owner_name, distribution_name, distro_series_name,
> + name):
> + """See `ILiveFSSet`."""
> + owner = self._findOrRaise(
> + NoSuchPerson, owner_name, getUtility(IPersonSet).getByName)
> + distribution = self._findOrRaise(
> + NoSuchDistribution, distribution_name,
> + getUtility(IDistributionSet).getByName)
> + distro_series = self._findOrRaise(
> + NoSuchDistroSeries, distro_series_name,
> + getUtility(IDistroSeriesSet).queryByName, distribution)
> + return self.getByName(owner, distro_series, name)
> +
> + def getAll(self):
> + """See `ILiveFSSet`."""
> + store = IStore(LiveFS)
> + return store.find(LiveFS).order_by("name")
I guess lazr.restful will handle security here, but still, ew.
>
> === added file 'lib/lp/soyuz/model/livefsbuild.py'
> --- lib/lp/soyuz/model/livefsbuild.py 1970-01-01 00:00:00 +0000
> +++ lib/lp/soyuz/model/livefsbuild.py 2014-05-27 07:47:13 +0000
> @@ -0,0 +1,389 @@
> +# Copyright 2014 Canonical Ltd. This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +__metaclass__ = type
> +__all__ = [
> + 'LiveFSBuild',
> + 'LiveFSFile',
> + ]
> +
> +from datetime import timedelta
> +
> +import pytz
> +from storm.locals import (
> + Bool,
> + DateTime,
> + Desc,
> + Int,
> + JSON,
> + Reference,
> + Store,
> + Storm,
> + Unicode,
> + )
> +from storm.store import EmptyResultSet
> +from zope.component import getUtility
> +from zope.interface import implements
> +
> +from lp.app.errors import NotFoundError
> +from lp.buildmaster.enums import (
> + BuildFarmJobType,
> + BuildStatus,
> + )
> +from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJobSource
> +from lp.buildmaster.model.buildfarmjob import SpecificBuildFarmJobSourceMixin
> +from lp.buildmaster.model.packagebuild import PackageBuildMixin
> +from lp.registry.interfaces.pocket import PackagePublishingPocket
> +from lp.registry.model.person import Person
> +from lp.services.config import config
> +from lp.services.database.bulk import load_related
> +from lp.services.database.constants import DEFAULT
> +from lp.services.database.decoratedresultset import DecoratedResultSet
> +from lp.services.database.enumcol import DBEnum
> +from lp.services.database.interfaces import (
> + IMasterStore,
> + IStore,
> + )
> +from lp.services.features import getFeatureFlag
> +from lp.services.librarian.browser import ProxiedLibraryFileAlias
> +from lp.services.librarian.model import (
> + LibraryFileAlias,
> + LibraryFileContent,
> + )
> +from lp.soyuz.interfaces.component import IComponentSet
> +from lp.soyuz.interfaces.livefs import (
> + LIVEFS_FEATURE_FLAG,
> + LiveFSFeatureDisabled,
> + )
> +from lp.soyuz.interfaces.livefsbuild import (
> + ILiveFSBuild,
> + ILiveFSBuildSet,
> + ILiveFSFile,
> + )
> +from lp.soyuz.mail.livefsbuild import LiveFSBuildMailer
> +from lp.soyuz.model.archive import Archive
> +
> +
> +class LiveFSFile(Storm):
> + """See `ILiveFS`."""
> +
> + implements(ILiveFSFile)
> +
> + __storm_table__ = 'LiveFSFile'
> +
> + id = Int(name='id', primary=True)
> +
> + livefsbuild_id = Int(name='livefsbuild', allow_none=False)
> + livefsbuild = Reference(livefsbuild_id, 'LiveFSBuild.id')
> +
> + libraryfile_id = Int(name='libraryfile', allow_none=False)
> + libraryfile = Reference(libraryfile_id, 'LibraryFileAlias.id')
> +
> + def __init__(self, livefsbuild, libraryfile):
> + """Construct a `LiveFSFile`."""
> + super(LiveFSFile, self).__init__()
> + self.livefsbuild = livefsbuild
> + self.libraryfile = libraryfile
> +
> +
> +class LiveFSBuild(PackageBuildMixin, Storm):
> + """See `ILiveFSBuild`."""
> +
> + implements(ILiveFSBuild)
> +
> + __storm_table__ = 'LiveFSBuild'
> +
> + job_type = BuildFarmJobType.LIVEFSBUILD
> +
> + id = Int(name='id', primary=True)
> +
> + build_farm_job_id = Int(name='build_farm_job', allow_none=False)
> + build_farm_job = Reference(build_farm_job_id, 'BuildFarmJob.id')
> +
> + requester_id = Int(name='requester', allow_none=False)
> + requester = Reference(requester_id, 'Person.id')
> +
> + livefs_id = Int(name='livefs', allow_none=False)
> + livefs = Reference(livefs_id, 'LiveFS.id')
> +
> + archive_id = Int(name='archive', allow_none=False)
> + archive = Reference(archive_id, 'Archive.id')
> +
> + distro_arch_series_id = Int(name='distro_arch_series', allow_none=False)
> + distro_arch_series = Reference(
> + distro_arch_series_id, 'DistroArchSeries.id')
> +
> + pocket = DBEnum(enum=PackagePublishingPocket, allow_none=False)
> +
> + processor_id = Int(name='processor', allow_none=False)
> + processor = Reference(processor_id, 'Processor.id')
> + virtualized = Bool(name='virtualized')
> +
> + unique_key = Unicode(name='unique_key')
> +
> + metadata_override = JSON('json_data_override')
> +
> + date_created = DateTime(
> + name='date_created', tzinfo=pytz.UTC, allow_none=False)
> + date_started = DateTime(name='date_started', tzinfo=pytz.UTC)
> + date_finished = DateTime(name='date_finished', tzinfo=pytz.UTC)
> + date_first_dispatched = DateTime(
> + name='date_first_dispatched', tzinfo=pytz.UTC)
> +
> + builder_id = Int(name='builder')
> + builder = Reference(builder_id, 'Builder.id')
> +
> + status = DBEnum(name='status', enum=BuildStatus, allow_none=False)
> +
> + log_id = Int(name='log')
> + log = Reference(log_id, 'LibraryFileAlias.id')
> +
> + upload_log_id = Int(name='upload_log')
> + upload_log = Reference(upload_log_id, 'LibraryFileAlias.id')
> +
> + dependencies = Unicode(name='dependencies')
> +
> + failure_count = Int(name='failure_count', allow_none=False)
> +
> + def __init__(self, build_farm_job, requester, livefs, archive,
> + distro_arch_series, pocket, processor, virtualized,
> + unique_key, metadata_override, date_created):
> + """Construct a `LiveFSBuild`."""
> + if not getFeatureFlag(LIVEFS_FEATURE_FLAG):
> + raise LiveFSFeatureDisabled
> + super(LiveFSBuild, self).__init__()
> + self.build_farm_job = build_farm_job
> + self.requester = requester
> + self.livefs = livefs
> + self.archive = archive
> + self.distro_arch_series = distro_arch_series
> + self.pocket = pocket
> + self.processor = processor
> + self.virtualized = virtualized
> + self.unique_key = unique_key
> + self.metadata_override = metadata_override
> + self.date_created = date_created
> + self.status = BuildStatus.NEEDSBUILD
> +
> + @property
> + def is_private(self):
> + """See `IBuildFarmJob`."""
> + return self.livefs.owner.private or self.archive.private
> +
> + @property
> + def is_virtualized(self):
> + """See `ILiveFSBuild`."""
> + return self.archive.require_virtualized
I suspect it's possible to compromise non-virts with a dodgy livefs config, so we need to do something smarter here.
> +
> + @property
> + def title(self):
> + das = self.distro_arch_series
> + name = self.livefs.name
> + if self.unique_key is not None:
> + name += " (%s)" % self.unique_key
> + return "%s build of %s live filesystem in %s %s" % (
> + das.architecturetag, name, das.distroseries.distribution.name,
> + das.distroseries.getSuite(self.pocket))
I'd be tempted to abbreviate "filesystem" here, given how noisy /builders and various <titles> get.
> +
> + @property
> + def distribution(self):
> + """See `IPackageBuild`."""
> + return self.distro_arch_series.distroseries.distribution
> +
> + @property
> + def distro_series(self):
> + """See `IPackageBuild`."""
> + return self.distro_arch_series.distroseries
> +
> + @property
> + def current_component(self):
> + component = self.archive.default_component
> + if component is not None:
> + return component
> + else:
> + # XXX cjwatson 2014-04-22: Hardcode to universe for the time being.
> + return getUtility(IComponentSet)["universe"]
> +
> + @property
> + def version(self):
> + """See `ILiveFSBuild`."""
> + return self.date_created.strftime("%Y%m%d-%H%M%S")
> +
> + @property
> + def score(self):
> + """See `ILiveFSBuild`."""
> + if self.buildqueue_record is None:
> + return None
> + else:
> + return self.buildqueue_record.lastscore
> +
> + @property
> + def can_be_rescored(self):
> + """See `ILiveFSBuild`."""
> + return (
> + self.buildqueue_record is not None and
> + self.status is BuildStatus.NEEDSBUILD)
> +
> + @property
> + def can_be_cancelled(self):
> + """See `ILiveFSBuild`."""
> + if not self.buildqueue_record:
> + return False
> +
> + cancellable_statuses = [
> + BuildStatus.BUILDING,
> + BuildStatus.NEEDSBUILD,
> + ]
> + return self.status in cancellable_statuses
> +
> + def rescore(self, score):
> + """See `ILiveFSBuild`."""
> + assert self.can_be_rescored, "Build %s cannot be rescored" % self.id
> + self.buildqueue_record.manualScore(score)
> +
> + def cancel(self):
> + """See `ILiveFSBuild`."""
> + if not self.can_be_cancelled:
> + return
> + # BuildQueue.cancel() will decide whether to go straight to
> + # CANCELLED, or go through CANCELLING to let buildd-manager clean up
> + # the slave.
> + self.buildqueue_record.cancel()
> +
> + def calculateScore(self):
> + return 2505 + self.archive.relative_build_score
> +
> + def getMedianBuildDuration(self):
> + """Return the median duration of our successful builds."""
> + store = IStore(self)
> + result = store.find(
> + (LiveFSBuild.date_started, LiveFSBuild.date_finished),
> + LiveFSBuild.livefs == self.livefs_id,
> + LiveFSBuild.distro_arch_series == self.distro_arch_series_id,
> + LiveFSBuild.status == BuildStatus.FULLYBUILT)
> + result.order_by(Desc(LiveFSBuild.date_finished))
> + durations = [row[1] - row[0] for row in result[:9]]
> + if len(durations) == 0:
> + return None
> + durations.sort()
> + return durations[len(durations) // 2]
> +
> + def estimateDuration(self):
> + """See `IBuildFarmJob`."""
> + median = self.getMedianBuildDuration()
> + if median is not None:
> + return median
> + return timedelta(minutes=30)
> +
> + def getFiles(self):
> + """See `ILiveFSBuild`."""
> + store = Store.of(self)
Pointless local is pointless.
> + result = store.find(
> + (LiveFSFile, LibraryFileAlias, LibraryFileContent),
> + LiveFSFile.livefsbuild == self.id,
> + LibraryFileAlias.id == LiveFSFile.libraryfile_id,
> + LibraryFileContent.id == LibraryFileAlias.contentID)
> + return result.order_by(
> + [LibraryFileAlias.filename, LiveFSFile.id]).config()
No-op .config() is no-op.
> +
> + def getFileByName(self, filename):
> + """See `ILiveFSBuild`."""
> + if filename.endswith(".txt.gz"):
> + file_object = self.log
> + elif filename.endswith("_log.txt"):
> + file_object = self.upload_log
> + else:
> + file_object = Store.of(self).find(
> + LibraryFileAlias,
> + LiveFSFile.livefsbuild == self.id,
> + LibraryFileAlias.id == LiveFSFile.libraryfile_id,
> + LibraryFileAlias.filename == filename).one()
> +
> + if file_object is not None and file_object.filename == filename:
> + return file_object
> +
> + raise NotFoundError(filename)
> +
> + def addFile(self, lfa):
> + """See `ILiveFSBuild`."""
> + livefsfile = LiveFSFile(livefsbuild=self, libraryfile=lfa)
> + IMasterStore(LiveFSFile).add(livefsfile)
> + return livefsfile
> +
> + def verifySuccessfulUpload(self):
> + """See `IPackageBuild`."""
> + return not self.getFiles().is_empty()
> +
> + def notify(self, extra_info=None):
> + """See `IPackageBuild`."""
> + if not config.builddmaster.send_build_notification:
> + return
> + if self.status == BuildStatus.FULLYBUILT:
> + return
> + mailer = LiveFSBuildMailer.forStatus(self)
> + mailer.sendAll()
> +
> + def lfaUrl(self, lfa):
> + """Return the URL for a LibraryFileAlias in this context."""
> + if lfa is None:
> + return None
> + return ProxiedLibraryFileAlias(lfa, self).http_url
> +
> + @property
> + def log_url(self):
> + """See `IBuildFarmJob`."""
> + return self.lfaUrl(self.log)
> +
> + @property
> + def upload_log_url(self):
> + """See `IPackageBuild`."""
> + return self.lfaUrl(self.upload_log)
> +
> + def getFileUrls(self):
> + return [self.lfaUrl(lfa) for _, lfa, _ in self.getFiles()]
> +
> +
> +class LiveFSBuildSet(SpecificBuildFarmJobSourceMixin):
> + implements(ILiveFSBuildSet)
> +
> + def new(self, requester, livefs, archive, distro_arch_series, pocket,
> + unique_key=None, metadata_override=None, date_created=DEFAULT):
> + """See `ILiveFSBuildSet`."""
> + store = IMasterStore(LiveFSBuild)
> + build_farm_job = getUtility(IBuildFarmJobSource).new(
> + LiveFSBuild.job_type, BuildStatus.NEEDSBUILD, date_created, None,
> + archive)
> + livefsbuild = LiveFSBuild(
> + build_farm_job, requester, livefs, archive, distro_arch_series,
> + pocket, distro_arch_series.processor, archive.require_virtualized,
archive.require_virtualized is probably inappropriate here; see my comment on LiveFSBuild.is_virtualized.
> + unique_key, metadata_override, date_created)
> + store.add(livefsbuild)
> + return livefsbuild
> +
> + def getByID(self, build_id):
> + """See `ISpecificBuildFarmJobSource`."""
> + store = IMasterStore(LiveFSBuild)
> + return store.get(LiveFSBuild, build_id)
> +
> + def getByBuildFarmJob(self, build_farm_job):
> + """See `ISpecificBuildFarmJobSource`."""
> + return Store.of(build_farm_job).find(
> + LiveFSBuild, build_farm_job_id=build_farm_job.id).one()
> +
> + def preloadBuildsData(self, builds):
> + # Circular import.
> + from lp.soyuz.model.livefs import LiveFS
> + load_related(Person, builds, ["requester_id"])
> + load_related(LibraryFileAlias, builds, ["log_id"])
> + archives = load_related(Archive, builds, ["archive_id"])
> + load_related(Person, archives, ["ownerID"])
> + load_related(LiveFS, builds, ["livefs_id"])
> +
> + def getByBuildFarmJobs(self, build_farm_jobs):
> + """See `ISpecificBuildFarmJobSource`."""
> + if len(build_farm_jobs) == 0:
> + return EmptyResultSet()
> + rows = Store.of(build_farm_jobs[0]).find(
> + LiveFSBuild, LiveFSBuild.build_farm_job_id.is_in(
> + bfj.id for bfj in build_farm_jobs))
> + return DecoratedResultSet(rows, pre_iter_hook=self.preloadBuildsData)
>
> === added file 'lib/lp/soyuz/model/livefsbuildbehaviour.py'
> --- lib/lp/soyuz/model/livefsbuildbehaviour.py 1970-01-01 00:00:00 +0000
> +++ lib/lp/soyuz/model/livefsbuildbehaviour.py 2014-05-27 07:47:13 +0000
> @@ -0,0 +1,147 @@
> +# Copyright 2014 Canonical Ltd. This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""An `IBuildFarmJobBehaviour` for `LiveFSBuild`.
> +
> +Dispatches live filesystem build jobs to build-farm slaves.
> +"""
> +
> +__metaclass__ = type
> +__all__ = [
> + 'LiveFSBuildBehaviour',
> + ]
> +
> +from twisted.internet import defer
> +from zope.component import adapts
> +from zope.interface import implements
> +from zope.security.proxy import removeSecurityProxy
> +
> +from lp.buildmaster.interfaces.builder import CannotBuild
> +from lp.buildmaster.interfaces.buildfarmjobbehaviour import (
> + IBuildFarmJobBehaviour,
> + )
> +from lp.buildmaster.model.buildfarmjobbehaviour import (
> + BuildFarmJobBehaviourBase,
> + )
> +from lp.registry.interfaces.series import SeriesStatus
> +from lp.soyuz.adapters.archivedependencies import get_sources_list_for_building
> +from lp.soyuz.interfaces.livefsbuild import ILiveFSBuild
> +
> +
> +class LiveFSBuildBehaviour(BuildFarmJobBehaviourBase):
> + """Dispatches `LiveFSBuild` jobs to slaves."""
> +
> + adapts(ILiveFSBuild)
> + implements(IBuildFarmJobBehaviour)
> +
> + # Identify the type of job to the slave.
> + build_type = 'livefs'
> +
> + @property
> + def displayname(self):
> + ret = self.build.title
> + if self._builder is not None:
> + ret += " (on %s)" % self._builder.url
> + return ret
> +
> + def logStartBuild(self, logger):
> + """See `IBuildFarmJobBehaviour`."""
> + logger.info("startBuild(%s)", self.displayname)
> +
> + def getLogFileName(self):
> + das = self.build.distro_arch_series
> + archname = das.architecturetag
> + if self.build.unique_key:
> + archname += '_%s' % self.build.unique_key
> +
> + # Examples:
> + # buildlog_ubuntu_trusty_i386_ubuntu-desktop_FULLYBUILT.txt
> + return 'buildlog_%s_%s_%s_%s_%s.txt' % (
> + das.distroseries.distribution.name, das.distroseries.name,
> + archname, self.build.livefs.name, self.build.status.name)
> +
> + def verifyBuildRequest(self, logger):
> + """Assert some pre-build checks.
> +
> + The build request is checked:
> + * Virtualized builds can't build on a non-virtual builder
> + * Ensure that we have a chroot
> + """
> + build = self.build
> + if build.is_virtualized and not self._builder.virtualized:
> + raise AssertionError(
> + "Attempt to build virtual item on a non-virtual builder.")
> +
> + chroot = build.distro_arch_series.getChroot()
> + if chroot is None:
> + raise CannotBuild(
> + "Missing chroot for %s" % build.distro_arch_series.displayname)
> +
> + def _extraBuildArgs(self):
> + """
> + Return the extra arguments required by the slave for the given build.
> + """
> + build = self.build
> + # Non-trivial metadata values may have been security-wrapped, which
> + # is pointless here and just gets in the way of xmlrpclib
> + # serialisation.
> + args = dict(removeSecurityProxy(build.livefs.metadata))
> + if build.metadata_override is not None:
> + args.update(removeSecurityProxy(build.metadata_override))
> + args["suite"] = build.distro_series.getSuite(build.pocket)
> + args["arch_tag"] = build.distro_arch_series.architecturetag
> + args["datestamp"] = build.version
> + args["archives"] = get_sources_list_for_building(
> + build, build.distro_arch_series, None)
> + args["archive_private"] = build.archive.private
> + return args
> +
> + @defer.inlineCallbacks
> + def dispatchBuildToSlave(self, build_queue_id, logger):
> + """See `IBuildFarmJobBehaviour`."""
> +
> + # Start the build on the slave builder. First we send the chroot.
> + distro_arch_series = self.build.distro_arch_series
> + chroot = distro_arch_series.getChroot()
> + if chroot is None:
> + raise CannotBuild(
> + "Unable to find a chroot for %s" %
> + distro_arch_series.displayname)
> + logger.info(
> + "Sending chroot file for live filesystem build to %s" %
> + self._builder.name)
> + yield self._slave.cacheFile(logger, chroot)
> +
> + # Generate a string which can be used to cross-check when obtaining
> + # results so we know we are referring to the right database object
> + # in subsequent runs.
> + buildid = "%s-%s" % (self.build.id, build_queue_id)
> + logger.info("Initiating build %s on %s" % (buildid, self._builder.url))
> +
> + cookie = self.getBuildCookie()
> + args = self._extraBuildArgs()
> + status, info = yield self._slave.build(
> + cookie, "livefs", chroot.content.sha1, {}, args)
> +
> + message = """%s (%s):
> + ***** RESULT *****
> + %s
> + %s: %s
> + ******************
> + """ % (
> + self._builder.name,
> + self._builder.url,
> + args,
> + status,
> + info,
> + )
> + logger.info(message)
> +
> + def verifySuccessfulBuild(self):
> + """See `IBuildFarmJobBehaviour`."""
> + # The implementation in BuildFarmJobBehaviourBase checks whether the
> + # target suite is modifiable in the target archive. However, a
> + # `LiveFSBuild`'s archive is a source rather than a target, so that
> + # check does not make sense. We do, however, refuse to build for
> + # obsolete series.
> + assert self.build.distro_series.status != SeriesStatus.OBSOLETE
>
> === modified file 'lib/lp/soyuz/tests/test_binarypackagebuildbehaviour.py'
> --- lib/lp/soyuz/tests/test_binarypackagebuildbehaviour.py 2014-02-03 14:43:08 +0000
> +++ lib/lp/soyuz/tests/test_binarypackagebuildbehaviour.py 2014-05-27 07:47:13 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2010-2013 Canonical Ltd. This software is licensed under the
> +# Copyright 2010-2014 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> """Tests for BinaryPackageBuildBehaviour."""
> @@ -39,6 +39,7 @@
> from lp.buildmaster.tests.test_buildfarmjobbehaviour import (
> TestGetUploadMethodsMixin,
> TestHandleStatusMixin,
> + TestVerifySuccessfulBuildMixin,
> )
> from lp.buildmaster.tests.test_manager import MockBuilderFactory
> from lp.registry.interfaces.pocket import (
> @@ -537,6 +538,13 @@
> build.queueBuild()
> return build
>
> + def makeUnmodifiableBuild(self):
> + build = self.factory.makeBinaryPackageBuild()
> + build.distro_series.status = SeriesStatus.CURRENT
> + build.updateStatus(BuildStatus.BUILDING)
> + build.queueBuild()
> + return build
> +
>
> class TestGetUploadMethodsForBinaryPackageBuild(
> MakeBinaryPackageBuildMixin, TestGetUploadMethodsMixin,
> @@ -544,6 +552,12 @@
> """IPackageBuild.getUpload-related methods work with binary builds."""
>
>
> +class TestVerifySuccessfulBuildForBinaryPackageBuild(
> + MakeBinaryPackageBuildMixin, TestVerifySuccessfulBuildMixin,
> + TestCaseWithFactory):
> + """IBuildFarmJobBehaviour.verifySuccessfulBuild works."""
> +
> +
> class TestHandleStatusForBinaryPackageBuild(
> MakeBinaryPackageBuildMixin, TestHandleStatusMixin, TrialTestCase):
> """IPackageBuild.handleStatus works with binary builds."""
>
> === added file 'lib/lp/soyuz/tests/test_livefs.py'
> --- lib/lp/soyuz/tests/test_livefs.py 1970-01-01 00:00:00 +0000
> +++ lib/lp/soyuz/tests/test_livefs.py 2014-05-27 07:47:13 +0000
> @@ -0,0 +1,522 @@
> +# Copyright 2014 Canonical Ltd. This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Test live filesystems."""
> +
> +__metaclass__ = type
> +
> +from datetime import (
> + datetime,
> + timedelta,
> + )
> +
> +from lazr.lifecycle.event import ObjectModifiedEvent
> +import pytz
> +from storm.locals import Store
> +from testtools.matchers import Equals
> +import transaction
> +from zope.component import getUtility
> +from zope.event import notify
> +from zope.security.proxy import removeSecurityProxy
> +
> +from lp.buildmaster.enums import (
> + BuildQueueStatus,
> + BuildStatus,
> + )
> +from lp.buildmaster.interfaces.buildqueue import IBuildQueue
> +from lp.buildmaster.model.buildqueue import BuildQueue
> +from lp.registry.interfaces.pocket import PackagePublishingPocket
> +from lp.services.database.constants import UTC_NOW
> +from lp.services.features.testing import FeatureFixture
> +from lp.services.webapp.interfaces import OAuthPermission
> +from lp.soyuz.interfaces.livefs import (
> + ILiveFS,
> + ILiveFSSet,
> + ILiveFSView,
> + LIVEFS_FEATURE_FLAG,
> + LiveFSBuildAlreadyPending,
> + LiveFSFeatureDisabled,
> + )
> +from lp.soyuz.interfaces.livefsbuild import ILiveFSBuild
> +from lp.testing import (
> + ANONYMOUS,
> + api_url,
> + login,
> + logout,
> + person_logged_in,
> + StormStatementRecorder,
> + TestCaseWithFactory,
> + )
> +from lp.testing.layers import (
> + DatabaseFunctionalLayer,
> + LaunchpadZopelessLayer,
> + )
> +from lp.testing.matchers import (
> + DoesNotSnapshot,
> + HasQueryCount,
> + )
> +from lp.testing.pages import webservice_for_person
> +
> +
> +class TestLiveFSFeatureFlag(TestCaseWithFactory):
> +
> + layer = LaunchpadZopelessLayer
> +
> + def test_feature_flag_disabled(self):
> + # Without a feature flag, we will not create new LiveFSes.
> + person = self.factory.makePerson()
> + self.assertRaises(
> + LiveFSFeatureDisabled, getUtility(ILiveFSSet).new,
> + person, person, None, None, None)
> +
> +
> +class TestLiveFS(TestCaseWithFactory):
> +
> + layer = DatabaseFunctionalLayer
> +
> + def setUp(self):
> + super(TestLiveFS, self).setUp()
> + self.useFixture(FeatureFixture({LIVEFS_FEATURE_FLAG: u"on"}))
> +
> + def test_implements_interfaces(self):
> + # LiveFS implements ILiveFS.
> + livefs = self.factory.makeLiveFS()
> + self.assertProvides(livefs, ILiveFS)
> +
> + def test_class_implements_interfaces(self):
> + # The LiveFS class implements ILiveFSSet.
> + self.assertProvides(getUtility(ILiveFSSet), ILiveFSSet)
> +
> + def test_avoids_problematic_snapshots(self):
> + self.assertThat(
> + self.factory.makeLiveFS(),
> + DoesNotSnapshot(
> + ["builds", "completed_builds", "pending_builds"], ILiveFSView))
> +
> + def makeLiveFSComponents(self, metadata={}):
> + """Return a dict of values that can be used to make a LiveFS.
> +
> + Suggested use: provide as kwargs to ILiveFSSet.new.
> +
> + :param metadata: A dict to set as LiveFS.metadata.
> + """
> + registrant = self.factory.makePerson()
> + return dict(
> + registrant=registrant,
> + owner=self.factory.makeTeam(owner=registrant),
> + distro_series=self.factory.makeDistroSeries(),
> + name=self.factory.getUniqueString(u"livefs-name"),
> + metadata=metadata)
> +
> + def test_creation(self):
> + # The metadata entries supplied when a LiveFS is created are present
> + # on the new object.
> + components = self.makeLiveFSComponents(metadata={"project": "foo"})
> + livefs = getUtility(ILiveFSSet).new(**components)
> + transaction.commit()
> + self.assertEqual(components["registrant"], livefs.registrant)
> + self.assertEqual(components["owner"], livefs.owner)
> + self.assertEqual(components["distro_series"], livefs.distro_series)
> + self.assertEqual(components["name"], livefs.name)
> + self.assertEqual(components["metadata"], livefs.metadata)
> +
> + def test_initial_date_last_modified(self):
> + # The initial value of date_last_modified is date_created.
> + livefs = self.factory.makeLiveFS(
> + date_created=datetime(2014, 04, 25, 10, 38, 0, tzinfo=pytz.UTC))
> + self.assertEqual(livefs.date_created, livefs.date_last_modified)
> +
> + def test_modifiedevent_sets_date_last_modified(self):
> + # When a LiveFS receives an object modified event, the last modified
> + # date is set to UTC_NOW.
> + livefs = self.factory.makeLiveFS(
> + date_created=datetime(2014, 04, 25, 10, 38, 0, tzinfo=pytz.UTC))
> + notify(ObjectModifiedEvent(
> + removeSecurityProxy(livefs), livefs, [ILiveFS["name"]]))
> + self.assertSqlAttributeEqualsDate(
> + livefs, "date_last_modified", UTC_NOW)
> +
> + def test_exists(self):
> + # ILiveFSSet.exists checks for matching LiveFSes.
> + livefs = self.factory.makeLiveFS()
> + self.assertTrue(
> + getUtility(ILiveFSSet).exists(
> + livefs.owner, livefs.distro_series, livefs.name))
> + self.assertFalse(
> + getUtility(ILiveFSSet).exists(
> + self.factory.makePerson(), livefs.distro_series, livefs.name))
> + self.assertFalse(
> + getUtility(ILiveFSSet).exists(
> + livefs.owner, self.factory.makeDistroSeries(), livefs.name))
> + self.assertFalse(
> + getUtility(ILiveFSSet).exists(
> + livefs.owner, livefs.distro_series, u"different"))
> +
> + def test_requestBuild(self):
> + # requestBuild creates a new LiveFSBuild.
> + livefs = self.factory.makeLiveFS()
> + distroarchseries = self.factory.makeDistroArchSeries(
> + distroseries=livefs.distro_series)
> + build = livefs.requestBuild(
> + livefs.owner, livefs.distro_series.main_archive, distroarchseries,
> + PackagePublishingPocket.RELEASE)
> + self.assertTrue(ILiveFSBuild.providedBy(build))
> + self.assertEqual(livefs.owner, build.requester)
> + self.assertEqual(livefs.distro_series.main_archive, build.archive)
> + self.assertEqual(distroarchseries, build.distro_arch_series)
> + self.assertEqual(PackagePublishingPocket.RELEASE, build.pocket)
> + self.assertIsNone(build.unique_key)
> + self.assertIsNone(build.metadata_override)
> + self.assertEqual(BuildStatus.NEEDSBUILD, build.status)
> + store = Store.of(build)
> + store.flush()
> + build_queue = store.find(
> + BuildQueue,
> + BuildQueue._build_farm_job_id ==
> + removeSecurityProxy(build).build_farm_job_id).one()
> + self.assertProvides(build_queue, IBuildQueue)
> + self.assertEqual(
> + livefs.distro_series.main_archive.require_virtualized,
> + build_queue.virtualized)
> + self.assertEqual(BuildQueueStatus.WAITING, build_queue.status)
> +
> + def test_requestBuild_score(self):
> + # Build requests have a relatively low queue score (2505).
> + livefs = self.factory.makeLiveFS()
> + distroarchseries = self.factory.makeDistroArchSeries(
> + distroseries=livefs.distro_series)
> + build = livefs.requestBuild(
> + livefs.owner, livefs.distro_series.main_archive, distroarchseries,
> + PackagePublishingPocket.RELEASE)
> + queue_record = build.buildqueue_record
> + queue_record.score()
> + self.assertEqual(2505, queue_record.lastscore)
> +
> + def test_requestBuild_relative_build_score(self):
> + # Offsets for archives are respected.
> + livefs = self.factory.makeLiveFS()
> + archive = self.factory.makeArchive(owner=livefs.owner)
> + removeSecurityProxy(archive).relative_build_score = 100
> + distroarchseries = self.factory.makeDistroArchSeries(
> + distroseries=livefs.distro_series)
> + build = livefs.requestBuild(
> + livefs.owner, archive, distroarchseries,
> + PackagePublishingPocket.RELEASE)
> + queue_record = build.buildqueue_record
> + queue_record.score()
> + self.assertEqual(2605, queue_record.lastscore)
> +
> + def test_requestBuild_rejects_repeats(self):
> + # requestBuild refuses if there is already a pending build.
> + livefs = self.factory.makeLiveFS()
> + distroarchseries = self.factory.makeDistroArchSeries(
> + distroseries=livefs.distro_series)
> + old_build = livefs.requestBuild(
> + livefs.owner, livefs.distro_series.main_archive, distroarchseries,
> + PackagePublishingPocket.RELEASE)
> + self.assertRaises(
> + LiveFSBuildAlreadyPending, livefs.requestBuild,
> + livefs.owner, livefs.distro_series.main_archive, distroarchseries,
> + PackagePublishingPocket.RELEASE)
> + # We can build for a different archive.
> + livefs.requestBuild(
> + livefs.owner, self.factory.makeArchive(owner=livefs.owner),
> + distroarchseries, PackagePublishingPocket.RELEASE)
> + # We can build for a different distroarchseries.
> + livefs.requestBuild(
> + livefs.owner, livefs.distro_series.main_archive,
> + self.factory.makeDistroArchSeries(
> + distroseries=livefs.distro_series),
> + PackagePublishingPocket.RELEASE)
> + # Changing the status of the old build allows a new build.
> + old_build.updateStatus(BuildStatus.BUILDING)
> + old_build.updateStatus(BuildStatus.FULLYBUILT)
> + livefs.requestBuild(
> + livefs.owner, livefs.distro_series.main_archive, distroarchseries,
> + PackagePublishingPocket.RELEASE)
> +
> + def test_getBuilds(self):
> + # Test the various getBuilds methods.
> + livefs = self.factory.makeLiveFS()
> + builds = [
> + self.factory.makeLiveFSBuild(livefs=livefs) for x in range(3)]
> + # We want the latest builds first.
> + builds.reverse()
> +
> + self.assertEqual(builds, list(livefs.builds))
> + self.assertEqual([], list(livefs.completed_builds))
> + self.assertEqual(builds, list(livefs.pending_builds))
> +
> + # Change the status of one of the builds and retest.
> + builds[0].updateStatus(BuildStatus.BUILDING)
> + builds[0].updateStatus(BuildStatus.FULLYBUILT)
> + self.assertEqual(builds[1:] + builds[:1], list(livefs.builds))
> + self.assertEqual(builds[:1], list(livefs.completed_builds))
> + self.assertEqual(builds[1:], list(livefs.pending_builds))
> +
> +
> +class TestLiveFSWebservice(TestCaseWithFactory):
> +
> + layer = DatabaseFunctionalLayer
> +
> + def setUp(self):
> + super(TestLiveFSWebservice, self).setUp()
> + self.useFixture(FeatureFixture({LIVEFS_FEATURE_FLAG: u"on"}))
> + self.person = self.factory.makePerson(displayname="Test Person")
> + self.webservice = webservice_for_person(
> + self.person, permission=OAuthPermission.WRITE_PUBLIC)
> + self.webservice.default_api_version = "devel"
> + login(ANONYMOUS)
> +
> + def getURL(self, obj):
> + return self.webservice.getAbsoluteUrl(api_url(obj))
> +
> + def makeLiveFS(self, owner=None, distroseries=None, metadata=None,
> + webservice=None):
> + if owner is None:
> + owner = self.person
> + if distroseries is None:
> + distroseries = self.factory.makeDistroSeries(registrant=owner)
> + if metadata is None:
> + metadata = {"project": "flavour"}
> + if webservice is None:
> + webservice = self.webservice
> + transaction.commit()
> + distroseries_url = api_url(distroseries)
> + owner_url = api_url(owner)
> + logout()
> + response = webservice.named_post(
> + "/livefses", "new", owner=owner_url,
> + distro_series=distroseries_url, name="flavour-desktop",
> + metadata=metadata)
> + self.assertEqual(201, response.status)
> + livefs = webservice.get(response.getHeader("Location")).jsonBody()
> + return livefs, distroseries_url
> +
> + def getCollectionLinks(self, entry, member):
> + """Return a list of self_link attributes of entries in a collection."""
> + collection = self.webservice.get(
> + entry["%s_collection_link" % member]).jsonBody()
> + return [entry["self_link"] for entry in collection["entries"]]
> +
> + def test_new(self):
> + # Ensure LiveFS creation works.
> + team = self.factory.makeTeam(owner=self.person)
> + livefs, distroseries_url = self.makeLiveFS(owner=team)
> + with person_logged_in(self.person):
> + self.assertEqual(
> + self.getURL(self.person), livefs["registrant_link"])
> + self.assertEqual(self.getURL(team), livefs["owner_link"])
> + self.assertEqual("flavour-desktop", livefs["name"])
> + self.assertEqual(
> + self.webservice.getAbsoluteUrl(distroseries_url),
> + livefs["distro_series_link"])
> + self.assertEqual({"project": "flavour"}, livefs["metadata"])
> +
> + def test_duplicate(self):
> + # An attempt to create a duplicate LiveFS fails.
> + team = self.factory.makeTeam(owner=self.person)
> + _, distroseries_url = self.makeLiveFS(owner=team)
> + with person_logged_in(self.person):
> + owner_url = api_url(team)
> + response = self.webservice.named_post(
> + "/livefses", "new", owner=owner_url,
> + distro_series=distroseries_url, name="flavour-desktop",
> + metadata={})
> + self.assertEqual(400, response.status)
> + self.assertEqual(
> + "There is already a live filesystem with the same name, owner, "
> + "and distroseries.", response.body)
> +
> + def test_not_owner(self):
> + # If the registrant is not the owner or a member of the owner team,
> + # LiveFS creation fails.
> + other_person = self.factory.makePerson(displayname="Other Person")
> + other_team = self.factory.makeTeam(
> + owner=other_person, displayname="Other Team")
> + distroseries = self.factory.makeDistroSeries(registrant=self.person)
> + transaction.commit()
> + other_person_url = api_url(other_person)
> + other_team_url = api_url(other_team)
> + distroseries_url = api_url(distroseries)
> + logout()
> + response = self.webservice.named_post(
> + "/livefses", "new", owner=other_person_url,
> + distro_series=distroseries_url, name="dummy", metadata={})
> + self.assertEqual(401, response.status)
> + self.assertEqual(
> + "Test Person cannot create live filesystems owned by Other "
> + "Person.", response.body)
> + response = self.webservice.named_post(
> + "/livefses", "new", owner=other_team_url,
> + distro_series=distroseries_url, name="dummy", metadata={})
> + self.assertEqual(401, response.status)
> + self.assertEqual(
> + "Test Person is not a member of Other Team.", response.body)
> +
> + def test_getByName(self):
> + # lp.livefses.getByName returns a matching LiveFS.
> + livefs, distroseries_url = self.makeLiveFS()
> + with person_logged_in(self.person):
> + owner_url = api_url(self.person)
> + response = self.webservice.named_get(
> + "/livefses", "getByName", owner=owner_url,
> + distro_series=distroseries_url, name="flavour-desktop")
> + self.assertEqual(200, response.status)
> + self.assertEqual(livefs, response.jsonBody())
> +
> + def test_getByName_missing(self):
> + # lp.livefses.getByName returns 404 for a non-existent LiveFS.
> + livefs, distroseries_url = self.makeLiveFS()
> + with person_logged_in(self.person):
> + owner_url = api_url(self.person)
> + response = self.webservice.named_get(
> + "/livefses", "getByName", owner=owner_url,
> + distro_series=distroseries_url, name="nonexistent")
> + self.assertEqual(404, response.status)
> + self.assertEqual(
> + "No such live filesystem with this owner/distroseries: "
> + "'nonexistent'.", response.body)
> +
> + def test_requestBuild(self):
> + # Build requests can be performed and end up in livefs.builds and
> + # livefs.pending_builds.
> + distroseries = self.factory.makeDistroSeries(registrant=self.person)
> + distroarchseries = self.factory.makeDistroArchSeries(
> + distroseries=distroseries, owner=self.person)
> + distroarchseries_url = api_url(distroarchseries)
> + archive_url = api_url(distroseries.main_archive)
> + livefs, _ = self.makeLiveFS(distroseries=distroseries)
> + response = self.webservice.named_post(
> + livefs["self_link"], "requestBuild", archive=archive_url,
> + distro_arch_series=distroarchseries_url, pocket="Release")
> + self.assertEqual(201, response.status)
> + build = self.webservice.get(response.getHeader("Location")).jsonBody()
> + self.assertEqual(
> + [build["self_link"]], self.getCollectionLinks(livefs, "builds"))
> + self.assertEqual(
> + [], self.getCollectionLinks(livefs, "completed_builds"))
> + self.assertEqual(
> + [build["self_link"]],
> + self.getCollectionLinks(livefs, "pending_builds"))
> +
> + def test_requestBuild_rejects_repeats(self):
> + # Build requests are rejected if already pending.
> + distroseries = self.factory.makeDistroSeries(registrant=self.person)
> + distroarchseries = self.factory.makeDistroArchSeries(
> + distroseries=distroseries, owner=self.person)
> + distroarchseries_url = api_url(distroarchseries)
> + archive_url = api_url(distroseries.main_archive)
> + livefs, _ = self.makeLiveFS(distroseries=distroseries)
> + response = self.webservice.named_post(
> + livefs["self_link"], "requestBuild", archive=archive_url,
> + distro_arch_series=distroarchseries_url, pocket="Release")
> + self.assertEqual(201, response.status)
> + response = self.webservice.named_post(
> + livefs["self_link"], "requestBuild", archive=archive_url,
> + distro_arch_series=distroarchseries_url, pocket="Release")
> + self.assertEqual(400, response.status)
> + self.assertEqual(
> + "An identical build of this live filesystem image is already "
> + "pending.", response.body)
> +
> + def test_requestBuild_not_owner(self):
> + # If the requester is not the owner or a member of the owner team,
> + # build requests are rejected.
> + other_team = self.factory.makeTeam(displayname="Other Team")
> + distroseries = self.factory.makeDistroSeries(registrant=self.person)
> + distroarchseries = self.factory.makeDistroArchSeries(
> + distroseries=distroseries, owner=self.person)
> + distroarchseries_url = api_url(distroarchseries)
> + archive_url = api_url(distroseries.main_archive)
> + other_webservice = webservice_for_person(
> + other_team.teamowner, permission=OAuthPermission.WRITE_PUBLIC)
> + other_webservice.default_api_version = "devel"
> + login(ANONYMOUS)
> + livefs, _ = self.makeLiveFS(
> + owner=other_team, distroseries=distroseries,
> + webservice=other_webservice)
> + response = self.webservice.named_post(
> + livefs["self_link"], "requestBuild", archive=archive_url,
> + distro_arch_series=distroarchseries_url, pocket="Release")
> + self.assertEqual(401, response.status)
> + self.assertEqual(
> + "Test Person cannot create live filesystem builds owned by Other "
> + "Team.", response.body)
> +
> + def test_getBuilds(self):
> + # The builds, completed_builds, and pending_builds properties are as
> + # expected.
> + distroseries = self.factory.makeDistroSeries(registrant=self.person)
> + distroarchseries = self.factory.makeDistroArchSeries(
> + distroseries=distroseries, owner=self.person)
> + distroarchseries_url = api_url(distroarchseries)
> + archives = [
> + self.factory.makeArchive(
> + distribution=distroseries.distribution, owner=self.person)
> + for x in range(4)]
> + archive_urls = [api_url(archive) for archive in archives]
> + livefs, _ = self.makeLiveFS(distroseries=distroseries)
> + builds = []
> + for archive_url in archive_urls:
> + response = self.webservice.named_post(
> + livefs["self_link"], "requestBuild", archive=archive_url,
> + distro_arch_series=distroarchseries_url, pocket="Proposed")
> + self.assertEqual(201, response.status)
> + build = self.webservice.get(
> + response.getHeader("Location")).jsonBody()
> + builds.insert(0, build["self_link"])
> + self.assertEqual(builds, self.getCollectionLinks(livefs, "builds"))
> + self.assertEqual(
> + [], self.getCollectionLinks(livefs, "completed_builds"))
> + self.assertEqual(
> + builds, self.getCollectionLinks(livefs, "pending_builds"))
> + livefs = self.webservice.get(livefs["self_link"]).jsonBody()
> +
> + with person_logged_in(self.person):
> + db_livefs = getUtility(ILiveFSSet).getByName(
> + self.person, distroseries, livefs["name"])
> + db_builds = list(db_livefs.builds)
> + db_builds[0].updateStatus(
> + BuildStatus.BUILDING, date_started=db_livefs.date_created)
> + db_builds[0].updateStatus(
> + BuildStatus.FULLYBUILT,
> + date_finished=db_livefs.date_created + timedelta(minutes=10))
> + livefs = self.webservice.get(livefs["self_link"]).jsonBody()
> + # Builds that have not yet been started are listed first (since DESC
> + # defaults to NULLS FIRST).
> + self.assertEqual(
> + builds[1:] + builds[:1], self.getCollectionLinks(livefs, "builds"))
> + self.assertEqual(
> + builds[:1], self.getCollectionLinks(livefs, "completed_builds"))
> + self.assertEqual(
> + builds[1:], self.getCollectionLinks(livefs, "pending_builds"))
> +
> + with person_logged_in(self.person):
> + db_builds[1].updateStatus(
> + BuildStatus.BUILDING, date_started=db_livefs.date_created)
> + db_builds[1].updateStatus(
> + BuildStatus.FULLYBUILT,
> + date_finished=db_livefs.date_created + timedelta(minutes=20))
> + livefs = self.webservice.get(livefs["self_link"]).jsonBody()
> + self.assertEqual(
> + [builds[2], builds[3], builds[1], builds[0]],
> + self.getCollectionLinks(livefs, "builds"))
> + self.assertEqual(
> + [builds[1], builds[0]],
> + self.getCollectionLinks(livefs, "completed_builds"))
> + self.assertEqual(
> + builds[2:], self.getCollectionLinks(livefs, "pending_builds"))
> +
> + def test_query_count(self):
> + # LiveFS has a reasonable query count.
> + livefs = self.factory.makeLiveFS(
> + registrant=self.person, owner=self.person)
> + url = api_url(livefs)
> + logout()
> + store = Store.of(livefs)
> + store.flush()
> + store.invalidate()
> + with StormStatementRecorder() as recorder:
> + self.webservice.get(url)
> + self.assertThat(recorder, HasQueryCount(Equals(21)))
>
> === added file 'lib/lp/soyuz/tests/test_livefsbuild.py'
> --- lib/lp/soyuz/tests/test_livefsbuild.py 1970-01-01 00:00:00 +0000
> +++ lib/lp/soyuz/tests/test_livefsbuild.py 2014-05-27 07:47:13 +0000
> @@ -0,0 +1,494 @@
> +# Copyright 2014 Canonical Ltd. This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Test live filesystem build features."""
> +
> +__metaclass__ = type
> +
> +from datetime import (
> + datetime,
> + timedelta,
> + )
> +from urllib2 import (
> + HTTPError,
> + urlopen,
> + )
> +
> +import pytz
> +from zope.component import getUtility
> +from zope.security.proxy import removeSecurityProxy
> +from zope.testbrowser.browser import Browser
> +from zope.testbrowser.testing import PublisherMechanizeBrowser
> +
> +from lp.app.errors import NotFoundError
> +from lp.app.interfaces.launchpad import ILaunchpadCelebrities
> +from lp.buildmaster.enums import BuildStatus
> +from lp.buildmaster.interfaces.buildqueue import IBuildQueue
> +from lp.buildmaster.interfaces.packagebuild import IPackageBuild
> +from lp.registry.enums import PersonVisibility
> +from lp.services.config import config
> +from lp.services.features.testing import FeatureFixture
> +from lp.services.librarian.browser import ProxiedLibraryFileAlias
> +from lp.services.webapp.interfaces import OAuthPermission
> +from lp.soyuz.enums import ArchivePurpose
> +from lp.soyuz.interfaces.livefs import (
> + LIVEFS_FEATURE_FLAG,
> + LiveFSFeatureDisabled,
> + )
> +from lp.soyuz.interfaces.livefsbuild import (
> + ILiveFSBuild,
> + ILiveFSBuildSet,
> + )
> +from lp.soyuz.interfaces.processor import IProcessorSet
> +from lp.testing import (
> + ANONYMOUS,
> + api_url,
> + login,
> + logout,
> + person_logged_in,
> + TestCaseWithFactory,
> + )
> +from lp.testing.layers import (
> + LaunchpadFunctionalLayer,
> + LaunchpadZopelessLayer,
> + )
> +from lp.testing.mail_helpers import pop_notifications
> +from lp.testing.pages import webservice_for_person
> +
> +
> +class TestLiveFSBuildFeatureFlag(TestCaseWithFactory):
> +
> + layer = LaunchpadZopelessLayer
> +
> + def test_feature_flag_disabled(self):
> + # Without a feature flag, we will not create new LiveFSBuilds.
> + self.assertRaises(
> + LiveFSFeatureDisabled, getUtility(ILiveFSBuildSet).new,
> + None, None, self.factory.makeArchive(),
> + self.factory.makeDistroArchSeries(), None, None, None)
> +
> +
> +expected_body = """\
> + * Live Filesystem: livefs-1
> + * Version: 20140425-103800
> + * Archive: distro primary archive
> + * Distroseries: distro unstable
> + * Architecture: i386
> + * Pocket: RELEASE
> + * State: Failed to build
> + * Duration: 10 minutes
> + * Build Log: %s
> + * Upload Log: %s
> + * Builder: http://launchpad.dev/builders/bob
> +"""
> +
> +
> +class TestLiveFSBuild(TestCaseWithFactory):
> +
> + layer = LaunchpadZopelessLayer
> +
> + def setUp(self):
> + super(TestLiveFSBuild, self).setUp()
> + self.useFixture(FeatureFixture({LIVEFS_FEATURE_FLAG: u"on"}))
> + self.build = self.factory.makeLiveFSBuild()
> +
> + def test_implements_interfaces(self):
> + # LiveFSBuild implements IPackageBuild and ILiveFSBuild.
> + self.assertProvides(self.build, IPackageBuild)
> + self.assertProvides(self.build, ILiveFSBuild)
> +
> + def test_queueBuild(self):
> + # LiveFSBuild can create the queue entry for itself.
> + bq = self.build.queueBuild()
> + self.assertProvides(bq, IBuildQueue)
> + self.assertEqual(
> + self.build.build_farm_job, removeSecurityProxy(bq)._build_farm_job)
> + self.assertEqual(self.build, bq.specific_build)
> + self.assertEqual(self.build.virtualized, bq.virtualized)
> + self.assertIsNotNone(bq.processor)
> + self.assertEqual(bq, self.build.buildqueue_record)
> +
> + def test_current_component_primary(self):
> + # LiveFSBuilds for primary archives always build in universe for the
> + # time being.
> + self.assertEqual(ArchivePurpose.PRIMARY, self.build.archive.purpose)
> + self.assertEqual("universe", self.build.current_component.name)
> +
> + def test_current_component_ppa(self):
> + # PPAs only have indices for main, so LiveFSBuilds for PPAs always
> + # build in main.
> + build = self.factory.makeLiveFSBuild(
> + archive=self.factory.makeArchive())
> + self.assertEqual("main", build.current_component.name)
> +
> + def test_is_private(self):
> + # A LiveFSBuild is private iff its LiveFS and archive are.
> + self.assertFalse(self.build.is_private)
> + private_team = self.factory.makeTeam(
> + visibility=PersonVisibility.PRIVATE)
> + with person_logged_in(private_team.teamowner):
> + build = self.factory.makeLiveFSBuild(
> + requester=private_team.teamowner, owner=private_team)
> + self.assertTrue(build.is_private)
> + private_archive = self.factory.makeArchive(private=True)
> + with person_logged_in(private_archive.owner):
> + build = self.factory.makeLiveFSBuild(archive=private_archive)
> + self.assertTrue(build.is_private)
> +
> + def test_can_be_cancelled(self):
> + # For all states that can be cancelled, can_be_cancelled returns True.
> + ok_cases = [
> + BuildStatus.BUILDING,
> + BuildStatus.NEEDSBUILD,
> + ]
> + for status in BuildStatus:
> + if status in ok_cases:
> + self.assertTrue(self.build.can_be_cancelled)
> + else:
> + self.assertFalse(self.build.can_be_cancelled)
> +
> + def test_cancel_not_in_progress(self):
> + # The cancel() method for a pending build leaves it in the CANCELLED
> + # state.
> + self.build.queueBuild()
> + self.build.cancel()
> + self.assertEqual(BuildStatus.CANCELLED, self.build.status)
> + self.assertIsNone(self.build.buildqueue_record)
> +
> + def test_cancel_in_progress(self):
> + # The cancel() method for a building build leaves it in the
> + # CANCELLING state.
> + bq = self.build.queueBuild()
> + bq.markAsBuilding(self.factory.makeBuilder())
> + self.build.cancel()
> + self.assertEqual(BuildStatus.CANCELLING, self.build.status)
> + self.assertEqual(bq, self.build.buildqueue_record)
> +
> + def test_estimateDuration(self):
> + # Without previous builds, the default time estimate is 30m.
> + self.assertEqual(1800, self.build.estimateDuration().seconds)
> +
> + def test_estimateDuration_with_history(self):
> + # Previous successful builds of the same live filesystem are used
> + # for estimates.
> + self.factory.makeLiveFSBuild(
> + requester=self.build.requester, livefs=self.build.livefs,
> + distroarchseries=self.build.distro_arch_series,
> + status=BuildStatus.FULLYBUILT, duration=timedelta(seconds=335))
> + for i in range(3):
> + self.factory.makeLiveFSBuild(
> + requester=self.build.requester, livefs=self.build.livefs,
> + distroarchseries=self.build.distro_arch_series,
> + status=BuildStatus.FAILEDTOBUILD,
> + duration=timedelta(seconds=20))
> + self.assertEqual(335, self.build.estimateDuration().seconds)
> +
> + def test_getFileByName_logs(self):
> + # getFileByName returns the logs when requested by name.
> + self.build.setLog(
> + self.factory.makeLibraryFileAlias(filename="buildlog.txt.gz"))
> + self.assertEqual(
> + self.build.log, self.build.getFileByName("buildlog.txt.gz"))
> + self.assertRaises(NotFoundError, self.build.getFileByName, "foo")
> + self.build.storeUploadLog("uploaded")
> + self.assertEqual(
> + self.build.upload_log,
> + self.build.getFileByName(self.build.upload_log.filename))
> +
> + def test_getFileByName_uploaded_files(self):
> + # getFileByName returns uploaded files when requested by name.
> + filenames = ("ubuntu.squashfs", "ubuntu.manifest")
> + lfas = []
> + for filename in filenames:
> + lfa = self.factory.makeLibraryFileAlias(filename=filename)
> + lfas.append(lfa)
> + self.build.addFile(lfa)
> + self.assertContentEqual(
> + lfas, [row[1] for row in self.build.getFiles()])
> + for filename, lfa in zip(filenames, lfas):
> + self.assertEqual(lfa, self.build.getFileByName(filename))
> + self.assertRaises(NotFoundError, self.build.getFileByName, "missing")
> +
> + def test_verifySuccessfulUpload(self):
> + self.assertFalse(self.build.verifySuccessfulUpload())
> + self.factory.makeLiveFSFile(livefsbuild=self.build)
> + self.assertTrue(self.build.verifySuccessfulUpload())
> +
> + def test_notify_fullybuilt(self):
> + # notify does not send mail when a LiveFSBuild completes normally.
> + person = self.factory.makePerson(name="person")
> + build = self.factory.makeLiveFSBuild(
> + requester=person, status=BuildStatus.FULLYBUILT)
> + build.notify()
> + self.assertEqual(0, len(pop_notifications()))
> +
> + def test_notify_packagefail(self):
> + # notify sends mail when a LiveFSBuild fails.
> + person = self.factory.makePerson(name="person")
> + distribution = self.factory.makeDistribution(name="distro")
> + distroseries = self.factory.makeDistroSeries(
> + distribution=distribution, name="unstable")
> + processor = getUtility(IProcessorSet).getByName("386")
> + distroarchseries = self.factory.makeDistroArchSeries(
> + distroseries=distroseries, architecturetag="i386",
> + processor=processor)
> + build = self.factory.makeLiveFSBuild(
> + name=u"livefs-1", requester=person, owner=person,
> + distroarchseries=distroarchseries,
> + date_created=datetime(2014, 04, 25, 10, 38, 0, tzinfo=pytz.UTC),
> + status=BuildStatus.FAILEDTOBUILD,
> + builder=self.factory.makeBuilder(name="bob"),
> + duration=timedelta(minutes=10))
> + build.setLog(self.factory.makeLibraryFileAlias())
> + build.notify()
> + [notification] = pop_notifications()
> + self.assertEqual(
> + config.canonical.noreply_from_address, notification["From"])
> + self.assertEqual(
> + "Person <%s>" % person.preferredemail.email, notification["To"])
> + subject = notification["Subject"].replace("\n ", " ")
> + self.assertEqual(
> + "[LiveFS build #%d] i386 build of livefs-1 live filesystem in "
> + "distro unstable" % build.id, subject)
> + self.assertEqual(
> + "Requester", notification["X-Launchpad-Message-Rationale"])
> + self.assertEqual(
> + "livefs-build-status",
> + notification["X-Launchpad-Notification-Type"])
> + self.assertEqual(
> + "FAILEDTOBUILD", notification["X-Launchpad-Build-State"])
> + body, footer = notification.get_payload(decode=True).split("\n-- \n")
> + self.assertEqual(expected_body % (build.log_url, ""), body)
> + self.assertEqual(
> + "http://launchpad.dev/~person/+livefs/distro/unstable/livefs-1/"
> + "+livefsbuild/%d\n"
> + "You are the requester of the build.\n" % build.id, footer)
> +
> + def addFakeBuildLog(self, build):
> + build.setLog(self.factory.makeLibraryFileAlias("mybuildlog.txt"))
> +
> + def test_log_url(self):
> + # The log URL for a live filesystem build will use the archive context.
> + self.addFakeBuildLog(self.build)
> + self.assertEqual(
> + "http://launchpad.dev/~%s/+livefs/%s/%s/%s/+livefsbuild/%d/+files/"
> + "mybuildlog.txt" % (
> + self.build.livefs.owner.name, self.build.distribution.name,
> + self.build.distro_series.name, self.build.livefs.name,
> + self.build.id),
> + self.build.log_url)
> +
> +
> +class TestLiveFSBuildSet(TestCaseWithFactory):
> +
> + layer = LaunchpadZopelessLayer
> +
> + def setUp(self):
> + super(TestLiveFSBuildSet, self).setUp()
> + self.useFixture(FeatureFixture({LIVEFS_FEATURE_FLAG: u"on"}))
> +
> + def test_getByBuildFarmJob_works(self):
> + build = self.factory.makeLiveFSBuild()
> + self.assertEqual(
> + build,
> + getUtility(ILiveFSBuildSet).getByBuildFarmJob(
> + build.build_farm_job))
> +
> + def test_getByBuildFarmJob_returns_None_when_missing(self):
> + bpb = self.factory.makeBinaryPackageBuild()
> + self.assertIsNone(
> + getUtility(ILiveFSBuildSet).getByBuildFarmJob(bpb.build_farm_job))
> +
> + def test_getByBuildFarmJobs_works(self):
> + builds = [self.factory.makeLiveFSBuild() for i in range(10)]
> + self.assertContentEqual(
> + builds,
> + getUtility(ILiveFSBuildSet).getByBuildFarmJobs(
> + [build.build_farm_job for build in builds]))
> +
> + def test_getByBuildFarmJobs_works_empty(self):
> + self.assertContentEqual(
> + [], getUtility(ILiveFSBuildSet).getByBuildFarmJobs([]))
> +
> +
> +class NonRedirectingMechanizeBrowser(PublisherMechanizeBrowser):
> + """A `mechanize.Browser` that does not handle redirects."""
> +
> + default_features = [
> + feature for feature in PublisherMechanizeBrowser.default_features
> + if feature != "_redirect"]
> +
> +
> +class TestLiveFSBuildWebservice(TestCaseWithFactory):
> +
> + layer = LaunchpadFunctionalLayer
> +
> + def setUp(self):
> + super(TestLiveFSBuildWebservice, self).setUp()
> + self.useFixture(FeatureFixture({LIVEFS_FEATURE_FLAG: u"on"}))
> + self.person = self.factory.makePerson()
> + self.webservice = webservice_for_person(
> + self.person, permission=OAuthPermission.WRITE_PRIVATE)
> + self.webservice.default_api_version = "devel"
> + login(ANONYMOUS)
> +
> + def getURL(self, obj):
> + return self.webservice.getAbsoluteUrl(api_url(obj))
> +
> + def test_properties(self):
> + # The basic properties of a LiveFSBuild are sensible.
> + db_build = self.factory.makeLiveFSBuild(
> + requester=self.person, unique_key=u"foo",
> + metadata_override={"image_format": "plain"},
> + date_created=datetime(2014, 04, 25, 10, 38, 0, tzinfo=pytz.UTC))
> + build_url = api_url(db_build)
> + logout()
> + build = self.webservice.get(build_url).jsonBody()
> + with person_logged_in(self.person):
> + self.assertEqual(self.getURL(self.person), build["requester_link"])
> + self.assertEqual(
> + self.getURL(db_build.livefs), build["livefs_link"])
> + self.assertEqual(
> + self.getURL(db_build.archive), build["archive_link"])
> + self.assertEqual(
> + self.getURL(db_build.distro_arch_series),
> + build["distro_arch_series_link"])
> + self.assertEqual("Release", build["pocket"])
> + self.assertEqual("foo", build["unique_key"])
> + self.assertEqual(
> + {"image_format": "plain"}, build["metadata_override"])
> + self.assertEqual("20140425-103800", build["version"])
> + self.assertIsNone(build["score"])
> + self.assertFalse(build["can_be_rescored"])
> + self.assertFalse(build["can_be_cancelled"])
> +
> + def test_public(self):
> + # A LiveFSBuild with a public LiveFS and archive is itself public.
> + db_build = self.factory.makeLiveFSBuild()
> + build_url = api_url(db_build)
> + unpriv_webservice = webservice_for_person(
> + self.factory.makePerson(), permission=OAuthPermission.WRITE_PUBLIC)
> + unpriv_webservice.default_api_version = "devel"
> + logout()
> + self.assertEqual(200, self.webservice.get(build_url).status)
> + self.assertEqual(200, unpriv_webservice.get(build_url).status)
> +
> + def test_private_livefs(self):
> + # A LiveFSBuild with a private LiveFS is private.
> + db_team = self.factory.makeTeam(
> + owner=self.person, visibility=PersonVisibility.PRIVATE)
> + with person_logged_in(self.person):
> + db_build = self.factory.makeLiveFSBuild(
> + requester=self.person, owner=db_team)
> + build_url = api_url(db_build)
> + unpriv_webservice = webservice_for_person(
> + self.factory.makePerson(), permission=OAuthPermission.WRITE_PUBLIC)
> + unpriv_webservice.default_api_version = "devel"
> + logout()
> + self.assertEqual(200, self.webservice.get(build_url).status)
> + # 404 since we aren't allowed to know that the private team exists.
> + self.assertEqual(404, unpriv_webservice.get(build_url).status)
> +
> + def test_private_archive(self):
> + # A LiveFSBuild with a private archive is private.
> + db_archive = self.factory.makeArchive(owner=self.person, private=True)
> + with person_logged_in(self.person):
> + db_build = self.factory.makeLiveFSBuild(archive=db_archive)
> + build_url = api_url(db_build)
> + unpriv_webservice = webservice_for_person(
> + self.factory.makePerson(), permission=OAuthPermission.WRITE_PUBLIC)
> + unpriv_webservice.default_api_version = "devel"
> + logout()
> + self.assertEqual(200, self.webservice.get(build_url).status)
> + self.assertEqual(401, unpriv_webservice.get(build_url).status)
> +
> + def test_cancel(self):
> + # The owner of a build can cancel it.
> + db_build = self.factory.makeLiveFSBuild(requester=self.person)
> + db_build.queueBuild()
> + build_url = api_url(db_build)
> + unpriv_webservice = webservice_for_person(
> + self.factory.makePerson(), permission=OAuthPermission.WRITE_PUBLIC)
> + unpriv_webservice.default_api_version = "devel"
> + logout()
> + build = self.webservice.get(build_url).jsonBody()
> + self.assertTrue(build["can_be_cancelled"])
> + response = unpriv_webservice.named_post(build["self_link"], "cancel")
> + self.assertEqual(401, response.status)
> + response = self.webservice.named_post(build["self_link"], "cancel")
> + self.assertEqual(200, response.status)
> + build = self.webservice.get(build_url).jsonBody()
> + self.assertFalse(build["can_be_cancelled"])
> + with person_logged_in(self.person):
> + self.assertEqual(BuildStatus.CANCELLED, db_build.status)
> +
> + def test_rescore(self):
> + # Buildd administrators can rescore builds.
> + db_build = self.factory.makeLiveFSBuild(requester=self.person)
> + db_build.queueBuild()
> + build_url = api_url(db_build)
> + buildd_admin = self.factory.makePerson(
> + member_of=[getUtility(ILaunchpadCelebrities).buildd_admin])
> + buildd_admin_webservice = webservice_for_person(
> + buildd_admin, permission=OAuthPermission.WRITE_PUBLIC)
> + buildd_admin_webservice.default_api_version = "devel"
> + logout()
> + build = self.webservice.get(build_url).jsonBody()
> + self.assertEqual(2505, build["score"])
> + self.assertTrue(build["can_be_rescored"])
> + response = self.webservice.named_post(
> + build["self_link"], "rescore", score=5000)
> + self.assertEqual(401, response.status)
> + response = buildd_admin_webservice.named_post(
> + build["self_link"], "rescore", score=5000)
> + self.assertEqual(200, response.status)
> + build = self.webservice.get(build_url).jsonBody()
> + self.assertEqual(5000, build["score"])
> +
> + def makeNonRedirectingBrowser(self, person):
> + # The test browser can only work with the appserver, not the
> + # librarian, so follow one layer of redirection through the
> + # appserver and then ask the librarian for the real file.
> + browser = Browser(mech_browser=NonRedirectingMechanizeBrowser())
> + browser.handleErrors = False
> + with person_logged_in(person):
> + browser.addHeader(
> + "Authorization", "Basic %s:test" % person.preferredemail.email)
> + return browser
> +
> + def assertCanOpenRedirectedUrl(self, browser, url):
> + redirection = self.assertRaises(HTTPError, browser.open, url)
> + self.assertEqual(303, redirection.code)
> + urlopen(redirection.hdrs["Location"]).close()
> +
> + def test_logs(self):
> + # API clients can fetch the build and upload logs.
> + db_build = self.factory.makeLiveFSBuild(requester=self.person)
> + db_build.setLog(self.factory.makeLibraryFileAlias("buildlog.txt.gz"))
> + db_build.storeUploadLog("uploaded")
> + build_url = api_url(db_build)
> + logout()
> + build = self.webservice.get(build_url).jsonBody()
> + browser = self.makeNonRedirectingBrowser(self.person)
> + self.assertIsNotNone(build["build_log_url"])
> + self.assertCanOpenRedirectedUrl(browser, build["build_log_url"])
> + self.assertIsNotNone(build["upload_log_url"])
> + self.assertCanOpenRedirectedUrl(browser, build["upload_log_url"])
> +
> + def test_getFileUrls(self):
> + # API clients can fetch files attached to builds.
> + db_build = self.factory.makeLiveFSBuild(requester=self.person)
> + db_files = [
> + self.factory.makeLiveFSFile(livefsbuild=db_build)
> + for i in range(2)]
> + build_url = api_url(db_build)
> + file_urls = [
> + ProxiedLibraryFileAlias(file.libraryfile, db_build).http_url
> + for file in db_files]
> + logout()
> + response = self.webservice.named_get(build_url, "getFileUrls")
> + self.assertEqual(200, response.status)
> + self.assertContentEqual(file_urls, response.jsonBody())
> + browser = self.makeNonRedirectingBrowser(self.person)
> + for file_url in file_urls:
> + self.assertCanOpenRedirectedUrl(browser, file_url)
>
> === added file 'lib/lp/soyuz/tests/test_livefsbuildbehaviour.py'
> --- lib/lp/soyuz/tests/test_livefsbuildbehaviour.py 1970-01-01 00:00:00 +0000
> +++ lib/lp/soyuz/tests/test_livefsbuildbehaviour.py 2014-05-27 07:47:13 +0000
> @@ -0,0 +1,240 @@
> +# Copyright 2014 Canonical Ltd. This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Test live filesystem build behaviour."""
> +
> +__metaclass__ = type
> +
> +from datetime import datetime
> +from textwrap import dedent
> +
> +import fixtures
> +import pytz
> +from testtools import run_test_with
> +from testtools.deferredruntest import (
> + assert_fails_with,
> + AsynchronousDeferredRunTest,
> + )
> +import transaction
> +from twisted.internet import defer
> +from twisted.trial.unittest import TestCase as TrialTestCase
> +from zope.component import getUtility
> +from zope.security.proxy import (
> + Proxy,
> + removeSecurityProxy,
> + )
> +
> +from lp.buildmaster.enums import BuildStatus
> +from lp.buildmaster.interfaces.builder import CannotBuild
> +from lp.buildmaster.interfaces.buildfarmjobbehaviour import (
> + IBuildFarmJobBehaviour,
> + )
> +from lp.buildmaster.tests.mock_slaves import (
> + MockBuilder,
> + OkSlave,
> + )
> +from lp.buildmaster.tests.test_buildfarmjobbehaviour import (
> + TestGetUploadMethodsMixin,
> + TestHandleStatusMixin,
> + TestVerifySuccessfulBuildMixin,
> + )
> +from lp.registry.interfaces.pocket import PackagePublishingPocket
> +from lp.registry.interfaces.series import SeriesStatus
> +from lp.services.features.testing import FeatureFixture
> +from lp.services.log.logger import BufferLogger
> +from lp.soyuz.adapters.archivedependencies import get_sources_list_for_building
> +from lp.soyuz.interfaces.livefs import LIVEFS_FEATURE_FLAG
> +from lp.soyuz.interfaces.processor import IProcessorSet
> +from lp.soyuz.model.livefsbuildbehaviour import LiveFSBuildBehaviour
> +from lp.testing import TestCaseWithFactory
> +from lp.testing.layers import LaunchpadZopelessLayer
> +
> +
> +class TestLiveFSBuildBehaviour(TestCaseWithFactory):
> +
> + layer = LaunchpadZopelessLayer
> +
> + def setUp(self):
> + super(TestLiveFSBuildBehaviour, self).setUp()
> + self.useFixture(FeatureFixture({LIVEFS_FEATURE_FLAG: u"on"}))
> +
> + def makeJob(self, **kwargs):
> + """Create a sample `ILiveFSBuildBehaviour`."""
> + distribution = self.factory.makeDistribution(name="distro")
> + distroseries = self.factory.makeDistroSeries(
> + distribution=distribution, name="unstable")
> + processor = getUtility(IProcessorSet).getByName("386")
> + distroarchseries = self.factory.makeDistroArchSeries(
> + distroseries=distroseries, architecturetag="i386",
> + processor=processor)
> + build = self.factory.makeLiveFSBuild(
> + distroarchseries=distroarchseries,
> + pocket=PackagePublishingPocket.RELEASE, name=u"livefs", **kwargs)
> + return IBuildFarmJobBehaviour(build)
> +
> + def test_provides_interface(self):
> + # LiveFSBuildBehaviour provides IBuildFarmJobBehaviour.
> + job = LiveFSBuildBehaviour(None)
> + self.assertProvides(job, IBuildFarmJobBehaviour)
> +
> + def test_adapts_ILiveFSBuild(self):
> + # IBuildFarmJobBehaviour adapts an ILiveFSBuild.
> + build = self.factory.makeLiveFSBuild()
> + job = IBuildFarmJobBehaviour(build)
> + self.assertProvides(job, IBuildFarmJobBehaviour)
> +
> + def test_displayname(self):
> + # displayname contains a reasonable description of the job.
> + job = self.makeJob()
> + self.assertEqual(
> + "i386 build of livefs live filesystem in distro unstable",
> + job.displayname)
> +
> + def test_logStartBuild(self):
> + # logStartBuild will properly report the image that's being built.
> + job = self.makeJob()
> + logger = BufferLogger()
> + job.logStartBuild(logger)
> + self.assertEqual(
> + "INFO startBuild(i386 build of livefs live filesystem in distro "
> + "unstable)\n", logger.getLogBuffer())
> +
> + def test_verifyBuildRequest_valid(self):
> + # verifyBuildRequest doesn't raise any exceptions when called with a
> + # valid builder set.
> + job = self.makeJob()
> + lfa = self.factory.makeLibraryFileAlias()
> + transaction.commit()
> + job.build.distro_arch_series.addOrUpdateChroot(lfa)
> + builder = MockBuilder()
> + job.setBuilder(builder, OkSlave())
> + logger = BufferLogger()
> + job.verifyBuildRequest(logger)
> + self.assertEqual("", logger.getLogBuffer())
> +
> + def test_verifyBuildRequest_virtual_mismatch(self):
> + # verifyBuildRequest raises on an attempt to build a virtualized
> + # build on a non-virtual builder.
> + job = self.makeJob()
> + lfa = self.factory.makeLibraryFileAlias()
> + transaction.commit()
> + job.build.distro_arch_series.addOrUpdateChroot(lfa)
> + builder = MockBuilder(virtualized=False)
> + job.setBuilder(builder, OkSlave())
> + logger = BufferLogger()
> + e = self.assertRaises(AssertionError, job.verifyBuildRequest, logger)
> + self.assertEqual(
> + "Attempt to build virtual item on a non-virtual builder.", str(e))
> +
> + def test_verifyBuildRequest_no_chroot(self):
> + # verifyBuildRequest raises when the DAS has no chroot.
> + job = self.makeJob()
> + builder = MockBuilder()
> + job.setBuilder(builder, OkSlave())
> + logger = BufferLogger()
> + e = self.assertRaises(CannotBuild, job.verifyBuildRequest, logger)
> + self.assertIn("Missing chroot", str(e))
> +
> + def test_getBuildCookie(self):
> + # A build cookie is made up of the job type and record id. The
> + # uploadprocessor relies on this format.
> + job = self.makeJob()
> + cookie = removeSecurityProxy(job).getBuildCookie()
> + self.assertEqual("LIVEFSBUILD-%s" % job.build.id, cookie)
> +
> + def test_extraBuildArgs(self):
> + # _extraBuildArgs returns a reasonable set of additional arguments.
> + job = self.makeJob(
> + date_created=datetime(2014, 04, 25, 10, 38, 0, tzinfo=pytz.UTC),
> + metadata={"project": "distro", "subproject": "special"})
> + expected_archives = get_sources_list_for_building(
> + job.build, job.build.distro_arch_series, None)
> + self.assertEqual({
> + "archive_private": False,
> + "archives": expected_archives,
> + "arch_tag": "i386",
> + "datestamp": "20140425-103800",
> + "project": "distro",
> + "subproject": "special",
> + "suite": "unstable",
> + }, job._extraBuildArgs())
> +
> + def test_extraBuildArgs_no_security_proxy(self):
> + # _extraBuildArgs returns an object without security wrapping, even
> + # if values in the metadata are (say) lists and hence get proxied by
> + # Zope.
> + job = self.makeJob(metadata={"lb_args": ["--option=value"]})
> + args = job._extraBuildArgs()
> + self.assertEqual(["--option=value"], args["lb_args"])
> + self.assertIsNot(Proxy, type(args["lb_args"]))
> +
> + @run_test_with(AsynchronousDeferredRunTest)
> + @defer.inlineCallbacks
> + def test_dispatchBuildToSlave(self):
> + # dispatchBuildToSlave makes the proper calls to the slave.
> + job = self.makeJob()
> + lfa = self.factory.makeLibraryFileAlias()
> + transaction.commit()
> + job.build.distro_arch_series.addOrUpdateChroot(lfa)
> + slave = OkSlave()
> + builder = MockBuilder("bob")
> + builder.processor = getUtility(IProcessorSet).getByName("386")
> + job.setBuilder(builder, slave)
> + logger = BufferLogger()
> + yield job.dispatchBuildToSlave("someid", logger)
> + self.assertStartsWith(
> + logger.getLogBuffer(),
> + dedent("""\
> + INFO Sending chroot file for live filesystem build to bob
> + INFO Initiating build 1-someid on http://fake:0000
> + """))
> + self.assertEqual(
> + ["ensurepresent", "build"], [call[0] for call in slave.call_log])
> + build_args = slave.call_log[1][1:]
> + self.assertEqual(job.getBuildCookie(), build_args[0])
> + self.assertEqual("livefs", build_args[1])
> + self.assertEqual([], build_args[3])
> + self.assertEqual(job._extraBuildArgs(), build_args[4])
> +
> + @run_test_with(AsynchronousDeferredRunTest)
> + def test_dispatchBuildToSlave_no_chroot(self):
> + # dispatchBuildToSlave fails when the DAS has no chroot.
> + job = self.makeJob()
> + builder = MockBuilder()
> + builder.processor = getUtility(IProcessorSet).getByName("386")
> + job.setBuilder(builder, OkSlave())
> + d = job.dispatchBuildToSlave("someid", BufferLogger())
> + return assert_fails_with(d, CannotBuild)
> +
> +
> +class MakeLiveFSBuildMixin:
> + """Provide the common makeBuild method returning a queued build."""
> +
> + def makeBuild(self):
> + self.useFixture(FeatureFixture({LIVEFS_FEATURE_FLAG: u"on"}))
> + build = self.factory.makeLiveFSBuild(status=BuildStatus.BUILDING)
> + build.queueBuild()
> + return build
> +
> + def makeUnmodifiableBuild(self):
> + self.useFixture(FeatureFixture({LIVEFS_FEATURE_FLAG: u"on"}))
> + build = self.factory.makeLiveFSBuild(status=BuildStatus.BUILDING)
> + build.distro_series.status = SeriesStatus.OBSOLETE
> + build.queueBuild()
> + return build
> +
> +
> +class TestGetUploadMethodsForLiveFSBuild(
> + MakeLiveFSBuildMixin, TestGetUploadMethodsMixin, TestCaseWithFactory):
> + """IPackageBuild.getUpload-related methods work with LiveFS builds."""
> +
> +
> +class TestVerifySuccessfulBuildForLiveFSBuild(
> + MakeLiveFSBuildMixin, TestVerifySuccessfulBuildMixin, TestCaseWithFactory):
> + """IBuildFarmJobBehaviour.verifySuccessfulBuild works."""
> +
> +
> +class TestHandleStatusForLiveFSBuild(
> + MakeLiveFSBuildMixin, TestHandleStatusMixin, TrialTestCase,
> + fixtures.TestWithFixtures):
> + """IPackageBuild.handleStatus works with LiveFS builds."""
>
> === modified file 'lib/lp/testing/factory.py'
> --- lib/lp/testing/factory.py 2014-04-24 02:16:27 +0000
> +++ lib/lp/testing/factory.py 2014-05-27 07:47:13 +0000
> @@ -2,7 +2,7 @@
> # NOTE: The first line above must stay first; do not move the copyright
> # notice to the top. See http://www.python.org/dev/peps/pep-0263/.
> #
> -# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
> +# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> """Testing infrastructure for the Launchpad application.
> @@ -283,6 +283,8 @@
> IComponent,
> IComponentSet,
> )
> +from lp.soyuz.interfaces.livefs import ILiveFSSet
> +from lp.soyuz.interfaces.livefsbuild import ILiveFSBuildSet
> from lp.soyuz.interfaces.packagecopyjob import IPlainPackageCopyJobSource
> from lp.soyuz.interfaces.packageset import IPackagesetSet
> from lp.soyuz.interfaces.processor import IProcessorSet
> @@ -297,6 +299,7 @@
> BinaryPackageFile,
> SourcePackageReleaseFile,
> )
> +from lp.soyuz.model.livefsbuild import LiveFSFile
> from lp.soyuz.model.packagediff import PackageDiff
> from lp.testing import (
> admin_logged_in,
> @@ -4316,6 +4319,80 @@
> product.redeemSubscriptionVoucher(
> self.getUniqueString(), person, person, months)
>
> + def makeLiveFS(self, registrant=None, owner=None, distroseries=None,
> + name=None, metadata=None, date_created=DEFAULT):
> + """Make a new LiveFS."""
> + if registrant is None:
> + registrant = self.makePerson()
> + if owner is None:
> + owner = self.makeTeam(registrant)
> + if distroseries is None:
> + distroseries = self.makeDistroSeries()
> + if name is None:
> + name = self.getUniqueString(u"livefs-name")
> + if metadata is None:
> + metadata = {}
> + livefs = getUtility(ILiveFSSet).new(
> + registrant, owner, distroseries, name, metadata,
> + date_created=date_created)
> + IStore(livefs).flush()
> + return livefs
> +
> + def makeLiveFSBuild(self, requester=None, registrant=None, livefs=None,
> + archive=None, distroarchseries=None, pocket=None,
> + unique_key=None, metadata_override=None,
> + date_created=DEFAULT, status=BuildStatus.NEEDSBUILD,
> + builder=None, duration=None, **kwargs):
> + """Make a new LiveFSBuild."""
> + if requester is None:
> + requester = self.makePerson()
> + if livefs is None:
> + if "distroseries" in kwargs:
> + distroseries = kwargs["distroseries"]
> + del kwargs["distroseries"]
> + elif distroarchseries is not None:
> + distroseries = distroarchseries.distroseries
> + elif archive is not None:
> + distroseries = self.makeDistroSeries(
> + distribution=archive.distribution)
> + else:
> + distroseries = None
> + if registrant is None:
> + registrant = requester
> + livefs = self.makeLiveFS(
> + registrant=registrant, distroseries=distroseries, **kwargs)
> + if archive is None:
> + archive = livefs.distro_series.main_archive
> + if distroarchseries is None:
> + distroarchseries = self.makeDistroArchSeries(
> + distroseries=livefs.distro_series)
> + if pocket is None:
> + pocket = PackagePublishingPocket.RELEASE
> + livefsbuild = getUtility(ILiveFSBuildSet).new(
> + requester, livefs, archive, distroarchseries, pocket,
> + unique_key=unique_key, metadata_override=metadata_override,
> + date_created=date_created)
> + if duration is not None:
> + removeSecurityProxy(livefsbuild).updateStatus(
> + BuildStatus.BUILDING, builder=builder,
> + date_started=livefsbuild.date_created)
> + removeSecurityProxy(livefsbuild).updateStatus(
> + status, builder=builder,
> + date_finished=livefsbuild.date_started + duration)
> + else:
> + removeSecurityProxy(livefsbuild).updateStatus(
> + status, builder=builder)
> + IStore(livefsbuild).flush()
> + return livefsbuild
> +
> + def makeLiveFSFile(self, livefsbuild=None, libraryfile=None):
> + if livefsbuild is None:
> + livefsbuild = self.makeLiveFSBuild()
> + if libraryfile is None:
> + libraryfile = self.makeLibraryFileAlias()
> + return ProxyFactory(
> + LiveFSFile(livefsbuild=livefsbuild, libraryfile=libraryfile))
> +
>
> # Some factory methods return simple Python types. We don't add
> # security wrappers for them, as well as for objects created by
>
--
https://code.launchpad.net/~cjwatson/launchpad/livefs/+merge/217261
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.
References