Merge lp:~cjwatson/launchpad/snapbuild-basic-model into lp:launchpad
- snapbuild-basic-model
- Merge into devel
Proposed by
Colin Watson
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 17662 | ||||
Proposed branch: | lp:~cjwatson/launchpad/snapbuild-basic-model | ||||
Merge into: | lp:launchpad | ||||
Prerequisite: | lp:~cjwatson/launchpad/snap-basic-model | ||||
Diff against target: |
1010 lines (+923/-0) 10 files modified
lib/lp/buildmaster/enums.py (+6/-0) lib/lp/security.py (+31/-0) lib/lp/snappy/browser/configure.zcml (+10/-0) lib/lp/snappy/browser/snap.py (+28/-0) lib/lp/snappy/browser/snapbuild.py (+17/-0) lib/lp/snappy/configure.zcml (+31/-0) lib/lp/snappy/interfaces/snapbuild.py (+161/-0) lib/lp/snappy/model/snapbuild.py (+358/-0) lib/lp/snappy/tests/test_snapbuild.py (+226/-0) lib/lp/testing/factory.py (+55/-0) |
||||
To merge this branch: | bzr merge lp:~cjwatson/launchpad/snapbuild-basic-model | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+265691@code.launchpad.net |
Commit message
Add basic model for snap builds.
Description of the change
Add basic model for snap builds. The links between Snap and SnapBuild aren't in place yet for the sake of branch size, but that will come next; this completes the core database modelling.
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 'lib/lp/buildmaster/enums.py' |
2 | --- lib/lp/buildmaster/enums.py 2014-06-20 11:12:40 +0000 |
3 | +++ lib/lp/buildmaster/enums.py 2015-08-03 09:55:48 +0000 |
4 | @@ -157,6 +157,12 @@ |
5 | Build a live filesystem from an archive. |
6 | """) |
7 | |
8 | + SNAPBUILD = DBItem(6, """ |
9 | + Snap package build |
10 | + |
11 | + Build a snap package from a recipe. |
12 | + """) |
13 | + |
14 | |
15 | class BuildQueueStatus(DBEnumeratedType): |
16 | """Build queue status. |
17 | |
18 | === modified file 'lib/lp/security.py' |
19 | --- lib/lp/security.py 2015-07-23 14:32:50 +0000 |
20 | +++ lib/lp/security.py 2015-08-03 09:55:48 +0000 |
21 | @@ -192,6 +192,7 @@ |
22 | ILanguageSet, |
23 | ) |
24 | from lp.snappy.interfaces.snap import ISnap |
25 | +from lp.snappy.interfaces.snapbuild import ISnapBuild |
26 | from lp.soyuz.interfaces.archive import IArchive |
27 | from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthToken |
28 | from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet |
29 | @@ -3116,3 +3117,33 @@ |
30 | return ( |
31 | user.in_ppa_self_admins |
32 | and EditSnap(self.obj).checkAuthenticated(user)) |
33 | + |
34 | + |
35 | +class ViewSnapBuild(DelegatedAuthorization): |
36 | + permission = 'launchpad.View' |
37 | + usedfor = ISnapBuild |
38 | + |
39 | + def iter_objects(self): |
40 | + yield self.obj.snap |
41 | + yield self.obj.archive |
42 | + |
43 | + |
44 | +class EditSnapBuild(AdminByBuilddAdmin): |
45 | + permission = 'launchpad.Edit' |
46 | + usedfor = ISnapBuild |
47 | + |
48 | + def checkAuthenticated(self, user): |
49 | + """Check edit access for snap package builds. |
50 | + |
51 | + Allow admins, buildd admins, and the owner of the snap package. |
52 | + (Note that the requester of the build is required to be in the team |
53 | + that owns the snap package.) |
54 | + """ |
55 | + auth_snap = EditSnap(self.obj.snap) |
56 | + if auth_snap.checkAuthenticated(user): |
57 | + return True |
58 | + return super(EditSnapBuild, self).checkAuthenticated(user) |
59 | + |
60 | + |
61 | +class AdminSnapBuild(AdminByBuilddAdmin): |
62 | + usedfor = ISnapBuild |
63 | |
64 | === modified file 'lib/lp/snappy/browser/configure.zcml' |
65 | --- lib/lp/snappy/browser/configure.zcml 2015-07-23 14:32:50 +0000 |
66 | +++ lib/lp/snappy/browser/configure.zcml 2015-08-03 09:55:48 +0000 |
67 | @@ -13,5 +13,15 @@ |
68 | for="lp.snappy.interfaces.snap.ISnap" |
69 | path_expression="string:+snap/${name}" |
70 | attribute_to_parent="owner" /> |
71 | + <browser:navigation |
72 | + module="lp.snappy.browser.snap" |
73 | + classes="SnapNavigation" /> |
74 | + <browser:url |
75 | + for="lp.snappy.interfaces.snapbuild.ISnapBuild" |
76 | + path_expression="string:+build/${id}" |
77 | + attribute_to_parent="snap" /> |
78 | + <browser:navigation |
79 | + module="lp.snappy.browser.snapbuild" |
80 | + classes="SnapBuildNavigation" /> |
81 | </facet> |
82 | </configure> |
83 | |
84 | === added file 'lib/lp/snappy/browser/snap.py' |
85 | --- lib/lp/snappy/browser/snap.py 1970-01-01 00:00:00 +0000 |
86 | +++ lib/lp/snappy/browser/snap.py 2015-08-03 09:55:48 +0000 |
87 | @@ -0,0 +1,28 @@ |
88 | +# Copyright 2015 Canonical Ltd. This software is licensed under the |
89 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
90 | + |
91 | +"""Snap views.""" |
92 | + |
93 | +__metaclass__ = type |
94 | +__all__ = [ |
95 | + 'SnapNavigation', |
96 | + ] |
97 | + |
98 | +from lp.services.webapp import ( |
99 | + Navigation, |
100 | + stepthrough, |
101 | + ) |
102 | +from lp.snappy.interfaces.snap import ISnap |
103 | +from lp.snappy.interfaces.snapbuild import ISnapBuildSet |
104 | +from lp.soyuz.browser.build import get_build_by_id_str |
105 | + |
106 | + |
107 | +class SnapNavigation(Navigation): |
108 | + usedfor = ISnap |
109 | + |
110 | + @stepthrough('+build') |
111 | + def traverse_build(self, name): |
112 | + build = get_build_by_id_str(ISnapBuildSet, name) |
113 | + if build is None or build.snap != self.context: |
114 | + return None |
115 | + return build |
116 | |
117 | === added file 'lib/lp/snappy/browser/snapbuild.py' |
118 | --- lib/lp/snappy/browser/snapbuild.py 1970-01-01 00:00:00 +0000 |
119 | +++ lib/lp/snappy/browser/snapbuild.py 2015-08-03 09:55:48 +0000 |
120 | @@ -0,0 +1,17 @@ |
121 | +# Copyright 2015 Canonical Ltd. This software is licensed under the |
122 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
123 | + |
124 | +"""SnapBuild views.""" |
125 | + |
126 | +__metaclass__ = type |
127 | +__all__ = [ |
128 | + 'SnapBuildNavigation', |
129 | + ] |
130 | + |
131 | +from lp.services.librarian.browser import FileNavigationMixin |
132 | +from lp.services.webapp import Navigation |
133 | +from lp.snappy.interfaces.snapbuild import ISnapBuild |
134 | + |
135 | + |
136 | +class SnapBuildNavigation(Navigation, FileNavigationMixin): |
137 | + usedfor = ISnapBuild |
138 | |
139 | === modified file 'lib/lp/snappy/configure.zcml' |
140 | --- lib/lp/snappy/configure.zcml 2015-07-30 12:25:44 +0000 |
141 | +++ lib/lp/snappy/configure.zcml 2015-08-03 09:55:48 +0000 |
142 | @@ -40,4 +40,35 @@ |
143 | <allow interface="lp.snappy.interfaces.snap.ISnapSet" /> |
144 | </securedutility> |
145 | |
146 | + <!-- SnapBuild --> |
147 | + <class class="lp.snappy.model.snapbuild.SnapBuild"> |
148 | + <require |
149 | + permission="launchpad.View" |
150 | + interface="lp.snappy.interfaces.snapbuild.ISnapBuildView" /> |
151 | + <require |
152 | + permission="launchpad.Edit" |
153 | + interface="lp.snappy.interfaces.snapbuild.ISnapBuildEdit" /> |
154 | + <require |
155 | + permission="launchpad.Admin" |
156 | + interface="lp.snappy.interfaces.snapbuild.ISnapBuildAdmin" /> |
157 | + </class> |
158 | + |
159 | + <!-- SnapBuildSet --> |
160 | + <securedutility |
161 | + class="lp.snappy.model.snapbuild.SnapBuildSet" |
162 | + provides="lp.snappy.interfaces.snapbuild.ISnapBuildSet"> |
163 | + <allow interface="lp.snappy.interfaces.snapbuild.ISnapBuildSet" /> |
164 | + </securedutility> |
165 | + <securedutility |
166 | + class="lp.snappy.model.snapbuild.SnapBuildSet" |
167 | + provides="lp.buildmaster.interfaces.buildfarmjob.ISpecificBuildFarmJobSource" |
168 | + name="SNAPBUILD"> |
169 | + <allow interface="lp.buildmaster.interfaces.buildfarmjob.ISpecificBuildFarmJobSource" /> |
170 | + </securedutility> |
171 | + |
172 | + <!-- SnapFile --> |
173 | + <class class="lp.snappy.model.snapbuild.SnapFile"> |
174 | + <allow interface="lp.snappy.interfaces.snapbuild.ISnapFile" /> |
175 | + </class> |
176 | + |
177 | </configure> |
178 | |
179 | === added file 'lib/lp/snappy/interfaces/snapbuild.py' |
180 | --- lib/lp/snappy/interfaces/snapbuild.py 1970-01-01 00:00:00 +0000 |
181 | +++ lib/lp/snappy/interfaces/snapbuild.py 2015-08-03 09:55:48 +0000 |
182 | @@ -0,0 +1,161 @@ |
183 | +# Copyright 2015 Canonical Ltd. This software is licensed under the |
184 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
185 | + |
186 | +"""Snap package build interfaces.""" |
187 | + |
188 | +__metaclass__ = type |
189 | + |
190 | +__all__ = [ |
191 | + 'ISnapBuild', |
192 | + 'ISnapBuildSet', |
193 | + 'ISnapFile', |
194 | + ] |
195 | + |
196 | +from lazr.restful.fields import Reference |
197 | +from zope.interface import ( |
198 | + Attribute, |
199 | + Interface, |
200 | + ) |
201 | +from zope.schema import ( |
202 | + Bool, |
203 | + Choice, |
204 | + Int, |
205 | + ) |
206 | + |
207 | +from lp import _ |
208 | +from lp.buildmaster.interfaces.buildfarmjob import ISpecificBuildFarmJobSource |
209 | +from lp.buildmaster.interfaces.packagebuild import IPackageBuild |
210 | +from lp.registry.interfaces.person import IPerson |
211 | +from lp.registry.interfaces.pocket import PackagePublishingPocket |
212 | +from lp.services.database.constants import DEFAULT |
213 | +from lp.services.librarian.interfaces import ILibraryFileAlias |
214 | +from lp.snappy.interfaces.snap import ISnap |
215 | +from lp.soyuz.interfaces.archive import IArchive |
216 | +from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries |
217 | + |
218 | + |
219 | +class ISnapFile(Interface): |
220 | + """A file produced by a snap package build.""" |
221 | + |
222 | + snapbuild = Attribute("The snap package build producing this file.") |
223 | + |
224 | + libraryfile = Reference( |
225 | + ILibraryFileAlias, title=_("The library file alias for this file."), |
226 | + required=True, readonly=True) |
227 | + |
228 | + |
229 | +class ISnapBuildView(IPackageBuild): |
230 | + """`ISnapBuild` attributes that require launchpad.View permission.""" |
231 | + |
232 | + requester = Reference( |
233 | + IPerson, |
234 | + title=_("The person who requested this build."), |
235 | + required=True, readonly=True) |
236 | + |
237 | + snap = Reference( |
238 | + ISnap, |
239 | + title=_("The snap package to build."), |
240 | + required=True, readonly=True) |
241 | + |
242 | + archive = Reference( |
243 | + IArchive, |
244 | + title=_("The archive from which to build the snap package."), |
245 | + required=True, readonly=True) |
246 | + |
247 | + distro_arch_series = Reference( |
248 | + IDistroArchSeries, |
249 | + title=_("The series and architecture for which to build."), |
250 | + required=True, readonly=True) |
251 | + |
252 | + pocket = Choice( |
253 | + title=_("The pocket for which to build."), |
254 | + vocabulary=PackagePublishingPocket, required=True, readonly=True) |
255 | + |
256 | + virtualized = Bool( |
257 | + title=_("If True, this build is virtualized."), readonly=True) |
258 | + |
259 | + score = Int( |
260 | + title=_("Score of the related build farm job (if any)."), |
261 | + required=False, readonly=True) |
262 | + |
263 | + can_be_rescored = Bool( |
264 | + title=_("Can be rescored"), |
265 | + required=True, readonly=True, |
266 | + description=_("Whether this build record can be rescored manually.")) |
267 | + |
268 | + can_be_cancelled = Bool( |
269 | + title=_("Can be cancelled"), |
270 | + required=True, readonly=True, |
271 | + description=_("Whether this build record can be cancelled.")) |
272 | + |
273 | + def getFiles(): |
274 | + """Retrieve the build's `ISnapFile` records. |
275 | + |
276 | + :return: A result set of (`ISnapFile`, `ILibraryFileAlias`, |
277 | + `ILibraryFileContent`). |
278 | + """ |
279 | + |
280 | + def getFileByName(filename): |
281 | + """Return the corresponding `ILibraryFileAlias` in this context. |
282 | + |
283 | + The following file types (and extension) can be looked up: |
284 | + |
285 | + * Build log: '.txt.gz' |
286 | + * Upload log: '_log.txt' |
287 | + |
288 | + Any filename not matching one of these extensions is looked up as a |
289 | + snap package output file. |
290 | + |
291 | + :param filename: The filename to look up. |
292 | + :raises NotFoundError: if no file exists with the given name. |
293 | + :return: The corresponding `ILibraryFileAlias`. |
294 | + """ |
295 | + |
296 | + def getFileUrls(): |
297 | + """URLs for all the files produced by this build. |
298 | + |
299 | + :return: A collection of URLs for this build.""" |
300 | + |
301 | + |
302 | +class ISnapBuildEdit(Interface): |
303 | + """`ISnapBuild` attributes that require launchpad.Edit.""" |
304 | + |
305 | + def addFile(lfa): |
306 | + """Add a file to this build. |
307 | + |
308 | + :param lfa: An `ILibraryFileAlias`. |
309 | + :return: An `ISnapFile`. |
310 | + """ |
311 | + |
312 | + def cancel(): |
313 | + """Cancel the build if it is either pending or in progress. |
314 | + |
315 | + Check the can_be_cancelled property prior to calling this method to |
316 | + find out if cancelling the build is possible. |
317 | + |
318 | + If the build is in progress, it is marked as CANCELLING until the |
319 | + buildd manager terminates the build and marks it CANCELLED. If the |
320 | + build is not in progress, it is marked CANCELLED immediately and is |
321 | + removed from the build queue. |
322 | + |
323 | + If the build is not in a cancellable state, this method is a no-op. |
324 | + """ |
325 | + |
326 | + |
327 | +class ISnapBuildAdmin(Interface): |
328 | + """`ISnapBuild` attributes that require launchpad.Admin.""" |
329 | + |
330 | + def rescore(score): |
331 | + """Change the build's score.""" |
332 | + |
333 | + |
334 | +class ISnapBuild(ISnapBuildView, ISnapBuildEdit, ISnapBuildAdmin): |
335 | + """Build information for snap package builds.""" |
336 | + |
337 | + |
338 | +class ISnapBuildSet(ISpecificBuildFarmJobSource): |
339 | + """Utility for `ISnapBuild`.""" |
340 | + |
341 | + def new(requester, snap, archive, distro_arch_series, pocket, |
342 | + date_created=DEFAULT): |
343 | + """Create an `ISnapBuild`.""" |
344 | |
345 | === added file 'lib/lp/snappy/model/snapbuild.py' |
346 | --- lib/lp/snappy/model/snapbuild.py 1970-01-01 00:00:00 +0000 |
347 | +++ lib/lp/snappy/model/snapbuild.py 2015-08-03 09:55:48 +0000 |
348 | @@ -0,0 +1,358 @@ |
349 | +# Copyright 2015 Canonical Ltd. This software is licensed under the |
350 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
351 | + |
352 | +__metaclass__ = type |
353 | +__all__ = [ |
354 | + 'SnapBuild', |
355 | + 'SnapFile', |
356 | + ] |
357 | + |
358 | +from datetime import timedelta |
359 | + |
360 | +import pytz |
361 | +from storm.locals import ( |
362 | + Bool, |
363 | + DateTime, |
364 | + Desc, |
365 | + Int, |
366 | + Reference, |
367 | + Store, |
368 | + Storm, |
369 | + Unicode, |
370 | + ) |
371 | +from storm.store import EmptyResultSet |
372 | +from zope.component import getUtility |
373 | +from zope.interface import implementer |
374 | + |
375 | +from lp.app.errors import NotFoundError |
376 | +from lp.buildmaster.enums import ( |
377 | + BuildFarmJobType, |
378 | + BuildStatus, |
379 | + ) |
380 | +from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJobSource |
381 | +from lp.buildmaster.model.buildfarmjob import SpecificBuildFarmJobSourceMixin |
382 | +from lp.buildmaster.model.packagebuild import PackageBuildMixin |
383 | +from lp.registry.interfaces.pocket import PackagePublishingPocket |
384 | +from lp.registry.model.person import Person |
385 | +from lp.services.database.bulk import load_related |
386 | +from lp.services.database.constants import DEFAULT |
387 | +from lp.services.database.decoratedresultset import DecoratedResultSet |
388 | +from lp.services.database.enumcol import DBEnum |
389 | +from lp.services.database.interfaces import ( |
390 | + IMasterStore, |
391 | + IStore, |
392 | + ) |
393 | +from lp.services.features import getFeatureFlag |
394 | +from lp.services.librarian.browser import ProxiedLibraryFileAlias |
395 | +from lp.services.librarian.model import ( |
396 | + LibraryFileAlias, |
397 | + LibraryFileContent, |
398 | + ) |
399 | +from lp.snappy.interfaces.snap import ( |
400 | + SNAP_FEATURE_FLAG, |
401 | + SnapFeatureDisabled, |
402 | + ) |
403 | +from lp.snappy.interfaces.snapbuild import ( |
404 | + ISnapBuild, |
405 | + ISnapBuildSet, |
406 | + ISnapFile, |
407 | + ) |
408 | +from lp.soyuz.interfaces.component import IComponentSet |
409 | +from lp.soyuz.model.archive import Archive |
410 | + |
411 | + |
412 | +@implementer(ISnapFile) |
413 | +class SnapFile(Storm): |
414 | + """See `ISnap`.""" |
415 | + |
416 | + __storm_table__ = 'SnapFile' |
417 | + |
418 | + id = Int(name='id', primary=True) |
419 | + |
420 | + snapbuild_id = Int(name='snapbuild', allow_none=False) |
421 | + snapbuild = Reference(snapbuild_id, 'SnapBuild.id') |
422 | + |
423 | + libraryfile_id = Int(name='libraryfile', allow_none=False) |
424 | + libraryfile = Reference(libraryfile_id, 'LibraryFileAlias.id') |
425 | + |
426 | + def __init__(self, snapbuild, libraryfile): |
427 | + """Construct a `SnapFile`.""" |
428 | + super(SnapFile, self).__init__() |
429 | + self.snapbuild = snapbuild |
430 | + self.libraryfile = libraryfile |
431 | + |
432 | + |
433 | +@implementer(ISnapBuild) |
434 | +class SnapBuild(PackageBuildMixin, Storm): |
435 | + """See `ISnapBuild`.""" |
436 | + |
437 | + __storm_table__ = 'SnapBuild' |
438 | + |
439 | + job_type = BuildFarmJobType.SNAPBUILD |
440 | + |
441 | + id = Int(name='id', primary=True) |
442 | + |
443 | + build_farm_job_id = Int(name='build_farm_job', allow_none=False) |
444 | + build_farm_job = Reference(build_farm_job_id, 'BuildFarmJob.id') |
445 | + |
446 | + requester_id = Int(name='requester', allow_none=False) |
447 | + requester = Reference(requester_id, 'Person.id') |
448 | + |
449 | + snap_id = Int(name='snap', allow_none=False) |
450 | + snap = Reference(snap_id, 'Snap.id') |
451 | + |
452 | + archive_id = Int(name='archive', allow_none=False) |
453 | + archive = Reference(archive_id, 'Archive.id') |
454 | + |
455 | + distro_arch_series_id = Int(name='distro_arch_series', allow_none=False) |
456 | + distro_arch_series = Reference( |
457 | + distro_arch_series_id, 'DistroArchSeries.id') |
458 | + |
459 | + pocket = DBEnum(enum=PackagePublishingPocket, allow_none=False) |
460 | + |
461 | + processor_id = Int(name='processor', allow_none=False) |
462 | + processor = Reference(processor_id, 'Processor.id') |
463 | + virtualized = Bool(name='virtualized') |
464 | + |
465 | + date_created = DateTime( |
466 | + name='date_created', tzinfo=pytz.UTC, allow_none=False) |
467 | + date_started = DateTime(name='date_started', tzinfo=pytz.UTC) |
468 | + date_finished = DateTime(name='date_finished', tzinfo=pytz.UTC) |
469 | + date_first_dispatched = DateTime( |
470 | + name='date_first_dispatched', tzinfo=pytz.UTC) |
471 | + |
472 | + builder_id = Int(name='builder') |
473 | + builder = Reference(builder_id, 'Builder.id') |
474 | + |
475 | + status = DBEnum(name='status', enum=BuildStatus, allow_none=False) |
476 | + |
477 | + log_id = Int(name='log') |
478 | + log = Reference(log_id, 'LibraryFileAlias.id') |
479 | + |
480 | + upload_log_id = Int(name='upload_log') |
481 | + upload_log = Reference(upload_log_id, 'LibraryFileAlias.id') |
482 | + |
483 | + dependencies = Unicode(name='dependencies') |
484 | + |
485 | + failure_count = Int(name='failure_count', allow_none=False) |
486 | + |
487 | + def __init__(self, build_farm_job, requester, snap, archive, |
488 | + distro_arch_series, pocket, processor, virtualized, |
489 | + date_created): |
490 | + """Construct a `SnapBuild`.""" |
491 | + if not getFeatureFlag(SNAP_FEATURE_FLAG): |
492 | + raise SnapFeatureDisabled |
493 | + super(SnapBuild, self).__init__() |
494 | + self.build_farm_job = build_farm_job |
495 | + self.requester = requester |
496 | + self.snap = snap |
497 | + self.archive = archive |
498 | + self.distro_arch_series = distro_arch_series |
499 | + self.pocket = pocket |
500 | + self.processor = processor |
501 | + self.virtualized = virtualized |
502 | + self.date_created = date_created |
503 | + self.status = BuildStatus.NEEDSBUILD |
504 | + |
505 | + @property |
506 | + def is_private(self): |
507 | + """See `IBuildFarmJob`.""" |
508 | + return self.snap.owner.private or self.archive.private |
509 | + |
510 | + @property |
511 | + def title(self): |
512 | + das = self.distro_arch_series |
513 | + name = self.snap.name |
514 | + return "%s build of %s snap package in %s %s" % ( |
515 | + das.architecturetag, name, das.distroseries.distribution.name, |
516 | + das.distroseries.getSuite(self.pocket)) |
517 | + |
518 | + @property |
519 | + def distribution(self): |
520 | + """See `IPackageBuild`.""" |
521 | + return self.distro_arch_series.distroseries.distribution |
522 | + |
523 | + @property |
524 | + def distro_series(self): |
525 | + """See `IPackageBuild`.""" |
526 | + return self.distro_arch_series.distroseries |
527 | + |
528 | + @property |
529 | + def current_component(self): |
530 | + component = self.archive.default_component |
531 | + if component is not None: |
532 | + return component |
533 | + else: |
534 | + # XXX cjwatson 2015-07-17: Hardcode to multiverse for the time |
535 | + # being. |
536 | + return getUtility(IComponentSet)["multiverse"] |
537 | + |
538 | + @property |
539 | + def score(self): |
540 | + """See `ISnapBuild`.""" |
541 | + if self.buildqueue_record is None: |
542 | + return None |
543 | + else: |
544 | + return self.buildqueue_record.lastscore |
545 | + |
546 | + @property |
547 | + def can_be_rescored(self): |
548 | + """See `ISnapBuild`.""" |
549 | + return ( |
550 | + self.buildqueue_record is not None and |
551 | + self.status is BuildStatus.NEEDSBUILD) |
552 | + |
553 | + @property |
554 | + def can_be_cancelled(self): |
555 | + """See `ISnapBuild`.""" |
556 | + if not self.buildqueue_record: |
557 | + return False |
558 | + |
559 | + cancellable_statuses = [ |
560 | + BuildStatus.BUILDING, |
561 | + BuildStatus.NEEDSBUILD, |
562 | + ] |
563 | + return self.status in cancellable_statuses |
564 | + |
565 | + def rescore(self, score): |
566 | + """See `ISnapBuild`.""" |
567 | + assert self.can_be_rescored, "Build %s cannot be rescored" % self.id |
568 | + self.buildqueue_record.manualScore(score) |
569 | + |
570 | + def cancel(self): |
571 | + """See `ISnapBuild`.""" |
572 | + if not self.can_be_cancelled: |
573 | + return |
574 | + # BuildQueue.cancel() will decide whether to go straight to |
575 | + # CANCELLED, or go through CANCELLING to let buildd-manager clean up |
576 | + # the slave. |
577 | + self.buildqueue_record.cancel() |
578 | + |
579 | + def calculateScore(self): |
580 | + return 2505 + self.archive.relative_build_score |
581 | + |
582 | + def getMedianBuildDuration(self): |
583 | + """Return the median duration of our successful builds.""" |
584 | + store = IStore(self) |
585 | + result = store.find( |
586 | + (SnapBuild.date_started, SnapBuild.date_finished), |
587 | + SnapBuild.snap == self.snap_id, |
588 | + SnapBuild.distro_arch_series == self.distro_arch_series_id, |
589 | + SnapBuild.status == BuildStatus.FULLYBUILT) |
590 | + result.order_by(Desc(SnapBuild.date_finished)) |
591 | + durations = [row[1] - row[0] for row in result[:9]] |
592 | + if len(durations) == 0: |
593 | + return None |
594 | + durations.sort() |
595 | + return durations[len(durations) // 2] |
596 | + |
597 | + def estimateDuration(self): |
598 | + """See `IBuildFarmJob`.""" |
599 | + median = self.getMedianBuildDuration() |
600 | + if median is not None: |
601 | + return median |
602 | + return timedelta(minutes=30) |
603 | + |
604 | + def getFiles(self): |
605 | + """See `ISnapBuild`.""" |
606 | + result = Store.of(self).find( |
607 | + (SnapFile, LibraryFileAlias, LibraryFileContent), |
608 | + SnapFile.snapbuild == self.id, |
609 | + LibraryFileAlias.id == SnapFile.libraryfile_id, |
610 | + LibraryFileContent.id == LibraryFileAlias.contentID) |
611 | + return result.order_by([LibraryFileAlias.filename, SnapFile.id]) |
612 | + |
613 | + def getFileByName(self, filename): |
614 | + """See `ISnapBuild`.""" |
615 | + if filename.endswith(".txt.gz"): |
616 | + file_object = self.log |
617 | + elif filename.endswith("_log.txt"): |
618 | + file_object = self.upload_log |
619 | + else: |
620 | + file_object = Store.of(self).find( |
621 | + LibraryFileAlias, |
622 | + SnapFile.snapbuild == self.id, |
623 | + LibraryFileAlias.id == SnapFile.libraryfile_id, |
624 | + LibraryFileAlias.filename == filename).one() |
625 | + |
626 | + if file_object is not None and file_object.filename == filename: |
627 | + return file_object |
628 | + |
629 | + raise NotFoundError(filename) |
630 | + |
631 | + def addFile(self, lfa): |
632 | + """See `ISnapBuild`.""" |
633 | + snapfile = SnapFile(snapbuild=self, libraryfile=lfa) |
634 | + IMasterStore(SnapFile).add(snapfile) |
635 | + return snapfile |
636 | + |
637 | + def verifySuccessfulUpload(self): |
638 | + """See `IPackageBuild`.""" |
639 | + return not self.getFiles().is_empty() |
640 | + |
641 | + def lfaUrl(self, lfa): |
642 | + """Return the URL for a LibraryFileAlias in this context.""" |
643 | + if lfa is None: |
644 | + return None |
645 | + return ProxiedLibraryFileAlias(lfa, self).http_url |
646 | + |
647 | + @property |
648 | + def log_url(self): |
649 | + """See `IBuildFarmJob`.""" |
650 | + return self.lfaUrl(self.log) |
651 | + |
652 | + @property |
653 | + def upload_log_url(self): |
654 | + """See `IPackageBuild`.""" |
655 | + return self.lfaUrl(self.upload_log) |
656 | + |
657 | + def getFileUrls(self): |
658 | + return [self.lfaUrl(lfa) for _, lfa, _ in self.getFiles()] |
659 | + |
660 | + |
661 | +@implementer(ISnapBuildSet) |
662 | +class SnapBuildSet(SpecificBuildFarmJobSourceMixin): |
663 | + |
664 | + def new(self, requester, snap, archive, distro_arch_series, pocket, |
665 | + date_created=DEFAULT): |
666 | + """See `ISnapBuildSet`.""" |
667 | + store = IMasterStore(SnapBuild) |
668 | + build_farm_job = getUtility(IBuildFarmJobSource).new( |
669 | + SnapBuild.job_type, BuildStatus.NEEDSBUILD, date_created, None, |
670 | + archive) |
671 | + snapbuild = SnapBuild( |
672 | + build_farm_job, requester, snap, archive, distro_arch_series, |
673 | + pocket, distro_arch_series.processor, |
674 | + not distro_arch_series.processor.supports_nonvirtualized |
675 | + or snap.require_virtualized or archive.require_virtualized, |
676 | + date_created) |
677 | + store.add(snapbuild) |
678 | + return snapbuild |
679 | + |
680 | + def getByID(self, build_id): |
681 | + """See `ISpecificBuildFarmJobSource`.""" |
682 | + store = IMasterStore(SnapBuild) |
683 | + return store.get(SnapBuild, build_id) |
684 | + |
685 | + def getByBuildFarmJob(self, build_farm_job): |
686 | + """See `ISpecificBuildFarmJobSource`.""" |
687 | + return Store.of(build_farm_job).find( |
688 | + SnapBuild, build_farm_job_id=build_farm_job.id).one() |
689 | + |
690 | + def preloadBuildsData(self, builds): |
691 | + # Circular import. |
692 | + from lp.snappy.model.snap import Snap |
693 | + load_related(Person, builds, ["requester_id"]) |
694 | + load_related(LibraryFileAlias, builds, ["log_id"]) |
695 | + archives = load_related(Archive, builds, ["archive_id"]) |
696 | + load_related(Person, archives, ["ownerID"]) |
697 | + load_related(Snap, builds, ["snap_id"]) |
698 | + |
699 | + def getByBuildFarmJobs(self, build_farm_jobs): |
700 | + """See `ISpecificBuildFarmJobSource`.""" |
701 | + if len(build_farm_jobs) == 0: |
702 | + return EmptyResultSet() |
703 | + rows = Store.of(build_farm_jobs[0]).find( |
704 | + SnapBuild, SnapBuild.build_farm_job_id.is_in( |
705 | + bfj.id for bfj in build_farm_jobs)) |
706 | + return DecoratedResultSet(rows, pre_iter_hook=self.preloadBuildsData) |
707 | |
708 | === added file 'lib/lp/snappy/tests/test_snapbuild.py' |
709 | --- lib/lp/snappy/tests/test_snapbuild.py 1970-01-01 00:00:00 +0000 |
710 | +++ lib/lp/snappy/tests/test_snapbuild.py 2015-08-03 09:55:48 +0000 |
711 | @@ -0,0 +1,226 @@ |
712 | +# Copyright 2015 Canonical Ltd. This software is licensed under the |
713 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
714 | + |
715 | +"""Test snap package build features.""" |
716 | + |
717 | +__metaclass__ = type |
718 | + |
719 | +from datetime import timedelta |
720 | + |
721 | +from zope.component import getUtility |
722 | +from zope.security.proxy import removeSecurityProxy |
723 | + |
724 | +from lp.app.errors import NotFoundError |
725 | +from lp.buildmaster.enums import BuildStatus |
726 | +from lp.buildmaster.interfaces.buildqueue import IBuildQueue |
727 | +from lp.buildmaster.interfaces.packagebuild import IPackageBuild |
728 | +from lp.registry.enums import PersonVisibility |
729 | +from lp.services.features.testing import FeatureFixture |
730 | +from lp.snappy.interfaces.snap import ( |
731 | + SNAP_FEATURE_FLAG, |
732 | + SnapFeatureDisabled, |
733 | + ) |
734 | +from lp.snappy.interfaces.snapbuild import ( |
735 | + ISnapBuild, |
736 | + ISnapBuildSet, |
737 | + ) |
738 | +from lp.soyuz.enums import ArchivePurpose |
739 | +from lp.testing import ( |
740 | + person_logged_in, |
741 | + TestCaseWithFactory, |
742 | + ) |
743 | +from lp.testing.layers import LaunchpadZopelessLayer |
744 | + |
745 | + |
746 | +class TestSnapBuildFeatureFlag(TestCaseWithFactory): |
747 | + |
748 | + layer = LaunchpadZopelessLayer |
749 | + |
750 | + def test_feature_flag_disabled(self): |
751 | + # Without a feature flag, we will not create new SnapBuilds. |
752 | + class MockSnap: |
753 | + require_virtualized = False |
754 | + |
755 | + self.assertRaises( |
756 | + SnapFeatureDisabled, getUtility(ISnapBuildSet).new, |
757 | + None, MockSnap(), self.factory.makeArchive(), |
758 | + self.factory.makeDistroArchSeries(), None) |
759 | + |
760 | + |
761 | +class TestSnapBuild(TestCaseWithFactory): |
762 | + |
763 | + layer = LaunchpadZopelessLayer |
764 | + |
765 | + def setUp(self): |
766 | + super(TestSnapBuild, self).setUp() |
767 | + self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"})) |
768 | + self.build = self.factory.makeSnapBuild() |
769 | + |
770 | + def test_implements_interfaces(self): |
771 | + # SnapBuild implements IPackageBuild and ISnapBuild. |
772 | + self.assertProvides(self.build, IPackageBuild) |
773 | + self.assertProvides(self.build, ISnapBuild) |
774 | + |
775 | + def test_queueBuild(self): |
776 | + # SnapBuild can create the queue entry for itself. |
777 | + bq = self.build.queueBuild() |
778 | + self.assertProvides(bq, IBuildQueue) |
779 | + self.assertEqual( |
780 | + self.build.build_farm_job, removeSecurityProxy(bq)._build_farm_job) |
781 | + self.assertEqual(self.build, bq.specific_build) |
782 | + self.assertEqual(self.build.virtualized, bq.virtualized) |
783 | + self.assertIsNotNone(bq.processor) |
784 | + self.assertEqual(bq, self.build.buildqueue_record) |
785 | + |
786 | + def test_current_component_primary(self): |
787 | + # SnapBuilds for primary archives always build in multiverse for the |
788 | + # time being. |
789 | + self.assertEqual(ArchivePurpose.PRIMARY, self.build.archive.purpose) |
790 | + self.assertEqual("multiverse", self.build.current_component.name) |
791 | + |
792 | + def test_current_component_ppa(self): |
793 | + # PPAs only have indices for main, so SnapBuilds for PPAs always |
794 | + # build in main. |
795 | + build = self.factory.makeSnapBuild(archive=self.factory.makeArchive()) |
796 | + self.assertEqual("main", build.current_component.name) |
797 | + |
798 | + def test_is_private(self): |
799 | + # A SnapBuild is private iff its Snap and archive are. |
800 | + self.assertFalse(self.build.is_private) |
801 | + private_team = self.factory.makeTeam( |
802 | + visibility=PersonVisibility.PRIVATE) |
803 | + with person_logged_in(private_team.teamowner): |
804 | + build = self.factory.makeSnapBuild( |
805 | + requester=private_team.teamowner, owner=private_team) |
806 | + self.assertTrue(build.is_private) |
807 | + private_archive = self.factory.makeArchive(private=True) |
808 | + with person_logged_in(private_archive.owner): |
809 | + build = self.factory.makeSnapBuild(archive=private_archive) |
810 | + self.assertTrue(build.is_private) |
811 | + |
812 | + def test_can_be_cancelled(self): |
813 | + # For all states that can be cancelled, can_be_cancelled returns True. |
814 | + ok_cases = [ |
815 | + BuildStatus.BUILDING, |
816 | + BuildStatus.NEEDSBUILD, |
817 | + ] |
818 | + for status in BuildStatus: |
819 | + if status in ok_cases: |
820 | + self.assertTrue(self.build.can_be_cancelled) |
821 | + else: |
822 | + self.assertFalse(self.build.can_be_cancelled) |
823 | + |
824 | + def test_cancel_not_in_progress(self): |
825 | + # The cancel() method for a pending build leaves it in the CANCELLED |
826 | + # state. |
827 | + self.build.queueBuild() |
828 | + self.build.cancel() |
829 | + self.assertEqual(BuildStatus.CANCELLED, self.build.status) |
830 | + self.assertIsNone(self.build.buildqueue_record) |
831 | + |
832 | + def test_cancel_in_progress(self): |
833 | + # The cancel() method for a building build leaves it in the |
834 | + # CANCELLING state. |
835 | + bq = self.build.queueBuild() |
836 | + bq.markAsBuilding(self.factory.makeBuilder()) |
837 | + self.build.cancel() |
838 | + self.assertEqual(BuildStatus.CANCELLING, self.build.status) |
839 | + self.assertEqual(bq, self.build.buildqueue_record) |
840 | + |
841 | + def test_estimateDuration(self): |
842 | + # Without previous builds, the default time estimate is 30m. |
843 | + self.assertEqual(1800, self.build.estimateDuration().seconds) |
844 | + |
845 | + def test_estimateDuration_with_history(self): |
846 | + # Previous successful builds of the same snap package are used for |
847 | + # estimates. |
848 | + self.factory.makeSnapBuild( |
849 | + requester=self.build.requester, snap=self.build.snap, |
850 | + distroarchseries=self.build.distro_arch_series, |
851 | + status=BuildStatus.FULLYBUILT, duration=timedelta(seconds=335)) |
852 | + for i in range(3): |
853 | + self.factory.makeSnapBuild( |
854 | + requester=self.build.requester, snap=self.build.snap, |
855 | + distroarchseries=self.build.distro_arch_series, |
856 | + status=BuildStatus.FAILEDTOBUILD, |
857 | + duration=timedelta(seconds=20)) |
858 | + self.assertEqual(335, self.build.estimateDuration().seconds) |
859 | + |
860 | + def test_build_cookie(self): |
861 | + build = self.factory.makeSnapBuild() |
862 | + self.assertEqual('SNAPBUILD-%d' % build.id, build.build_cookie) |
863 | + |
864 | + def test_getFileByName_logs(self): |
865 | + # getFileByName returns the logs when requested by name. |
866 | + self.build.setLog( |
867 | + self.factory.makeLibraryFileAlias(filename="buildlog.txt.gz")) |
868 | + self.assertEqual( |
869 | + self.build.log, self.build.getFileByName("buildlog.txt.gz")) |
870 | + self.assertRaises(NotFoundError, self.build.getFileByName, "foo") |
871 | + self.build.storeUploadLog("uploaded") |
872 | + self.assertEqual( |
873 | + self.build.upload_log, |
874 | + self.build.getFileByName(self.build.upload_log.filename)) |
875 | + |
876 | + def test_getFileByName_uploaded_files(self): |
877 | + # getFileByName returns uploaded files when requested by name. |
878 | + filenames = ("ubuntu.squashfs", "ubuntu.manifest") |
879 | + lfas = [] |
880 | + for filename in filenames: |
881 | + lfa = self.factory.makeLibraryFileAlias(filename=filename) |
882 | + lfas.append(lfa) |
883 | + self.build.addFile(lfa) |
884 | + self.assertContentEqual( |
885 | + lfas, [row[1] for row in self.build.getFiles()]) |
886 | + for filename, lfa in zip(filenames, lfas): |
887 | + self.assertEqual(lfa, self.build.getFileByName(filename)) |
888 | + self.assertRaises(NotFoundError, self.build.getFileByName, "missing") |
889 | + |
890 | + def test_verifySuccessfulUpload(self): |
891 | + self.assertFalse(self.build.verifySuccessfulUpload()) |
892 | + self.factory.makeSnapFile(snapbuild=self.build) |
893 | + self.assertTrue(self.build.verifySuccessfulUpload()) |
894 | + |
895 | + def addFakeBuildLog(self, build): |
896 | + build.setLog(self.factory.makeLibraryFileAlias("mybuildlog.txt")) |
897 | + |
898 | + def test_log_url(self): |
899 | + # The log URL for a snap package build will use the archive context. |
900 | + self.addFakeBuildLog(self.build) |
901 | + self.assertEqual( |
902 | + "http://launchpad.dev/~%s/+snap/%s/+build/%d/+files/" |
903 | + "mybuildlog.txt" % ( |
904 | + self.build.snap.owner.name, self.build.snap.name, |
905 | + self.build.id), |
906 | + self.build.log_url) |
907 | + |
908 | + |
909 | +class TestSnapBuildSet(TestCaseWithFactory): |
910 | + |
911 | + layer = LaunchpadZopelessLayer |
912 | + |
913 | + def setUp(self): |
914 | + super(TestSnapBuildSet, self).setUp() |
915 | + self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"})) |
916 | + |
917 | + def test_getByBuildFarmJob_works(self): |
918 | + build = self.factory.makeSnapBuild() |
919 | + self.assertEqual( |
920 | + build, |
921 | + getUtility(ISnapBuildSet).getByBuildFarmJob(build.build_farm_job)) |
922 | + |
923 | + def test_getByBuildFarmJob_returns_None_when_missing(self): |
924 | + bpb = self.factory.makeBinaryPackageBuild() |
925 | + self.assertIsNone( |
926 | + getUtility(ISnapBuildSet).getByBuildFarmJob(bpb.build_farm_job)) |
927 | + |
928 | + def test_getByBuildFarmJobs_works(self): |
929 | + builds = [self.factory.makeSnapBuild() for i in range(10)] |
930 | + self.assertContentEqual( |
931 | + builds, |
932 | + getUtility(ISnapBuildSet).getByBuildFarmJobs( |
933 | + [build.build_farm_job for build in builds])) |
934 | + |
935 | + def test_getByBuildFarmJobs_works_empty(self): |
936 | + self.assertContentEqual( |
937 | + [], getUtility(ISnapBuildSet).getByBuildFarmJobs([])) |
938 | |
939 | === modified file 'lib/lp/testing/factory.py' |
940 | --- lib/lp/testing/factory.py 2015-07-23 14:32:50 +0000 |
941 | +++ lib/lp/testing/factory.py 2015-08-03 09:55:48 +0000 |
942 | @@ -265,6 +265,8 @@ |
943 | from lp.services.worlddata.interfaces.country import ICountrySet |
944 | from lp.services.worlddata.interfaces.language import ILanguageSet |
945 | from lp.snappy.interfaces.snap import ISnapSet |
946 | +from lp.snappy.interfaces.snapbuild import ISnapBuildSet |
947 | +from lp.snappy.model.snapbuild import SnapFile |
948 | from lp.soyuz.adapters.overrides import SourceOverride |
949 | from lp.soyuz.adapters.packagelocation import PackageLocation |
950 | from lp.soyuz.enums import ( |
951 | @@ -4559,6 +4561,59 @@ |
952 | IStore(snap).flush() |
953 | return snap |
954 | |
955 | + def makeSnapBuild(self, requester=None, registrant=None, snap=None, |
956 | + archive=None, distroarchseries=None, pocket=None, |
957 | + date_created=DEFAULT, status=BuildStatus.NEEDSBUILD, |
958 | + builder=None, duration=None, **kwargs): |
959 | + """Make a new SnapBuild.""" |
960 | + if requester is None: |
961 | + requester = self.makePerson() |
962 | + if snap is None: |
963 | + if "distroseries" in kwargs: |
964 | + distroseries = kwargs["distroseries"] |
965 | + del kwargs["distroseries"] |
966 | + elif distroarchseries is not None: |
967 | + distroseries = distroarchseries.distroseries |
968 | + elif archive is not None: |
969 | + distroseries = self.makeDistroSeries( |
970 | + distribution=archive.distribution) |
971 | + else: |
972 | + distroseries = None |
973 | + if registrant is None: |
974 | + registrant = requester |
975 | + snap = self.makeSnap( |
976 | + registrant=registrant, distroseries=distroseries, **kwargs) |
977 | + if archive is None: |
978 | + archive = snap.distro_series.main_archive |
979 | + if distroarchseries is None: |
980 | + distroarchseries = self.makeDistroArchSeries( |
981 | + distroseries=snap.distro_series) |
982 | + if pocket is None: |
983 | + pocket = PackagePublishingPocket.RELEASE |
984 | + snapbuild = getUtility(ISnapBuildSet).new( |
985 | + requester, snap, archive, distroarchseries, pocket, |
986 | + date_created=date_created) |
987 | + if duration is not None: |
988 | + removeSecurityProxy(snapbuild).updateStatus( |
989 | + BuildStatus.BUILDING, builder=builder, |
990 | + date_started=snapbuild.date_created) |
991 | + removeSecurityProxy(snapbuild).updateStatus( |
992 | + status, builder=builder, |
993 | + date_finished=snapbuild.date_started + duration) |
994 | + else: |
995 | + removeSecurityProxy(snapbuild).updateStatus( |
996 | + status, builder=builder) |
997 | + IStore(snapbuild).flush() |
998 | + return snapbuild |
999 | + |
1000 | + def makeSnapFile(self, snapbuild=None, libraryfile=None): |
1001 | + if snapbuild is None: |
1002 | + snapbuild = self.makeSnapBuild() |
1003 | + if libraryfile is None: |
1004 | + libraryfile = self.makeLibraryFileAlias() |
1005 | + return ProxyFactory( |
1006 | + SnapFile(snapbuild=snapbuild, libraryfile=libraryfile)) |
1007 | + |
1008 | |
1009 | # Some factory methods return simple Python types. We don't add |
1010 | # security wrappers for them, as well as for objects created by |