Merge lp:~cjwatson/launchpad/snap-basic-model into lp:launchpad

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
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