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
=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
--- lib/lp/code/browser/sourcepackagerecipe.py 2010-06-08 18:28:16 +0000
+++ lib/lp/code/browser/sourcepackagerecipe.py 2010-06-09 15:35:44 +0000
@@ -218,6 +218,17 @@
218218
219 cancel_url = next_url219 cancel_url = next_url
220220
221 def validate(self, data):
222 over_quota_distroseries = []
223 for distroseries in data['distros']:
224 if self.context.isOverQuota(self.user, distroseries):
225 over_quota_distroseries.append(str(distroseries))
226 if len(over_quota_distroseries) > 0:
227 self.setFieldError(
228 'distros',
229 "You have exceeded today's quota for %s." %
230 ', '.join(over_quota_distroseries))
231
221 @action('Request builds', name='request')232 @action('Request builds', name='request')
222 def request_action(self, action, data):233 def request_action(self, action, data):
223 """User action for requesting a number of builds."""234 """User action for requesting a number of builds."""
224235
=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-06-08 18:07:46 +0000
+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-06-09 15:35:44 +0000
@@ -25,6 +25,7 @@
25 SourcePackageRecipeBuildView25 SourcePackageRecipeBuildView
26)26)
27from lp.code.interfaces.sourcepackagerecipe import MINIMAL_RECIPE_TEXT27from lp.code.interfaces.sourcepackagerecipe import MINIMAL_RECIPE_TEXT
28from lp.registry.interfaces.pocket import PackagePublishingPocket
28from lp.soyuz.model.processor import ProcessorFamily29from lp.soyuz.model.processor import ProcessorFamily
29from lp.testing import ANONYMOUS, BrowserTestCase, login, logout30from lp.testing import ANONYMOUS, BrowserTestCase, login, logout
3031
@@ -475,6 +476,25 @@
475 self.factory.makeSourcePackageRecipeBuild(recipe=recipe, archive=ppa2)476 self.factory.makeSourcePackageRecipeBuild(recipe=recipe, archive=ppa2)
476 self.assertEqual(ppa2, view.initial_values.get('archive'))477 self.assertEqual(ppa2, view.initial_values.get('archive'))
477478
479 def test_request_build_rejects_over_quota(self):
480 """Over-quota build requests cause validation failures."""
481 woody = self.factory.makeDistroSeries(
482 name='woody', displayname='Woody',
483 distribution=self.ppa.distribution)
484 woody.nominatedarchindep = woody.newArch(
485 'i386', ProcessorFamily.get(1), False, self.factory.makePerson(),
486 supports_virtualized=True)
487
488 recipe = self.makeRecipe()
489 [recipe.requestBuild(
490 self.ppa, self.chef, woody, PackagePublishingPocket.RELEASE)
491 for x in range(5)]
492
493 browser = self.getViewBrowser(recipe, '+request-builds')
494 browser.getControl('Woody').click()
495 browser.getControl('Request builds').click()
496 self.assertIn("You have exceeded today's quota for ubuntu woody.",
497 extract_text(find_main_content(browser.contents)))
478498
479499
480class TestSourcePackageRecipeBuildView(BrowserTestCase):500class TestSourcePackageRecipeBuildView(BrowserTestCase):
481501
=== modified file 'lib/lp/code/interfaces/sourcepackagerecipe.py'
--- lib/lp/code/interfaces/sourcepackagerecipe.py 2010-05-28 20:54:57 +0000
+++ lib/lp/code/interfaces/sourcepackagerecipe.py 2010-06-09 15:35:44 +0000
@@ -15,6 +15,7 @@
15 'ISourcePackageRecipeData',15 'ISourcePackageRecipeData',
16 'ISourcePackageRecipeSource',16 'ISourcePackageRecipeSource',
17 'MINIMAL_RECIPE_TEXT',17 'MINIMAL_RECIPE_TEXT',
18 'TooManyBuilds',
18 'TooNewRecipeFormat',19 'TooNewRecipeFormat',
19 ]20 ]
2021
@@ -26,7 +27,7 @@
26 operation_parameters, REQUEST_USER)27 operation_parameters, REQUEST_USER)
27from lazr.restful.fields import CollectionField, Reference28from lazr.restful.fields import CollectionField, Reference
28from zope.interface import Attribute, Interface29from zope.interface import Attribute, Interface
29from zope.schema import Bool, Choice, Datetime, Object, Text, TextLine30from zope.schema import Bool, Choice, Datetime, Int, Object, Text, TextLine
3031
31from canonical.launchpad import _32from canonical.launchpad import _
32from canonical.launchpad.fields import (33from canonical.launchpad.fields import (
@@ -54,6 +55,18 @@
54 self.instruction_name = instruction_name55 self.instruction_name = instruction_name
5556
5657
58class TooManyBuilds(Exception):
59 """A build was requested that exceeded the quota."""
60
61 def __init__(self, recipe, distroseries):
62 self.recipe = recipe
63 self.distroseries = distroseries
64 msg = (
65 'You have exceeded your quota for recipe %s for distroseries %s'
66 % (self.recipe, distroseries))
67 Exception.__init__(self, msg)
68
69
57class TooNewRecipeFormat(Exception):70class TooNewRecipeFormat(Exception):
58 """The format of the recipe supplied was too new."""71 """The format of the recipe supplied was too new."""
5972
@@ -87,6 +100,8 @@
87 """100 """
88 export_as_webservice_entry()101 export_as_webservice_entry()
89102
103 id = Int()
104
90 daily_build_archive = Reference(105 daily_build_archive = Reference(
91 IArchive, title=_("The archive to use for daily builds."))106 IArchive, title=_("The archive to use for daily builds."))
92107
@@ -136,6 +151,13 @@
136151
137 recipe_text = exported(Text())152 recipe_text = exported(Text())
138153
154 def isOverQuota(requester, distroseries):
155 """True if the recipe/requester/distroseries combo is >= quota.
156
157 :param requester: The Person requesting a build.
158 :param distroseries: The distroseries to build for.
159 """
160
139 @call_with(requester=REQUEST_USER)161 @call_with(requester=REQUEST_USER)
140 @operation_parameters(162 @operation_parameters(
141 archive=Reference(schema=IArchive),163 archive=Reference(schema=IArchive),
142164
=== modified file 'lib/lp/code/model/sourcepackagerecipe.py'
--- lib/lp/code/model/sourcepackagerecipe.py 2010-05-28 20:54:57 +0000
+++ lib/lp/code/model/sourcepackagerecipe.py 2010-06-09 15:35:44 +0000
@@ -24,7 +24,7 @@
2424
25from lp.code.interfaces.sourcepackagerecipe import (25from lp.code.interfaces.sourcepackagerecipe import (
26 ISourcePackageRecipe, ISourcePackageRecipeSource,26 ISourcePackageRecipe, ISourcePackageRecipeSource,
27 ISourcePackageRecipeData)27 ISourcePackageRecipeData, TooManyBuilds)
28from lp.code.interfaces.sourcepackagerecipebuild import (28from lp.code.interfaces.sourcepackagerecipebuild import (
29 ISourcePackageRecipeBuildSource)29 ISourcePackageRecipeBuildSource)
30from lp.code.model.sourcepackagerecipebuild import SourcePackageRecipeBuild30from lp.code.model.sourcepackagerecipebuild import SourcePackageRecipeBuild
@@ -53,6 +53,9 @@
5353
54 __storm_table__ = 'SourcePackageRecipe'54 __storm_table__ = 'SourcePackageRecipe'
5555
56 def __str__(self):
57 return '%s/%s' % (self.owner.name, self.name)
58
56 implements(ISourcePackageRecipe)59 implements(ISourcePackageRecipe)
5760
58 classProvides(ISourcePackageRecipeSource)61 classProvides(ISourcePackageRecipeSource)
@@ -155,6 +158,11 @@
155 store.remove(self._recipe_data)158 store.remove(self._recipe_data)
156 store.remove(self)159 store.remove(self)
157160
161 def isOverQuota(self, requester, distroseries):
162 """See `ISourcePackageRecipe`."""
163 return SourcePackageRecipeBuild.getRecentBuilds(
164 requester, self, distroseries).count() >= 5
165
158 def requestBuild(self, archive, requester, distroseries, pocket):166 def requestBuild(self, archive, requester, distroseries, pocket):
159 """See `ISourcePackageRecipe`."""167 """See `ISourcePackageRecipe`."""
160 if archive.purpose != ArchivePurpose.PPA:168 if archive.purpose != ArchivePurpose.PPA:
@@ -164,6 +172,8 @@
164 requester, self.distroseries, None, component, pocket)172 requester, self.distroseries, None, component, pocket)
165 if reject_reason is not None:173 if reject_reason is not None:
166 raise reject_reason174 raise reject_reason
175 if self.isOverQuota(requester, distroseries):
176 raise TooManyBuilds(self, distroseries)
167177
168 build = getUtility(ISourcePackageRecipeBuildSource).new(distroseries,178 build = getUtility(ISourcePackageRecipeBuildSource).new(distroseries,
169 self, requester, archive)179 self, requester, archive)
170180
=== modified file 'lib/lp/code/model/sourcepackagerecipebuild.py'
--- lib/lp/code/model/sourcepackagerecipebuild.py 2010-06-08 08:30:41 +0000
+++ lib/lp/code/model/sourcepackagerecipebuild.py 2010-06-09 15:35:44 +0000
@@ -12,6 +12,8 @@
1212
13import datetime13import datetime
1414
15from pytz import utc
16
15from canonical.database.constants import UTC_NOW17from canonical.database.constants import UTC_NOW
16from canonical.database.datetimecol import UtcDateTimeCol18from canonical.database.datetimecol import UtcDateTimeCol
17from canonical.database.enumcol import DBEnum19from canonical.database.enumcol import DBEnum
@@ -207,6 +209,16 @@
207 store = IMasterStore(SourcePackageRecipeBuild)209 store = IMasterStore(SourcePackageRecipeBuild)
208 return store.find(cls, cls.id == build_id).one()210 return store.find(cls, cls.id == build_id).one()
209211
212 @classmethod
213 def getRecentBuilds(cls, requester, recipe, distroseries, _now=None):
214 if _now is None:
215 _now = datetime.datetime.now(utc)
216 store = IMasterStore(SourcePackageRecipeBuild)
217 old_threshold = _now - datetime.timedelta(days=1)
218 return store.find(cls, cls.distroseries_id == distroseries.id,
219 cls.requester_id == requester.id, cls.recipe_id == recipe.id,
220 cls.datecreated > old_threshold)
221
210 def makeJob(self):222 def makeJob(self):
211 """See `ISourcePackageRecipeBuildJob`."""223 """See `ISourcePackageRecipeBuildJob`."""
212 store = Store.of(self)224 store = Store.of(self)
213225
=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipe.py'
--- lib/lp/code/model/tests/test_sourcepackagerecipe.py 2010-05-28 20:54:57 +0000
+++ lib/lp/code/model/tests/test_sourcepackagerecipe.py 2010-06-09 15:35:44 +0000
@@ -31,7 +31,7 @@
31from lp.buildmaster.model.buildqueue import BuildQueue31from lp.buildmaster.model.buildqueue import BuildQueue
32from lp.code.interfaces.sourcepackagerecipe import (32from lp.code.interfaces.sourcepackagerecipe import (
33 ForbiddenInstruction, ISourcePackageRecipe, ISourcePackageRecipeSource,33 ForbiddenInstruction, ISourcePackageRecipe, ISourcePackageRecipeSource,
34 TooNewRecipeFormat, MINIMAL_RECIPE_TEXT)34 TooManyBuilds, TooNewRecipeFormat, MINIMAL_RECIPE_TEXT)
35from lp.code.interfaces.sourcepackagerecipebuild import (35from lp.code.interfaces.sourcepackagerecipebuild import (
36 ISourcePackageRecipeBuild, ISourcePackageRecipeBuildJob)36 ISourcePackageRecipeBuild, ISourcePackageRecipeBuildJob)
37from lp.code.model.sourcepackagerecipebuild import (37from lp.code.model.sourcepackagerecipebuild import (
@@ -251,6 +251,22 @@
251 self.assertRaises(ArchiveDisabled, recipe.requestBuild, ppa,251 self.assertRaises(ArchiveDisabled, recipe.requestBuild, ppa,
252 ppa.owner, distroseries, PackagePublishingPocket.RELEASE)252 ppa.owner, distroseries, PackagePublishingPocket.RELEASE)
253253
254 def test_requestBuildRejectsOverQuota(self):
255 """Build requests that exceed quota raise an exception."""
256 requester = self.factory.makePerson(name='requester')
257 recipe = self.factory.makeSourcePackageRecipe(
258 name=u'myrecipe', owner=requester)
259 series = list(recipe.distroseries)[0]
260 archive = self.factory.makeArchive(owner=requester)
261 def request_build():
262 recipe.requestBuild(archive, requester, series,
263 PackagePublishingPocket.RELEASE)
264 [request_build() for num in range(5)]
265 e = self.assertRaises(TooManyBuilds, request_build)
266 self.assertIn(
267 'You have exceeded your quota for recipe requester/myrecipe',
268 str(e))
269
254 def test_sourcepackagerecipe_description(self):270 def test_sourcepackagerecipe_description(self):
255 """Ensure that the SourcePackageRecipe has a proper description."""271 """Ensure that the SourcePackageRecipe has a proper description."""
256 description = u'The whoozits and whatzits.'272 description = u'The whoozits and whatzits.'
257273
=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipebuild.py'
--- lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2010-06-08 08:30:41 +0000
+++ lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2010-06-09 15:35:44 +0000
@@ -27,6 +27,7 @@
27from lp.code.interfaces.sourcepackagerecipebuild import (27from lp.code.interfaces.sourcepackagerecipebuild import (
28 ISourcePackageRecipeBuildJob, ISourcePackageRecipeBuild,28 ISourcePackageRecipeBuildJob, ISourcePackageRecipeBuild,
29 ISourcePackageRecipeBuildSource)29 ISourcePackageRecipeBuildSource)
30from lp.code.model.sourcepackagerecipebuild import SourcePackageRecipeBuild
30from lp.soyuz.interfaces.processor import IProcessorFamilySet31from lp.soyuz.interfaces.processor import IProcessorFamilySet
31from lp.soyuz.model.processor import ProcessorFamily32from lp.soyuz.model.processor import ProcessorFamily
32from lp.testing import ANONYMOUS, login, person_logged_in, TestCaseWithFactory33from lp.testing import ANONYMOUS, login, person_logged_in, TestCaseWithFactory
@@ -195,6 +196,35 @@
195 Store.of(binary).flush()196 Store.of(binary).flush()
196 self.assertEqual([binary], list(spb.binary_builds))197 self.assertEqual([binary], list(spb.binary_builds))
197198
199 def test_getRecentBuilds(self):
200 """Recent builds match the same person, series and receipe.
201
202 Builds do not match if they are older than 24 hours, or have a
203 different requester, series or recipe.
204 """
205 requester = self.factory.makePerson()
206 recipe = self.factory.makeSourcePackageRecipe()
207 series = self.factory.makeDistroSeries()
208 now = self.factory.getUniqueDate()
209 build = self.factory.makeSourcePackageRecipeBuild(recipe=recipe,
210 requester=requester)
211 self.factory.makeSourcePackageRecipeBuild(
212 recipe=recipe, distroseries=series)
213 self.factory.makeSourcePackageRecipeBuild(
214 requester=requester, distroseries=series)
215 def get_recent():
216 Store.of(build).flush()
217 return SourcePackageRecipeBuild.getRecentBuilds(
218 requester, recipe, series, _now=now)
219 self.assertContentEqual([], get_recent())
220 yesterday = now - datetime.timedelta(days=1)
221 recent_build = self.factory.makeSourcePackageRecipeBuild(
222 recipe=recipe, distroseries=series, requester=requester,
223 date_created=yesterday)
224 self.assertContentEqual([], get_recent())
225 a_second = datetime.timedelta(seconds=1)
226 removeSecurityProxy(recent_build).datecreated += a_second
227 self.assertContentEqual([recent_build], get_recent())
198228
199class MakeSPRecipeBuildMixin:229class MakeSPRecipeBuildMixin:
200 """Provide the common makeBuild method returning a queued build."""230 """Provide the common makeBuild method returning a queued build."""
201231
=== modified file 'lib/lp/registry/interfaces/distroseries.py'
--- lib/lp/registry/interfaces/distroseries.py 2010-06-07 02:52:21 +0000
+++ lib/lp/registry/interfaces/distroseries.py 2010-06-09 15:35:44 +0000
@@ -361,6 +361,9 @@
361 given architecturetag.361 given architecturetag.
362 """362 """
363363
364 def __str__():
365 """Return the name of the distroseries."""
366
364 def getDistroArchSeriesByProcessor(processor):367 def getDistroArchSeriesByProcessor(processor):
365 """Return the distroarchseries for this distroseries with the368 """Return the distroarchseries for this distroseries with the
366 given architecturetag from a `IProcessor`.369 given architecturetag from a `IProcessor`.
367370
=== modified file 'lib/lp/registry/model/distroseries.py'
--- lib/lp/registry/model/distroseries.py 2010-06-09 08:26:26 +0000
+++ lib/lp/registry/model/distroseries.py 2010-06-09 15:35:44 +0000
@@ -216,6 +216,9 @@
216 """See `IDistroSeries`."""216 """See `IDistroSeries`."""
217 return self.getDistroArchSeries(archtag)217 return self.getDistroArchSeries(archtag)
218218
219 def __str__(self):
220 return '%s %s' % (self.distribution.name, self.name)
221
219 def getDistroArchSeries(self, archtag):222 def getDistroArchSeries(self, archtag):
220 """See `IDistroSeries`."""223 """See `IDistroSeries`."""
221 item = DistroArchSeries.selectOneBy(224 item = DistroArchSeries.selectOneBy(
222225
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2010-06-07 10:43:01 +0000
+++ lib/lp/testing/factory.py 2010-06-09 15:35:44 +0000
@@ -1789,7 +1789,7 @@
1789 def makeSourcePackageRecipeBuild(self, sourcepackage=None, recipe=None,1789 def makeSourcePackageRecipeBuild(self, sourcepackage=None, recipe=None,
1790 requester=None, archive=None,1790 requester=None, archive=None,
1791 sourcename=None, distroseries=None,1791 sourcename=None, distroseries=None,
1792 pocket=None,1792 pocket=None, date_created=None):
1793 status=BuildStatus.NEEDSBUILD):1793 status=BuildStatus.NEEDSBUILD):
1794 """Make a new SourcePackageRecipeBuild."""1794 """Make a new SourcePackageRecipeBuild."""
1795 if recipe is None:1795 if recipe is None:
@@ -1806,7 +1806,8 @@
1806 recipe=recipe,1806 recipe=recipe,
1807 archive=archive,1807 archive=archive,
1808 requester=requester,1808 requester=requester,
1809 pocket=pocket)1809 pocket=pocket,
1810 date_created=date_created)
1810 removeSecurityProxy(spr_build).buildstate = status1811 removeSecurityProxy(spr_build).buildstate = status
1811 return spr_build1812 return spr_build
18121813