Merge lp:~abentley/launchpad/build-quota into lp:launchpad

Proposed by Aaron Bentley
Status: Merged
Merged at revision: 10983
Proposed branch: lp:~abentley/launchpad/build-quota
Merge into: lp:launchpad
Diff against target: 330 lines (+133/-5)
10 files modified
lib/lp/code/browser/sourcepackagerecipe.py (+11/-0)
lib/lp/code/browser/tests/test_sourcepackagerecipe.py (+20/-0)
lib/lp/code/interfaces/sourcepackagerecipe.py (+23/-1)
lib/lp/code/model/sourcepackagerecipe.py (+11/-1)
lib/lp/code/model/sourcepackagerecipebuild.py (+12/-0)
lib/lp/code/model/tests/test_sourcepackagerecipe.py (+17/-1)
lib/lp/code/model/tests/test_sourcepackagerecipebuild.py (+30/-0)
lib/lp/registry/interfaces/distroseries.py (+3/-0)
lib/lp/registry/model/distroseries.py (+3/-0)
lib/lp/testing/factory.py (+3/-2)
To merge this branch: bzr merge lp:~abentley/launchpad/build-quota
Reviewer Review Type Date Requested Status
Paul Hummer (community) code Approve
Review via email: mp+26821@code.launchpad.net

Commit message

Impose a quota on source package recipe builds.

Description of the change

= Summary =
Fix bug #581901: On-demand builds should be limited.

== Proposed fix ==
Restrict builds to 5 per 24 hours per recipe per distroseries. (i.e. building
for a different distroseries or recipe won't hit the limit)

== Pre-implementation notes ==
The quotas were discussed with bigjools.

== Implementation details ==
All builds, not only on-demand builds are restricted. At the moment, there is
no way of distinguishing them. If it later turns out that daily builds are
being hindered by this, we can look at ways to relax this quota for them.

== Tests ==
bin/test -vt test_getRecentBuilds -t test_requestBuildRejectsOverQuota -t test_request_build_rejects_over_quota

== Demo and Q/A ==
Request 6 builds for a given recipe and distroseries. The last should error.

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/lp/code/model/tests/test_sourcepackagerecipebuild.py
  lib/lp/registry/interfaces/distroseries.py
  lib/lp/code/model/tests/test_sourcepackagerecipe.py
  lib/lp/testing/factory.py
  lib/lp/code/model/sourcepackagerecipebuild.py
  lib/lp/code/browser/sourcepackagerecipe.py
  lib/lp/code/model/sourcepackagerecipe.py
  lib/lp/code/browser/tests/test_sourcepackagerecipe.py
  lib/lp/code/interfaces/sourcepackagerecipe.py
  lib/lp/registry/model/distroseries.py

== Pylint notices ==

lib/lp/registry/interfaces/distroseries.py
    429: [C0322, IDistroSeriesPublic.getPackageUploads] Operator not preceded by a space
    description=_(
    ^
    "Return items that are more recent than this timestamp."),
    required=False),
    status=Choice(

    vocabulary=DBEnumeratedType,
    title=_("Package Upload Status"),
    description=_("Return only items that have this status."),
    required=False),
    archive=Reference(

    schema=Interface,
    title=_("Archive"),
    description=_("Return only items for this archive."),
    required=False),
    pocket=Choice(

    vocabulary=DBEnumeratedType,
    title=_("Pocket"),
    description=_("Return only items targeted to this pocket"),
    required=False),
    custom_type=Choice(

    vocabulary=DBEnumeratedType,
    title=_("Custom Type"),
    description=_("Return only items with custom files of this "
    "type."),
    required=False),
    )

    @operation_returns_collection_of(Interface)
    @export_read_operation()
    def getPackageUploads(created_since_date, status, archive, pocket,
    custom_type):

