Merge lp:~cjwatson/launchpad/snap-store-upload-job into lp:launchpad
- snap-store-upload-job
- Merge into devel
Proposed by
Colin Watson
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 18032 | ||||
Proposed branch: | lp:~cjwatson/launchpad/snap-store-upload-job | ||||
Merge into: | lp:launchpad | ||||
Prerequisite: | lp:~cjwatson/launchpad/snap-store-client | ||||
Diff against target: |
632 lines (+478/-2) 11 files modified
database/schema/security.cfg (+18/-0) lib/lp/archiveuploader/tests/test_snapupload.py (+28/-0) lib/lp/services/config/schema-lazr.conf (+5/-0) lib/lp/snappy/configure.zcml (+14/-0) lib/lp/snappy/interfaces/snapbuild.py (+10/-1) lib/lp/snappy/interfaces/snapbuildjob.py (+58/-0) lib/lp/snappy/model/snapbuild.py (+19/-0) lib/lp/snappy/model/snapbuildjob.py (+191/-0) lib/lp/snappy/subscribers/snapbuild.py (+7/-1) lib/lp/snappy/tests/test_snapbuild.py (+23/-0) lib/lp/snappy/tests/test_snapbuildjob.py (+105/-0) |
||||
To merge this branch: | bzr merge lp:~cjwatson/launchpad/snap-store-upload-job | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+294031@code.launchpad.net |
Commit message
Add a job to upload completed snap builds to the store.
Description of the change
Add a job to upload completed snap builds to the store.
We'll need to create a snap-build-job DB user before landing this.
To post a comment you must log in.
Revision history for this message
William Grant (wgrant) : | # |
review:
Approve
(code)
Revision history for this message
Colin Watson (cjwatson) : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'database/schema/security.cfg' | |||
2 | --- database/schema/security.cfg 2016-05-06 14:39:05 +0000 | |||
3 | +++ database/schema/security.cfg 2016-05-12 15:17:24 +0000 | |||
4 | @@ -1431,7 +1431,9 @@ | |||
5 | 1431 | public.snap = SELECT, UPDATE | 1431 | public.snap = SELECT, UPDATE |
6 | 1432 | public.snaparch = SELECT | 1432 | public.snaparch = SELECT |
7 | 1433 | public.snapbuild = SELECT, UPDATE | 1433 | public.snapbuild = SELECT, UPDATE |
8 | 1434 | public.snapbuildjob = SELECT, INSERT, UPDATE | ||
9 | 1434 | public.snapfile = SELECT, INSERT, UPDATE | 1435 | public.snapfile = SELECT, INSERT, UPDATE |
10 | 1436 | public.snappyseries = SELECT | ||
11 | 1435 | public.sourcepackagefilepublishing = SELECT | 1437 | public.sourcepackagefilepublishing = SELECT |
12 | 1436 | public.sourcepackageformatselection = SELECT | 1438 | public.sourcepackageformatselection = SELECT |
13 | 1437 | public.sourcepackagename = SELECT, INSERT | 1439 | public.sourcepackagename = SELECT, INSERT |
14 | @@ -2523,3 +2525,19 @@ | |||
15 | 2523 | public.sourcepackagename = SELECT | 2525 | public.sourcepackagename = SELECT |
16 | 2524 | public.webhook = SELECT | 2526 | public.webhook = SELECT |
17 | 2525 | public.webhookjob = SELECT, UPDATE | 2527 | public.webhookjob = SELECT, UPDATE |
18 | 2528 | |||
19 | 2529 | [snap-build-job] | ||
20 | 2530 | type=user | ||
21 | 2531 | groups=script | ||
22 | 2532 | public.distribution = SELECT | ||
23 | 2533 | public.distroarchseries = SELECT | ||
24 | 2534 | public.distroseries = SELECT | ||
25 | 2535 | public.job = SELECT, UPDATE | ||
26 | 2536 | public.libraryfilealias = SELECT | ||
27 | 2537 | public.libraryfilecontent = SELECT | ||
28 | 2538 | public.person = SELECT | ||
29 | 2539 | public.snap = SELECT | ||
30 | 2540 | public.snapbuild = SELECT, UPDATE | ||
31 | 2541 | public.snapbuildjob = SELECT, UPDATE | ||
32 | 2542 | public.snapfile = SELECT | ||
33 | 2543 | public.snappyseries = SELECT | ||
34 | 2526 | 2544 | ||
35 | === modified file 'lib/lp/archiveuploader/tests/test_snapupload.py' | |||
36 | --- lib/lp/archiveuploader/tests/test_snapupload.py 2015-08-03 15:07:29 +0000 | |||
37 | +++ lib/lp/archiveuploader/tests/test_snapupload.py 2016-05-12 15:17:24 +0000 | |||
38 | @@ -64,3 +64,31 @@ | |||
39 | 64 | "Snap upload failed\nGot: %s" % self.log.getLogBuffer()) | 64 | "Snap upload failed\nGot: %s" % self.log.getLogBuffer()) |
40 | 65 | self.assertEqual(BuildStatus.FULLYBUILT, self.build.status) | 65 | self.assertEqual(BuildStatus.FULLYBUILT, self.build.status) |
41 | 66 | self.assertTrue(self.build.verifySuccessfulUpload()) | 66 | self.assertTrue(self.build.verifySuccessfulUpload()) |
42 | 67 | |||
43 | 68 | def test_triggers_store_uploads(self): | ||
44 | 69 | # The upload processor triggers store uploads if appropriate. | ||
45 | 70 | self.pushConfig( | ||
46 | 71 | "snappy", store_url="http://sca.example/", | ||
47 | 72 | store_upload_url="http://updown.example/") | ||
48 | 73 | self.switchToAdmin() | ||
49 | 74 | self.snap.store_series = self.factory.makeSnappySeries( | ||
50 | 75 | usable_distro_series=[self.snap.distro_series]) | ||
51 | 76 | self.snap.store_name = self.snap.name | ||
52 | 77 | self.snap.store_upload = True | ||
53 | 78 | self.snap.store_secrets = { | ||
54 | 79 | "root": "dummy-root", "discharge": "dummy-discharge"} | ||
55 | 80 | Store.of(self.snap).flush() | ||
56 | 81 | self.switchToUploader() | ||
57 | 82 | self.assertFalse(self.build.verifySuccessfulUpload()) | ||
58 | 83 | upload_dir = os.path.join( | ||
59 | 84 | self.incoming_folder, "test", str(self.build.id), "ubuntu") | ||
60 | 85 | write_file(os.path.join(upload_dir, "wget_0_all.snap"), "snap") | ||
61 | 86 | handler = UploadHandler.forProcessor( | ||
62 | 87 | self.uploadprocessor, self.incoming_folder, "test", self.build) | ||
63 | 88 | result = handler.processSnap(self.log) | ||
64 | 89 | self.assertEqual( | ||
65 | 90 | UploadStatusEnum.ACCEPTED, result, | ||
66 | 91 | "Snap upload failed\nGot: %s" % self.log.getLogBuffer()) | ||
67 | 92 | self.assertEqual(BuildStatus.FULLYBUILT, self.build.status) | ||
68 | 93 | self.assertTrue(self.build.verifySuccessfulUpload()) | ||
69 | 94 | self.assertEqual(1, len(list(self.build.store_upload_jobs))) | ||
70 | 67 | 95 | ||
71 | === modified file 'lib/lp/services/config/schema-lazr.conf' | |||
72 | --- lib/lp/services/config/schema-lazr.conf 2016-05-03 16:38:52 +0000 | |||
73 | +++ lib/lp/services/config/schema-lazr.conf 2016-05-12 15:17:24 +0000 | |||
74 | @@ -1807,6 +1807,7 @@ | |||
75 | 1807 | IRemoveArtifactSubscriptionsJobSource, | 1807 | IRemoveArtifactSubscriptionsJobSource, |
76 | 1808 | ISelfRenewalNotificationJobSource, | 1808 | ISelfRenewalNotificationJobSource, |
77 | 1809 | ISevenDayCommercialExpirationJobSource, | 1809 | ISevenDayCommercialExpirationJobSource, |
78 | 1810 | ISnapStoreUploadJobSource, | ||
79 | 1810 | ITeamInvitationNotificationJobSource, | 1811 | ITeamInvitationNotificationJobSource, |
80 | 1811 | ITeamJoinNotificationJobSource, | 1812 | ITeamJoinNotificationJobSource, |
81 | 1812 | IThirtyDayCommercialExpirationJobSource | 1813 | IThirtyDayCommercialExpirationJobSource |
82 | @@ -1948,6 +1949,10 @@ | |||
83 | 1948 | dbuser: product-job | 1949 | dbuser: product-job |
84 | 1949 | crontab_group: MAIN | 1950 | crontab_group: MAIN |
85 | 1950 | 1951 | ||
86 | 1952 | [ISnapStoreUploadJobSource] | ||
87 | 1953 | module: lp.snappy.interfaces.snapbuildjob | ||
88 | 1954 | dbuser: snap-build-job | ||
89 | 1955 | |||
90 | 1951 | [ITeamInvitationNotificationJobSource] | 1956 | [ITeamInvitationNotificationJobSource] |
91 | 1952 | module: lp.registry.interfaces.persontransferjob | 1957 | module: lp.registry.interfaces.persontransferjob |
92 | 1953 | dbuser: person-transfer-job | 1958 | dbuser: person-transfer-job |
93 | 1954 | 1959 | ||
94 | === modified file 'lib/lp/snappy/configure.zcml' | |||
95 | --- lib/lp/snappy/configure.zcml 2016-05-06 13:14:32 +0000 | |||
96 | +++ lib/lp/snappy/configure.zcml 2016-05-12 15:17:24 +0000 | |||
97 | @@ -124,6 +124,20 @@ | |||
98 | 124 | <allow interface="lp.snappy.interfaces.snapstoreclient.ISnapStoreClient" /> | 124 | <allow interface="lp.snappy.interfaces.snapstoreclient.ISnapStoreClient" /> |
99 | 125 | </securedutility> | 125 | </securedutility> |
100 | 126 | 126 | ||
101 | 127 | <!-- Snap-related jobs --> | ||
102 | 128 | <class class="lp.snappy.model.snapbuildjob.SnapBuildJob"> | ||
103 | 129 | <allow interface="lp.snappy.interfaces.snapbuildjob.ISnapBuildJob" /> | ||
104 | 130 | </class> | ||
105 | 131 | <securedutility | ||
106 | 132 | component="lp.snappy.model.snapbuildjob.SnapStoreUploadJob" | ||
107 | 133 | provides="lp.snappy.interfaces.snapbuildjob.ISnapStoreUploadJobSource"> | ||
108 | 134 | <allow interface="lp.snappy.interfaces.snapbuildjob.ISnapStoreUploadJobSource" /> | ||
109 | 135 | </securedutility> | ||
110 | 136 | <class class="lp.snappy.model.snapbuildjob.SnapStoreUploadJob"> | ||
111 | 137 | <allow interface="lp.snappy.interfaces.snapbuildjob.ISnapBuildJob" /> | ||
112 | 138 | <allow interface="lp.snappy.interfaces.snapbuildjob.ISnapStoreUploadJob" /> | ||
113 | 139 | </class> | ||
114 | 140 | |||
115 | 127 | <webservice:register module="lp.snappy.interfaces.webservice" /> | 141 | <webservice:register module="lp.snappy.interfaces.webservice" /> |
116 | 128 | 142 | ||
117 | 129 | </configure> | 143 | </configure> |
118 | 130 | 144 | ||
119 | === modified file 'lib/lp/snappy/interfaces/snapbuild.py' | |||
120 | --- lib/lp/snappy/interfaces/snapbuild.py 2016-05-04 15:20:26 +0000 | |||
121 | +++ lib/lp/snappy/interfaces/snapbuild.py 2016-05-12 15:17:24 +0000 | |||
122 | @@ -20,7 +20,10 @@ | |||
123 | 20 | operation_for_version, | 20 | operation_for_version, |
124 | 21 | operation_parameters, | 21 | operation_parameters, |
125 | 22 | ) | 22 | ) |
127 | 23 | from lazr.restful.fields import Reference | 23 | from lazr.restful.fields import ( |
128 | 24 | CollectionField, | ||
129 | 25 | Reference, | ||
130 | 26 | ) | ||
131 | 24 | from zope.component.interfaces import IObjectEvent | 27 | from zope.component.interfaces import IObjectEvent |
132 | 25 | from zope.interface import Interface | 28 | from zope.interface import Interface |
133 | 26 | from zope.schema import ( | 29 | from zope.schema import ( |
134 | @@ -103,6 +106,12 @@ | |||
135 | 103 | required=True, readonly=True, | 106 | required=True, readonly=True, |
136 | 104 | description=_("Whether this build record can be cancelled."))) | 107 | description=_("Whether this build record can be cancelled."))) |
137 | 105 | 108 | ||
138 | 109 | store_upload_jobs = CollectionField( | ||
139 | 110 | title=_("Store upload jobs for this build."), | ||
140 | 111 | # Really ISnapStoreUploadJob. | ||
141 | 112 | value_type=Reference(schema=Interface), | ||
142 | 113 | readonly=True) | ||
143 | 114 | |||
144 | 106 | def getFiles(): | 115 | def getFiles(): |
145 | 107 | """Retrieve the build's `ISnapFile` records. | 116 | """Retrieve the build's `ISnapFile` records. |
146 | 108 | 117 | ||
147 | 109 | 118 | ||
148 | === added file 'lib/lp/snappy/interfaces/snapbuildjob.py' | |||
149 | --- lib/lp/snappy/interfaces/snapbuildjob.py 1970-01-01 00:00:00 +0000 | |||
150 | +++ lib/lp/snappy/interfaces/snapbuildjob.py 2016-05-12 15:17:24 +0000 | |||
151 | @@ -0,0 +1,58 @@ | |||
152 | 1 | # Copyright 2016 Canonical Ltd. This software is licensed under the | ||
153 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
154 | 3 | |||
155 | 4 | """Snap build job interfaces.""" | ||
156 | 5 | |||
157 | 6 | from __future__ import absolute_import, print_function, unicode_literals | ||
158 | 7 | |||
159 | 8 | __metaclass__ = type | ||
160 | 9 | __all__ = [ | ||
161 | 10 | 'ISnapBuildJob', | ||
162 | 11 | 'ISnapStoreUploadJob', | ||
163 | 12 | 'ISnapStoreUploadJobSource', | ||
164 | 13 | ] | ||
165 | 14 | |||
166 | 15 | from lazr.restful.fields import Reference | ||
167 | 16 | from zope.interface import ( | ||
168 | 17 | Attribute, | ||
169 | 18 | Interface, | ||
170 | 19 | ) | ||
171 | 20 | from zope.schema import TextLine | ||
172 | 21 | |||
173 | 22 | from lp import _ | ||
174 | 23 | from lp.services.job.interfaces.job import ( | ||
175 | 24 | IJob, | ||
176 | 25 | IJobSource, | ||
177 | 26 | IRunnableJob, | ||
178 | 27 | ) | ||
179 | 28 | from lp.snappy.interfaces.snapbuild import ISnapBuild | ||
180 | 29 | |||
181 | 30 | |||
182 | 31 | class ISnapBuildJob(Interface): | ||
183 | 32 | """A job related to a snap package.""" | ||
184 | 33 | |||
185 | 34 | job = Reference( | ||
186 | 35 | title=_("The common Job attributes."), schema=IJob, | ||
187 | 36 | required=True, readonly=True) | ||
188 | 37 | |||
189 | 38 | snapbuild = Reference( | ||
190 | 39 | title=_("The snap build to use for this job."), | ||
191 | 40 | schema=ISnapBuild, required=True, readonly=True) | ||
192 | 41 | |||
193 | 42 | metadata = Attribute(_("A dict of data about the job.")) | ||
194 | 43 | |||
195 | 44 | |||
196 | 45 | class ISnapStoreUploadJob(IRunnableJob): | ||
197 | 46 | """A Job that uploads a snap build to the store.""" | ||
198 | 47 | |||
199 | 48 | error_message = TextLine( | ||
200 | 49 | title=_("Error message"), required=False, readonly=True) | ||
201 | 50 | |||
202 | 51 | |||
203 | 52 | class ISnapStoreUploadJobSource(IJobSource): | ||
204 | 53 | |||
205 | 54 | def create(snapbuild): | ||
206 | 55 | """Upload a snap build to the store. | ||
207 | 56 | |||
208 | 57 | :param snapbuild: The snap build to upload. | ||
209 | 58 | """ | ||
210 | 0 | 59 | ||
211 | === modified file 'lib/lp/snappy/model/snapbuild.py' | |||
212 | --- lib/lp/snappy/model/snapbuild.py 2016-05-04 15:20:26 +0000 | |||
213 | +++ lib/lp/snappy/model/snapbuild.py 2016-05-12 15:17:24 +0000 | |||
214 | @@ -48,6 +48,7 @@ | |||
215 | 48 | IStore, | 48 | IStore, |
216 | 49 | ) | 49 | ) |
217 | 50 | from lp.services.features import getFeatureFlag | 50 | from lp.services.features import getFeatureFlag |
218 | 51 | from lp.services.job.model.job import Job | ||
219 | 51 | from lp.services.librarian.browser import ProxiedLibraryFileAlias | 52 | from lp.services.librarian.browser import ProxiedLibraryFileAlias |
220 | 52 | from lp.services.librarian.model import ( | 53 | from lp.services.librarian.model import ( |
221 | 53 | LibraryFileAlias, | 54 | LibraryFileAlias, |
222 | @@ -65,6 +66,10 @@ | |||
223 | 65 | ISnapFile, | 66 | ISnapFile, |
224 | 66 | ) | 67 | ) |
225 | 67 | from lp.snappy.mail.snapbuild import SnapBuildMailer | 68 | from lp.snappy.mail.snapbuild import SnapBuildMailer |
226 | 69 | from lp.snappy.model.snapbuildjob import ( | ||
227 | 70 | SnapBuildJob, | ||
228 | 71 | SnapBuildJobType, | ||
229 | 72 | ) | ||
230 | 68 | from lp.soyuz.interfaces.component import IComponentSet | 73 | from lp.soyuz.interfaces.component import IComponentSet |
231 | 69 | from lp.soyuz.model.archive import Archive | 74 | from lp.soyuz.model.archive import Archive |
232 | 70 | from lp.soyuz.model.distroarchseries import DistroArchSeries | 75 | from lp.soyuz.model.distroarchseries import DistroArchSeries |
233 | @@ -346,6 +351,20 @@ | |||
234 | 346 | def getFileUrls(self): | 351 | def getFileUrls(self): |
235 | 347 | return [self.lfaUrl(lfa) for _, lfa, _ in self.getFiles()] | 352 | return [self.lfaUrl(lfa) for _, lfa, _ in self.getFiles()] |
236 | 348 | 353 | ||
237 | 354 | @property | ||
238 | 355 | def store_upload_jobs(self): | ||
239 | 356 | jobs = Store.of(self).find( | ||
240 | 357 | SnapBuildJob, | ||
241 | 358 | SnapBuildJob.snapbuild == self, | ||
242 | 359 | SnapBuildJob.job_type == SnapBuildJobType.STORE_UPLOAD) | ||
243 | 360 | jobs.order_by(Desc(SnapBuildJob.job_id)) | ||
244 | 361 | |||
245 | 362 | def preload_jobs(rows): | ||
246 | 363 | load_related(Job, rows, ["job_id"]) | ||
247 | 364 | |||
248 | 365 | return DecoratedResultSet( | ||
249 | 366 | jobs, lambda job: job.makeDerived(), pre_iter_hook=preload_jobs) | ||
250 | 367 | |||
251 | 349 | 368 | ||
252 | 350 | @implementer(ISnapBuildSet) | 369 | @implementer(ISnapBuildSet) |
253 | 351 | class SnapBuildSet(SpecificBuildFarmJobSourceMixin): | 370 | class SnapBuildSet(SpecificBuildFarmJobSourceMixin): |
254 | 352 | 371 | ||
255 | === added file 'lib/lp/snappy/model/snapbuildjob.py' | |||
256 | --- lib/lp/snappy/model/snapbuildjob.py 1970-01-01 00:00:00 +0000 | |||
257 | +++ lib/lp/snappy/model/snapbuildjob.py 2016-05-12 15:17:24 +0000 | |||
258 | @@ -0,0 +1,191 @@ | |||
259 | 1 | # Copyright 2016 Canonical Ltd. This software is licensed under the | ||
260 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
261 | 3 | |||
262 | 4 | """Snap build jobs.""" | ||
263 | 5 | |||
264 | 6 | from __future__ import absolute_import, print_function, unicode_literals | ||
265 | 7 | |||
266 | 8 | __metaclass__ = type | ||
267 | 9 | __all__ = [ | ||
268 | 10 | 'SnapBuildJob', | ||
269 | 11 | 'SnapBuildJobType', | ||
270 | 12 | 'SnapStoreUploadJob', | ||
271 | 13 | ] | ||
272 | 14 | |||
273 | 15 | from lazr.delegates import delegate_to | ||
274 | 16 | from lazr.enum import ( | ||
275 | 17 | DBEnumeratedType, | ||
276 | 18 | DBItem, | ||
277 | 19 | ) | ||
278 | 20 | from storm.locals import ( | ||
279 | 21 | Int, | ||
280 | 22 | JSON, | ||
281 | 23 | Reference, | ||
282 | 24 | ) | ||
283 | 25 | import transaction | ||
284 | 26 | from zope.component import getUtility | ||
285 | 27 | from zope.interface import ( | ||
286 | 28 | implementer, | ||
287 | 29 | provider, | ||
288 | 30 | ) | ||
289 | 31 | |||
290 | 32 | from lp.app.errors import NotFoundError | ||
291 | 33 | from lp.services.config import config | ||
292 | 34 | from lp.services.database.enumcol import EnumCol | ||
293 | 35 | from lp.services.database.interfaces import ( | ||
294 | 36 | IMasterStore, | ||
295 | 37 | IStore, | ||
296 | 38 | ) | ||
297 | 39 | from lp.services.database.stormbase import StormBase | ||
298 | 40 | from lp.services.job.model.job import ( | ||
299 | 41 | EnumeratedSubclass, | ||
300 | 42 | Job, | ||
301 | 43 | ) | ||
302 | 44 | from lp.services.job.runner import BaseRunnableJob | ||
303 | 45 | from lp.snappy.interfaces.snapbuildjob import ( | ||
304 | 46 | ISnapBuildJob, | ||
305 | 47 | ISnapStoreUploadJob, | ||
306 | 48 | ISnapStoreUploadJobSource, | ||
307 | 49 | ) | ||
308 | 50 | from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient | ||
309 | 51 | |||
310 | 52 | |||
311 | 53 | class SnapBuildJobType(DBEnumeratedType): | ||
312 | 54 | """Values that `ISnapBuildJob.job_type` can take.""" | ||
313 | 55 | |||
314 | 56 | STORE_UPLOAD = DBItem(0, """ | ||
315 | 57 | Store upload | ||
316 | 58 | |||
317 | 59 | This job uploads a snap build to the store. | ||
318 | 60 | """) | ||
319 | 61 | |||
320 | 62 | |||
321 | 63 | @implementer(ISnapBuildJob) | ||
322 | 64 | class SnapBuildJob(StormBase): | ||
323 | 65 | """See `ISnapBuildJob`.""" | ||
324 | 66 | |||
325 | 67 | __storm_table__ = 'SnapBuildJob' | ||
326 | 68 | |||
327 | 69 | job_id = Int(name='job', primary=True, allow_none=False) | ||
328 | 70 | job = Reference(job_id, 'Job.id') | ||
329 | 71 | |||
330 | 72 | snapbuild_id = Int(name='snapbuild', allow_none=False) | ||
331 | 73 | snapbuild = Reference(snapbuild_id, 'SnapBuild.id') | ||
332 | 74 | |||
333 | 75 | job_type = EnumCol(enum=SnapBuildJobType, notNull=True) | ||
334 | 76 | |||
335 | 77 | metadata = JSON('json_data', allow_none=False) | ||
336 | 78 | |||
337 | 79 | def __init__(self, snapbuild, job_type, metadata, **job_args): | ||
338 | 80 | """Constructor. | ||
339 | 81 | |||
340 | 82 | Extra keyword arguments are used to construct the underlying Job | ||
341 | 83 | object. | ||
342 | 84 | |||
343 | 85 | :param snapbuild: The `ISnapBuild` this job relates to. | ||
344 | 86 | :param job_type: The `SnapBuildJobType` of this job. | ||
345 | 87 | :param metadata: The type-specific variables, as a JSON-compatible | ||
346 | 88 | dict. | ||
347 | 89 | """ | ||
348 | 90 | super(SnapBuildJob, self).__init__() | ||
349 | 91 | self.job = Job(**job_args) | ||
350 | 92 | self.snapbuild = snapbuild | ||
351 | 93 | self.job_type = job_type | ||
352 | 94 | self.metadata = metadata | ||
353 | 95 | |||
354 | 96 | def makeDerived(self): | ||
355 | 97 | return SnapBuildJobDerived.makeSubclass(self) | ||
356 | 98 | |||
357 | 99 | |||
358 | 100 | @delegate_to(ISnapBuildJob) | ||
359 | 101 | class SnapBuildJobDerived(BaseRunnableJob): | ||
360 | 102 | |||
361 | 103 | __metaclass__ = EnumeratedSubclass | ||
362 | 104 | |||
363 | 105 | def __init__(self, snap_build_job): | ||
364 | 106 | self.context = snap_build_job | ||
365 | 107 | |||
366 | 108 | def __repr__(self): | ||
367 | 109 | """An informative representation of the job.""" | ||
368 | 110 | return "<%s for %s>" % (self.__class__.__name__, self.snapbuild.title) | ||
369 | 111 | |||
370 | 112 | @classmethod | ||
371 | 113 | def get(cls, job_id): | ||
372 | 114 | """Get a job by id. | ||
373 | 115 | |||
374 | 116 | :return: The `SnapBuildJob` with the specified id, as the current | ||
375 | 117 | `SnapBuildJobDerived` subclass. | ||
376 | 118 | :raises: `NotFoundError` if there is no job with the specified id, | ||
377 | 119 | or its `job_type` does not match the desired subclass. | ||
378 | 120 | """ | ||
379 | 121 | snap_build_job = IStore(SnapBuildJob).get(SnapBuildJob, job_id) | ||
380 | 122 | if snap_build_job.job_type != cls.class_job_type: | ||
381 | 123 | raise NotFoundError( | ||
382 | 124 | "No object found with id %d and type %s" % | ||
383 | 125 | (job_id, cls.class_job_type.title)) | ||
384 | 126 | return cls(snap_build_job) | ||
385 | 127 | |||
386 | 128 | @classmethod | ||
387 | 129 | def iterReady(cls): | ||
388 | 130 | """See `IJobSource`.""" | ||
389 | 131 | jobs = IMasterStore(SnapBuildJob).find( | ||
390 | 132 | SnapBuildJob, | ||
391 | 133 | SnapBuildJob.job_type == cls.class_job_type, | ||
392 | 134 | SnapBuildJob.job == Job.id, | ||
393 | 135 | Job.id.is_in(Job.ready_jobs)) | ||
394 | 136 | return (cls(job) for job in jobs) | ||
395 | 137 | |||
396 | 138 | def getOopsVars(self): | ||
397 | 139 | """See `IRunnableJob`.""" | ||
398 | 140 | oops_vars = super(SnapBuildJobDerived, self).getOopsVars() | ||
399 | 141 | oops_vars.extend([ | ||
400 | 142 | ('job_id', self.context.job.id), | ||
401 | 143 | ('job_type', self.context.job_type.title), | ||
402 | 144 | ('snapbuild_id', self.context.snapbuild.id), | ||
403 | 145 | ('snap_owner_name', self.context.snapbuild.snap.owner.name), | ||
404 | 146 | ('snap_name', self.context.snapbuild.snap.name), | ||
405 | 147 | ]) | ||
406 | 148 | return oops_vars | ||
407 | 149 | |||
408 | 150 | |||
409 | 151 | @implementer(ISnapStoreUploadJob) | ||
410 | 152 | @provider(ISnapStoreUploadJobSource) | ||
411 | 153 | class SnapStoreUploadJob(SnapBuildJobDerived): | ||
412 | 154 | """A Job that uploads a snap build to the store.""" | ||
413 | 155 | |||
414 | 156 | class_job_type = SnapBuildJobType.STORE_UPLOAD | ||
415 | 157 | |||
416 | 158 | # XXX cjwatson 2016-05-04: identify transient upload failures and retry | ||
417 | 159 | |||
418 | 160 | config = config.ISnapStoreUploadJobSource | ||
419 | 161 | |||
420 | 162 | @classmethod | ||
421 | 163 | def create(cls, snapbuild): | ||
422 | 164 | """See `ISnapStoreUploadJobSource`.""" | ||
423 | 165 | snap_build_job = SnapBuildJob(snapbuild, cls.class_job_type, {}) | ||
424 | 166 | job = cls(snap_build_job) | ||
425 | 167 | job.celeryRunOnCommit() | ||
426 | 168 | return job | ||
427 | 169 | |||
428 | 170 | @property | ||
429 | 171 | def error_message(self): | ||
430 | 172 | """See `ISnapStoreUploadJob`.""" | ||
431 | 173 | return self.metadata.get("error_message") | ||
432 | 174 | |||
433 | 175 | @error_message.setter | ||
434 | 176 | def error_message(self, message): | ||
435 | 177 | """See `ISnapStoreUploadJob`.""" | ||
436 | 178 | self.metadata["error_message"] = message | ||
437 | 179 | |||
438 | 180 | def run(self): | ||
439 | 181 | """See `IRunnableJob`.""" | ||
440 | 182 | try: | ||
441 | 183 | getUtility(ISnapStoreClient).upload(self.snapbuild) | ||
442 | 184 | self.error_message = None | ||
443 | 185 | except Exception as e: | ||
444 | 186 | # Abort work done so far, but make sure that we commit the error | ||
445 | 187 | # message. | ||
446 | 188 | transaction.abort() | ||
447 | 189 | self.error_message = str(e) | ||
448 | 190 | transaction.commit() | ||
449 | 191 | raise | ||
450 | 0 | 192 | ||
451 | === modified file 'lib/lp/snappy/subscribers/snapbuild.py' | |||
452 | --- lib/lp/snappy/subscribers/snapbuild.py 2016-01-19 17:41:11 +0000 | |||
453 | +++ lib/lp/snappy/subscribers/snapbuild.py 2016-05-12 15:17:24 +0000 | |||
454 | @@ -9,16 +9,18 @@ | |||
455 | 9 | 9 | ||
456 | 10 | from zope.component import getUtility | 10 | from zope.component import getUtility |
457 | 11 | 11 | ||
458 | 12 | from lp.buildmaster.enums import BuildStatus | ||
459 | 12 | from lp.services.features import getFeatureFlag | 13 | from lp.services.features import getFeatureFlag |
460 | 13 | from lp.services.webapp.publisher import canonical_url | 14 | from lp.services.webapp.publisher import canonical_url |
461 | 14 | from lp.services.webhooks.interfaces import IWebhookSet | 15 | from lp.services.webhooks.interfaces import IWebhookSet |
462 | 15 | from lp.services.webhooks.payload import compose_webhook_payload | 16 | from lp.services.webhooks.payload import compose_webhook_payload |
463 | 16 | from lp.snappy.interfaces.snap import SNAP_WEBHOOKS_FEATURE_FLAG | 17 | from lp.snappy.interfaces.snap import SNAP_WEBHOOKS_FEATURE_FLAG |
464 | 17 | from lp.snappy.interfaces.snapbuild import ISnapBuild | 18 | from lp.snappy.interfaces.snapbuild import ISnapBuild |
465 | 19 | from lp.snappy.interfaces.snapbuildjob import ISnapStoreUploadJobSource | ||
466 | 18 | 20 | ||
467 | 19 | 21 | ||
468 | 20 | def snap_build_status_changed(snapbuild, event): | 22 | def snap_build_status_changed(snapbuild, event): |
470 | 21 | """Trigger webhooks when snap package build statuses change.""" | 23 | """Trigger events when snap package build statuses change.""" |
471 | 22 | if getFeatureFlag(SNAP_WEBHOOKS_FEATURE_FLAG): | 24 | if getFeatureFlag(SNAP_WEBHOOKS_FEATURE_FLAG): |
472 | 23 | payload = { | 25 | payload = { |
473 | 24 | "snap_build": canonical_url(snapbuild, force_local_path=True), | 26 | "snap_build": canonical_url(snapbuild, force_local_path=True), |
474 | @@ -28,3 +30,7 @@ | |||
475 | 28 | ISnapBuild, snapbuild, ["snap", "status"])) | 30 | ISnapBuild, snapbuild, ["snap", "status"])) |
476 | 29 | getUtility(IWebhookSet).trigger( | 31 | getUtility(IWebhookSet).trigger( |
477 | 30 | snapbuild.snap, "snap:build:0.1", payload) | 32 | snapbuild.snap, "snap:build:0.1", payload) |
478 | 33 | |||
479 | 34 | if (snapbuild.snap.can_upload_to_store and | ||
480 | 35 | snapbuild.status == BuildStatus.FULLYBUILT): | ||
481 | 36 | getUtility(ISnapStoreUploadJobSource).create(snapbuild) | ||
482 | 31 | 37 | ||
483 | === modified file 'lib/lp/snappy/tests/test_snapbuild.py' | |||
484 | --- lib/lp/snappy/tests/test_snapbuild.py 2016-03-02 21:21:26 +0000 | |||
485 | +++ lib/lp/snappy/tests/test_snapbuild.py 2016-05-12 15:17:24 +0000 | |||
486 | @@ -97,6 +97,9 @@ | |||
487 | 97 | def setUp(self): | 97 | def setUp(self): |
488 | 98 | super(TestSnapBuild, self).setUp() | 98 | super(TestSnapBuild, self).setUp() |
489 | 99 | self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS)) | 99 | self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS)) |
490 | 100 | self.pushConfig( | ||
491 | 101 | "snappy", store_url="http://sca.example/", | ||
492 | 102 | store_upload_url="http://updown.example/") | ||
493 | 100 | self.build = self.factory.makeSnapBuild() | 103 | self.build = self.factory.makeSnapBuild() |
494 | 101 | 104 | ||
495 | 102 | def test_implements_interfaces(self): | 105 | def test_implements_interfaces(self): |
496 | @@ -250,6 +253,26 @@ | |||
497 | 250 | hook.id, hook.target), | 253 | hook.id, hook.target), |
498 | 251 | repr(delivery)) | 254 | repr(delivery)) |
499 | 252 | 255 | ||
500 | 256 | def test_updateStatus_failure_does_not_trigger_store_uploads(self): | ||
501 | 257 | # A failed SnapBuild does not trigger store uploads. | ||
502 | 258 | self.build.snap.store_series = self.factory.makeSnappySeries() | ||
503 | 259 | self.build.snap.store_name = self.factory.getUniqueUnicode() | ||
504 | 260 | self.build.snap.store_upload = True | ||
505 | 261 | self.build.snap.store_secrets = { | ||
506 | 262 | "root": "dummy-root", "discharge": "dummy-discharge"} | ||
507 | 263 | self.build.updateStatus(BuildStatus.FAILEDTOBUILD) | ||
508 | 264 | self.assertContentEqual([], self.build.store_upload_jobs) | ||
509 | 265 | |||
510 | 266 | def test_updateStatus_fullybuilt_triggers_store_uploads(self): | ||
511 | 267 | # A completed SnapBuild triggers store uploads. | ||
512 | 268 | self.build.snap.store_series = self.factory.makeSnappySeries() | ||
513 | 269 | self.build.snap.store_name = self.factory.getUniqueUnicode() | ||
514 | 270 | self.build.snap.store_upload = True | ||
515 | 271 | self.build.snap.store_secrets = { | ||
516 | 272 | "root": "dummy-root", "discharge": "dummy-discharge"} | ||
517 | 273 | self.build.updateStatus(BuildStatus.FULLYBUILT) | ||
518 | 274 | self.assertEqual(1, len(list(self.build.store_upload_jobs))) | ||
519 | 275 | |||
520 | 253 | def test_notify_fullybuilt(self): | 276 | def test_notify_fullybuilt(self): |
521 | 254 | # notify does not send mail when a SnapBuild completes normally. | 277 | # notify does not send mail when a SnapBuild completes normally. |
522 | 255 | person = self.factory.makePerson(name="person") | 278 | person = self.factory.makePerson(name="person") |
523 | 256 | 279 | ||
524 | === added file 'lib/lp/snappy/tests/test_snapbuildjob.py' | |||
525 | --- lib/lp/snappy/tests/test_snapbuildjob.py 1970-01-01 00:00:00 +0000 | |||
526 | +++ lib/lp/snappy/tests/test_snapbuildjob.py 2016-05-12 15:17:24 +0000 | |||
527 | @@ -0,0 +1,105 @@ | |||
528 | 1 | # Copyright 2016 Canonical Ltd. This software is licensed under the | ||
529 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
530 | 3 | |||
531 | 4 | """Tests for snap build jobs.""" | ||
532 | 5 | |||
533 | 6 | from __future__ import absolute_import, print_function, unicode_literals | ||
534 | 7 | |||
535 | 8 | __metaclass__ = type | ||
536 | 9 | |||
537 | 10 | from zope.interface import implementer | ||
538 | 11 | |||
539 | 12 | from lp.services.config import config | ||
540 | 13 | from lp.services.features.testing import FeatureFixture | ||
541 | 14 | from lp.services.job.runner import JobRunner | ||
542 | 15 | from lp.snappy.interfaces.snap import SNAP_TESTING_FLAGS | ||
543 | 16 | from lp.snappy.interfaces.snapbuildjob import ( | ||
544 | 17 | ISnapBuildJob, | ||
545 | 18 | ISnapStoreUploadJob, | ||
546 | 19 | ) | ||
547 | 20 | from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient | ||
548 | 21 | from lp.snappy.model.snapbuildjob import ( | ||
549 | 22 | SnapBuildJob, | ||
550 | 23 | SnapBuildJobType, | ||
551 | 24 | SnapStoreUploadJob, | ||
552 | 25 | ) | ||
553 | 26 | from lp.testing import TestCaseWithFactory | ||
554 | 27 | from lp.testing.dbuser import dbuser | ||
555 | 28 | from lp.testing.fakemethod import FakeMethod | ||
556 | 29 | from lp.testing.fixture import ZopeUtilityFixture | ||
557 | 30 | from lp.testing.layers import ( | ||
558 | 31 | DatabaseFunctionalLayer, | ||
559 | 32 | LaunchpadZopelessLayer, | ||
560 | 33 | ) | ||
561 | 34 | |||
562 | 35 | |||
563 | 36 | @implementer(ISnapStoreClient) | ||
564 | 37 | class FakeSnapStoreClient: | ||
565 | 38 | |||
566 | 39 | def __init__(self): | ||
567 | 40 | self.upload = FakeMethod() | ||
568 | 41 | |||
569 | 42 | |||
570 | 43 | class TestSnapBuildJob(TestCaseWithFactory): | ||
571 | 44 | |||
572 | 45 | layer = DatabaseFunctionalLayer | ||
573 | 46 | |||
574 | 47 | def setUp(self): | ||
575 | 48 | super(TestSnapBuildJob, self).setUp() | ||
576 | 49 | self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS)) | ||
577 | 50 | |||
578 | 51 | def test_provides_interface(self): | ||
579 | 52 | # `SnapBuildJob` objects provide `ISnapBuildJob`. | ||
580 | 53 | snapbuild = self.factory.makeSnapBuild() | ||
581 | 54 | self.assertProvides( | ||
582 | 55 | SnapBuildJob(snapbuild, SnapBuildJobType.STORE_UPLOAD, {}), | ||
583 | 56 | ISnapBuildJob) | ||
584 | 57 | |||
585 | 58 | |||
586 | 59 | class TestSnapStoreUploadJob(TestCaseWithFactory): | ||
587 | 60 | |||
588 | 61 | layer = LaunchpadZopelessLayer | ||
589 | 62 | |||
590 | 63 | def setUp(self): | ||
591 | 64 | super(TestSnapStoreUploadJob, self).setUp() | ||
592 | 65 | self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS)) | ||
593 | 66 | |||
594 | 67 | def test_provides_interface(self): | ||
595 | 68 | # `SnapStoreUploadJob` objects provide `ISnapStoreUploadJob`. | ||
596 | 69 | snapbuild = self.factory.makeSnapBuild() | ||
597 | 70 | job = SnapStoreUploadJob.create(snapbuild) | ||
598 | 71 | self.assertProvides(job, ISnapStoreUploadJob) | ||
599 | 72 | |||
600 | 73 | def test___repr__(self): | ||
601 | 74 | # `SnapStoreUploadJob` objects have an informative __repr__. | ||
602 | 75 | snapbuild = self.factory.makeSnapBuild() | ||
603 | 76 | job = SnapStoreUploadJob.create(snapbuild) | ||
604 | 77 | self.assertEqual( | ||
605 | 78 | "<SnapStoreUploadJob for %s>" % snapbuild.title, repr(job)) | ||
606 | 79 | |||
607 | 80 | def test_run(self): | ||
608 | 81 | # The job uploads the build to the store. | ||
609 | 82 | snapbuild = self.factory.makeSnapBuild() | ||
610 | 83 | self.assertContentEqual([], snapbuild.store_upload_jobs) | ||
611 | 84 | job = SnapStoreUploadJob.create(snapbuild) | ||
612 | 85 | client = FakeSnapStoreClient() | ||
613 | 86 | self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient)) | ||
614 | 87 | with dbuser(config.ISnapStoreUploadJobSource.dbuser): | ||
615 | 88 | JobRunner([job]).runAll() | ||
616 | 89 | self.assertEqual([((snapbuild,), {})], client.upload.calls) | ||
617 | 90 | self.assertContentEqual([job], snapbuild.store_upload_jobs) | ||
618 | 91 | self.assertIsNone(job.error_message) | ||
619 | 92 | |||
620 | 93 | def test_run_failed(self): | ||
621 | 94 | # A failed run sets the store upload status to FAILED. | ||
622 | 95 | snapbuild = self.factory.makeSnapBuild() | ||
623 | 96 | self.assertContentEqual([], snapbuild.store_upload_jobs) | ||
624 | 97 | job = SnapStoreUploadJob.create(snapbuild) | ||
625 | 98 | client = FakeSnapStoreClient() | ||
626 | 99 | client.upload.failure = ValueError("An upload failure") | ||
627 | 100 | self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient)) | ||
628 | 101 | with dbuser(config.ISnapStoreUploadJobSource.dbuser): | ||
629 | 102 | JobRunner([job]).runAll() | ||
630 | 103 | self.assertEqual([((snapbuild,), {})], client.upload.calls) | ||
631 | 104 | self.assertContentEqual([job], snapbuild.store_upload_jobs) | ||
632 | 105 | self.assertEqual("An upload failure", job.error_message) |