Merge lp:~cjwatson/launchpad/snap-request-builds-job into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18723
Proposed branch: lp:~cjwatson/launchpad/snap-request-builds-job
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/snap-parse-architectures
Diff against target: 855 lines (+680/-9)
9 files modified
database/schema/security.cfg (+15/-2)
lib/lp/services/config/schema-lazr.conf (+6/-0)
lib/lp/snappy/configure.zcml (+13/-1)
lib/lp/snappy/interfaces/snap.py (+15/-0)
lib/lp/snappy/interfaces/snapjob.py (+97/-0)
lib/lp/snappy/model/snap.py (+47/-6)
lib/lp/snappy/model/snapjob.py (+271/-0)
lib/lp/snappy/tests/test_snap.py (+61/-0)
lib/lp/snappy/tests/test_snapjob.py (+155/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/snap-request-builds-job
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+348058@code.launchpad.net

Commit message

Add a job to request builds of a snap for relevant architectures.

Description of the change

See also https://code.launchpad.net/~cjwatson/launchpad/db-snap-job/+merge/348057.

I considered whether to use the existing request-daily-builds DB user or the existing snap-build-job DB user, as I needed a bit of both, or indeed whether to create an entirely new user. In the end I decided it wasn't worth a new user and that snap-build-job was close enough that I could just extend it a bit.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2018-05-22 06:42:35 +0000
+++ database/schema/security.cfg 2018-06-15 13:00:33 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2017 Canonical Ltd. This software is licensed under the1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
3#3#
4# Possible permissions: SELECT, INSERT, UPDATE, EXECUTE4# Possible permissions: SELECT, INSERT, UPDATE, EXECUTE
@@ -287,6 +287,7 @@
287public.snapbuild = SELECT, INSERT, UPDATE, DELETE287public.snapbuild = SELECT, INSERT, UPDATE, DELETE
288public.snapbuildjob = SELECT, INSERT, UPDATE, DELETE288public.snapbuildjob = SELECT, INSERT, UPDATE, DELETE
289public.snapfile = SELECT, INSERT, UPDATE, DELETE289public.snapfile = SELECT, INSERT, UPDATE, DELETE
290public.snapjob = SELECT, INSERT, UPDATE, DELETE
290public.snappydistroseries = SELECT, INSERT, UPDATE, DELETE291public.snappydistroseries = SELECT, INSERT, UPDATE, DELETE
291public.snappyseries = SELECT, INSERT, UPDATE, DELETE292public.snappyseries = SELECT, INSERT, UPDATE, DELETE
292public.sourcepackageformatselection = SELECT293public.sourcepackageformatselection = SELECT
@@ -2537,21 +2538,33 @@
2537groups=script2538groups=script
2538public.account = SELECT2539public.account = SELECT
2539public.archive = SELECT2540public.archive = SELECT
2541public.branch = SELECT
2540public.builder = SELECT2542public.builder = SELECT
2543public.buildfarmjob = SELECT, INSERT
2544public.buildqueue = SELECT, INSERT, UPDATE
2541public.distribution = SELECT2545public.distribution = SELECT
2542public.distroarchseries = SELECT2546public.distroarchseries = SELECT
2543public.distroseries = SELECT2547public.distroseries = SELECT
2544public.emailaddress = SELECT2548public.emailaddress = SELECT
2549public.gitref = SELECT
2550public.gitrepository = SELECT
2545public.job = SELECT, INSERT, UPDATE2551public.job = SELECT, INSERT, UPDATE
2546public.libraryfilealias = SELECT2552public.libraryfilealias = SELECT
2547public.libraryfilecontent = SELECT2553public.libraryfilecontent = SELECT
2548public.person = SELECT2554public.person = SELECT
2549public.personsettings = SELECT2555public.personsettings = SELECT
2556public.pocketchroot = SELECT
2557public.processor = SELECT
2558public.product = SELECT
2550public.snap = SELECT, UPDATE2559public.snap = SELECT, UPDATE
2551public.snapbuild = SELECT, UPDATE2560public.snaparch = SELECT
2561public.snapbuild = SELECT, INSERT, UPDATE
2552public.snapbuildjob = SELECT, UPDATE2562public.snapbuildjob = SELECT, UPDATE
2553public.snapfile = SELECT2563public.snapfile = SELECT
2564public.snapjob = SELECT, UPDATE
2554public.snappyseries = SELECT2565public.snappyseries = SELECT
2566public.sourcepackagename = SELECT
2555public.teammembership = SELECT2567public.teammembership = SELECT
2568public.teamparticipation = SELECT
2556public.webhook = SELECT2569public.webhook = SELECT
2557public.webhookjob = SELECT, INSERT2570public.webhookjob = SELECT, INSERT
25582571
=== modified file 'lib/lp/services/config/schema-lazr.conf'
--- lib/lp/services/config/schema-lazr.conf 2018-06-15 13:00:33 +0000
+++ lib/lp/services/config/schema-lazr.conf 2018-06-15 13:00:33 +0000
@@ -1860,6 +1860,7 @@
1860 IRemoveArtifactSubscriptionsJobSource,1860 IRemoveArtifactSubscriptionsJobSource,
1861 ISelfRenewalNotificationJobSource,1861 ISelfRenewalNotificationJobSource,
1862 ISevenDayCommercialExpirationJobSource,1862 ISevenDayCommercialExpirationJobSource,
1863 ISnapRequestBuildsJobSource,
1863 ISnapStoreUploadJobSource,1864 ISnapStoreUploadJobSource,
1864 ITeamInvitationNotificationJobSource,1865 ITeamInvitationNotificationJobSource,
1865 ITeamJoinNotificationJobSource,1866 ITeamJoinNotificationJobSource,
@@ -2002,6 +2003,11 @@
2002dbuser: product-job2003dbuser: product-job
2003crontab_group: MAIN2004crontab_group: MAIN
20042005
2006[ISnapRequestBuildsJobSource]
2007module: lp.snappy.interfaces.snapjob
2008dbuser: snap-build-job
2009crontab_group: MAIN
2010
2005[ISnapStoreUploadJobSource]2011[ISnapStoreUploadJobSource]
2006module: lp.snappy.interfaces.snapbuildjob2012module: lp.snappy.interfaces.snapbuildjob
2007dbuser: snap-build-job2013dbuser: snap-build-job
20082014
=== modified file 'lib/lp/snappy/configure.zcml'
--- lib/lp/snappy/configure.zcml 2017-03-20 00:03:52 +0000
+++ lib/lp/snappy/configure.zcml 2018-06-15 13:00:33 +0000
@@ -1,4 +1,4 @@
1<!-- Copyright 2015-2017 Canonical Ltd. This software is licensed under the1<!-- Copyright 2015-2018 Canonical Ltd. This software is licensed under the
2 GNU Affero General Public License version 3 (see the file LICENSE).2 GNU Affero General Public License version 3 (see the file LICENSE).
3-->3-->
44
@@ -127,6 +127,18 @@
127 </securedutility>127 </securedutility>
128128
129 <!-- Snap-related jobs -->129 <!-- Snap-related jobs -->
130 <class class="lp.snappy.model.snapjob.SnapJob">
131 <allow interface="lp.snappy.interfaces.snapjob.ISnapJob" />
132 </class>
133 <securedutility
134 component="lp.snappy.model.snapjob.SnapRequestBuildsJob"
135 provides="lp.snappy.interfaces.snapjob.ISnapRequestBuildsJobSource">
136 <allow interface="lp.snappy.interfaces.snapjob.ISnapRequestBuildsJobSource" />
137 </securedutility>
138 <class class="lp.snappy.model.snapjob.SnapRequestBuildsJob">
139 <allow interface="lp.snappy.interfaces.snapjob.ISnapJob" />
140 <allow interface="lp.snappy.interfaces.snapjob.ISnapRequestBuildsJob" />
141 </class>
130 <class class="lp.snappy.model.snapbuildjob.SnapBuildJob">142 <class class="lp.snappy.model.snapbuildjob.SnapBuildJob">
131 <allow interface="lp.snappy.interfaces.snapbuildjob.ISnapBuildJob" />143 <allow interface="lp.snappy.interfaces.snapbuildjob.ISnapBuildJob" />
132 </class>144 </class>
133145
=== modified file 'lib/lp/snappy/interfaces/snap.py'
--- lib/lp/snappy/interfaces/snap.py 2018-06-15 13:00:33 +0000
+++ lib/lp/snappy/interfaces/snap.py 2018-06-15 13:00:33 +0000
@@ -316,6 +316,21 @@
316 :return: `ISnapBuild`.316 :return: `ISnapBuild`.
317 """317 """
318318
319 def requestBuildsFromJob(requester, archive, pocket, channels=None,
320 logger=None):
321 """Synchronous part of `Snap.requestBuilds`.
322
323 Request that the snap package be built for relevant architectures.
324
325 :param requester: The person requesting the builds.
326 :param archive: The IArchive to associate the builds with.
327 :param pocket: The pocket that should be targeted.
328 :param channels: A dictionary mapping snap names to channels to use
329 for these builds.
330 :param logger: An optional logger.
331 :return: A sequence of `ISnapBuild` instances.
332 """
333
319 @operation_parameters(334 @operation_parameters(
320 snap_build_ids=List(335 snap_build_ids=List(
321 title=_("A list of snap build ids."),336 title=_("A list of snap build ids."),
322337
=== added file 'lib/lp/snappy/interfaces/snapjob.py'
--- lib/lp/snappy/interfaces/snapjob.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/interfaces/snapjob.py 2018-06-15 13:00:33 +0000
@@ -0,0 +1,97 @@
1# Copyright 2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Snap job interfaces."""
5
6from __future__ import absolute_import, print_function, unicode_literals
7
8__metaclass__ = type
9__all__ = [
10 'ISnapJob',
11 'ISnapRequestBuildsJob',
12 'ISnapRequestBuildsJobSource',
13 ]
14
15from lazr.restful.fields import Reference
16from zope.interface import (
17 Attribute,
18 Interface,
19 )
20from zope.schema import (
21 Choice,
22 Dict,
23 List,
24 TextLine,
25 )
26
27from lp import _
28from lp.registry.interfaces.person import IPerson
29from lp.registry.interfaces.pocket import PackagePublishingPocket
30from lp.services.job.interfaces.job import (
31 IJob,
32 IJobSource,
33 IRunnableJob,
34 )
35from lp.snappy.interfaces.snap import ISnap
36from lp.snappy.interfaces.snapbuild import ISnapBuild
37from lp.soyuz.interfaces.archive import IArchive
38
39
40class ISnapJob(Interface):
41 """A job related to a snap package."""
42
43 job = Reference(
44 title=_("The common Job attributes."), schema=IJob,
45 required=True, readonly=True)
46
47 snap = Reference(
48 title=_("The snap package to use for this job."),
49 schema=ISnap, required=True, readonly=True)
50
51 metadata = Attribute(_("A dict of data about the job."))
52
53
54class ISnapRequestBuildsJob(IRunnableJob):
55 """A Job that processes a request for builds of a snap package."""
56
57 requester = Reference(
58 title=_("The person requesting the builds."), schema=IPerson,
59 required=True, readonly=True)
60
61 archive = Reference(
62 title=_("The archive to associate the builds with."), schema=IArchive,
63 required=True, readonly=True)
64
65 pocket = Choice(
66 title=_("The pocket that should be targeted."),
67 vocabulary=PackagePublishingPocket, required=True, readonly=True)
68
69 channels = Dict(
70 title=_("Source snap channels to use for these builds."),
71 description=_(
72 "A dictionary mapping snap names to channels to use for these "
73 "builds. Currently only 'core' and 'snapcraft' keys are "
74 "supported."),
75 key_type=TextLine(), required=False, readonly=True)
76
77 error_message = TextLine(
78 title=_("Error message resulting from running this job."),
79 required=False, readonly=True)
80
81 builds = List(
82 title=_("The builds created by this request."),
83 value_type=Reference(schema=ISnapBuild), required=True, readonly=True)
84
85
86class ISnapRequestBuildsJobSource(IJobSource):
87
88 def create(snap, requester, archive, pocket, channels):
89 """Request builds of a snap package.
90
91 :param snap: The snap package to build.
92 :param requester: The person requesting the builds.
93 :param archive: The IArchive to associate the builds with.
94 :param pocket: The pocket that should be targeted.
95 :param channels: A dictionary mapping snap names to channels to use
96 for these builds.
97 """
098
=== modified file 'lib/lp/snappy/model/snap.py'
--- lib/lp/snappy/model/snap.py 2018-06-15 13:00:33 +0000
+++ lib/lp/snappy/model/snap.py 2018-06-15 13:00:33 +0000
@@ -6,10 +6,12 @@
6 'Snap',6 'Snap',
7 ]7 ]
88
9from collections import OrderedDict
9from datetime import (10from datetime import (
10 datetime,11 datetime,
11 timedelta,12 timedelta,
12 )13 )
14from operator import attrgetter
13from urlparse import urlsplit15from urlparse import urlsplit
1416
15from pymacaroons import Macaroon17from pymacaroons import Macaroon
@@ -116,6 +118,7 @@
116from lp.services.webapp.interfaces import ILaunchBag118from lp.services.webapp.interfaces import ILaunchBag
117from lp.services.webhooks.interfaces import IWebhookSet119from lp.services.webhooks.interfaces import IWebhookSet
118from lp.services.webhooks.model import WebhookTargetMixin120from lp.services.webhooks.model import WebhookTargetMixin
121from lp.snappy.adapters.buildarch import determine_architectures_to_build
119from lp.snappy.interfaces.snap import (122from lp.snappy.interfaces.snap import (
120 BadSnapSearchContext,123 BadSnapSearchContext,
121 BadSnapSource,124 BadSnapSource,
@@ -463,21 +466,25 @@
463 return False466 return False
464 return True467 return True
465468
466 def requestBuild(self, requester, archive, distro_arch_series, pocket,469 def _checkRequestBuild(self, requester, archive):
467 channels=None):470 """May `requester` request builds of this snap from `archive`?"""
468 """See `ISnap`."""
469 if not requester.inTeam(self.owner):471 if not requester.inTeam(self.owner):
470 raise SnapNotOwner(472 raise SnapNotOwner(
471 "%s cannot create snap package builds owned by %s." %473 "%s cannot create snap package builds owned by %s." %
472 (requester.displayname, self.owner.displayname))474 (requester.displayname, self.owner.displayname))
473 if not archive.enabled:475 if not archive.enabled:
474 raise ArchiveDisabled(archive.displayname)476 raise ArchiveDisabled(archive.displayname)
475 if distro_arch_series not in self.getAllowedArchitectures():
476 raise SnapBuildDisallowedArchitecture(distro_arch_series)
477 if archive.private and self.owner != archive.owner:477 if archive.private and self.owner != archive.owner:
478 # See rationale in `SnapBuildArchiveOwnerMismatch` docstring.478 # See rationale in `SnapBuildArchiveOwnerMismatch` docstring.
479 raise SnapBuildArchiveOwnerMismatch()479 raise SnapBuildArchiveOwnerMismatch()
480480
481 def requestBuild(self, requester, archive, distro_arch_series, pocket,
482 channels=None):
483 """See `ISnap`."""
484 self._checkRequestBuild(requester, archive)
485 if distro_arch_series not in self.getAllowedArchitectures():
486 raise SnapBuildDisallowedArchitecture(distro_arch_series)
487
481 pending = IStore(self).find(488 pending = IStore(self).find(
482 SnapBuild,489 SnapBuild,
483 SnapBuild.snap_id == self.id,490 SnapBuild.snap_id == self.id,
@@ -495,8 +502,42 @@
495 build.queueBuild()502 build.queueBuild()
496 return build503 return build
497504
505 def requestBuildsFromJob(self, requester, archive, pocket, channels=None,
506 logger=None):
507 """See `ISnap`."""
508 snapcraft_data = removeSecurityProxy(
509 getUtility(ISnapSet).getSnapcraftYaml(self))
510 # Sort by Processor.id for determinism. This is chosen to be the
511 # same order as in BinaryPackageBuildSet.createForSource, to
512 # minimise confusion.
513 supported_arches = OrderedDict(
514 (das.architecturetag, das) for das in sorted(
515 self.getAllowedArchitectures(),
516 key=attrgetter("processor.id")))
517 architectures_to_build = determine_architectures_to_build(
518 snapcraft_data, supported_arches.keys())
519
520 builds = []
521 for build_instance in architectures_to_build:
522 arch = build_instance.architecture
523 try:
524 build = self.requestBuild(
525 requester, archive, supported_arches[arch], pocket,
526 channels)
527 if logger is not None:
528 logger.debug(
529 " - %s/%s/%s: Build requested.",
530 self.owner.name, self.name, arch)
531 builds.append(build)
532 except SnapBuildAlreadyPending as e:
533 if logger is not None:
534 logger.warning(
535 " - %s/%s/%s: %s",
536 self.owner.name, self.name, arch, e)
537 return builds
538
498 def requestAutoBuilds(self, allow_failures=False, logger=None):539 def requestAutoBuilds(self, allow_failures=False, logger=None):
499 """See `ISnapSet`."""540 """See `ISnap`."""
500 builds = []541 builds = []
501 if self.auto_build_archive is None:542 if self.auto_build_archive is None:
502 raise CannotRequestAutoBuilds("auto_build_archive")543 raise CannotRequestAutoBuilds("auto_build_archive")
503544
=== added file 'lib/lp/snappy/model/snapjob.py'
--- lib/lp/snappy/model/snapjob.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/model/snapjob.py 2018-06-15 13:00:33 +0000
@@ -0,0 +1,271 @@
1# Copyright 2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Snap package jobs."""
5
6from __future__ import absolute_import, print_function, unicode_literals
7
8__metaclass__ = type
9__all__ = [
10 'SnapJob',
11 'SnapJobType',
12 'SnapRequestBuildsJob',
13 ]
14
15from lazr.delegates import delegate_to
16from lazr.enum import (
17 DBEnumeratedType,
18 DBItem,
19 )
20from storm.locals import (
21 Int,
22 JSON,
23 Reference,
24 )
25from storm.store import EmptyResultSet
26import transaction
27from zope.component import getUtility
28from zope.interface import (
29 implementer,
30 provider,
31 )
32
33from lp.app.errors import NotFoundError
34from lp.registry.interfaces.person import IPersonSet
35from lp.registry.interfaces.pocket import PackagePublishingPocket
36from lp.services.config import config
37from lp.services.database.enumcol import EnumCol
38from lp.services.database.interfaces import (
39 IMasterStore,
40 IStore,
41 )
42from lp.services.database.stormbase import StormBase
43from lp.services.job.model.job import (
44 EnumeratedSubclass,
45 Job,
46 )
47from lp.services.job.runner import BaseRunnableJob
48from lp.services.mail.sendmail import format_address_for_person
49from lp.services.propertycache import cachedproperty
50from lp.services.scripts import log
51from lp.snappy.interfaces.snap import (
52 CannotFetchSnapcraftYaml,
53 CannotParseSnapcraftYaml,
54 )
55from lp.snappy.interfaces.snapjob import (
56 ISnapJob,
57 ISnapRequestBuildsJob,
58 ISnapRequestBuildsJobSource,
59 )
60from lp.snappy.model.snapbuild import SnapBuild
61from lp.soyuz.model.archive import Archive
62
63
64class SnapJobType(DBEnumeratedType):
65 """Values that `ISnapJob.job_type` can take."""
66
67 REQUEST_BUILDS = DBItem(0, """
68 Request builds
69
70 This job requests builds of a snap package.
71 """)
72
73
74@implementer(ISnapJob)
75class SnapJob(StormBase):
76 """See `ISnapJob`."""
77
78 __storm_table__ = 'SnapJob'
79
80 job_id = Int(name='job', primary=True, allow_none=False)
81 job = Reference(job_id, 'Job.id')
82
83 snap_id = Int(name='snap', allow_none=False)
84 snap = Reference(snap_id, 'Snap.id')
85
86 job_type = EnumCol(enum=SnapJobType, notNull=True)
87
88 metadata = JSON('json_data', allow_none=False)
89
90 def __init__(self, snap, job_type, metadata, **job_args):
91 """Constructor.
92
93 Extra keyword arguments are used to construct the underlying Job
94 object.
95
96 :param snap: The `ISnap` this job relates to.
97 :param job_type: The `SnapJobType` of this job.
98 :param metadata: The type-specific variables, as a JSON-compatible
99 dict.
100 """
101 super(SnapJob, self).__init__()
102 self.job = Job(**job_args)
103 self.snap = snap
104 self.job_type = job_type
105 self.metadata = metadata
106
107 def makeDerived(self):
108 return SnapJobDerived.makeSubclass(self)
109
110
111@delegate_to(ISnapJob)
112class SnapJobDerived(BaseRunnableJob):
113
114 __metaclass__ = EnumeratedSubclass
115
116 def __init__(self, snap_job):
117 self.context = snap_job
118
119 def __repr__(self):
120 """An informative representation of the job."""
121 return "<%s for ~%s/+snap/%s>" % (
122 self.__class__.__name__, self.snap.owner.name, self.snap.name)
123
124 @classmethod
125 def get(cls, job_id):
126 """Get a job by id.
127
128 :return: The `SnapJob` with the specified id, as the current
129 `SnapJobDerived` subclass.
130 :raises: `NotFoundError` if there is no job with the specified id,
131 or its `job_type` does not match the desired subclass.
132 """
133 snap_job = IStore(SnapJob).get(SnapJob, job_id)
134 if snap_job.job_type != cls.class_job_type:
135 raise NotFoundError(
136 "No object found with id %d and type %s" %
137 (job_id, cls.class_job_type.title))
138 return cls(snap_job)
139
140 @classmethod
141 def iterReady(cls):
142 """See `IJobSource`."""
143 jobs = IMasterStore(SnapJob).find(
144 SnapJob,
145 SnapJob.job_type == cls.class_job_type,
146 SnapJob.job == Job.id,
147 Job.id.is_in(Job.ready_jobs))
148 return (cls(job) for job in jobs)
149
150 def getOopsVars(self):
151 """See `IRunnableJob`."""
152 oops_vars = super(SnapJobDerived, self).getOopsVars()
153 oops_vars.extend([
154 ("job_id", self.context.job.id),
155 ("job_type", self.context.job_type.title),
156 ("snap_owner_name", self.context.snap.owner.name),
157 ("snap_name", self.context.snap.name),
158 ])
159 return oops_vars
160
161
162@implementer(ISnapRequestBuildsJob)
163@provider(ISnapRequestBuildsJobSource)
164class SnapRequestBuildsJob(SnapJobDerived):
165 """A Job that processes a request for builds of a snap package."""
166
167 class_job_type = SnapJobType.REQUEST_BUILDS
168
169 user_error_types = (CannotParseSnapcraftYaml, NotFoundError)
170 retry_error_types = (CannotFetchSnapcraftYaml,)
171
172 max_retries = 5
173
174 config = config.ISnapRequestBuildsJobSource
175
176 @classmethod
177 def create(cls, snap, requester, archive, pocket, channels):
178 """See `ISnapRequestBuildsJobSource`."""
179 metadata = {
180 "requester": requester.id,
181 "archive": archive.id,
182 "pocket": pocket.value,
183 "channels": channels,
184 }
185 snap_job = SnapJob(snap, cls.class_job_type, metadata)
186 job = cls(snap_job)
187 job.celeryRunOnCommit()
188 return job
189
190 def getOperationDescription(self):
191 return "requesting builds of %s" % self.snap.name
192
193 def getErrorRecipients(self):
194 if self.requester is None or self.requester.preferredemail is None:
195 return []
196 return [format_address_for_person(self.requester)]
197
198 @cachedproperty
199 def requester(self):
200 """See `ISnapRequestBuildsJob`."""
201 requester_id = self.metadata["requester"]
202 return getUtility(IPersonSet).get(requester_id)
203
204 @cachedproperty
205 def archive(self):
206 """See `ISnapRequestBuildsJob`."""
207 archive_id = self.metadata["archive"]
208 return IStore(Archive).find(Archive, Archive.id == archive_id).one()
209
210 @property
211 def pocket(self):
212 """See `ISnapRequestBuildsJob`."""
213 name = self.metadata["pocket"]
214 return PackagePublishingPocket.items[name]
215
216 @property
217 def channels(self):
218 """See `ISnapRequestBuildsJob`."""
219 return self.metadata["channels"]
220
221 @property
222 def error_message(self):
223 """See `ISnapRequestBuildsJob`."""
224 return self.metadata.get("error_message")
225
226 @error_message.setter
227 def error_message(self, message):
228 """See `ISnapRequestBuildsJob`."""
229 self.metadata["error_message"] = message
230
231 @property
232 def builds(self):
233 """See `ISnapRequestBuildsJob`."""
234 build_ids = self.metadata.get("builds")
235 if build_ids is None:
236 return EmptyResultSet()
237 else:
238 return IStore(SnapBuild).find(
239 SnapBuild, SnapBuild.id.is_in(build_ids))
240
241 @builds.setter
242 def builds(self, builds):
243 """See `ISnapRequestBuildsJob`."""
244 self.metadata["builds"] = [build.id for build in builds]
245
246 def run(self):
247 """See `IRunnableJob`."""
248 requester = self.requester
249 if requester is None:
250 log.info(
251 "Skipping %r because the requester has been deleted." % self)
252 return
253 archive = self.archive
254 if archive is None:
255 log.info(
256 "Skipping %r because the archive has been deleted." % self)
257 return
258 try:
259 self.builds = self.snap.requestBuildsFromJob(
260 requester, archive, self.pocket, channels=self.channels,
261 logger=log)
262 self.error_message = None
263 except self.retry_error_types:
264 raise
265 except Exception as e:
266 self.error_message = str(e)
267 # The normal job infrastructure will abort the transaction, but
268 # we want to commit instead: the only database changes we make
269 # are to this job's metadata and should be preserved.
270 transaction.commit()
271 raise
0272
=== modified file 'lib/lp/snappy/tests/test_snap.py'
--- lib/lp/snappy/tests/test_snap.py 2018-06-15 13:00:33 +0000
+++ lib/lp/snappy/tests/test_snap.py 2018-06-15 13:00:33 +0000
@@ -12,6 +12,7 @@
12 timedelta,12 timedelta,
13 )13 )
14import json14import json
15from textwrap import dedent
15from urlparse import urlsplit16from urlparse import urlsplit
1617
17from fixtures import MockPatch18from fixtures import MockPatch
@@ -91,6 +92,7 @@
91 ISnapBuildSet,92 ISnapBuildSet,
92 )93 )
93from lp.snappy.interfaces.snapbuildjob import ISnapStoreUploadJobSource94from lp.snappy.interfaces.snapbuildjob import ISnapStoreUploadJobSource
95from lp.snappy.interfaces.snapjob import ISnapRequestBuildsJobSource
94from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient96from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient
95from lp.snappy.model.snap import SnapSet97from lp.snappy.model.snap import SnapSet
96from lp.snappy.model.snapbuild import SnapFile98from lp.snappy.model.snapbuild import SnapFile
@@ -361,6 +363,65 @@
361 snap.owner, snap.distro_series.main_archive, distroarchseries,363 snap.owner, snap.distro_series.main_archive, distroarchseries,
362 PackagePublishingPocket.UPDATES)364 PackagePublishingPocket.UPDATES)
363365
366 def makeRequestBuildsJob(self, arch_tags):
367 distro = self.factory.makeDistribution()
368 distroseries = self.factory.makeDistroSeries(distribution=distro)
369 processors = [
370 self.factory.makeProcessor(
371 name=arch_tag, supports_virtualized=True)
372 for arch_tag in arch_tags]
373 for processor in processors:
374 das = self.factory.makeDistroArchSeries(
375 distroseries=distroseries, architecturetag=processor.name,
376 processor=processor)
377 das.addOrUpdateChroot(self.factory.makeLibraryFileAlias(
378 filename="fake_chroot.tar.gz", db_only=True))
379 [git_ref] = self.factory.makeGitRefs()
380 snap = self.factory.makeSnap(
381 git_ref=git_ref, distroseries=distroseries, processors=processors)
382 return getUtility(ISnapRequestBuildsJobSource).create(
383 snap, snap.owner.teamowner, distro.main_archive,
384 PackagePublishingPocket.RELEASE, {"snapcraft": "edge"})
385
386 def assertRequestedBuildsMatch(self, builds, job, arch_tags):
387 self.assertThat(builds, MatchesSetwise(
388 *(MatchesStructure(
389 requester=Equals(job.requester),
390 snap=Equals(job.snap),
391 archive=Equals(job.archive),
392 distro_arch_series=Equals(job.snap.distro_series[arch_tag]),
393 pocket=Equals(job.pocket),
394 channels=Equals(job.channels))
395 for arch_tag in arch_tags)))
396
397 def test_requestBuildsFromJob_restricts_explicit_list(self):
398 # requestBuildsFromJob limits builds targeted at an explicit list of
399 # architectures to those allowed for the snap.
400 self.useFixture(GitHostingFixture(blob=dedent("""\
401 architectures:
402 - build-on: sparc
403 - build-on: i386
404 - build-on: avr
405 """)))
406 job = self.makeRequestBuildsJob(["sparc"])
407 with person_logged_in(job.requester):
408 builds = job.snap.requestBuildsFromJob(
409 job.requester, job.archive, job.pocket,
410 removeSecurityProxy(job.channels))
411 self.assertRequestedBuildsMatch(builds, job, ["sparc"])
412
413 def test_requestBuildsFromJob_no_explicit_architectures(self):
414 # If the snap doesn't specify any architectures,
415 # requestBuildsFromJob requests builds for all configured
416 # architectures.
417 self.useFixture(GitHostingFixture(blob="name: foo\n"))
418 job = self.makeRequestBuildsJob(["mips64el", "riscv64"])
419 with person_logged_in(job.requester):
420 builds = job.snap.requestBuildsFromJob(
421 job.requester, job.archive, job.pocket,
422 removeSecurityProxy(job.channels))
423 self.assertRequestedBuildsMatch(builds, job, ["mips64el", "riscv64"])
424
364 def test_requestAutoBuilds(self):425 def test_requestAutoBuilds(self):
365 # requestAutoBuilds creates new builds for all configured426 # requestAutoBuilds creates new builds for all configured
366 # architectures with appropriate parameters.427 # architectures with appropriate parameters.
367428
=== added file 'lib/lp/snappy/tests/test_snapjob.py'
--- lib/lp/snappy/tests/test_snapjob.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/tests/test_snapjob.py 2018-06-15 13:00:33 +0000
@@ -0,0 +1,155 @@
1# Copyright 2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for snap package jobs."""
5
6from __future__ import absolute_import, print_function, unicode_literals
7
8__metaclass__ = type
9
10from textwrap import dedent
11
12from testtools.matchers import (
13 AfterPreprocessing,
14 ContainsDict,
15 Equals,
16 Is,
17 MatchesSetwise,
18 MatchesStructure,
19 )
20
21from lp.code.tests.helpers import GitHostingFixture
22from lp.registry.interfaces.pocket import PackagePublishingPocket
23from lp.services.config import config
24from lp.services.job.interfaces.job import JobStatus
25from lp.services.job.runner import JobRunner
26from lp.services.mail.sendmail import format_address_for_person
27from lp.snappy.interfaces.snap import CannotParseSnapcraftYaml
28from lp.snappy.interfaces.snapjob import (
29 ISnapJob,
30 ISnapRequestBuildsJob,
31 )
32from lp.snappy.model.snapjob import (
33 SnapJob,
34 SnapJobType,
35 SnapRequestBuildsJob,
36 )
37from lp.testing import TestCaseWithFactory
38from lp.testing.dbuser import dbuser
39from lp.testing.layers import ZopelessDatabaseLayer
40
41
42class TestSnapJob(TestCaseWithFactory):
43
44 layer = ZopelessDatabaseLayer
45
46 def test_provides_interface(self):
47 # `SnapJob` objects provide `ISnapJob`.
48 snap = self.factory.makeSnap()
49 self.assertProvides(
50 SnapJob(snap, SnapJobType.REQUEST_BUILDS, {}), ISnapJob)
51
52
53class TestSnapRequestBuildsJob(TestCaseWithFactory):
54
55 layer = ZopelessDatabaseLayer
56
57 def test_provides_interface(self):
58 # `SnapRequestBuildsJob` objects provide `ISnapRequestBuildsJob`."""
59 snap = self.factory.makeSnap()
60 archive = self.factory.makeArchive()
61 job = SnapRequestBuildsJob.create(
62 snap, snap.registrant, archive, PackagePublishingPocket.RELEASE,
63 None)
64 self.assertProvides(job, ISnapRequestBuildsJob)
65
66 def test___repr__(self):
67 # `SnapRequestBuildsJob` objects have an informative __repr__.
68 snap = self.factory.makeSnap()
69 archive = self.factory.makeArchive()
70 job = SnapRequestBuildsJob.create(
71 snap, snap.registrant, archive, PackagePublishingPocket.RELEASE,
72 None)
73 self.assertEqual(
74 "<SnapRequestBuildsJob for ~%s/+snap/%s>" % (
75 snap.owner.name, snap.name),
76 repr(job))
77
78 def makeSeriesAndProcessors(self, arch_tags):
79 distro = self.factory.makeDistribution()
80 distroseries = self.factory.makeDistroSeries(distribution=distro)
81 processors = [
82 self.factory.makeProcessor(
83 name=arch_tag, supports_virtualized=True)
84 for arch_tag in arch_tags]
85 for processor in processors:
86 das = self.factory.makeDistroArchSeries(
87 distroseries=distroseries, architecturetag=processor.name,
88 processor=processor)
89 das.addOrUpdateChroot(self.factory.makeLibraryFileAlias(
90 filename="fake_chroot.tar.gz", db_only=True))
91 return distroseries, processors
92
93 def test_run(self):
94 # The job requests builds and records the result.
95 distroseries, processors = self.makeSeriesAndProcessors(
96 ["avr2001", "sparc64", "x32"])
97 [git_ref] = self.factory.makeGitRefs()
98 snap = self.factory.makeSnap(
99 git_ref=git_ref, distroseries=distroseries, processors=processors)
100 job = SnapRequestBuildsJob.create(
101 snap, snap.registrant, distroseries.main_archive,
102 PackagePublishingPocket.RELEASE, {"core": "stable"})
103 snapcraft_yaml = dedent("""\
104 architectures:
105 - build-on: avr2001
106 - build-on: x32
107 """)
108 self.useFixture(GitHostingFixture(blob=snapcraft_yaml))
109 with dbuser(config.ISnapRequestBuildsJobSource.dbuser):
110 JobRunner([job]).runAll()
111 self.assertEmailQueueLength(0)
112 self.assertThat(job, MatchesStructure(
113 job=MatchesStructure.byEquality(status=JobStatus.COMPLETED),
114 error_message=Is(None),
115 builds=AfterPreprocessing(set, MatchesSetwise(*[
116 MatchesStructure.byEquality(
117 requester=snap.registrant,
118 snap=snap,
119 archive=distroseries.main_archive,
120 distro_arch_series=distroseries[arch],
121 pocket=PackagePublishingPocket.RELEASE,
122 channels={"core": "stable"})
123 for arch in ("avr2001", "x32")]))))
124
125 def test_run_failed(self):
126 # A failed run sets the job status to FAILED and records the error
127 # message.
128 # The job requests builds and records the result.
129 distroseries, processors = self.makeSeriesAndProcessors(
130 ["avr2001", "sparc64", "x32"])
131 [git_ref] = self.factory.makeGitRefs()
132 snap = self.factory.makeSnap(
133 git_ref=git_ref, distroseries=distroseries, processors=processors)
134 job = SnapRequestBuildsJob.create(
135 snap, snap.registrant, distroseries.main_archive,
136 PackagePublishingPocket.RELEASE, {"core": "stable"})
137 self.useFixture(GitHostingFixture()).getBlob.failure = (
138 CannotParseSnapcraftYaml("Nonsense on stilts"))
139 with dbuser(config.ISnapRequestBuildsJobSource.dbuser):
140 JobRunner([job]).runAll()
141 [notification] = self.assertEmailQueueLength(1)
142 self.assertThat(dict(notification), ContainsDict({
143 "From": Equals(config.canonical.noreply_from_address),
144 "To": Equals(format_address_for_person(snap.registrant)),
145 "Subject": Equals(
146 "Launchpad error while requesting builds of %s" % snap.name),
147 }))
148 self.assertEqual(
149 "Launchpad encountered an error during the following operation: "
150 "requesting builds of %s. Nonsense on stilts" % snap.name,
151 notification.get_payload(decode=True))
152 self.assertThat(job, MatchesStructure(
153 job=MatchesStructure.byEquality(status=JobStatus.FAILED),
154 error_message=Equals("Nonsense on stilts"),
155 builds=AfterPreprocessing(set, MatchesSetwise())))