lib/lp/code/interfaces/sourcepackagerecipe.py
    172: [C0322, ISourcePackageRecipe.requestBuild] Operator not preceded by a space
    distroseries=Reference(schema=IDistroSeries),
    ^
    )
    @export_write_operation()
    def requestBuild(archive, distroseries, requester, pocket):

To post a comment you must log in.
Revision history for this message
Paul Hummer (rockstar) :
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/code/browser/sourcepackagerecipe.py'
2--- lib/lp/code/browser/sourcepackagerecipe.py 2010-06-08 18:28:16 +0000
3+++ lib/lp/code/browser/sourcepackagerecipe.py 2010-06-09 15:35:44 +0000
4@@ -218,6 +218,17 @@
5
6 cancel_url = next_url
7
8+ def validate(self, data):
9+ over_quota_distroseries = []
10+ for distroseries in data['distros']:
11+ if self.context.isOverQuota(self.user, distroseries):
12+ over_quota_distroseries.append(str(distroseries))
13+ if len(over_quota_distroseries) > 0:
14+ self.setFieldError(
15+ 'distros',
16+ "You have exceeded today's quota for %s." %
17+ ', '.join(over_quota_distroseries))
18+
19 @action('Request builds', name='request')
20 def request_action(self, action, data):
21 """User action for requesting a number of builds."""
22
23=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
24--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-06-08 18:07:46 +0000
25+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-06-09 15:35:44 +0000
26@@ -25,6 +25,7 @@
27 SourcePackageRecipeBuildView
28 )
29 from lp.code.interfaces.sourcepackagerecipe import MINIMAL_RECIPE_TEXT
30+from lp.registry.interfaces.pocket import PackagePublishingPocket
31 from lp.soyuz.model.processor import ProcessorFamily
32 from lp.testing import ANONYMOUS, BrowserTestCase, login, logout
33
34@@ -475,6 +476,25 @@
35 self.factory.makeSourcePackageRecipeBuild(recipe=recipe, archive=ppa2)
36 self.assertEqual(ppa2, view.initial_values.get('archive'))
37
38+ def test_request_build_rejects_over_quota(self):
39+ """Over-quota build requests cause validation failures."""
40+ woody = self.factory.makeDistroSeries(
41+ name='woody', displayname='Woody',
42+ distribution=self.ppa.distribution)
43+ woody.nominatedarchindep = woody.newArch(
44+ 'i386', ProcessorFamily.get(1), False, self.factory.makePerson(),
45+ supports_virtualized=True)
46+
47+ recipe = self.makeRecipe()
48+ [recipe.requestBuild(
49+ self.ppa, self.chef, woody, PackagePublishingPocket.RELEASE)
50+ for x in range(5)]
51+
52+ browser = self.getViewBrowser(recipe, '+request-builds')
53+ browser.getControl('Woody').click()
54+ browser.getControl('Request builds').click()
55+ self.assertIn("You have exceeded today's quota for ubuntu woody.",
56+ extract_text(find_main_content(browser.contents)))
57
58
59 class TestSourcePackageRecipeBuildView(BrowserTestCase):
60
61=== modified file 'lib/lp/code/interfaces/sourcepackagerecipe.py'
62--- lib/lp/code/interfaces/sourcepackagerecipe.py 2010-05-28 20:54:57 +0000
63+++ lib/lp/code/interfaces/sourcepackagerecipe.py 2010-06-09 15:35:44 +0000
64@@ -15,6 +15,7 @@
65 'ISourcePackageRecipeData',
66 'ISourcePackageRecipeSource',
67 'MINIMAL_RECIPE_TEXT',
68+ 'TooManyBuilds',
69 'TooNewRecipeFormat',
70 ]
71
72@@ -26,7 +27,7 @@
73 operation_parameters, REQUEST_USER)
74 from lazr.restful.fields import CollectionField, Reference
75 from zope.interface import Attribute, Interface
76-from zope.schema import Bool, Choice, Datetime, Object, Text, TextLine
77+from zope.schema import Bool, Choice, Datetime, Int, Object, Text, TextLine
78
79 from canonical.launchpad import _
80 from canonical.launchpad.fields import (
81@@ -54,6 +55,18 @@
82 self.instruction_name = instruction_name
83
84
85+class TooManyBuilds(Exception):
86+ """A build was requested that exceeded the quota."""
87+
88+ def __init__(self, recipe, distroseries):
89+ self.recipe = recipe
90+ self.distroseries = distroseries
91+ msg = (
92+ 'You have exceeded your quota for recipe %s for distroseries %s'
93+ % (self.recipe, distroseries))
94+ Exception.__init__(self, msg)
95+
96+
97 class TooNewRecipeFormat(Exception):
98 """The format of the recipe supplied was too new."""
99
100@@ -87,6 +100,8 @@
101 """
102 export_as_webservice_entry()
103
104+ id = Int()
105+
106 daily_build_archive = Reference(
107 IArchive, title=_("The archive to use for daily builds."))
108
109@@ -136,6 +151,13 @@
110
111 recipe_text = exported(Text())
112
113+ def isOverQuota(requester, distroseries):
114+ """True if the recipe/requester/distroseries combo is >= quota.
115+
116+ :param requester: The Person requesting a build.
117+ :param distroseries: The distroseries to build for.
118+ """
119+
120 @call_with(requester=REQUEST_USER)
121 @operation_parameters(
122 archive=Reference(schema=IArchive),
123
124=== modified file 'lib/lp/code/model/sourcepackagerecipe.py'
125--- lib/lp/code/model/sourcepackagerecipe.py 2010-05-28 20:54:57 +0000
126+++ lib/lp/code/model/sourcepackagerecipe.py 2010-06-09 15:35:44 +0000
127@@ -24,7 +24,7 @@
128
129 from lp.code.interfaces.sourcepackagerecipe import (
130 ISourcePackageRecipe, ISourcePackageRecipeSource,
131- ISourcePackageRecipeData)
132+ ISourcePackageRecipeData, TooManyBuilds)
133 from lp.code.interfaces.sourcepackagerecipebuild import (
134 ISourcePackageRecipeBuildSource)
135 from lp.code.model.sourcepackagerecipebuild import SourcePackageRecipeBuild
136@@ -53,6 +53,9 @@
137
138 __storm_table__ = 'SourcePackageRecipe'
139
140+ def __str__(self):
141+ return '%s/%s' % (self.owner.name, self.name)
142+
143 implements(ISourcePackageRecipe)
144
145 classProvides(ISourcePackageRecipeSource)
146@@ -155,6 +158,11 @@
147 store.remove(self._recipe_data)
148 store.remove(self)
149
150+ def isOverQuota(self, requester, distroseries):
151+ """See `ISourcePackageRecipe`."""
152+ return SourcePackageRecipeBuild.getRecentBuilds(
153+ requester, self, distroseries).count() >= 5
154+
155 def requestBuild(self, archive, requester, distroseries, pocket):
156 """See `ISourcePackageRecipe`."""
157 if archive.purpose != ArchivePurpose.PPA:
158@@ -164,6 +172,8 @@
159 requester, self.distroseries, None, component, pocket)
160 if reject_reason is not None:
161 raise reject_reason
162+ if self.isOverQuota(requester, distroseries):
163+ raise TooManyBuilds(self, distroseries)
164
165 build = getUtility(ISourcePackageRecipeBuildSource).new(distroseries,
166 self, requester, archive)
167
168=== modified file 'lib/lp/code/model/sourcepackagerecipebuild.py'
169--- lib/lp/code/model/sourcepackagerecipebuild.py 2010-06-08 08:30:41 +0000
170+++ lib/lp/code/model/sourcepackagerecipebuild.py 2010-06-09 15:35:44 +0000
171@@ -12,6 +12,8 @@
172
173 import datetime
174
175+from pytz import utc
176+
177 from canonical.database.constants import UTC_NOW
178 from canonical.database.datetimecol import UtcDateTimeCol
179 from canonical.database.enumcol import DBEnum
180@@ -207,6 +209,16 @@
181 store = IMasterStore(SourcePackageRecipeBuild)
182 return store.find(cls, cls.id == build_id).one()
183
184+ @classmethod
185+ def getRecentBuilds(cls, requester, recipe, distroseries, _now=None):
186+ if _now is None:
187+ _now = datetime.datetime.now(utc)
188+ store = IMasterStore(SourcePackageRecipeBuild)
189+ old_threshold = _now - datetime.timedelta(days=1)
190+ return store.find(cls, cls.distroseries_id == distroseries.id,
191+ cls.requester_id == requester.id, cls.recipe_id == recipe.id,
192+ cls.datecreated > old_threshold)
193+
194 def makeJob(self):
195 """See `ISourcePackageRecipeBuildJob`."""
196 store = Store.of(self)
197
198=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipe.py'
199--- lib/lp/code/model/tests/test_sourcepackagerecipe.py 2010-05-28 20:54:57 +0000
200+++ lib/lp/code/model/tests/test_sourcepackagerecipe.py 2010-06-09 15:35:44 +0000
201@@ -31,7 +31,7 @@
202 from lp.buildmaster.model.buildqueue import BuildQueue
203 from lp.code.interfaces.sourcepackagerecipe import (
204 ForbiddenInstruction, ISourcePackageRecipe, ISourcePackageRecipeSource,
205- TooNewRecipeFormat, MINIMAL_RECIPE_TEXT)
206+ TooManyBuilds, TooNewRecipeFormat, MINIMAL_RECIPE_TEXT)
207 from lp.code.interfaces.sourcepackagerecipebuild import (
208 ISourcePackageRecipeBuild, ISourcePackageRecipeBuildJob)
209 from lp.code.model.sourcepackagerecipebuild import (
210@@ -251,6 +251,22 @@
211 self.assertRaises(ArchiveDisabled, recipe.requestBuild, ppa,
212 ppa.owner, distroseries, PackagePublishingPocket.RELEASE)
213
214+ def test_requestBuildRejectsOverQuota(self):
215+ """Build requests that exceed quota raise an exception."""
216+ requester = self.factory.makePerson(name='requester')
217+ recipe = self.factory.makeSourcePackageRecipe(
218+ name=u'myrecipe', owner=requester)
219+ series = list(recipe.distroseries)[0]
220+ archive = self.factory.makeArchive(owner=requester)
221+ def request_build():
222+ recipe.requestBuild(archive, requester, series,
223+ PackagePublishingPocket.RELEASE)
224+ [request_build() for num in range(5)]
225+ e = self.assertRaises(TooManyBuilds, request_build)
226+ self.assertIn(
227+ 'You have exceeded your quota for recipe requester/myrecipe',
228+ str(e))
229+
230 def test_sourcepackagerecipe_description(self):
231 """Ensure that the SourcePackageRecipe has a proper description."""
232 description = u'The whoozits and whatzits.'
233
234=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipebuild.py'
235--- lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2010-06-08 08:30:41 +0000
236+++ lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2010-06-09 15:35:44 +0000
237@@ -27,6 +27,7 @@
238 from lp.code.interfaces.sourcepackagerecipebuild import (
239 ISourcePackageRecipeBuildJob, ISourcePackageRecipeBuild,
240 ISourcePackageRecipeBuildSource)
241+from lp.code.model.sourcepackagerecipebuild import SourcePackageRecipeBuild
242 from lp.soyuz.interfaces.processor import IProcessorFamilySet
243 from lp.soyuz.model.processor import ProcessorFamily
244 from lp.testing import ANONYMOUS, login, person_logged_in, TestCaseWithFactory
245@@ -195,6 +196,35 @@
246 Store.of(binary).flush()
247 self.assertEqual([binary], list(spb.binary_builds))
248
249+ def test_getRecentBuilds(self):
250+ """Recent builds match the same person, series and receipe.
251+
252+ Builds do not match if they are older than 24 hours, or have a
253+ different requester, series or recipe.
254+ """
255+ requester = self.factory.makePerson()
256+ recipe = self.factory.makeSourcePackageRecipe()
257+ series = self.factory.makeDistroSeries()
258+ now = self.factory.getUniqueDate()
259+ build = self.factory.makeSourcePackageRecipeBuild(recipe=recipe,
260+ requester=requester)
261+ self.factory.makeSourcePackageRecipeBuild(
262+ recipe=recipe, distroseries=series)
263+ self.factory.makeSourcePackageRecipeBuild(
264+ requester=requester, distroseries=series)
265+ def get_recent():
266+ Store.of(build).flush()
267+ return SourcePackageRecipeBuild.getRecentBuilds(
268+ requester, recipe, series, _now=now)
269+ self.assertContentEqual([], get_recent())
270+ yesterday = now - datetime.timedelta(days=1)
271+ recent_build = self.factory.makeSourcePackageRecipeBuild(
272+ recipe=recipe, distroseries=series, requester=requester,
273+ date_created=yesterday)
274+ self.assertContentEqual([], get_recent())
275+ a_second = datetime.timedelta(seconds=1)
276+ removeSecurityProxy(recent_build).datecreated += a_second
277+ self.assertContentEqual([recent_build], get_recent())
278
279 class MakeSPRecipeBuildMixin:
280 """Provide the common makeBuild method returning a queued build."""
281
282=== modified file 'lib/lp/registry/interfaces/distroseries.py'
283--- lib/lp/registry/interfaces/distroseries.py 2010-06-07 02:52:21 +0000
284+++ lib/lp/registry/interfaces/distroseries.py 2010-06-09 15:35:44 +0000
285@@ -361,6 +361,9 @@
286 given architecturetag.
287 """
288
289+ def __str__():
290+ """Return the name of the distroseries."""
291+
292 def getDistroArchSeriesByProcessor(processor):
293 """Return the distroarchseries for this distroseries with the
294 given architecturetag from a `IProcessor`.
295
296=== modified file 'lib/lp/registry/model/distroseries.py'
297--- lib/lp/registry/model/distroseries.py 2010-06-09 08:26:26 +0000
298+++ lib/lp/registry/model/distroseries.py 2010-06-09 15:35:44 +0000
299@@ -216,6 +216,9 @@
300 """See `IDistroSeries`."""
301 return self.getDistroArchSeries(archtag)
302
303+ def __str__(self):
304+ return '%s %s' % (self.distribution.name, self.name)
305+
306 def getDistroArchSeries(self, archtag):
307 """See `IDistroSeries`."""
308 item = DistroArchSeries.selectOneBy(
309
310=== modified file 'lib/lp/testing/factory.py'
311--- lib/lp/testing/factory.py 2010-06-07 10:43:01 +0000
312+++ lib/lp/testing/factory.py 2010-06-09 15:35:44 +0000
313@@ -1789,7 +1789,7 @@
314 def makeSourcePackageRecipeBuild(self, sourcepackage=None, recipe=None,
315 requester=None, archive=None,
316 sourcename=None, distroseries=None,
317- pocket=None,
318+ pocket=None, date_created=None):
319 status=BuildStatus.NEEDSBUILD):
320 """Make a new SourcePackageRecipeBuild."""
321 if recipe is None:
322@@ -1806,7 +1806,8 @@
323 recipe=recipe,
324 archive=archive,
325 requester=requester,
326- pocket=pocket)
327+ pocket=pocket,
328+ date_created=date_created)
329 removeSecurityProxy(spr_build).buildstate = status
330 return spr_build
331