launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #16664
[Merge] lp:~cjwatson/launchpad/livefs into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/livefs into lp:launchpad.
Commit message:
Implement live filesystem building.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1247461 in Launchpad itself: "Move live filesystem building into Launchpad"
https://bugs.launchpad.net/launchpad/+bug/1247461
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/livefs/+merge/217261
WIP for live filesystem building. Not actually ready for review yet, but this will produce a diff.
--
https://code.launchpad.net/~cjwatson/launchpad/livefs/+merge/217261
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/livefs into lp:launchpad.
=== 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-04-25 15:26: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,14 @@
)
from lp.soyuz.interfaces.buildrecords import IHasBuildRecords
from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
+from lp.soyuz.interfaces.livefs import (
+ ILiveFS,
+ ILiveFSView,
+ )
+from lp.soyuz.interfaces.livefsbuild import (
+ ILiveFSBuild,
+ ILiveFSFile,
+ )
from lp.soyuz.interfaces.packageset import (
IPackageset,
IPackagesetSet,
@@ -350,6 +358,10 @@
Reference(schema=IDistroSeries))
patch_plain_parameter_type(IPerson, 'createRecipe', 'daily_build_archive',
IArchive)
+patch_entry_return_type(IPerson, 'createLiveFS', ILiveFS)
+patch_plain_parameter_type(IPerson, 'createLiveFS', 'owner', IPerson)
+patch_plain_parameter_type(IPerson, 'createLiveFS', 'distroseries',
+ IDistroSeries)
patch_plain_parameter_type(IPerson, 'getArchiveSubscriptionURL', 'archive',
IArchive)
patch_collection_return_type(
@@ -576,6 +588,14 @@
# 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
+patch_reference_property(ILiveFSView, 'last_completed_build', ILiveFSBuild)
+
# IPackageset
patch_collection_return_type(
IPackageset, 'setsIncluded', IPackageset)
=== 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-04-25 15:26: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/registry/browser/person.py'
--- lib/lp/registry/browser/person.py 2014-03-21 03:34:57 +0000
+++ lib/lp/registry/browser/person.py 2014-04-25 15:26: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,26 @@
"""Traverse to this person's merge queues."""
return self.context.getMergeQueue(name)
+ @stepto('+livefs')
+ def traverse_livefs(self):
+ """Traverse to this person's live filesystem images."""
+ def get_segments(pillar_name):
+ base = [self.context.name, pillar_name]
+ return itertools.chain(iter(base), iter(self.request.stepstogo))
+
+ pillar_name = self.request.stepstogo.next()
+ livefs = getUtility(ILiveFSSet).traverse(get_segments(pillar_name))
+ if livefs is None:
+ raise NotFoundError
+
+ if livefs.distroseries.distribution.name != pillar_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-04-25 15:26: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."""
@@ -84,6 +84,7 @@
Bool,
Choice,
Datetime,
+ Dict,
Int,
List,
Object,
@@ -1024,12 +1025,32 @@
def getRecipe(name):
"""Return the person's recipe with the given name."""
+ @call_with(registrant=REQUEST_USER)
+ @operation_parameters(
+ owner=Reference(
+ Interface,
+ title=_("The person who registered this live filesystem image.")),
+ distroseries=Reference(
+ Interface, title=_("The owner of this live filesystem image.")),
+ name=TextLine(
+ title=_("The series for which the image should be built.")),
+ metadata=Dict(
+ title=_(
+ "A dict of data about the image. Entries here will be passed "
+ "to the builder slave."),
+ key_type=TextLine()),
+ )
+ @export_factory_operation(Interface, [])
+ @operation_for_version("devel")
+ def createLiveFS(registrant, owner, distroseries, name, metadata):
+ """Create a `LiveFS` owned by this person."""
+
def getMergeQueue(name):
"""Return the person's merge queue with the given name."""
@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-04-25 15:26: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."""
@@ -311,6 +311,7 @@
)
from lp.soyuz.interfaces.archive import IArchiveSet
from lp.soyuz.interfaces.archivesubscriber import IArchiveSubscriberSet
+from lp.soyuz.interfaces.livefs import ILiveFSSet
from lp.soyuz.model.archive import (
Archive,
validate_ppa,
@@ -2951,6 +2952,13 @@
SourcePackageRecipe, SourcePackageRecipe.owner == self,
SourcePackageRecipe.name == name).one()
+ def createLiveFS(self, registrant, owner, distroseries, name, metadata):
+ """See `IPerson`."""
+ livefs = getUtility(ILiveFSSet).new(
+ registrant, owner, distroseries, name, metadata)
+ Store.of(livefs).flush()
+ return livefs
+
def getMergeQueue(self, name):
from lp.code.model.branchmergequeue import BranchMergeQueue
return Store.of(self).find(
=== modified file 'lib/lp/security.py'
--- lib/lp/security.py 2014-03-17 21:50:33 +0000
+++ lib/lp/security.py 2014-04-25 15:26: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 __init__(self, obj):
+ super(ViewLiveFSBuild, self).__init__(
+ obj, obj.livefs, 'launchpad.View')
+
+
+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
+ 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/soyuz/adapters/archivedependencies.py'
--- lib/lp/soyuz/adapters/archivedependencies.py 2013-05-02 00:40:14 +0000
+++ lib/lp/soyuz/adapters/archivedependencies.py 2014-04-25 15:26: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,9 +119,12 @@
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:
+ ancestries = []
+ else:
+ ancestries = primary_archive.getPublishedSources(
+ name=sourcepackagename,
+ distroseries=distroseries, exact_match=True)
try:
return ancestries[0].component.name
=== modified file 'lib/lp/soyuz/browser/build.py'
--- lib/lp/soyuz/browser/build.py 2014-02-26 03:05:44 +0000
+++ lib/lp/soyuz/browser/build.py 2014-04-25 15:26: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."""
@@ -84,6 +84,7 @@
IBinaryPackageBuildSet,
IBuildRescoreForm,
)
+from lp.soyuz.interfaces.livefsbuild import ILiveFSBuildSet
class BuildUrl:
@@ -149,6 +150,17 @@
except NotFoundError:
return None
+ @stepthrough('+livefsbuild')
+ def traverse_livefsbuild(self, name):
+ try:
+ build_id = int(name)
+ except ValueError:
+ return None
+ try:
+ return getUtility(ILiveFSBuildSet).getByID(build_id)
+ except NotFoundError:
+ return None
+
class BuildContextMenu(ContextMenu):
"""Overview menu for build records """
=== 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-04-25 15:26: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,15 @@
template="../templates/packagerelationship-list.pt"
/>
</browser:pages>
+ <browser:url
+ for="lp.soyuz.interfaces.livefs.ILiveFS"
+ path_expression="string:+livefs/${distroseries/distribution/name}/${distroseries/name}/${name}"
+ attribute_to_parent="owner"
+ />
+ <browser:url
+ for="lp.soyuz.interfaces.livefsbuild.ILiveFSBuild"
+ path_expression="string:+livefsbuild/${id}"
+ attribute_to_parent="archive"
+ />
</facet>
</configure>
=== 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-04-25 15:26: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.distroseries.distribution.name,
+ livefs.distroseries.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-04-25 15:26: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,58 @@
<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"/>
+ </class>
+
+ <!-- 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"/>
+
<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-04-25 15:26: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
=== 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-04-25 15:26:13 +0000
@@ -0,0 +1,230 @@
+# 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__ = [
+ 'ILiveFS',
+ 'ILiveFSEditableAttributes',
+ 'ILiveFSSet',
+ 'ILiveFSView',
+ 'InvalidLiveFSNamespace',
+ 'LIVEFS_FEATURE_FLAG',
+ 'LiveFSBuildAlreadyPending',
+ 'LiveFSFeatureDisabled',
+ 'NoSuchLiveFS',
+ ]
+
+import httplib
+
+from lazr.lifecycle.snapshot import doNotSnapshot
+from lazr.restful.declarations import (
+ call_with,
+ error_status,
+ export_as_webservice_entry,
+ export_factory_operation,
+ export_write_operation,
+ exported,
+ operation_for_version,
+ operation_parameters,
+ 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.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.")
+
+
+class InvalidLiveFSNamespace(Exception):
+ """Raised when someone tries to lookup a namespace with a bad name.
+
+ By 'bad', we mean that the name is unparsable. It might be too short,
+ too long, or malformed in some other way.
+ """
+
+ def __init__(self, name):
+ self.name = name
+ super(InvalidLiveFSNamespace, self).__init__(
+ "Cannot understand namespace name: '%s'" % name)
+
+
+class NoSuchLiveFS(NameLookupFailed):
+ """Raised when we try to load a live filesystem that does not exist."""
+
+ _message_prefix = "No such live filesystem"
+
+
+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),
+ distroarchseries=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 JSON string with a dict of data about the image."),
+ key_type=TextLine(), required=False))
+ # Really ILiveFSBuild, patched in _schema_circular_imports.py.
+ @export_factory_operation(Interface, [])
+ @export_write_operation()
+ @operation_for_version("devel")
+ def requestBuild(requester, archive, distroarchseries, 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 distroarchseries: 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)))
+
+ last_completed_build = exported(Reference(
+ # Really ILiveFSBuild, patched in _schema_circular_imports.py.
+ Interface,
+ title=_("The most recent completed build of this live filesystem."),
+ readonly=True))
+
+
+class ILiveFSEditableAttributes(IHasOwner):
+ """`ILiveFS` attributes that can be edited.
+
+ These attributes need launchpad.View to see, and launchpad.Edit to change.
+ """
+ owner = exported(PersonChoice(
+ title=_("Owner"), required=True, readonly=False,
+ vocabulary="AllUserTeamsParticipationPlusSelf",
+ description=_("The owner of this live filesystem image.")))
+
+ distroseries = 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."""
+
+ export_as_webservice_entry(
+ singular_name="livefs", plural_name="livefses", as_of="devel")
+
+
+class ILiveFSSet(Interface):
+ """A utility to create and access live filesystems."""
+
+ def new(registrant, owner, distroseries, name, metadata,
+ date_created=None):
+ """Create an `ILiveFS`."""
+
+ def exists(owner, distroseries, name):
+ """Check to see if a matching live filesystem exists."""
+
+ def get(owner, distroseries, name):
+ """Return the appropriate `ILiveFS` for the given objects."""
+
+ def traverse(segments):
+ """Look up the `ILiveFS` at the path given by 'segments'.
+
+ The iterable 'segments' will be consumed until a live filesystem is
+ found. As soon as a live filesystem is found, it will be returned
+ and the consumption of segments will stop. Thus, there will often
+ be unconsumed segments that can be used for further traversal.
+
+ :param segments: An iterable of names of Launchpad components.
+ The first segment is the username, *not* preceded by a '~'.
+ :raise InvalidNamespace: if there are not enough segments to define
+ a live filesystem.
+ :raise NoSuchPerson: if the person referred to cannot be found.
+ :raise NoSuchDistribution: if the distribution referred to cannot be
+ found.
+ :raise NoSuchDistroSeries: if the distroseries referred to cannot be
+ found.
+ :raise NoSuchLiveFS: if the live filesystem referred to cannot be
+ found.
+ :return: `ILiveFS`.
+ """
=== 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-04-25 15:26:13 +0000
@@ -0,0 +1,185 @@
+# 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_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))
+
+ distroarchseries = 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_retried = exported(Bool(
+ title=_("Can be retried"),
+ required=True, readonly=True,
+ description=_("Whether this build record can be retried.")))
+
+ 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` records.
+ """
+
+ 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`.
+ """
+
+
+class ILiveFSBuildEdit(Interface):
+ """`ILiveFSBuild` attributes that require launchpad.Edit."""
+
+ @export_write_operation()
+ @operation_for_version("devel")
+ 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.
+
+ 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."""
+
+ export_as_webservice_entry(singular_name="livefs_build", as_of="devel")
+
+
+class ILiveFSBuildSet(ISpecificBuildFarmJobSource):
+ """Utility for `ILiveFSBuild`."""
+
+ def new(requester, livefs, archive, distroarchseries, 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-04-25 15:26: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.
@@ -86,6 +86,8 @@
)
from lp.soyuz.interfaces.buildrecords import IncompatibleArguments
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.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-04-25 15:26:13 +0000
@@ -0,0 +1,88 @@
+# 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", 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")
+ 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.distroseries,
+ "architecturetag": build.distroarchseries.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-04-25 15:26:13 +0000
@@ -0,0 +1,217 @@
+# 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.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
+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 (
+ ILiveFS,
+ ILiveFSSet,
+ InvalidLiveFSNamespace,
+ LIVEFS_FEATURE_FLAG,
+ LiveFSBuildAlreadyPending,
+ LiveFSFeatureDisabled,
+ NoSuchLiveFS,
+ )
+from lp.soyuz.interfaces.livefsbuild import ILiveFSBuildSet
+from lp.soyuz.model.archive import Archive
+from lp.soyuz.model.livefsbuild import LiveFSBuild
+
+
+class LiveFS(Storm):
+ """See `ILiveFS`."""
+
+ __storm_table__ = 'LiveFS'
+
+ def __str__(self):
+ return '%s %s' % (self.distroseries, self.name)
+
+ implements(ILiveFS, IHasOwner)
+
+ id = Int(primary=True)
+
+ date_created = DateTime(
+ name='date_created', 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')
+
+ distroseries_id = Int(name='distroseries', allow_none=False)
+ distroseries = Reference(distroseries_id, 'DistroSeries.id')
+
+ name = Unicode(name='name', allow_none=False)
+
+ metadata = JSON('json_data')
+
+ def __init__(self, registrant, owner, distroseries, 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.distroseries = distroseries
+ self.name = name
+ self.metadata = metadata
+ self.date_created = date_created
+
+ def requestBuild(self, requester, archive, distroarchseries, pocket,
+ unique_key=None, metadata_override=None):
+ """See `ILiveFS`."""
+ pending = IStore(self).find(
+ LiveFSBuild,
+ LiveFSBuild.livefs_id == self.id,
+ LiveFSBuild.archive_id == archive.id,
+ LiveFSBuild.distroarchseries_id == distroarchseries.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, distroarchseries, 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
+
+ @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 last_completed_build(self):
+ """See `ILiveFS`."""
+ filter_term = (LiveFSBuild.status != BuildStatus.NEEDSBUILD)
+ return self._getBuilds(
+ filter_term, Desc(LiveFSBuild.date_finished)).first()
+
+
+class LiveFSSet:
+ """See `ILiveFSSet`."""
+
+ implements(ILiveFSSet)
+
+ def new(self, registrant, owner, distroseries, name, metadata,
+ date_created=DEFAULT):
+ """See `ILiveFSSet`."""
+ store = IMasterStore(LiveFS)
+ livefs = LiveFS(
+ registrant, owner, distroseries, name, metadata, date_created)
+ store.add(livefs)
+ return livefs
+
+ def exists(self, owner, distroseries, name):
+ """See `ILiveFSSet`."""
+ livefs = self.get(owner, distroseries, name)
+ if livefs:
+ return True
+ else:
+ return False
+
+ def get(self, owner, distroseries, name):
+ """See `ILiveFSSet`."""
+ store = IMasterStore(LiveFS)
+ return store.find(
+ LiveFS,
+ LiveFS.owner == owner,
+ LiveFS.distroseries == distroseries,
+ LiveFS.name == name).one()
+
+ 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 traverse(self, segments):
+ """See `ILiveFSSet`."""
+ traversed_segments = []
+
+ def get_next_segment():
+ try:
+ result = segments.next()
+ except StopIteration:
+ raise InvalidLiveFSNamespace("/".join(traversed_segments))
+ if result is None:
+ raise AssertionError("None segment passed to traverse()")
+ traversed_segments.append(result)
+ return result
+
+ person_name = get_next_segment()
+ person = self._findOrRaise(
+ NoSuchPerson, person_name, getUtility(IPersonSet).getByName)
+ distribution_name = get_next_segment()
+ distribution = self._findOrRaise(
+ NoSuchDistribution, distribution_name,
+ getUtility(IDistributionSet).getByName)
+ distroseries_name = get_next_segment()
+ distroseries = self._findOrRaise(
+ NoSuchDistroSeries, distroseries_name,
+ getUtility(IDistroSeriesSet).queryByName, distribution)
+ livefs_name = get_next_segment()
+ return self._findOrRaise(
+ NoSuchLiveFS, livefs_name, self.get, person, distroseries)
=== 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-04-25 15:26:13 +0000
@@ -0,0 +1,375 @@
+# 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,
+ 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
+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')
+
+
+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')
+
+ distroarchseries_id = Int(name='distroarchseries', allow_none=False)
+ distroarchseries = Reference(distroarchseries_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,
+ distroarchseries, 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.distroarchseries = distroarchseries
+ self.pocket = pocket
+ self.processor = processor
+ self.virtualized = virtualized
+ self.unique_key = unique_key
+ if metadata_override is None:
+ metadata_override = {}
+ self.metadata_override = metadata_override
+ self.date_created = date_created
+ self.status = BuildStatus.NEEDSBUILD
+
+ @property
+ def is_virtualized(self):
+ """See `ILiveFSBuild`."""
+ return self.archive.require_virtualized
+
+ @property
+ def title(self):
+ das = self.distroarchseries
+ name = self.livefs.name
+ if self.unique_key is not None:
+ name += " (%s)" % self.unique_key
+ return "%s build of %s in %s %s %s" % (
+ das.architecturetag, name, das.distroseries.distribution.name,
+ das.distroseries.name, self.pocket.name)
+
+ @property
+ def distribution(self):
+ """See `IPackageBuild`."""
+ return self.distroarchseries.distroseries.distribution
+
+ @property
+ def distro_series(self):
+ """See `IPackageBuild`."""
+ return self.distroarchseries.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_retried(self):
+ """See `ILiveFSBuild`."""
+ # We provide this property for API convenience, but live filesystem
+ # builds cannot be retried. Request another build using
+ # LiveFS.requestBuild instead.
+ return False
+
+ @property
+ def can_be_rescored(self):
+ """See `ILiveFSBuild`."""
+ return 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 builds of this live filesystem."""
+ store = IStore(self)
+ result = store.find(
+ LiveFSBuild,
+ LiveFSBuild.livefs == self.livefs_id,
+ LiveFSBuild.distroarchseries == self.distroarchseries_id,
+ LiveFSBuild.date_finished != None)
+ durations = [
+ build.date_finished - build.date_started for build in result]
+ if len(durations) == 0:
+ return None
+ durations.sort(reverse=True)
+ 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)
+ result = store.find(
+ LiveFSFile,
+ LiveFSFile.livefsbuild == self.id,
+ LibraryFileAlias.id == LiveFSFile.libraryfile_id)
+ return result.order_by(
+ [LibraryFileAlias.filename, LiveFSFile.id]).config(distinct=True)
+
+ 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(
+ LiveFSFile,
+ 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 verifySuccessfulUpload(self):
+ """See `IPackageBuild`."""
+ return bool(self.getFiles())
+
+ 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 getUploader(self, changes):
+ """See `IPackageBuild`."""
+ return self.requester
+
+ 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)
+
+
+class LiveFSBuildSet(SpecificBuildFarmJobSourceMixin):
+ implements(ILiveFSBuildSet)
+
+ def new(self, requester, livefs, archive, distroarchseries, 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, distroarchseries,
+ pocket, distroarchseries.processor, archive.require_virtualized,
+ unique_key, metadata_override, date_created)
+ store.add(livefsbuild)
+ return livefsbuild
+
+ def getByID(self, build_id):
+ """See `ISpecificBuildFarmJobSource`."""
+ store = IMasterStore(LiveFSBuild)
+ return store.find(LiveFSBuild, LiveFSBuild.id == build_id).one()
+
+ 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-04-25 15:26:13 +0000
@@ -0,0 +1,133 @@
+# 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 lp.buildmaster.interfaces.builder import CannotBuild
+from lp.buildmaster.interfaces.buildfarmjobbehaviour import (
+ IBuildFarmJobBehaviour,
+ )
+from lp.buildmaster.model.buildfarmjobbehaviour import (
+ BuildFarmJobBehaviourBase,
+ )
+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.distroarchseries
+ 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.distroarchseries.getChroot()
+ if chroot is None:
+ raise CannotBuild(
+ "Missing chroot for %s" % build.distroarchseries.displayname)
+
+ def _extraBuildArgs(self):
+ """
+ Return the extra arguments required by the slave for the given build.
+ """
+ build = self.build
+ args = dict(build.livefs.metadata)
+ args.update(build.metadata_override)
+ args["suite"] = build.distroarchseries.distroseries.getSuite(
+ build.pocket)
+ args["arch_tag"] = build.distroarchseries.architecturetag
+ args["datestamp"] = build.version
+ args["archives"] = get_sources_list_for_building(
+ build, build.distroarchseries, 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.
+ distroarchseries = self.build.distroarchseries
+ chroot = distroarchseries.getChroot()
+ if chroot is None:
+ raise CannotBuild(
+ "Unable to find a chroot for %s" %
+ distroarchseries.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)
=== 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-04-25 15:26:13 +0000
@@ -0,0 +1,384 @@
+# 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 timedelta
+
+from storm.locals import Store
+from testtools.matchers import Equals
+import transaction
+from zope.component import getUtility
+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.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.
+ self.assertRaises(
+ LiveFSFeatureDisabled, getUtility(ILiveFSSet).new,
+ None, None, 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_snapshot(self):
+ self.assertThat(
+ self.factory.makeLiveFS(),
+ DoesNotSnapshot(["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),
+ distroseries=self.factory.makeDistroSeries(),
+ name=self.factory.getUniqueString(u"recipe-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["distroseries"], livefs.distroseries)
+ self.assertEqual(components["name"], livefs.name)
+ self.assertEqual(components["metadata"], livefs.metadata)
+
+ def test_exists(self):
+ # ILiveFSSet.exists checks for matching LiveFSes.
+ livefs = self.factory.makeLiveFS()
+ self.assertTrue(
+ getUtility(ILiveFSSet).exists(
+ livefs.owner, livefs.distroseries, livefs.name))
+ self.assertFalse(
+ getUtility(ILiveFSSet).exists(
+ self.factory.makePerson(), livefs.distroseries, livefs.name))
+ self.assertFalse(
+ getUtility(ILiveFSSet).exists(
+ livefs.owner, self.factory.makeDistroSeries(), livefs.name))
+ self.assertFalse(
+ getUtility(ILiveFSSet).exists(
+ livefs.owner, livefs.distroseries, u"different"))
+
+ def test_requestBuild(self):
+ # requestBuild creates a new LiveFSBuild.
+ livefs = self.factory.makeLiveFS()
+ requester = self.factory.makePerson()
+ distroarchseries = self.factory.makeDistroArchSeries(
+ distroseries=livefs.distroseries)
+ build = livefs.requestBuild(
+ requester, livefs.distroseries.main_archive, distroarchseries,
+ PackagePublishingPocket.RELEASE)
+ self.assertTrue(ILiveFSBuild.providedBy(build))
+ self.assertEqual(requester, build.requester)
+ self.assertEqual(livefs.distroseries.main_archive, build.archive)
+ self.assertEqual(distroarchseries, build.distroarchseries)
+ self.assertEqual(PackagePublishingPocket.RELEASE, build.pocket)
+ self.assertIsNone(build.unique_key)
+ self.assertEqual({}, 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.distroseries.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.distroseries)
+ build = livefs.requestBuild(
+ livefs.owner, livefs.distroseries.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.distroseries)
+ 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.distroseries)
+ old_build = livefs.requestBuild(
+ livefs.owner, livefs.distroseries.main_archive, distroarchseries,
+ PackagePublishingPocket.RELEASE)
+ self.assertRaises(
+ LiveFSBuildAlreadyPending, livefs.requestBuild,
+ livefs.owner, livefs.distroseries.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.distroseries.main_archive,
+ self.factory.makeDistroArchSeries(
+ distroseries=livefs.distroseries),
+ PackagePublishingPocket.RELEASE)
+ # Changing the status of the old build allows a new build.
+ old_build.updateStatus(BuildStatus.FULLYBUILT)
+ livefs.requestBuild(
+ livefs.owner, livefs.distroseries.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.assertIsNone(livefs.last_completed_build)
+
+ # Change the status of one of the builds and retest.
+ builds[0].updateStatus(BuildStatus.FULLYBUILT)
+ self.assertEqual(builds, list(livefs.builds))
+ self.assertEqual(builds[0], livefs.last_completed_build)
+
+
+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()
+ 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, registrant=None, owner=None, distroseries=None,
+ metadata=None):
+ if registrant is None:
+ registrant = self.person
+ if owner is None:
+ owner = registrant
+ if metadata is None:
+ metadata = {"project": "flavour"}
+ if distroseries is None:
+ distroseries = self.factory.makeDistroSeries(registrant=registrant)
+ transaction.commit()
+ distroseries_url = api_url(distroseries)
+ registrant_url = api_url(registrant)
+ owner_url = api_url(owner)
+ logout()
+ response = self.webservice.named_post(
+ registrant_url, "createLiveFS", owner=owner_url,
+ distroseries=distroseries_url, name="flavour-desktop",
+ metadata=metadata)
+ self.assertEqual(201, response.status)
+ livefs = self.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_createLiveFS(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["distroseries_link"])
+ self.assertEqual({"project": "flavour"}, livefs["metadata"])
+
+ def test_requestBuild(self):
+ # Build requests can be performed and end up in livefs.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, distroseries_url = self.makeLiveFS(distroseries=distroseries)
+ response = self.webservice.named_post(
+ livefs["self_link"], "requestBuild", archive=archive_url,
+ distroarchseries=distroarchseries_url, pocket="Release")
+ self.assertEqual(201, response.status)
+ build = self.webservice.get(response.getHeader("Location")).jsonBody()
+ builds_collection = self.webservice.get(
+ livefs["builds_collection_link"]).jsonBody()
+ self.assertEqual(
+ [build["self_link"]], self.getCollectionLinks(livefs, "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, ws_distroseries = self.makeLiveFS(distroseries=distroseries)
+ response = self.webservice.named_post(
+ livefs["self_link"], "requestBuild", archive=archive_url,
+ distroarchseries=distroarchseries_url, pocket="Release")
+ self.assertEqual(201, response.status)
+ response = self.webservice.named_post(
+ livefs["self_link"], "requestBuild", archive=archive_url,
+ distroarchseries=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_getBuilds(self):
+ # builds and last_completed_build 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, distroseries_url = self.makeLiveFS(distroseries=distroseries)
+ builds = []
+ for archive_url in archive_urls:
+ response = self.webservice.named_post(
+ livefs["self_link"], "requestBuild", archive=archive_url,
+ distroarchseries = 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"))
+ livefs = self.webservice.get(livefs["self_link"]).jsonBody()
+ self.assertIsNone(livefs["last_completed_build_link"])
+
+ with person_logged_in(self.person):
+ db_livefs = getUtility(ILiveFSSet).get(
+ 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()
+ self.assertEqual(builds[0], livefs["last_completed_build_link"])
+
+ 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[1], livefs["last_completed_build_link"])
+
+ 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(22)))
=== 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-04-25 15:26:13 +0000
@@ -0,0 +1,299 @@
+# 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,
+ )
+
+import pytz
+from zope.component import getUtility
+from zope.security.proxy import removeSecurityProxy
+
+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.services.database.interfaces import IStore
+from lp.services.features.testing import FeatureFixture
+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.testing import (
+ ANONYMOUS,
+ api_url,
+ login,
+ logout,
+ person_logged_in,
+ TestCaseWithFactory,
+ )
+from lp.testing.layers import (
+ DatabaseFunctionalLayer,
+ 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)
+
+
+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 owner is.
+ self.assertFalse(self.build.is_private)
+ # TODO archive too? need to override PackageBuild.is_private in
+ # LiveFSBuild?
+
+ 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 builds of the same live filesystem are used for estimates.
+ self.factory.makeLiveFSBuild(
+ requester=self.build.requester, livefs=self.build.livefs,
+ distroarchseries=self.build.distroarchseries,
+ status=BuildStatus.FULLYBUILT, duration=timedelta(seconds=335))
+ self.assertEqual(335, self.build.estimateDuration().seconds)
+
+ def test_getFileByName(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))
+
+ # TODO getFileByName on other files
+
+ 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)
+ build.updateStatus(BuildStatus.FULLYBUILT)
+ IStore(build).flush()
+ build.notify()
+ self.assertEqual(0, len(pop_notifications()))
+
+ # TODO real notification
+
+ 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/+archive/primary/+livefsbuild/%d/+files/"
+ "mybuildlog.txt" % (self.build.distribution.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 TestLiveFSBuildWebservice(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ 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_PUBLIC)
+ 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.distroarchseries),
+ build["distroarchseries_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.assertTrue(build["can_be_rescored"])
+ self.assertFalse(build["can_be_retried"])
+ self.assertFalse(build["can_be_cancelled"])
+
+ 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)
+ 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)
+ 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"])
=== 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-04-25 15:26:13 +0000
@@ -0,0 +1,185 @@
+# 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 pytz
+from testtools import run_test_with
+from testtools.deferredruntest import (
+ assert_fails_with,
+ AsynchronousDeferredRunTest,
+ )
+import transaction
+from twisted.internet import defer
+from zope.component import getUtility
+from zope.security.proxy import removeSecurityProxy
+
+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.registry.interfaces.pocket import PackagePublishingPocket
+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 in distro unstable RELEASE", 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 in distro unstable "
+ "RELEASE)\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.distroarchseries.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.distroarchseries.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.distroarchseries, None)
+ self.assertEqual({
+ "archive_private": False,
+ "archives": expected_archives,
+ "arch_tag": "i386",
+ "datestamp": "20140425-103800",
+ "project": "distro",
+ "subproject": "special",
+ "suite": "unstable",
+ }, job._extraBuildArgs())
+
+ @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.distroarchseries.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)
=== 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-04-25 15:26: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
@@ -4316,6 +4318,66 @@
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.makePerson()
+ 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, livefs=None, archive=None,
+ distroarchseries=None, pocket=None, unique_key=None,
+ metadata_override=None, date_created=DEFAULT,
+ status=BuildStatus.NEEDSBUILD, duration=None,
+ **kwargs):
+ """Make a new LiveFSBuild."""
+ 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
+ livefs = self.makeLiveFS(distroseries=distroseries, **kwargs)
+ if requester is None:
+ requester = self.makePerson()
+ if archive is None:
+ archive = livefs.distroseries.main_archive
+ if distroarchseries is None:
+ distroarchseries = self.makeDistroArchSeries(
+ distroseries=livefs.distroseries)
+ 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, date_started=livefsbuild.date_created)
+ removeSecurityProxy(livefsbuild).updateStatus(
+ status, date_finished=livefsbuild.date_started + duration)
+ else:
+ removeSecurityProxy(livefsbuild).updateStatus(status)
+ IStore(livefsbuild).flush()
+ return livefsbuild
+
# Some factory methods return simple Python types. We don't add
# security wrappers for them, as well as for objects created by