Merge lp:~cjwatson/launchpad/snap-basic-model into lp:launchpad
- snap-basic-model
- Merge into devel
Proposed by
Colin Watson
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 17659 | ||||
Proposed branch: | lp:~cjwatson/launchpad/snap-basic-model | ||||
Merge into: | lp:launchpad | ||||
Prerequisite: | lp:~cjwatson/launchpad/snap-personmerge-whitelist | ||||
Diff against target: |
1014 lines (+877/-1) 12 files modified
lib/lp/app/browser/configure.zcml (+6/-0) lib/lp/app/browser/tales.py (+11/-0) lib/lp/configure.zcml (+1/-0) lib/lp/registry/browser/person.py (+6/-0) lib/lp/security.py (+37/-1) lib/lp/snappy/browser/configure.zcml (+17/-0) lib/lp/snappy/configure.zcml (+43/-0) lib/lp/snappy/interfaces/snap.py (+262/-0) lib/lp/snappy/model/snap.py (+240/-0) lib/lp/snappy/tests/test_snap.py (+225/-0) lib/lp/testing/factory.py (+28/-0) utilities/snakefood/lp-sfood-packages (+1/-0) |
||||
To merge this branch: | bzr merge lp:~cjwatson/launchpad/snap-basic-model | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+265671@code.launchpad.net |
Commit message
Add basic model for snap packages.
Description of the change
Add basic model for snap packages.
For the sake of the branch not being enormous, this isn't very useful yet, as SnapBuild and SnapFile aren't modelled.
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
1 | === modified file 'lib/lp/app/browser/configure.zcml' |
2 | --- lib/lp/app/browser/configure.zcml 2015-04-21 10:49:24 +0000 |
3 | +++ lib/lp/app/browser/configure.zcml 2015-07-30 17:14:40 +0000 |
4 | @@ -841,6 +841,12 @@ |
5 | name="fmt" |
6 | /> |
7 | <adapter |
8 | + for="lp.snappy.interfaces.snap.ISnap" |
9 | + provides="zope.traversing.interfaces.IPathAdapter" |
10 | + factory="lp.app.browser.tales.SnapFormatterAPI" |
11 | + name="fmt" |
12 | + /> |
13 | + <adapter |
14 | for="lp.blueprints.interfaces.specification.ISpecification" |
15 | provides="zope.traversing.interfaces.IPathAdapter" |
16 | factory="lp.app.browser.tales.SpecificationFormatterAPI" |
17 | |
18 | === modified file 'lib/lp/app/browser/tales.py' |
19 | --- lib/lp/app/browser/tales.py 2015-07-09 12:18:51 +0000 |
20 | +++ lib/lp/app/browser/tales.py 2015-07-30 17:14:40 +0000 |
21 | @@ -1858,6 +1858,17 @@ |
22 | 'owner': self._context.owner.displayname} |
23 | |
24 | |
25 | +class SnapFormatterAPI(CustomizableFormatter): |
26 | + """Adapter providing fmt support for ISnap objects.""" |
27 | + |
28 | + _link_summary_template = _( |
29 | + 'Snap %(name)s for %(owner)s') |
30 | + |
31 | + def _link_summary_values(self): |
32 | + return {'name': self._context.name, |
33 | + 'owner': self._context.owner.displayname} |
34 | + |
35 | + |
36 | class SpecificationFormatterAPI(CustomizableFormatter): |
37 | """Adapter providing fmt support for Specification objects""" |
38 | |
39 | |
40 | === modified file 'lib/lp/configure.zcml' |
41 | --- lib/lp/configure.zcml 2013-06-03 07:26:52 +0000 |
42 | +++ lib/lp/configure.zcml 2015-07-30 17:14:40 +0000 |
43 | @@ -30,6 +30,7 @@ |
44 | <include package="lp.code" /> |
45 | <include package="lp.coop.answersbugs" /> |
46 | <include package="lp.hardwaredb" /> |
47 | + <include package="lp.snappy" /> |
48 | <include package="lp.soyuz" /> |
49 | <include package="lp.translations" /> |
50 | <include package="lp.testing" /> |
51 | |
52 | === modified file 'lib/lp/registry/browser/person.py' |
53 | --- lib/lp/registry/browser/person.py 2015-07-21 09:04:01 +0000 |
54 | +++ lib/lp/registry/browser/person.py 2015-07-30 17:14:40 +0000 |
55 | @@ -274,6 +274,7 @@ |
56 | from lp.services.webapp.publisher import LaunchpadView |
57 | from lp.services.worlddata.interfaces.country import ICountry |
58 | from lp.services.worlddata.interfaces.language import ILanguageSet |
59 | +from lp.snappy.interfaces.snap import ISnapSet |
60 | from lp.soyuz.browser.archivesubscription import ( |
61 | traverse_archive_subscription_for_subscriber, |
62 | ) |
63 | @@ -619,6 +620,11 @@ |
64 | |
65 | return livefs |
66 | |
67 | + @stepthrough('+snap') |
68 | + def traverse_snap(self, name): |
69 | + """Traverse to this person's snap packages.""" |
70 | + return getUtility(ISnapSet).getByName(self.context, name) |
71 | + |
72 | |
73 | class PersonSetNavigation(Navigation): |
74 | |
75 | |
76 | === modified file 'lib/lp/security.py' |
77 | --- lib/lp/security.py 2015-07-12 23:48:01 +0000 |
78 | +++ lib/lp/security.py 2015-07-30 17:14:40 +0000 |
79 | @@ -29,7 +29,6 @@ |
80 | from lp.answers.interfaces.questionsperson import IQuestionsPerson |
81 | from lp.answers.interfaces.questiontarget import IQuestionTarget |
82 | from lp.app.interfaces.security import IAuthorization |
83 | -from lp.app.interfaces.services import IService |
84 | from lp.app.security import ( |
85 | AnonymousAuthorization, |
86 | AuthorizationBase, |
87 | @@ -192,6 +191,7 @@ |
88 | ILanguage, |
89 | ILanguageSet, |
90 | ) |
91 | +from lp.snappy.interfaces.snap import ISnap |
92 | from lp.soyuz.interfaces.archive import IArchive |
93 | from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthToken |
94 | from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet |
95 | @@ -3080,3 +3080,39 @@ |
96 | def __init__(self, obj): |
97 | super(ViewWebhookDeliveryJob, self).__init__( |
98 | obj, obj.webhook, 'launchpad.View') |
99 | + |
100 | + |
101 | +class ViewSnap(DelegatedAuthorization): |
102 | + permission = 'launchpad.View' |
103 | + usedfor = ISnap |
104 | + |
105 | + def __init__(self, obj): |
106 | + super(ViewSnap, self).__init__(obj, obj.owner, 'launchpad.View') |
107 | + |
108 | + |
109 | +class EditSnap(AuthorizationBase): |
110 | + permission = 'launchpad.Edit' |
111 | + usedfor = ISnap |
112 | + |
113 | + def checkAuthenticated(self, user): |
114 | + return ( |
115 | + user.isOwner(self.obj) or |
116 | + user.in_commercial_admin or user.in_admin) |
117 | + |
118 | + |
119 | +class AdminSnap(AuthorizationBase): |
120 | + """Restrict changing build settings on snap packages. |
121 | + |
122 | + The security of the non-virtualised build farm depends on these |
123 | + settings, so they can only be changed by commercial admins, or by "PPA" |
124 | + self admins on snap packages that they can already edit. |
125 | + """ |
126 | + permission = 'launchpad.Admin' |
127 | + usedfor = ISnap |
128 | + |
129 | + def checkAuthenticated(self, user): |
130 | + if user.in_commercial_admin or user.in_admin: |
131 | + return True |
132 | + return ( |
133 | + user.in_ppa_self_admins |
134 | + and EditSnap(self.obj).checkAuthenticated(user)) |
135 | |
136 | === added directory 'lib/lp/snappy' |
137 | === added file 'lib/lp/snappy/__init__.py' |
138 | === added directory 'lib/lp/snappy/browser' |
139 | === added file 'lib/lp/snappy/browser/__init__.py' |
140 | === added file 'lib/lp/snappy/browser/configure.zcml' |
141 | --- lib/lp/snappy/browser/configure.zcml 1970-01-01 00:00:00 +0000 |
142 | +++ lib/lp/snappy/browser/configure.zcml 2015-07-30 17:14:40 +0000 |
143 | @@ -0,0 +1,17 @@ |
144 | +<!-- Copyright 2015 Canonical Ltd. This software is licensed under the |
145 | + GNU Affero General Public License version 3 (see the file LICENSE). |
146 | +--> |
147 | + |
148 | +<configure |
149 | + xmlns="http://namespaces.zope.org/zope" |
150 | + xmlns:browser="http://namespaces.zope.org/browser" |
151 | + xmlns:i18n="http://namespaces.zope.org/i18n" |
152 | + xmlns:xmlrpc="http://namespaces.zope.org/xmlrpc" |
153 | + i18n_domain="launchpad"> |
154 | + <facet facet="overview"> |
155 | + <browser:url |
156 | + for="lp.snappy.interfaces.snap.ISnap" |
157 | + path_expression="string:+snap/${name}" |
158 | + attribute_to_parent="owner" /> |
159 | + </facet> |
160 | +</configure> |
161 | |
162 | === added file 'lib/lp/snappy/configure.zcml' |
163 | --- lib/lp/snappy/configure.zcml 1970-01-01 00:00:00 +0000 |
164 | +++ lib/lp/snappy/configure.zcml 2015-07-30 17:14:40 +0000 |
165 | @@ -0,0 +1,43 @@ |
166 | +<!-- Copyright 2015 Canonical Ltd. This software is licensed under the |
167 | + GNU Affero General Public License version 3 (see the file LICENSE). |
168 | +--> |
169 | + |
170 | +<configure |
171 | + xmlns="http://namespaces.zope.org/zope" |
172 | + xmlns:browser="http://namespaces.zope.org/browser" |
173 | + xmlns:i18n="http://namespaces.zope.org/i18n" |
174 | + xmlns:lp="http://namespaces.canonical.com/lp" |
175 | + xmlns:webservice="http://namespaces.canonical.com/webservice" |
176 | + xmlns:xmlrpc="http://namespaces.zope.org/xmlrpc" |
177 | + i18n_domain="launchpad"> |
178 | + |
179 | + <include package=".browser" /> |
180 | + |
181 | + <!-- Snap --> |
182 | + <class class="lp.snappy.model.snap.Snap"> |
183 | + <require |
184 | + permission="launchpad.View" |
185 | + interface="lp.snappy.interfaces.snap.ISnapView |
186 | + lp.snappy.interfaces.snap.ISnapEditableAttributes |
187 | + lp.snappy.interfaces.snap.ISnapAdminAttributes" /> |
188 | + <require |
189 | + permission="launchpad.Edit" |
190 | + interface="lp.snappy.interfaces.snap.ISnapEdit" |
191 | + set_schema="lp.snappy.interfaces.snap.ISnapEditableAttributes" /> |
192 | + <require |
193 | + permission="launchpad.Admin" |
194 | + interface="lp.snappy.interfaces.snap.ISnapAdmin" |
195 | + set_schema="lp.snappy.interfaces.snap.ISnapAdminAttributes" /> |
196 | + </class> |
197 | + <subscriber |
198 | + for="lp.snappy.interfaces.snap.ISnap zope.lifecycleevent.interfaces.IObjectModifiedEvent" |
199 | + handler="lp.snappy.model.snap.snap_modified" /> |
200 | + |
201 | + <!-- SnapSet --> |
202 | + <securedutility |
203 | + class="lp.snappy.model.snap.SnapSet" |
204 | + provides="lp.snappy.interfaces.snap.ISnapSet"> |
205 | + <allow interface="lp.snappy.interfaces.snap.ISnapSet" /> |
206 | + </securedutility> |
207 | + |
208 | +</configure> |
209 | |
210 | === added directory 'lib/lp/snappy/interfaces' |
211 | === added file 'lib/lp/snappy/interfaces/__init__.py' |
212 | === added file 'lib/lp/snappy/interfaces/snap.py' |
213 | --- lib/lp/snappy/interfaces/snap.py 1970-01-01 00:00:00 +0000 |
214 | +++ lib/lp/snappy/interfaces/snap.py 2015-07-30 17:14:40 +0000 |
215 | @@ -0,0 +1,262 @@ |
216 | +# Copyright 2015 Canonical Ltd. This software is licensed under the |
217 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
218 | + |
219 | +"""Snap package interfaces.""" |
220 | + |
221 | +__metaclass__ = type |
222 | + |
223 | +__all__ = [ |
224 | + 'CannotDeleteSnap', |
225 | + 'DuplicateSnapName', |
226 | + 'ISnap', |
227 | + 'ISnapSet', |
228 | + 'ISnapView', |
229 | + 'NoSourceForSnap', |
230 | + 'NoSuchSnap', |
231 | + 'SNAP_FEATURE_FLAG', |
232 | + 'SnapBuildAlreadyPending', |
233 | + 'SnapBuildArchiveOwnerMismatch', |
234 | + 'SnapFeatureDisabled', |
235 | + 'SnapNotOwner', |
236 | + ] |
237 | + |
238 | +from lazr.restful.fields import ( |
239 | + CollectionField, |
240 | + Reference, |
241 | + ReferenceChoice, |
242 | + ) |
243 | +from zope.interface import ( |
244 | + Attribute, |
245 | + Interface, |
246 | + ) |
247 | +from zope.schema import ( |
248 | + Bool, |
249 | + Datetime, |
250 | + Int, |
251 | + Text, |
252 | + TextLine, |
253 | + ) |
254 | +from zope.security.interfaces import ( |
255 | + Forbidden, |
256 | + Unauthorized, |
257 | + ) |
258 | + |
259 | +from lp import _ |
260 | +from lp.app.errors import NameLookupFailed |
261 | +from lp.app.validators.name import name_validator |
262 | +from lp.buildmaster.interfaces.processor import IProcessor |
263 | +from lp.code.interfaces.branch import IBranch |
264 | +from lp.code.interfaces.gitrepository import IGitRepository |
265 | +from lp.registry.interfaces.distroseries import IDistroSeries |
266 | +from lp.registry.interfaces.role import IHasOwner |
267 | +from lp.services.fields import ( |
268 | + PersonChoice, |
269 | + PublicPersonChoice, |
270 | + ) |
271 | + |
272 | + |
273 | +SNAP_FEATURE_FLAG = u"snap.allow_new" |
274 | + |
275 | + |
276 | +class SnapBuildAlreadyPending(Exception): |
277 | + """A build was requested when an identical build was already pending.""" |
278 | + |
279 | + def __init__(self): |
280 | + super(SnapBuildAlreadyPending, self).__init__( |
281 | + "An identical build of this snap package is already pending.") |
282 | + |
283 | + |
284 | +class SnapBuildArchiveOwnerMismatch(Forbidden): |
285 | + """Builds against private archives require that owners match. |
286 | + |
287 | + The snap package owner must have write permission on the archive, so |
288 | + that a malicious snap package build can't steal any secrets that its |
289 | + owner didn't already have access to. Furthermore, we want to make sure |
290 | + that future changes to the team owning the snap package don't grant it |
291 | + retrospective access to information about a private archive. For now, |
292 | + the simplest way to do this is to require that the owners match exactly. |
293 | + """ |
294 | + |
295 | + def __init__(self): |
296 | + super(SnapBuildArchiveOwnerMismatch, self).__init__( |
297 | + "Snap package builds against private archives are only allowed " |
298 | + "if the snap package owner and the archive owner are equal.") |
299 | + |
300 | + |
301 | +class SnapFeatureDisabled(Unauthorized): |
302 | + """Only certain users can create new snap-related objects.""" |
303 | + |
304 | + def __init__(self): |
305 | + super(SnapFeatureDisabled, self).__init__( |
306 | + "You do not have permission to create new snaps or new snap " |
307 | + "builds.") |
308 | + |
309 | + |
310 | +class DuplicateSnapName(Exception): |
311 | + """Raised for snap packages with duplicate name/owner.""" |
312 | + |
313 | + def __init__(self): |
314 | + super(DuplicateSnapName, self).__init__( |
315 | + "There is already a snap package with the same name and owner.") |
316 | + |
317 | + |
318 | +class SnapNotOwner(Unauthorized): |
319 | + """The registrant/requester is not the owner or a member of its team.""" |
320 | + |
321 | + |
322 | +class NoSuchSnap(NameLookupFailed): |
323 | + """The requested snap package does not exist.""" |
324 | + _message_prefix = "No such snap package with this owner" |
325 | + |
326 | + |
327 | +class NoSourceForSnap(Exception): |
328 | + """Snap packages must have a source (Bazaar branch or Git repository).""" |
329 | + |
330 | + def __init__(self): |
331 | + super(NoSourceForSnap, self).__init__( |
332 | + "New snap packages must have either a Bazaar branch or a Git " |
333 | + "repository.") |
334 | + |
335 | + |
336 | +class CannotDeleteSnap(Exception): |
337 | + """This snap package cannot be deleted.""" |
338 | + |
339 | + |
340 | +class ISnapView(Interface): |
341 | + """`ISnap` attributes that require launchpad.View permission.""" |
342 | + |
343 | + id = Int(title=_("ID"), required=True, readonly=True) |
344 | + |
345 | + date_created = Datetime( |
346 | + title=_("Date created"), required=True, readonly=True) |
347 | + |
348 | + registrant = PublicPersonChoice( |
349 | + title=_("Registrant"), required=True, readonly=True, |
350 | + vocabulary="ValidPersonOrTeam", |
351 | + description=_("The person who registered this snap package.")) |
352 | + |
353 | + def requestBuild(requester, archive, distro_arch_series, pocket): |
354 | + """Request that the snap package be built. |
355 | + |
356 | + :param requester: The person requesting the build. |
357 | + :param archive: The IArchive to associate the build with. |
358 | + :param distro_arch_series: The architecture to build for. |
359 | + :param pocket: The pocket that should be targeted. |
360 | + :return: `ISnapBuild`. |
361 | + """ |
362 | + |
363 | + builds = Attribute("All builds of this snap package.") |
364 | + |
365 | + completed_builds = Attribute("Completed builds of this snap package.") |
366 | + |
367 | + pending_builds = Attribute("Pending builds of this snap package.") |
368 | + |
369 | + |
370 | +class ISnapEdit(Interface): |
371 | + """`ISnap` methods that require launchpad.Edit permission.""" |
372 | + |
373 | + def destroySelf(): |
374 | + """Delete this snap package, provided that it has no builds.""" |
375 | + |
376 | + |
377 | +class ISnapEditableAttributes(IHasOwner): |
378 | + """`ISnap` attributes that can be edited. |
379 | + |
380 | + These attributes need launchpad.View to see, and launchpad.Edit to change. |
381 | + """ |
382 | + date_last_modified = Datetime( |
383 | + title=_("Date last modified"), required=True, readonly=True) |
384 | + |
385 | + owner = PersonChoice( |
386 | + title=_("Owner"), required=True, readonly=False, |
387 | + vocabulary="AllUserTeamsParticipationPlusSelf", |
388 | + description=_("The owner of this snap package.")) |
389 | + |
390 | + distro_series = Reference( |
391 | + IDistroSeries, title=_("Distro Series"), required=True, readonly=False, |
392 | + description=_( |
393 | + "The series for which the snap package should be built.")) |
394 | + |
395 | + name = TextLine( |
396 | + title=_("Name"), required=True, readonly=False, |
397 | + constraint=name_validator, |
398 | + description=_("The name of the snap package.")) |
399 | + |
400 | + description = Text( |
401 | + title=_("Description"), required=False, readonly=False, |
402 | + description=_("A description of the snap package.")) |
403 | + |
404 | + branch = ReferenceChoice( |
405 | + title=_("Bazaar branch"), schema=IBranch, vocabulary="Branch", |
406 | + required=False, readonly=False, |
407 | + description=_( |
408 | + "A Bazaar branch containing a snapcraft.yaml recipe at the top " |
409 | + "level.")) |
410 | + |
411 | + git_repository = ReferenceChoice( |
412 | + title=_("Git repository"), |
413 | + schema=IGitRepository, vocabulary="GitRepository", |
414 | + required=False, readonly=False, |
415 | + description=_( |
416 | + "A Git repository with a branch containing a snapcraft.yaml " |
417 | + "recipe at the top level.")) |
418 | + |
419 | + git_path = TextLine( |
420 | + title=_("Git branch path"), required=False, readonly=False, |
421 | + description=_( |
422 | + "The path of the Git branch containing a snapcraft.yaml recipe at " |
423 | + "the top level.")) |
424 | + |
425 | + |
426 | +class ISnapAdminAttributes(Interface): |
427 | + """`ISnap` attributes that can be edited by admins. |
428 | + |
429 | + These attributes need launchpad.View to see, and launchpad.Admin to change. |
430 | + """ |
431 | + require_virtualized = Bool( |
432 | + title=_("Require virtualized builders"), required=True, readonly=False, |
433 | + description=_("Only build this snap package on virtual builders.")) |
434 | + |
435 | + processors = CollectionField( |
436 | + title=_("Processors"), |
437 | + description=_( |
438 | + "The architectures for which the snap package should be built."), |
439 | + value_type=Reference(schema=IProcessor), |
440 | + readonly=False) |
441 | + |
442 | + |
443 | +class ISnapAdmin(Interface): |
444 | + """`ISnap` methods that require launchpad.Admin permission.""" |
445 | + |
446 | + def setProcessors(processors): |
447 | + """Set the architectures for which the snap package should be built.""" |
448 | + |
449 | + |
450 | +class ISnap( |
451 | + ISnapView, ISnapEdit, ISnapEditableAttributes, ISnapAdminAttributes, |
452 | + ISnapAdmin): |
453 | + """A buildable snap package.""" |
454 | + |
455 | + |
456 | +class ISnapSet(Interface): |
457 | + """A utility to create and access snap packages.""" |
458 | + |
459 | + def new(registrant, owner, distro_series, name, description=None, |
460 | + branch=None, git_repository=None, git_path=None, |
461 | + require_virtualized=True, processors=None, date_created=None): |
462 | + """Create an `ISnap`.""" |
463 | + |
464 | + def exists(owner, name): |
465 | + """Check to see if a matching snap exists.""" |
466 | + |
467 | + def getByName(owner, name): |
468 | + """Return the appropriate `ISnap` for the given objects.""" |
469 | + |
470 | + def findByPerson(owner): |
471 | + """Return all snap packages with the given `owner`.""" |
472 | + |
473 | + def empty_list(): |
474 | + """Return an empty collection of snap packages. |
475 | + |
476 | + This only exists to keep lazr.restful happy. |
477 | + """ |
478 | |
479 | === added directory 'lib/lp/snappy/model' |
480 | === added file 'lib/lp/snappy/model/__init__.py' |
481 | === added file 'lib/lp/snappy/model/snap.py' |
482 | --- lib/lp/snappy/model/snap.py 1970-01-01 00:00:00 +0000 |
483 | +++ lib/lp/snappy/model/snap.py 2015-07-30 17:14:40 +0000 |
484 | @@ -0,0 +1,240 @@ |
485 | +# Copyright 2015 Canonical Ltd. This software is licensed under the |
486 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
487 | + |
488 | +__metaclass__ = type |
489 | +__all__ = [ |
490 | + 'Snap', |
491 | + ] |
492 | + |
493 | +import pytz |
494 | +from storm.exceptions import IntegrityError |
495 | +from storm.locals import ( |
496 | + Bool, |
497 | + DateTime, |
498 | + Int, |
499 | + Reference, |
500 | + Storm, |
501 | + Unicode, |
502 | + ) |
503 | +from storm.store import Store |
504 | +from zope.component import getUtility |
505 | +from zope.interface import implementer |
506 | + |
507 | +from lp.buildmaster.interfaces.processor import IProcessorSet |
508 | +from lp.buildmaster.model.processor import Processor |
509 | +from lp.registry.interfaces.role import IHasOwner |
510 | +from lp.services.database.constants import ( |
511 | + DEFAULT, |
512 | + UTC_NOW, |
513 | + ) |
514 | +from lp.services.database.interfaces import ( |
515 | + IMasterStore, |
516 | + IStore, |
517 | + ) |
518 | +from lp.services.features import getFeatureFlag |
519 | +from lp.snappy.interfaces.snap import ( |
520 | + DuplicateSnapName, |
521 | + ISnap, |
522 | + ISnapSet, |
523 | + SNAP_FEATURE_FLAG, |
524 | + SnapFeatureDisabled, |
525 | + SnapNotOwner, |
526 | + NoSourceForSnap, |
527 | + NoSuchSnap, |
528 | + ) |
529 | + |
530 | + |
531 | +def snap_modified(snap, event): |
532 | + """Update the date_last_modified property when a Snap is modified. |
533 | + |
534 | + This method is registered as a subscriber to `IObjectModifiedEvent` |
535 | + events on snap packages. |
536 | + """ |
537 | + snap.date_last_modified = UTC_NOW |
538 | + |
539 | + |
540 | +@implementer(ISnap, IHasOwner) |
541 | +class Snap(Storm): |
542 | + """See `ISnap`.""" |
543 | + |
544 | + __storm_table__ = 'Snap' |
545 | + |
546 | + id = Int(primary=True) |
547 | + |
548 | + date_created = DateTime( |
549 | + name='date_created', tzinfo=pytz.UTC, allow_none=False) |
550 | + date_last_modified = DateTime( |
551 | + name='date_last_modified', tzinfo=pytz.UTC, allow_none=False) |
552 | + |
553 | + registrant_id = Int(name='registrant', allow_none=False) |
554 | + registrant = Reference(registrant_id, 'Person.id') |
555 | + |
556 | + owner_id = Int(name='owner', allow_none=False) |
557 | + owner = Reference(owner_id, 'Person.id') |
558 | + |
559 | + distro_series_id = Int(name='distro_series', allow_none=False) |
560 | + distro_series = Reference(distro_series_id, 'DistroSeries.id') |
561 | + |
562 | + name = Unicode(name='name', allow_none=False) |
563 | + |
564 | + description = Unicode(name='description', allow_none=True) |
565 | + |
566 | + branch_id = Int(name='branch', allow_none=True) |
567 | + branch = Reference(branch_id, 'Branch.id') |
568 | + |
569 | + git_repository_id = Int(name='git_repository', allow_none=True) |
570 | + git_repository = Reference(git_repository_id, 'GitRepository.id') |
571 | + |
572 | + git_path = Unicode(name='git_path', allow_none=True) |
573 | + |
574 | + require_virtualized = Bool(name='require_virtualized') |
575 | + |
576 | + def __init__(self, registrant, owner, distro_series, name, |
577 | + description=None, branch=None, git_repository=None, |
578 | + git_path=None, require_virtualized=True, |
579 | + date_created=DEFAULT): |
580 | + """Construct a `Snap`.""" |
581 | + if not getFeatureFlag(SNAP_FEATURE_FLAG): |
582 | + raise SnapFeatureDisabled |
583 | + super(Snap, self).__init__() |
584 | + self.registrant = registrant |
585 | + self.owner = owner |
586 | + self.distro_series = distro_series |
587 | + self.name = name |
588 | + self.description = description |
589 | + self.branch = branch |
590 | + self.git_repository = git_repository |
591 | + self.git_path = git_path |
592 | + self.require_virtualized = require_virtualized |
593 | + self.date_created = date_created |
594 | + self.date_last_modified = date_created |
595 | + |
596 | + def _getProcessors(self): |
597 | + return list(Store.of(self).find( |
598 | + Processor, |
599 | + Processor.id == SnapArch.processor_id, |
600 | + SnapArch.snap == self)) |
601 | + |
602 | + def setProcessors(self, processors): |
603 | + """See `ISnap`.""" |
604 | + enablements = dict(Store.of(self).find( |
605 | + (Processor, SnapArch), |
606 | + Processor.id == SnapArch.processor_id, |
607 | + SnapArch.snap == self)) |
608 | + for proc in enablements: |
609 | + if proc not in processors: |
610 | + Store.of(self).remove(enablements[proc]) |
611 | + for proc in processors: |
612 | + if proc not in self.processors: |
613 | + snaparch = SnapArch() |
614 | + snaparch.snap = self |
615 | + snaparch.processor = proc |
616 | + Store.of(self).add(snaparch) |
617 | + |
618 | + processors = property(_getProcessors, setProcessors) |
619 | + |
620 | + def requestBuild(self, requester, archive, distro_arch_series, pocket): |
621 | + """See `ISnap`.""" |
622 | + raise NotImplementedError |
623 | + |
624 | + @property |
625 | + def builds(self): |
626 | + """See `ISnap`.""" |
627 | + return [] |
628 | + |
629 | + @property |
630 | + def _pending_states(self): |
631 | + """All the build states we consider pending (non-final).""" |
632 | + raise NotImplementedError |
633 | + |
634 | + @property |
635 | + def completed_builds(self): |
636 | + """See `ISnap`.""" |
637 | + return [] |
638 | + |
639 | + @property |
640 | + def pending_builds(self): |
641 | + """See `ISnap`.""" |
642 | + return [] |
643 | + |
644 | + def destroySelf(self): |
645 | + """See `ISnap`.""" |
646 | + raise NotImplementedError |
647 | + |
648 | + |
649 | +class SnapArch(Storm): |
650 | + """Link table to back `Snap.processors`.""" |
651 | + |
652 | + __storm_table__ = 'SnapArch' |
653 | + __storm_primary__ = ('snap_id', 'processor_id') |
654 | + |
655 | + snap_id = Int(name='snap', allow_none=False) |
656 | + snap = Reference(snap_id, 'Snap.id') |
657 | + |
658 | + processor_id = Int(name='processor', allow_none=False) |
659 | + processor = Reference(processor_id, 'Processor.id') |
660 | + |
661 | + |
662 | +@implementer(ISnapSet) |
663 | +class SnapSet: |
664 | + """See `ISnapSet`.""" |
665 | + |
666 | + def new(self, registrant, owner, distro_series, name, description=None, |
667 | + branch=None, git_repository=None, git_path=None, |
668 | + require_virtualized=True, processors=None, date_created=DEFAULT): |
669 | + """See `ISnapSet`.""" |
670 | + if not registrant.inTeam(owner): |
671 | + if owner.is_team: |
672 | + raise SnapNotOwner( |
673 | + "%s is not a member of %s." % |
674 | + (registrant.displayname, owner.displayname)) |
675 | + else: |
676 | + raise SnapNotOwner( |
677 | + "%s cannot create snap packages owned by %s." % |
678 | + (registrant.displayname, owner.displayname)) |
679 | + |
680 | + if branch is None and git_repository is None: |
681 | + raise NoSourceForSnap |
682 | + |
683 | + store = IMasterStore(Snap) |
684 | + snap = Snap( |
685 | + registrant, owner, distro_series, name, description=description, |
686 | + branch=branch, git_repository=git_repository, git_path=git_path, |
687 | + require_virtualized=require_virtualized, date_created=date_created) |
688 | + store.add(snap) |
689 | + |
690 | + if processors is None: |
691 | + processors = [ |
692 | + p for p in getUtility(IProcessorSet).getAll() |
693 | + if p.build_by_default] |
694 | + snap.setProcessors(processors) |
695 | + |
696 | + try: |
697 | + store.flush() |
698 | + except IntegrityError: |
699 | + raise DuplicateSnapName |
700 | + |
701 | + return snap |
702 | + |
703 | + def _getByName(self, owner, name): |
704 | + return IStore(Snap).find( |
705 | + Snap, Snap.owner == owner, Snap.name == name).one() |
706 | + |
707 | + def exists(self, owner, name): |
708 | + """See `ISnapSet`.""" |
709 | + return self._getByName(owner, name) is not None |
710 | + |
711 | + def getByName(self, owner, name): |
712 | + """See `ISnapSet`.""" |
713 | + snap = self._getByName(owner, name) |
714 | + if snap is None: |
715 | + raise NoSuchSnap(name) |
716 | + return snap |
717 | + |
718 | + def findByPerson(self, owner): |
719 | + """See `ISnapSet`.""" |
720 | + return IStore(Snap).find(Snap, Snap.owner == owner) |
721 | + |
722 | + def empty_list(self): |
723 | + """See `ISnapSet`.""" |
724 | + return [] |
725 | |
726 | === added directory 'lib/lp/snappy/tests' |
727 | === added file 'lib/lp/snappy/tests/__init__.py' |
728 | === added file 'lib/lp/snappy/tests/test_snap.py' |
729 | --- lib/lp/snappy/tests/test_snap.py 1970-01-01 00:00:00 +0000 |
730 | +++ lib/lp/snappy/tests/test_snap.py 2015-07-30 17:14:40 +0000 |
731 | @@ -0,0 +1,225 @@ |
732 | +# Copyright 2015 Canonical Ltd. This software is licensed under the |
733 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
734 | + |
735 | +"""Test snap packages.""" |
736 | + |
737 | +__metaclass__ = type |
738 | + |
739 | +from datetime import datetime |
740 | + |
741 | +from lazr.lifecycle.event import ObjectModifiedEvent |
742 | +import pytz |
743 | +import transaction |
744 | +from zope.component import getUtility |
745 | +from zope.event import notify |
746 | +from zope.security.proxy import removeSecurityProxy |
747 | + |
748 | +from lp.buildmaster.interfaces.processor import IProcessorSet |
749 | +from lp.services.database.constants import UTC_NOW |
750 | +from lp.services.features.testing import FeatureFixture |
751 | +from lp.snappy.interfaces.snap import ( |
752 | + ISnap, |
753 | + ISnapSet, |
754 | + NoSourceForSnap, |
755 | + SNAP_FEATURE_FLAG, |
756 | + SnapFeatureDisabled, |
757 | + ) |
758 | +from lp.testing import ( |
759 | + admin_logged_in, |
760 | + TestCaseWithFactory, |
761 | + ) |
762 | +from lp.testing.layers import ( |
763 | + DatabaseFunctionalLayer, |
764 | + LaunchpadZopelessLayer, |
765 | + ) |
766 | + |
767 | + |
768 | +class TestSnapFeatureFlag(TestCaseWithFactory): |
769 | + |
770 | + layer = LaunchpadZopelessLayer |
771 | + |
772 | + def test_feature_flag_disabled(self): |
773 | + # Without a feature flag, we will not create new Snaps. |
774 | + person = self.factory.makePerson() |
775 | + self.assertRaises( |
776 | + SnapFeatureDisabled, getUtility(ISnapSet).new, |
777 | + person, person, None, None, branch=self.factory.makeAnyBranch()) |
778 | + |
779 | + |
780 | +class TestSnap(TestCaseWithFactory): |
781 | + |
782 | + layer = DatabaseFunctionalLayer |
783 | + |
784 | + def setUp(self): |
785 | + super(TestSnap, self).setUp() |
786 | + self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"})) |
787 | + |
788 | + def test_implements_interfaces(self): |
789 | + # Snap implements ISnap. |
790 | + snap = self.factory.makeSnap() |
791 | + with admin_logged_in(): |
792 | + self.assertProvides(snap, ISnap) |
793 | + |
794 | + def test_initial_date_last_modified(self): |
795 | + # The initial value of date_last_modified is date_created. |
796 | + snap = self.factory.makeSnap( |
797 | + date_created=datetime(2014, 04, 25, 10, 38, 0, tzinfo=pytz.UTC)) |
798 | + self.assertEqual(snap.date_created, snap.date_last_modified) |
799 | + |
800 | + def test_modifiedevent_sets_date_last_modified(self): |
801 | + # When a Snap receives an object modified event, the last modified |
802 | + # date is set to UTC_NOW. |
803 | + snap = self.factory.makeSnap( |
804 | + date_created=datetime(2014, 04, 25, 10, 38, 0, tzinfo=pytz.UTC)) |
805 | + notify(ObjectModifiedEvent( |
806 | + removeSecurityProxy(snap), snap, [ISnap["name"]])) |
807 | + self.assertSqlAttributeEqualsDate(snap, "date_last_modified", UTC_NOW) |
808 | + |
809 | + |
810 | +class TestSnapSet(TestCaseWithFactory): |
811 | + |
812 | + layer = DatabaseFunctionalLayer |
813 | + |
814 | + def setUp(self): |
815 | + super(TestSnapSet, self).setUp() |
816 | + self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"})) |
817 | + |
818 | + def test_class_implements_interfaces(self): |
819 | + # The SnapSet class implements ISnapSet. |
820 | + self.assertProvides(getUtility(ISnapSet), ISnapSet) |
821 | + |
822 | + def makeSnapComponents(self, branch=None, git_ref=None): |
823 | + """Return a dict of values that can be used to make a Snap. |
824 | + |
825 | + Suggested use: provide as kwargs to ISnapSet.new. |
826 | + |
827 | + :param branch: An `IBranch`, or None. |
828 | + :param git_ref: An `IGitRef`, or None. |
829 | + """ |
830 | + registrant = self.factory.makePerson() |
831 | + components = dict( |
832 | + registrant=registrant, |
833 | + owner=self.factory.makeTeam(owner=registrant), |
834 | + distro_series=self.factory.makeDistroSeries(), |
835 | + name=self.factory.getUniqueString(u"snap-name")) |
836 | + if branch is None and git_ref is None: |
837 | + branch = self.factory.makeAnyBranch() |
838 | + if branch is not None: |
839 | + components["branch"] = branch |
840 | + else: |
841 | + components["git_repository"] = git_ref.repository |
842 | + components["git_path"] = git_ref.path |
843 | + return components |
844 | + |
845 | + def test_creation_bzr(self): |
846 | + # The metadata entries supplied when a Snap is created for a Bazaar |
847 | + # branch are present on the new object. |
848 | + branch = self.factory.makeAnyBranch() |
849 | + components = self.makeSnapComponents(branch=branch) |
850 | + snap = getUtility(ISnapSet).new(**components) |
851 | + transaction.commit() |
852 | + self.assertEqual(components["registrant"], snap.registrant) |
853 | + self.assertEqual(components["owner"], snap.owner) |
854 | + self.assertEqual(components["distro_series"], snap.distro_series) |
855 | + self.assertEqual(components["name"], snap.name) |
856 | + self.assertEqual(branch, snap.branch) |
857 | + self.assertIsNone(snap.git_repository) |
858 | + self.assertIsNone(snap.git_path) |
859 | + self.assertTrue(snap.require_virtualized) |
860 | + |
861 | + def test_creation_git(self): |
862 | + # The metadata entries supplied when a Snap is created for a Git |
863 | + # branch are present on the new object. |
864 | + [ref] = self.factory.makeGitRefs() |
865 | + components = self.makeSnapComponents(git_ref=ref) |
866 | + snap = getUtility(ISnapSet).new(**components) |
867 | + transaction.commit() |
868 | + self.assertEqual(components["registrant"], snap.registrant) |
869 | + self.assertEqual(components["owner"], snap.owner) |
870 | + self.assertEqual(components["distro_series"], snap.distro_series) |
871 | + self.assertEqual(components["name"], snap.name) |
872 | + self.assertIsNone(snap.branch) |
873 | + self.assertEqual(ref.repository, snap.git_repository) |
874 | + self.assertEqual(ref.path, snap.git_path) |
875 | + self.assertTrue(snap.require_virtualized) |
876 | + |
877 | + def test_creation_no_source(self): |
878 | + # Attempting to create a Snap with neither a Bazaar branch nor a Git |
879 | + # repository fails. |
880 | + registrant = self.factory.makePerson() |
881 | + self.assertRaises( |
882 | + NoSourceForSnap, getUtility(ISnapSet).new, |
883 | + registrant, registrant, self.factory.makeDistroSeries(), |
884 | + self.factory.getUniqueString(u"snap-name")) |
885 | + |
886 | + def test_exists(self): |
887 | + # ISnapSet.exists checks for matching Snaps. |
888 | + snap = self.factory.makeSnap() |
889 | + self.assertTrue(getUtility(ISnapSet).exists(snap.owner, snap.name)) |
890 | + self.assertFalse( |
891 | + getUtility(ISnapSet).exists(self.factory.makePerson(), snap.name)) |
892 | + self.assertFalse(getUtility(ISnapSet).exists(snap.owner, u"different")) |
893 | + |
894 | + def test_findByPerson(self): |
895 | + # ISnapSet.findByPerson returns all Snaps with the given owner. |
896 | + owners = [self.factory.makePerson() for i in range(2)] |
897 | + snaps = [] |
898 | + for owner in owners: |
899 | + for i in range(2): |
900 | + snaps.append(self.factory.makeSnap( |
901 | + registrant=owner, owner=owner)) |
902 | + self.assertContentEqual( |
903 | + snaps[:2], getUtility(ISnapSet).findByPerson(owners[0])) |
904 | + self.assertContentEqual( |
905 | + snaps[2:], getUtility(ISnapSet).findByPerson(owners[1])) |
906 | + |
907 | + |
908 | +class TestSnapProcessors(TestCaseWithFactory): |
909 | + |
910 | + layer = LaunchpadZopelessLayer |
911 | + |
912 | + def setUp(self): |
913 | + super(TestSnapProcessors, self).setUp() |
914 | + self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"})) |
915 | + self.default_procs = [ |
916 | + getUtility(IProcessorSet).getByName("386"), |
917 | + getUtility(IProcessorSet).getByName("amd64")] |
918 | + self.unrestricted_procs = ( |
919 | + self.default_procs + [getUtility(IProcessorSet).getByName("hppa")]) |
920 | + self.arm = self.factory.makeProcessor( |
921 | + name="arm", restricted=True, build_by_default=False) |
922 | + |
923 | + def test_new_default_processors(self): |
924 | + # SnapSet.new creates a SnapArch for each Processor with |
925 | + # build_by_default set. |
926 | + self.factory.makeProcessor(name="default", build_by_default=True) |
927 | + self.factory.makeProcessor(name="nondefault", build_by_default=False) |
928 | + owner = self.factory.makePerson() |
929 | + snap = getUtility(ISnapSet).new( |
930 | + registrant=owner, owner=owner, |
931 | + distro_series=self.factory.makeDistroSeries(), name=u"snap", |
932 | + branch=self.factory.makeAnyBranch()) |
933 | + self.assertContentEqual( |
934 | + ["386", "amd64", "hppa", "default"], |
935 | + [processor.name for processor in snap.processors]) |
936 | + |
937 | + def test_new_override_processors(self): |
938 | + # SnapSet.new can be given a custom set of processors. |
939 | + owner = self.factory.makePerson() |
940 | + snap = getUtility(ISnapSet).new( |
941 | + registrant=owner, owner=owner, |
942 | + distro_series=self.factory.makeDistroSeries(), name=u"snap", |
943 | + branch=self.factory.makeAnyBranch(), processors=[self.arm]) |
944 | + self.assertContentEqual( |
945 | + ["arm"], [processor.name for processor in snap.processors]) |
946 | + |
947 | + def test_set(self): |
948 | + # The property remembers its value correctly. |
949 | + snap = self.factory.makeSnap() |
950 | + snap.setProcessors([self.arm]) |
951 | + self.assertContentEqual([self.arm], snap.processors) |
952 | + snap.setProcessors(self.unrestricted_procs + [self.arm]) |
953 | + self.assertContentEqual( |
954 | + self.unrestricted_procs + [self.arm], snap.processors) |
955 | + snap.processors = [] |
956 | + self.assertContentEqual([], snap.processors) |
957 | |
958 | === modified file 'lib/lp/testing/factory.py' |
959 | --- lib/lp/testing/factory.py 2015-07-07 04:02:36 +0000 |
960 | +++ lib/lp/testing/factory.py 2015-07-30 17:14:40 +0000 |
961 | @@ -264,6 +264,7 @@ |
962 | from lp.services.webhooks.interfaces import IWebhookSource |
963 | from lp.services.worlddata.interfaces.country import ICountrySet |
964 | from lp.services.worlddata.interfaces.language import ILanguageSet |
965 | +from lp.snappy.interfaces.snap import ISnapSet |
966 | from lp.soyuz.adapters.overrides import SourceOverride |
967 | from lp.soyuz.adapters.packagelocation import PackageLocation |
968 | from lp.soyuz.enums import ( |
969 | @@ -4531,6 +4532,33 @@ |
970 | target, self.makePerson(), delivery_url, [], True, |
971 | self.getUniqueUnicode()) |
972 | |
973 | + def makeSnap(self, registrant=None, owner=None, distroseries=None, |
974 | + name=None, branch=None, git_ref=None, |
975 | + require_virtualized=True, date_created=DEFAULT): |
976 | + """Make a new Snap.""" |
977 | + if registrant is None: |
978 | + registrant = self.makePerson() |
979 | + if owner is None: |
980 | + owner = self.makeTeam(registrant) |
981 | + if distroseries is None: |
982 | + distroseries = self.makeDistroSeries() |
983 | + if name is None: |
984 | + name = self.getUniqueString(u"snap-name") |
985 | + kwargs = {} |
986 | + if branch is None and git_ref is None: |
987 | + branch = self.makeAnyBranch() |
988 | + if branch is not None: |
989 | + kwargs["branch"] = branch |
990 | + elif git_ref is not None: |
991 | + kwargs["git_repository"] = git_ref.repository |
992 | + kwargs["git_path"] = git_ref.path |
993 | + snap = getUtility(ISnapSet).new( |
994 | + registrant, owner, distroseries, name, |
995 | + require_virtualized=require_virtualized, date_created=date_created, |
996 | + **kwargs) |
997 | + IStore(snap).flush() |
998 | + return snap |
999 | + |
1000 | |
1001 | # Some factory methods return simple Python types. We don't add |
1002 | # security wrappers for them, as well as for objects created by |
1003 | |
1004 | === modified file 'utilities/snakefood/lp-sfood-packages' |
1005 | --- utilities/snakefood/lp-sfood-packages 2015-01-13 14:07:42 +0000 |
1006 | +++ utilities/snakefood/lp-sfood-packages 2015-07-30 17:14:40 +0000 |
1007 | @@ -2,6 +2,7 @@ |
1008 | lp/testopenid |
1009 | lp/testing |
1010 | lp/soyuz |
1011 | +lp/snappy |
1012 | lp/services |
1013 | lp/scripts |
1014 | lp/registry |