Merge ~cjwatson/launchpad:refactor-build-retry-cancel-rescore into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 618b6d0313a33166db0d82bb11bb52ececfee337
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:refactor-build-retry-cancel-rescore
Merge into: launchpad:master
Diff against target: 1627 lines (+316/-627)
24 files modified
lib/lp/_schema_circular_imports.py (+0/-2)
lib/lp/buildmaster/interfaces/buildfarmjob.py (+93/-3)
lib/lp/buildmaster/interfaces/packagebuild.py (+11/-3)
lib/lp/buildmaster/interfaces/webservice.py (+7/-1)
lib/lp/buildmaster/model/buildfarmjob.py (+77/-0)
lib/lp/buildmaster/tests/test_buildfarmjob.py (+3/-1)
lib/lp/buildmaster/tests/test_packagebuild.py (+1/-0)
lib/lp/charms/interfaces/charmrecipebuild.py (+14/-53)
lib/lp/charms/model/charmrecipebuild.py (+2/-60)
lib/lp/code/configure.zcml (+3/-0)
lib/lp/code/interfaces/sourcepackagerecipebuild.py (+21/-39)
lib/lp/code/model/sourcepackagerecipebuild.py (+1/-25)
lib/lp/oci/interfaces/ocirecipebuild.py (+13/-53)
lib/lp/oci/model/ocirecipebuild.py (+2/-60)
lib/lp/security.py (+0/-12)
lib/lp/snappy/interfaces/snapbuild.py (+14/-53)
lib/lp/snappy/model/snapbuild.py (+2/-60)
lib/lp/soyuz/interfaces/binarypackagebuild.py (+26/-57)
lib/lp/soyuz/interfaces/livefsbuild.py (+14/-40)
lib/lp/soyuz/interfaces/webservice.py (+1/-5)
lib/lp/soyuz/model/binarypackagebuild.py (+5/-60)
lib/lp/soyuz/model/livefsbuild.py (+1/-32)
lib/lp/soyuz/stories/webservice/xx-builds.txt (+3/-4)
lib/lp/soyuz/tests/test_build.py (+2/-4)
Reviewer Review Type Date Requested Status
Jürgen Gmach Approve
Review via email: mp+413831@code.launchpad.net

Commit message

Push retry/cancel/rescore down to IBuildFarmJob

Description of the change

Previously, each build type implemented retry/cancel/rescore logic itself, even though the implementations were almost identical. Push this down to `IBuildFarmJob` and `BuildFarmJobMixin` so that creating a new build type involves a bit less copy-and-paste.

I had to split `IBuildFarmJob` into `IBuildFarmJob{View,Edit,Admin}`, since the retry/cancel/rescore operations require varying amounts of privilege.

Source package recipe builds gain a `rescore` API operation, and live filesystem and source package recipe builds both gain `can_be_retried` and `retry` methods (although `can_be_retried` is currently always False for those build types, and `retry` will always fail, but that might change in future).

Attempting to retry or rescore a build that cannot be retried or rescored respectively now consistently returns HTTP 400 rather than returning HTTP 500 in some cases.

To post a comment you must log in.
Revision history for this message
Jürgen Gmach (jugmac00) wrote :

LGTM + tests pass

review: Approve
Revision history for this message
Colin Watson (cjwatson) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/lib/lp/_schema_circular_imports.py b/lib/lp/_schema_circular_imports.py
index 09fe6e4..af12d9a 100644
--- a/lib/lp/_schema_circular_imports.py
+++ b/lib/lp/_schema_circular_imports.py
@@ -742,8 +742,6 @@ patch_entry_explicit_version(IArchiveSubscriber, 'beta')
742742
743# IBinaryPackageBuild743# IBinaryPackageBuild
744patch_entry_explicit_version(IBinaryPackageBuild, 'beta')744patch_entry_explicit_version(IBinaryPackageBuild, 'beta')
745patch_operations_explicit_version(
746 IBinaryPackageBuild, 'beta', "rescore", "retry")
747745
748# IBinaryPackagePublishingHistory746# IBinaryPackagePublishingHistory
749patch_entry_explicit_version(IBinaryPackagePublishingHistory, 'beta')747patch_entry_explicit_version(IBinaryPackagePublishingHistory, 'beta')
diff --git a/lib/lp/buildmaster/interfaces/buildfarmjob.py b/lib/lp/buildmaster/interfaces/buildfarmjob.py
index 86d62e5..9e8d461 100644
--- a/lib/lp/buildmaster/interfaces/buildfarmjob.py
+++ b/lib/lp/buildmaster/interfaces/buildfarmjob.py
@@ -4,17 +4,28 @@
4"""Interface for Soyuz build farm jobs."""4"""Interface for Soyuz build farm jobs."""
55
6__all__ = [6__all__ = [
7 'CannotBeRescored',
8 'CannotBeRetried',
7 'IBuildFarmJob',9 'IBuildFarmJob',
10 'IBuildFarmJobAdmin',
8 'IBuildFarmJobDB',11 'IBuildFarmJobDB',
12 'IBuildFarmJobEdit',
9 'IBuildFarmJobSet',13 'IBuildFarmJobSet',
10 'IBuildFarmJobSource',14 'IBuildFarmJobSource',
15 'IBuildFarmJobView',
11 'InconsistentBuildFarmJobError',16 'InconsistentBuildFarmJobError',
12 'ISpecificBuildFarmJobSource',17 'ISpecificBuildFarmJobSource',
13 ]18 ]
1419
20import http.client
21
15from lazr.restful.declarations import (22from lazr.restful.declarations import (
23 error_status,
24 export_write_operation,
16 exported,25 exported,
17 exported_as_webservice_entry,26 exported_as_webservice_entry,
27 operation_for_version,
28 operation_parameters,
18 )29 )
19from lazr.restful.fields import Reference30from lazr.restful.fields import Reference
20from zope.interface import (31from zope.interface import (
@@ -49,6 +60,22 @@ class InconsistentBuildFarmJobError(Exception):
49 """60 """
5061
5162
63@error_status(http.client.BAD_REQUEST)
64class CannotBeRetried(Exception):
65 """Raised when retrying a build that cannot be retried."""
66
67 def __init__(self, build_id):
68 super().__init__("Build %s cannot be retried." % build_id)
69
70
71@error_status(http.client.BAD_REQUEST)
72class CannotBeRescored(Exception):
73 """Raised when rescoring a build that cannot be rescored."""
74
75 def __init__(self, build_id):
76 super().__init__("Build %s cannot be rescored." % build_id)
77
78
52class IBuildFarmJobDB(Interface):79class IBuildFarmJobDB(Interface):
53 """Operations on a `BuildFarmJob` DB row.80 """Operations on a `BuildFarmJob` DB row.
5481
@@ -63,9 +90,8 @@ class IBuildFarmJobDB(Interface):
63 description=_("The specific type of job."))90 description=_("The specific type of job."))
6491
6592
66@exported_as_webservice_entry(as_of='beta')93class IBuildFarmJobView(Interface):
67class IBuildFarmJob(Interface):94 """`IBuildFarmJob` attributes that require launchpad.View."""
68 """Operations that jobs for the build farm must implement."""
6995
70 id = Attribute('The build farm job ID.')96 id = Attribute('The build farm job ID.')
7197
@@ -238,6 +264,70 @@ class IBuildFarmJob(Interface):
238 "Newline-separated list of repositories to be used to retrieve any "264 "Newline-separated list of repositories to be used to retrieve any "
239 "external build-dependencies when performing this build.")265 "external build-dependencies when performing this build.")
240266
267 can_be_rescored = exported(Bool(
268 title=_("Can be rescored"), required=True, readonly=True,
269 description=_(
270 "Whether this build record can be rescored manually.")))
271
272 can_be_retried = exported(Bool(
273 title=_("Can be retried"), required=True, readonly=True,
274 description=_("Whether this build record can be retried.")))
275
276 can_be_cancelled = exported(Bool(
277 title=_("Can be cancelled"), required=True, readonly=True,
278 description=_("Whether this build record can be cancelled.")))
279
280
281class IBuildFarmJobEdit(Interface):
282 """`IBuildFarmJob` methods that require launchpad.Edit."""
283
284 def resetBuild():
285 """Reset this build record to a clean state.
286
287 This method should only be called by `BuildFarmJobMixin.retry`, but
288 subclasses may override it to reset additional state.
289 """
290
291 @export_write_operation()
292 @operation_for_version("devel")
293 def retry():
294 """Restore the build record to its initial state.
295
296 Build record loses its history, is moved to NEEDSBUILD and a new
297 non-scored BuildQueue entry is created for it.
298 """
299
300 @export_write_operation()
301 @operation_for_version("devel")
302 def cancel():
303 """Cancel the build if it is either pending or in progress.
304
305 Check the can_be_cancelled property prior to calling this method to
306 find out if cancelling the build is possible.
307
308 If the build is in progress, it is marked as CANCELLING until the
309 buildd manager terminates the build and marks it CANCELLED. If the
310 build is not in progress, it is marked CANCELLED immediately and is
311 removed from the build queue.
312
313 If the build is not in a cancellable state, this method is a no-op.
314 """
315
316
317class IBuildFarmJobAdmin(Interface):
318 """`IBuildFarmJob` methods that require launchpad.Admin."""
319
320 @operation_parameters(score=Int(title=_("Score"), required=True))
321 @export_write_operation()
322 @operation_for_version("devel")
323 def rescore(score):
324 """Change the build's score."""
325
326
327@exported_as_webservice_entry(as_of='beta')
328class IBuildFarmJob(IBuildFarmJobView, IBuildFarmJobEdit, IBuildFarmJobAdmin):
329 """Operations that jobs for the build farm must implement."""
330
241331
242class ISpecificBuildFarmJobSource(Interface):332class ISpecificBuildFarmJobSource(Interface):
243 """A utility for retrieving objects of a specific IBuildFarmJob type.333 """A utility for retrieving objects of a specific IBuildFarmJob type.
diff --git a/lib/lp/buildmaster/interfaces/packagebuild.py b/lib/lp/buildmaster/interfaces/packagebuild.py
index 8d6b836..3af81f7 100644
--- a/lib/lp/buildmaster/interfaces/packagebuild.py
+++ b/lib/lp/buildmaster/interfaces/packagebuild.py
@@ -5,6 +5,7 @@
55
6__all__ = [6__all__ = [
7 'IPackageBuild',7 'IPackageBuild',
8 'IPackageBuildView',
8 ]9 ]
910
1011
@@ -18,7 +19,10 @@ from zope.schema import (
18 )19 )
1920
20from lp import _21from lp import _
21from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJob22from lp.buildmaster.interfaces.buildfarmjob import (
23 IBuildFarmJob,
24 IBuildFarmJobView,
25 )
22from lp.registry.interfaces.distribution import IDistribution26from lp.registry.interfaces.distribution import IDistribution
23from lp.registry.interfaces.distroseries import IDistroSeries27from lp.registry.interfaces.distroseries import IDistroSeries
24from lp.registry.interfaces.pocket import PackagePublishingPocket28from lp.registry.interfaces.pocket import PackagePublishingPocket
@@ -26,8 +30,8 @@ from lp.services.librarian.interfaces import ILibraryFileAlias
26from lp.soyuz.interfaces.archive import IArchive30from lp.soyuz.interfaces.archive import IArchive
2731
2832
29class IPackageBuild(IBuildFarmJob):33class IPackageBuildView(IBuildFarmJobView):
30 """Attributes and operations specific to package build jobs."""34 """`IPackageBuild` methods that require launchpad.View."""
3135
32 archive = exported(36 archive = exported(
33 Reference(37 Reference(
@@ -95,3 +99,7 @@ class IPackageBuild(IBuildFarmJob):
9599
96 :param changes: Changes file from the upload.100 :param changes: Changes file from the upload.
97 """101 """
102
103
104class IPackageBuild(IPackageBuildView, IBuildFarmJob):
105 """Attributes and operations specific to package build jobs."""
diff --git a/lib/lp/buildmaster/interfaces/webservice.py b/lib/lp/buildmaster/interfaces/webservice.py
index b899a8f..e6ca23e 100644
--- a/lib/lp/buildmaster/interfaces/webservice.py
+++ b/lib/lp/buildmaster/interfaces/webservice.py
@@ -10,6 +10,8 @@ which tells `lazr.restful` that it should look for webservice exports here.
10"""10"""
1111
12__all__ = [12__all__ = [
13 'CannotBeRescored',
14 'CannotBeRetried',
13 'IBuilder',15 'IBuilder',
14 'IBuilderSet',16 'IBuilderSet',
15 'IBuildFarmJob',17 'IBuildFarmJob',
@@ -21,7 +23,11 @@ from lp.buildmaster.interfaces.builder import (
21 IBuilder,23 IBuilder,
22 IBuilderSet,24 IBuilderSet,
23 )25 )
24from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJob26from lp.buildmaster.interfaces.buildfarmjob import (
27 CannotBeRescored,
28 CannotBeRetried,
29 IBuildFarmJob,
30 )
25from lp.buildmaster.interfaces.processor import (31from lp.buildmaster.interfaces.processor import (
26 IProcessor,32 IProcessor,
27 IProcessorSet,33 IProcessorSet,
diff --git a/lib/lp/buildmaster/model/buildfarmjob.py b/lib/lp/buildmaster/model/buildfarmjob.py
index 876546d..181be99 100644
--- a/lib/lp/buildmaster/model/buildfarmjob.py
+++ b/lib/lp/buildmaster/model/buildfarmjob.py
@@ -34,6 +34,8 @@ from lp.buildmaster.enums import (
34 BuildStatus,34 BuildStatus,
35 )35 )
36from lp.buildmaster.interfaces.buildfarmjob import (36from lp.buildmaster.interfaces.buildfarmjob import (
37 CannotBeRescored,
38 CannotBeRetried,
37 IBuildFarmJob,39 IBuildFarmJob,
38 IBuildFarmJobDB,40 IBuildFarmJobDB,
39 IBuildFarmJobSet,41 IBuildFarmJobSet,
@@ -262,6 +264,81 @@ class BuildFarmJobMixin:
262 del get_property_cache(self).buildqueue_record264 del get_property_cache(self).buildqueue_record
263 return queue_entry265 return queue_entry
264266
267 @property
268 def can_be_retried(self):
269 """See `IBuildFarmJob`.
270
271 Implementations should override this method to first check whether
272 their associated build behaviour would accept the build if it
273 succeeded.
274 """
275 failed_statuses = [
276 BuildStatus.FAILEDTOBUILD,
277 BuildStatus.MANUALDEPWAIT,
278 BuildStatus.CHROOTWAIT,
279 BuildStatus.FAILEDTOUPLOAD,
280 BuildStatus.CANCELLED,
281 BuildStatus.SUPERSEDED,
282 ]
283
284 # If the build is currently in any of the failed states,
285 # it may be retried.
286 return self.status in failed_statuses
287
288 @property
289 def can_be_rescored(self):
290 """See `IBuildFarmJob`."""
291 return (
292 self.buildqueue_record is not None and
293 self.status is BuildStatus.NEEDSBUILD)
294
295 @property
296 def can_be_cancelled(self):
297 """See `IBuildFarmJob`."""
298 if not self.buildqueue_record:
299 return False
300
301 cancellable_statuses = [
302 BuildStatus.BUILDING,
303 BuildStatus.NEEDSBUILD,
304 ]
305 return self.status in cancellable_statuses
306
307 def resetBuild(self):
308 """See `IBuildFarmJob`."""
309 self.build_farm_job.status = self.status = BuildStatus.NEEDSBUILD
310 self.build_farm_job.date_finished = self.date_finished = None
311 self.date_started = None
312 self.build_farm_job.builder = self.builder = None
313 self.log = None
314 self.upload_log = None
315 self.dependencies = None
316 self.failure_count = 0
317
318 def retry(self):
319 """See `IBuildFarmJob`."""
320 if not self.can_be_retried:
321 raise CannotBeRetried(self.id)
322
323 self.resetBuild()
324 self.queueBuild()
325
326 def rescore(self, score):
327 """See `IBuildFarmJob`."""
328 if not self.can_be_rescored:
329 raise CannotBeRescored(self.id)
330
331 self.buildqueue_record.manualScore(score)
332
333 def cancel(self):
334 """See `IBuildFarmJob`."""
335 if not self.can_be_cancelled:
336 return
337 # BuildQueue.cancel() will decide whether to go straight to
338 # CANCELLED, or go through CANCELLING to let buildd-manager clean up
339 # the builder.
340 self.buildqueue_record.cancel()
341
265342
266class SpecificBuildFarmJobSourceMixin:343class SpecificBuildFarmJobSourceMixin:
267344
diff --git a/lib/lp/buildmaster/tests/test_buildfarmjob.py b/lib/lp/buildmaster/tests/test_buildfarmjob.py
index a10dfb0..911582f 100644
--- a/lib/lp/buildmaster/tests/test_buildfarmjob.py
+++ b/lib/lp/buildmaster/tests/test_buildfarmjob.py
@@ -21,6 +21,7 @@ from lp.buildmaster.enums import (
21 BuildStatus,21 BuildStatus,
22 )22 )
23from lp.buildmaster.interfaces.buildfarmjob import (23from lp.buildmaster.interfaces.buildfarmjob import (
24 CannotBeRetried,
24 IBuildFarmJob,25 IBuildFarmJob,
25 IBuildFarmJobSet,26 IBuildFarmJobSet,
26 IBuildFarmJobSource,27 IBuildFarmJobSource,
@@ -119,6 +120,7 @@ class TestBuildFarmJobMixin(TestCaseWithFactory):
119120
120 def test_providesInterface(self):121 def test_providesInterface(self):
121 # BuildFarmJobMixin derivatives provide IBuildFarmJob122 # BuildFarmJobMixin derivatives provide IBuildFarmJob
123 login('admin@canonical.com')
122 self.assertProvides(self.build_farm_job, IBuildFarmJob)124 self.assertProvides(self.build_farm_job, IBuildFarmJob)
123125
124 def test_duration_none(self):126 def test_duration_none(self):
@@ -149,7 +151,7 @@ class TestBuildFarmJobMixin(TestCaseWithFactory):
149 def test_edit_build_farm_job(self):151 def test_edit_build_farm_job(self):
150 # Users with edit access can update attributes.152 # Users with edit access can update attributes.
151 login('admin@canonical.com')153 login('admin@canonical.com')
152 self.assertRaises(AssertionError, self.build_farm_job.retry)154 self.assertRaises(CannotBeRetried, self.build_farm_job.retry)
153155
154 def test_updateStatus_sets_status(self):156 def test_updateStatus_sets_status(self):
155 # updateStatus always sets status.157 # updateStatus always sets status.
diff --git a/lib/lp/buildmaster/tests/test_packagebuild.py b/lib/lp/buildmaster/tests/test_packagebuild.py
index 90d5adb..8a1b51c 100644
--- a/lib/lp/buildmaster/tests/test_packagebuild.py
+++ b/lib/lp/buildmaster/tests/test_packagebuild.py
@@ -35,6 +35,7 @@ class TestPackageBuildMixin(TestCaseWithFactory):
3535
36 def test_providesInterface(self):36 def test_providesInterface(self):
37 # PackageBuild provides IPackageBuild37 # PackageBuild provides IPackageBuild
38 login('admin@canonical.com')
38 self.assertProvides(self.package_build, IPackageBuild)39 self.assertProvides(self.package_build, IPackageBuild)
3940
40 def test_updateStatus_MANUALDEPWAIT_sets_dependencies(self):41 def test_updateStatus_MANUALDEPWAIT_sets_dependencies(self):
diff --git a/lib/lp/charms/interfaces/charmrecipebuild.py b/lib/lp/charms/interfaces/charmrecipebuild.py
index 4a33e19..2bbfe04 100644
--- a/lib/lp/charms/interfaces/charmrecipebuild.py
+++ b/lib/lp/charms/interfaces/charmrecipebuild.py
@@ -23,7 +23,6 @@ from lazr.restful.declarations import (
23 exported,23 exported,
24 exported_as_webservice_entry,24 exported_as_webservice_entry,
25 operation_for_version,25 operation_for_version,
26 operation_parameters,
27 )26 )
28from lazr.restful.fields import (27from lazr.restful.fields import (
29 CollectionField,28 CollectionField,
@@ -43,8 +42,15 @@ from zope.schema import (
43 )42 )
4443
45from lp import _44from lp import _
46from lp.buildmaster.interfaces.buildfarmjob import ISpecificBuildFarmJobSource45from lp.buildmaster.interfaces.buildfarmjob import (
47from lp.buildmaster.interfaces.packagebuild import IPackageBuild46 IBuildFarmJobAdmin,
47 IBuildFarmJobEdit,
48 ISpecificBuildFarmJobSource,
49 )
50from lp.buildmaster.interfaces.packagebuild import (
51 IPackageBuild,
52 IPackageBuildView,
53 )
48from lp.charms.interfaces.charmrecipe import (54from lp.charms.interfaces.charmrecipe import (
49 ICharmRecipe,55 ICharmRecipe,
50 ICharmRecipeBuildRequest,56 ICharmRecipeBuildRequest,
@@ -100,7 +106,7 @@ class CharmRecipeBuildStoreUploadStatus(EnumeratedType):
100 """)106 """)
101107
102108
103class ICharmRecipeBuildView(IPackageBuild):109class ICharmRecipeBuildView(IPackageBuildView):
104 """`ICharmRecipeBuild` attributes that require launchpad.View."""110 """`ICharmRecipeBuild` attributes that require launchpad.View."""
105111
106 build_request = Reference(112 build_request = Reference(
@@ -141,21 +147,6 @@ class ICharmRecipeBuildView(IPackageBuild):
141 title=_("Score of the related build farm job (if any)."),147 title=_("Score of the related build farm job (if any)."),
142 required=False, readonly=True))148 required=False, readonly=True))
143149
144 can_be_rescored = exported(Bool(
145 title=_("Can be rescored"),
146 required=True, readonly=True,
147 description=_("Whether this build record can be rescored manually.")))
148
149 can_be_retried = exported(Bool(
150 title=_("Can be retried"),
151 required=False, readonly=True,
152 description=_("Whether this build record can be retried.")))
153
154 can_be_cancelled = exported(Bool(
155 title=_("Can be cancelled"),
156 required=True, readonly=True,
157 description=_("Whether this build record can be cancelled.")))
158
159 eta = Datetime(150 eta = Datetime(
160 title=_("The datetime when the build job is estimated to complete."),151 title=_("The datetime when the build job is estimated to complete."),
161 readonly=True)152 readonly=True)
@@ -229,7 +220,7 @@ class ICharmRecipeBuildView(IPackageBuild):
229 """220 """
230221
231222
232class ICharmRecipeBuildEdit(Interface):223class ICharmRecipeBuildEdit(IBuildFarmJobEdit):
233 """`ICharmRecipeBuild` methods that require launchpad.Edit."""224 """`ICharmRecipeBuild` methods that require launchpad.Edit."""
234225
235 def addFile(lfa):226 def addFile(lfa):
@@ -248,48 +239,18 @@ class ICharmRecipeBuildEdit(Interface):
248 where an upload can be scheduled.239 where an upload can be scheduled.
249 """240 """
250241
251 @export_write_operation()
252 @operation_for_version("devel")
253 def retry():
254 """Restore the build record to its initial state.
255
256 Build record loses its history, is moved to NEEDSBUILD and a new
257 non-scored BuildQueue entry is created for it.
258 """
259
260 @export_write_operation()
261 @operation_for_version("devel")
262 def cancel():
263 """Cancel the build if it is either pending or in progress.
264
265 Check the can_be_cancelled property prior to calling this method to
266 find out if cancelling the build is possible.
267
268 If the build is in progress, it is marked as CANCELLING until the
269 buildd manager terminates the build and marks it CANCELLED. If the
270 build is not in progress, it is marked CANCELLED immediately and is
271 removed from the build queue.
272
273 If the build is not in a cancellable state, this method is a no-op.
274 """
275
276242
277class ICharmRecipeBuildAdmin(Interface):243class ICharmRecipeBuildAdmin(IBuildFarmJobAdmin):
278 """`ICharmRecipeBuild` methods that require launchpad.Admin."""244 """`ICharmRecipeBuild` methods that require launchpad.Admin."""
279245
280 @operation_parameters(score=Int(title=_("Score"), required=True))
281 @export_write_operation()
282 @operation_for_version("devel")
283 def rescore(score):
284 """Change the build's score."""
285
286246
287# XXX cjwatson 2021-09-15 bug=760849: "beta" is a lie to get WADL247# XXX cjwatson 2021-09-15 bug=760849: "beta" is a lie to get WADL
288# generation working. Individual attributes must set their version to248# generation working. Individual attributes must set their version to
289# "devel".249# "devel".
290@exported_as_webservice_entry(as_of="beta")250@exported_as_webservice_entry(as_of="beta")
291class ICharmRecipeBuild(251class ICharmRecipeBuild(
292 ICharmRecipeBuildView, ICharmRecipeBuildEdit, ICharmRecipeBuildAdmin):252 ICharmRecipeBuildView, ICharmRecipeBuildEdit, ICharmRecipeBuildAdmin,
253 IPackageBuild):
293 """A build record for a charm recipe."""254 """A build record for a charm recipe."""
294255
295256
diff --git a/lib/lp/charms/model/charmrecipebuild.py b/lib/lp/charms/model/charmrecipebuild.py
index cbcdd9e..2374924 100644
--- a/lib/lp/charms/model/charmrecipebuild.py
+++ b/lib/lp/charms/model/charmrecipebuild.py
@@ -220,70 +220,12 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
220220
221 @property221 @property
222 def can_be_retried(self):222 def can_be_retried(self):
223 """See `ICharmRecipeBuild`."""223 """See `IBuildFarmJob`."""
224 # First check that the behaviour would accept the build if it224 # First check that the behaviour would accept the build if it
225 # succeeded.225 # succeeded.
226 if self.distro_series.status == SeriesStatus.OBSOLETE:226 if self.distro_series.status == SeriesStatus.OBSOLETE:
227 return False227 return False
228228 return super().can_be_retried
229 failed_statuses = [
230 BuildStatus.FAILEDTOBUILD,
231 BuildStatus.MANUALDEPWAIT,
232 BuildStatus.CHROOTWAIT,
233 BuildStatus.FAILEDTOUPLOAD,
234 BuildStatus.CANCELLED,
235 BuildStatus.SUPERSEDED,
236 ]
237
238 # If the build is currently in any of the failed states,
239 # it may be retried.
240 return self.status in failed_statuses
241
242 @property
243 def can_be_rescored(self):
244 """See `ICharmRecipeBuild`."""
245 return (
246 self.buildqueue_record is not None and
247 self.status is BuildStatus.NEEDSBUILD)
248
249 @property
250 def can_be_cancelled(self):
251 """See `ICharmRecipeBuild`."""
252 if not self.buildqueue_record:
253 return False
254
255 cancellable_statuses = [
256 BuildStatus.BUILDING,
257 BuildStatus.NEEDSBUILD,
258 ]
259 return self.status in cancellable_statuses
260
261 def retry(self):
262 """See `ICharmRecipeBuild`."""
263 assert self.can_be_retried, "Build %s cannot be retried" % self.id
264 self.build_farm_job.status = self.status = BuildStatus.NEEDSBUILD
265 self.build_farm_job.date_finished = self.date_finished = None
266 self.date_started = None
267 self.build_farm_job.builder = self.builder = None
268 self.log = None
269 self.upload_log = None
270 self.dependencies = None
271 self.failure_count = 0
272 self.queueBuild()
273
274 def rescore(self, score):
275 """See `ICharmRecipeBuild`."""
276 assert self.can_be_rescored, "Build %s cannot be rescored" % self.id
277 self.buildqueue_record.manualScore(score)
278
279 def cancel(self):
280 """See `ICharmRecipeBuild`."""
281 if not self.can_be_cancelled:
282 return
283 # BuildQueue.cancel() will decide whether to go straight to
284 # CANCELLED, or go through CANCELLING to let buildd-manager clean up
285 # the slave.
286 self.buildqueue_record.cancel()
287229
288 def calculateScore(self):230 def calculateScore(self):
289 """See `IBuildFarmJob`."""231 """See `IBuildFarmJob`."""
diff --git a/lib/lp/code/configure.zcml b/lib/lp/code/configure.zcml
index fb8e0d5..b5399af 100644
--- a/lib/lp/code/configure.zcml
+++ b/lib/lp/code/configure.zcml
@@ -1196,6 +1196,9 @@
1196 <require1196 <require
1197 permission="launchpad.Edit"1197 permission="launchpad.Edit"
1198 interface="lp.code.interfaces.sourcepackagerecipebuild.ISourcePackageRecipeBuildEdit"/>1198 interface="lp.code.interfaces.sourcepackagerecipebuild.ISourcePackageRecipeBuildEdit"/>
1199 <require
1200 permission="launchpad.Admin"
1201 interface="lp.code.interfaces.sourcepackagerecipebuild.ISourcePackageRecipeBuildAdmin"/>
1199 </class>1202 </class>
12001203
1201 <securedutility1204 <securedutility
diff --git a/lib/lp/code/interfaces/sourcepackagerecipebuild.py b/lib/lp/code/interfaces/sourcepackagerecipebuild.py
index 82375f7..12fe659 100644
--- a/lib/lp/code/interfaces/sourcepackagerecipebuild.py
+++ b/lib/lp/code/interfaces/sourcepackagerecipebuild.py
@@ -8,26 +8,26 @@ __all__ = [
8 'ISourcePackageRecipeBuildSource',8 'ISourcePackageRecipeBuildSource',
9 ]9 ]
1010
11from lazr.restful.declarations import (11from lazr.restful.declarations import exported_as_webservice_entry
12 export_write_operation,
13 exported,
14 exported_as_webservice_entry,
15 operation_for_version,
16 )
17from lazr.restful.fields import (12from lazr.restful.fields import (
18 CollectionField,13 CollectionField,
19 Reference,14 Reference,
20 )15 )
21from zope.interface import Interface
22from zope.schema import (16from zope.schema import (
23 Bool,
24 Int,17 Int,
25 Object,18 Object,
26 )19 )
2720
28from lp import _21from lp import _
29from lp.buildmaster.interfaces.buildfarmjob import ISpecificBuildFarmJobSource22from lp.buildmaster.interfaces.buildfarmjob import (
30from lp.buildmaster.interfaces.packagebuild import IPackageBuild23 IBuildFarmJobAdmin,
24 IBuildFarmJobEdit,
25 ISpecificBuildFarmJobSource,
26 )
27from lp.buildmaster.interfaces.packagebuild import (
28 IPackageBuild,
29 IPackageBuildView,
30 )
31from lp.code.interfaces.sourcepackagerecipe import (31from lp.code.interfaces.sourcepackagerecipe import (
32 ISourcePackageRecipe,32 ISourcePackageRecipe,
33 ISourcePackageRecipeData,33 ISourcePackageRecipeData,
@@ -38,7 +38,8 @@ from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuild
38from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease38from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease
3939
4040
41class ISourcePackageRecipeBuildView(IPackageBuild):41class ISourcePackageRecipeBuildView(IPackageBuildView):
42 """`ISourcePackageRecipeBuild` attributes that require launchpad.View."""
4243
43 id = Int(title=_("Identifier for this build."))44 id = Int(title=_("Identifier for this build."))
4445
@@ -57,16 +58,6 @@ class ISourcePackageRecipeBuildView(IPackageBuild):
57 recipe = Object(58 recipe = Object(
58 schema=ISourcePackageRecipe, title=_("The recipe being built."))59 schema=ISourcePackageRecipe, title=_("The recipe being built."))
5960
60 can_be_rescored = exported(Bool(
61 title=_("Can be rescored"),
62 required=True, readonly=True,
63 description=_("Whether this build record can be rescored manually.")))
64
65 can_be_cancelled = exported(Bool(
66 title=_("Can be cancelled"),
67 required=True, readonly=True,
68 description=_("Whether this build record can be cancelled.")))
69
70 manifest = Object(61 manifest = Object(
71 schema=ISourcePackageRecipeData, title=_(62 schema=ISourcePackageRecipeData, title=_(
72 'A snapshot of the recipe for this build.'))63 'A snapshot of the recipe for this build.'))
@@ -82,31 +73,22 @@ class ISourcePackageRecipeBuildView(IPackageBuild):
82 """Return the file under +files with specified name."""73 """Return the file under +files with specified name."""
8374
8475
85class ISourcePackageRecipeBuildEdit(Interface):76class ISourcePackageRecipeBuildEdit(IBuildFarmJobEdit):
8677 """`ISourcePackageRecipeBuild` attributes that require launchpad.Edit."""
87 @export_write_operation()
88 @operation_for_version("devel")
89 def cancel():
90 """Cancel the build if it is either pending or in progress.
91
92 Check the can_be_cancelled property prior to calling this method to
93 find out if cancelling the build is possible.
94
95 If the build is in progress, it is marked as CANCELLING until the
96 buildd manager terminates the build and marks it CANCELLED. If the
97 build is not in progress, it is marked CANCELLED immediately and is
98 removed from the build queue.
99
100 If the build is not in a cancellable state, this method is a no-op.
101 """
10278
103 def destroySelf():79 def destroySelf():
104 """Delete the build itself."""80 """Delete the build itself."""
10581
10682
83class ISourcePackageRecipeBuildAdmin(IBuildFarmJobAdmin):
84 """`ISourcePackageRecipeBuild` attributes that require launchpad.Admin."""
85
86
107@exported_as_webservice_entry()87@exported_as_webservice_entry()
108class ISourcePackageRecipeBuild(ISourcePackageRecipeBuildView,88class ISourcePackageRecipeBuild(ISourcePackageRecipeBuildView,
109 ISourcePackageRecipeBuildEdit):89 ISourcePackageRecipeBuildEdit,
90 ISourcePackageRecipeBuildAdmin,
91 IPackageBuild):
110 """A build of a source package."""92 """A build of a source package."""
11193
11294
diff --git a/lib/lp/code/model/sourcepackagerecipebuild.py b/lib/lp/code/model/sourcepackagerecipebuild.py
index 7be41ee..273d606 100644
--- a/lib/lp/code/model/sourcepackagerecipebuild.py
+++ b/lib/lp/code/model/sourcepackagerecipebuild.py
@@ -263,31 +263,7 @@ class SourcePackageRecipeBuild(SpecificBuildFarmJobSourceMixin,
263 builds.append(build)263 builds.append(build)
264 return builds264 return builds
265265
266 @property266 can_be_retried = False
267 def can_be_rescored(self):
268 """See `IBuild`."""
269 return self.status is BuildStatus.NEEDSBUILD
270
271 @property
272 def can_be_cancelled(self):
273 """See `ISourcePackageRecipeBuild`."""
274 if not self.buildqueue_record:
275 return False
276
277 cancellable_statuses = [
278 BuildStatus.BUILDING,
279 BuildStatus.NEEDSBUILD,
280 ]
281 return self.status in cancellable_statuses
282
283 def cancel(self):
284 """See `ISourcePackageRecipeBuild`."""
285 if not self.can_be_cancelled:
286 return
287 # BuildQueue.cancel() will decide whether to go straight to
288 # CANCELLED, or go through CANCELLING to let buildd-manager
289 # clean up the slave.
290 self.buildqueue_record.cancel()
291267
292 def destroySelf(self):268 def destroySelf(self):
293 if self.buildqueue_record is not None:269 if self.buildqueue_record is not None:
diff --git a/lib/lp/oci/interfaces/ocirecipebuild.py b/lib/lp/oci/interfaces/ocirecipebuild.py
index 32aabe0..b366f87 100644
--- a/lib/lp/oci/interfaces/ocirecipebuild.py
+++ b/lib/lp/oci/interfaces/ocirecipebuild.py
@@ -25,7 +25,6 @@ from lazr.restful.declarations import (
25 exported,25 exported,
26 exported_as_webservice_entry,26 exported_as_webservice_entry,
27 operation_for_version,27 operation_for_version,
28 operation_parameters,
29 )28 )
30from lazr.restful.fields import (29from lazr.restful.fields import (
31 CollectionField,30 CollectionField,
@@ -47,8 +46,15 @@ from zope.schema import (
4746
48from lp import _47from lp import _
49from lp.app.interfaces.launchpad import IPrivacy48from lp.app.interfaces.launchpad import IPrivacy
50from lp.buildmaster.interfaces.buildfarmjob import ISpecificBuildFarmJobSource49from lp.buildmaster.interfaces.buildfarmjob import (
51from lp.buildmaster.interfaces.packagebuild import IPackageBuild50 IBuildFarmJobAdmin,
51 IBuildFarmJobEdit,
52 ISpecificBuildFarmJobSource,
53 )
54from lp.buildmaster.interfaces.packagebuild import (
55 IPackageBuild,
56 IPackageBuildView,
57 )
52from lp.oci.interfaces.ocirecipe import (58from lp.oci.interfaces.ocirecipe import (
53 IOCIRecipe,59 IOCIRecipe,
54 IOCIRecipeBuildRequest,60 IOCIRecipeBuildRequest,
@@ -141,7 +147,7 @@ class OCIRecipeBuildSetRegistryUploadStatus(EnumeratedType):
141 """)147 """)
142148
143149
144class IOCIRecipeBuildView(IPackageBuild, IPrivacy):150class IOCIRecipeBuildView(IPackageBuildView, IPrivacy):
145 """`IOCIRecipeBuild` attributes that require launchpad.View permission."""151 """`IOCIRecipeBuild` attributes that require launchpad.View permission."""
146152
147 build_request = Reference(153 build_request = Reference(
@@ -207,21 +213,6 @@ class IOCIRecipeBuildView(IPackageBuild, IPrivacy):
207 title=_("Score of the related build farm job (if any)."),213 title=_("Score of the related build farm job (if any)."),
208 required=False, readonly=True))214 required=False, readonly=True))
209215
210 can_be_rescored = exported(Bool(
211 title=_("Can be rescored"),
212 required=True, readonly=True,
213 description=_("Whether this build record can be rescored manually.")))
214
215 can_be_retried = exported(Bool(
216 title=_("Can be retried"),
217 required=True, readonly=True,
218 description=_("Whether this build record can be retried.")))
219
220 can_be_cancelled = exported(Bool(
221 title=_("Can be cancelled"),
222 required=True, readonly=True,
223 description=_("Whether this build record can be cancelled.")))
224
225 manifest = Attribute(_("The manifest of the image."))216 manifest = Attribute(_("The manifest of the image."))
226217
227 digests = Attribute(_("File containing the image digests."))218 digests = Attribute(_("File containing the image digests."))
@@ -266,7 +257,7 @@ class IOCIRecipeBuildView(IPackageBuild, IPrivacy):
266 """257 """
267258
268259
269class IOCIRecipeBuildEdit(Interface):260class IOCIRecipeBuildEdit(IBuildFarmJobEdit):
270 """`IOCIRecipeBuild` attributes that require launchpad.Edit permission."""261 """`IOCIRecipeBuild` attributes that require launchpad.Edit permission."""
271262
272 def addFile(lfa, layer_file_digest):263 def addFile(lfa, layer_file_digest):
@@ -286,46 +277,15 @@ class IOCIRecipeBuildEdit(Interface):
286 where an upload can be scheduled.277 where an upload can be scheduled.
287 """278 """
288279
289 @export_write_operation()
290 @operation_for_version("devel")
291 def retry():
292 """Restore the build record to its initial state.
293
294 Build record loses its history, is moved to NEEDSBUILD and a new
295 non-scored BuildQueue entry is created for it.
296 """
297
298 @export_write_operation()
299 @operation_for_version("devel")
300 def cancel():
301 """Cancel the build if it is either pending or in progress.
302
303 Check the can_be_cancelled property prior to calling this method to
304 find out if cancelling the build is possible.
305
306 If the build is in progress, it is marked as CANCELLING until the
307 buildd manager terminates the build and marks it CANCELLED. If the
308 build is not in progress, it is marked CANCELLED immediately and is
309 removed from the build queue.
310
311 If the build is not in a cancellable state, this method is a no-op.
312 """
313
314280
315class IOCIRecipeBuildAdmin(Interface):281class IOCIRecipeBuildAdmin(IBuildFarmJobAdmin):
316 """`IOCIRecipeBuild` attributes that require launchpad.Admin permission."""282 """`IOCIRecipeBuild` attributes that require launchpad.Admin permission."""
317283
318 @operation_parameters(score=Int(title=_("Score"), required=True))
319 @export_write_operation()
320 @operation_for_version("devel")
321 def rescore(score):
322 """Change the build's score."""
323
324284
325@exported_as_webservice_entry(285@exported_as_webservice_entry(
326 publish_web_link=True, as_of="devel", singular_name="oci_recipe_build")286 publish_web_link=True, as_of="devel", singular_name="oci_recipe_build")
327class IOCIRecipeBuild(IOCIRecipeBuildAdmin, IOCIRecipeBuildEdit,287class IOCIRecipeBuild(IOCIRecipeBuildAdmin, IOCIRecipeBuildEdit,
328 IOCIRecipeBuildView):288 IOCIRecipeBuildView, IPackageBuild):
329 """A build record for an OCI recipe."""289 """A build record for an OCI recipe."""
330290
331291
diff --git a/lib/lp/oci/model/ocirecipebuild.py b/lib/lp/oci/model/ocirecipebuild.py
index fc4e3e2..13cc08a 100644
--- a/lib/lp/oci/model/ocirecipebuild.py
+++ b/lib/lp/oci/model/ocirecipebuild.py
@@ -214,43 +214,12 @@ class OCIRecipeBuild(PackageBuildMixin, StormBase):
214214
215 @property215 @property
216 def can_be_retried(self):216 def can_be_retried(self):
217 """See `IOCIRecipeBuild`."""217 """See `IBuildFarmJob`."""
218 # First check that the behaviour would accept the build if it218 # First check that the behaviour would accept the build if it
219 # succeeded.219 # succeeded.
220 if self.distro_series.status == SeriesStatus.OBSOLETE:220 if self.distro_series.status == SeriesStatus.OBSOLETE:
221 return False221 return False
222222 return super().can_be_retried
223 failed_statuses = [
224 BuildStatus.FAILEDTOBUILD,
225 BuildStatus.MANUALDEPWAIT,
226 BuildStatus.CHROOTWAIT,
227 BuildStatus.FAILEDTOUPLOAD,
228 BuildStatus.CANCELLED,
229 BuildStatus.SUPERSEDED,
230 ]
231
232 # If the build is currently in any of the failed states,
233 # it may be retried.
234 return self.status in failed_statuses
235
236 @property
237 def can_be_rescored(self):
238 """See `IOCIRecipeBuild`."""
239 return (
240 self.buildqueue_record is not None and
241 self.status is BuildStatus.NEEDSBUILD)
242
243 @property
244 def can_be_cancelled(self):
245 """See `IOCIRecipeBuild`."""
246 if not self.buildqueue_record:
247 return False
248
249 cancellable_statuses = [
250 BuildStatus.BUILDING,
251 BuildStatus.NEEDSBUILD,
252 ]
253 return self.status in cancellable_statuses
254223
255 @property224 @property
256 def is_private(self):225 def is_private(self):
@@ -271,33 +240,6 @@ class OCIRecipeBuild(PackageBuildMixin, StormBase):
271240
272 private = is_private241 private = is_private
273242
274 def retry(self):
275 """See `IOCIRecipeBuild`."""
276 assert self.can_be_retried, "Build %s cannot be retried" % self.id
277 self.build_farm_job.status = self.status = BuildStatus.NEEDSBUILD
278 self.build_farm_job.date_finished = self.date_finished = None
279 self.date_started = None
280 self.build_farm_job.builder = self.builder = None
281 self.log = None
282 self.upload_log = None
283 self.dependencies = None
284 self.failure_count = 0
285 self.queueBuild()
286
287 def rescore(self, score):
288 """See `IOCIRecipeBuild`."""
289 assert self.can_be_rescored, "Build %s cannot be rescored" % self.id
290 self.buildqueue_record.manualScore(score)
291
292 def cancel(self):
293 """See `IOCIRecipeBuild`."""
294 if not self.can_be_cancelled:
295 return
296 # BuildQueue.cancel() will decide whether to go straight to
297 # CANCELLED, or go through CANCELLING to let buildd-manager clean up
298 # the slave.
299 self.buildqueue_record.cancel()
300
301 def calculateScore(self):243 def calculateScore(self):
302 # XXX twom 2020-02-11 - This might need an addition?244 # XXX twom 2020-02-11 - This might need an addition?
303 return 2510245 return 2510
diff --git a/lib/lp/security.py b/lib/lp/security.py
index 9f6690e..03c580f 100644
--- a/lib/lp/security.py
+++ b/lib/lp/security.py
@@ -1666,18 +1666,6 @@ class EditCodeImportMachine(OnlyBazaarExpertsAndAdmins):
1666 usedfor = ICodeImportMachine1666 usedfor = ICodeImportMachine
16671667
16681668
1669class AdminSourcePackageRecipeBuilds(AuthorizationBase):
1670 """Control who can edit SourcePackageRecipeBuilds.
1671
1672 Access is restricted to Buildd Admins.
1673 """
1674 permission = 'launchpad.Admin'
1675 usedfor = ISourcePackageRecipeBuild
1676
1677 def checkAuthenticated(self, user):
1678 return user.in_buildd_admin
1679
1680
1681class AdminDistributionTranslations(AuthorizationBase):1669class AdminDistributionTranslations(AuthorizationBase):
1682 """Class for deciding who can administer distribution translations.1670 """Class for deciding who can administer distribution translations.
16831671
diff --git a/lib/lp/snappy/interfaces/snapbuild.py b/lib/lp/snappy/interfaces/snapbuild.py
index 6d41fbe..4012391 100644
--- a/lib/lp/snappy/interfaces/snapbuild.py
+++ b/lib/lp/snappy/interfaces/snapbuild.py
@@ -25,7 +25,6 @@ from lazr.restful.declarations import (
25 exported,25 exported,
26 exported_as_webservice_entry,26 exported_as_webservice_entry,
27 operation_for_version,27 operation_for_version,
28 operation_parameters,
29 )28 )
30from lazr.restful.fields import (29from lazr.restful.fields import (
31 CollectionField,30 CollectionField,
@@ -48,8 +47,15 @@ from zope.schema import (
4847
49from lp import _48from lp import _
50from lp.app.interfaces.launchpad import IPrivacy49from lp.app.interfaces.launchpad import IPrivacy
51from lp.buildmaster.interfaces.buildfarmjob import ISpecificBuildFarmJobSource50from lp.buildmaster.interfaces.buildfarmjob import (
52from lp.buildmaster.interfaces.packagebuild import IPackageBuild51 IBuildFarmJobAdmin,
52 IBuildFarmJobEdit,
53 ISpecificBuildFarmJobSource,
54 )
55from lp.buildmaster.interfaces.packagebuild import (
56 IPackageBuild,
57 IPackageBuildView,
58 )
53from lp.registry.interfaces.person import IPerson59from lp.registry.interfaces.person import IPerson
54from lp.registry.interfaces.pocket import PackagePublishingPocket60from lp.registry.interfaces.pocket import PackagePublishingPocket
55from lp.services.database.constants import DEFAULT61from lp.services.database.constants import DEFAULT
@@ -128,7 +134,7 @@ class SnapBuildStoreUploadStatus(EnumeratedType):
128 """)134 """)
129135
130136
131class ISnapBuildView(IPackageBuild, IPrivacy):137class ISnapBuildView(IPackageBuildView, IPrivacy):
132 """`ISnapBuild` attributes that require launchpad.View permission."""138 """`ISnapBuild` attributes that require launchpad.View permission."""
133139
134 build_request = Reference(140 build_request = Reference(
@@ -189,21 +195,6 @@ class ISnapBuildView(IPackageBuild, IPrivacy):
189 title=_("Score of the related build farm job (if any)."),195 title=_("Score of the related build farm job (if any)."),
190 required=False, readonly=True))196 required=False, readonly=True))
191197
192 can_be_rescored = exported(Bool(
193 title=_("Can be rescored"),
194 required=True, readonly=True,
195 description=_("Whether this build record can be rescored manually.")))
196
197 can_be_retried = exported(Bool(
198 title=_("Can be retried"),
199 required=False, readonly=True,
200 description=_("Whether this build record can be retried.")))
201
202 can_be_cancelled = exported(Bool(
203 title=_("Can be cancelled"),
204 required=True, readonly=True,
205 description=_("Whether this build record can be cancelled.")))
206
207 eta = Datetime(198 eta = Datetime(
208 title=_("The datetime when the build job is estimated to complete."),199 title=_("The datetime when the build job is estimated to complete."),
209 readonly=True)200 readonly=True)
@@ -298,7 +289,7 @@ class ISnapBuildView(IPackageBuild, IPrivacy):
298 :return: A collection of URLs for this build."""289 :return: A collection of URLs for this build."""
299290
300291
301class ISnapBuildEdit(Interface):292class ISnapBuildEdit(IBuildFarmJobEdit):
302 """`ISnapBuild` attributes that require launchpad.Edit."""293 """`ISnapBuild` attributes that require launchpad.Edit."""
303294
304 def addFile(lfa):295 def addFile(lfa):
@@ -317,47 +308,17 @@ class ISnapBuildEdit(Interface):
317 where an upload can be scheduled.308 where an upload can be scheduled.
318 """309 """
319310
320 @export_write_operation()
321 @operation_for_version("devel")
322 def retry():
323 """Restore the build record to its initial state.
324
325 Build record loses its history, is moved to NEEDSBUILD and a new
326 non-scored BuildQueue entry is created for it.
327 """
328
329 @export_write_operation()
330 @operation_for_version("devel")
331 def cancel():
332 """Cancel the build if it is either pending or in progress.
333
334 Check the can_be_cancelled property prior to calling this method to
335 find out if cancelling the build is possible.
336
337 If the build is in progress, it is marked as CANCELLING until the
338 buildd manager terminates the build and marks it CANCELLED. If the
339 build is not in progress, it is marked CANCELLED immediately and is
340 removed from the build queue.
341
342 If the build is not in a cancellable state, this method is a no-op.
343 """
344
345311
346class ISnapBuildAdmin(Interface):312class ISnapBuildAdmin(IBuildFarmJobAdmin):
347 """`ISnapBuild` attributes that require launchpad.Admin."""313 """`ISnapBuild` attributes that require launchpad.Admin."""
348314
349 @operation_parameters(score=Int(title=_("Score"), required=True))
350 @export_write_operation()
351 @operation_for_version("devel")
352 def rescore(score):
353 """Change the build's score."""
354
355315
356# XXX cjwatson 2014-05-06 bug=760849: "beta" is a lie to get WADL316# XXX cjwatson 2014-05-06 bug=760849: "beta" is a lie to get WADL
357# generation working. Individual attributes must set their version to317# generation working. Individual attributes must set their version to
358# "devel".318# "devel".
359@exported_as_webservice_entry(as_of="beta")319@exported_as_webservice_entry(as_of="beta")
360class ISnapBuild(ISnapBuildView, ISnapBuildEdit, ISnapBuildAdmin):320class ISnapBuild(
321 ISnapBuildView, ISnapBuildEdit, ISnapBuildAdmin, IPackageBuild):
361 """Build information for snap package builds."""322 """Build information for snap package builds."""
362323
363324
diff --git a/lib/lp/snappy/model/snapbuild.py b/lib/lp/snappy/model/snapbuild.py
index a4405de..24fa63f 100644
--- a/lib/lp/snappy/model/snapbuild.py
+++ b/lib/lp/snappy/model/snapbuild.py
@@ -284,70 +284,12 @@ class SnapBuild(PackageBuildMixin, Storm):
284284
285 @property285 @property
286 def can_be_retried(self):286 def can_be_retried(self):
287 """See `ISnapBuild`."""287 """See `IBuildFarmJob`."""
288 # First check that the behaviour would accept the build if it288 # First check that the behaviour would accept the build if it
289 # succeeded.289 # succeeded.
290 if self.distro_series.status == SeriesStatus.OBSOLETE:290 if self.distro_series.status == SeriesStatus.OBSOLETE:
291 return False291 return False
292292 return super().can_be_retried
293 failed_statuses = [
294 BuildStatus.FAILEDTOBUILD,
295 BuildStatus.MANUALDEPWAIT,
296 BuildStatus.CHROOTWAIT,
297 BuildStatus.FAILEDTOUPLOAD,
298 BuildStatus.CANCELLED,
299 BuildStatus.SUPERSEDED,
300 ]
301
302 # If the build is currently in any of the failed states,
303 # it may be retried.
304 return self.status in failed_statuses
305
306 @property
307 def can_be_rescored(self):
308 """See `ISnapBuild`."""
309 return (
310 self.buildqueue_record is not None and
311 self.status is BuildStatus.NEEDSBUILD)
312
313 @property
314 def can_be_cancelled(self):
315 """See `ISnapBuild`."""
316 if not self.buildqueue_record:
317 return False
318
319 cancellable_statuses = [
320 BuildStatus.BUILDING,
321 BuildStatus.NEEDSBUILD,
322 ]
323 return self.status in cancellable_statuses
324
325 def retry(self):
326 """See `ISnapBuild`."""
327 assert self.can_be_retried, "Build %s cannot be retried" % self.id
328 self.build_farm_job.status = self.status = BuildStatus.NEEDSBUILD
329 self.build_farm_job.date_finished = self.date_finished = None
330 self.date_started = None
331 self.build_farm_job.builder = self.builder = None
332 self.log = None
333 self.upload_log = None
334 self.dependencies = None
335 self.failure_count = 0
336 self.queueBuild()
337
338 def rescore(self, score):
339 """See `ISnapBuild`."""
340 assert self.can_be_rescored, "Build %s cannot be rescored" % self.id
341 self.buildqueue_record.manualScore(score)
342
343 def cancel(self):
344 """See `ISnapBuild`."""
345 if not self.can_be_cancelled:
346 return
347 # BuildQueue.cancel() will decide whether to go straight to
348 # CANCELLED, or go through CANCELLING to let buildd-manager clean up
349 # the slave.
350 self.buildqueue_record.cancel()
351293
352 def calculateScore(self):294 def calculateScore(self):
353 return 2510 + self.archive.relative_build_score295 return 2510 + self.archive.relative_build_score
diff --git a/lib/lp/soyuz/interfaces/binarypackagebuild.py b/lib/lp/soyuz/interfaces/binarypackagebuild.py
index 3771af8..edc245f 100644
--- a/lib/lp/soyuz/interfaces/binarypackagebuild.py
+++ b/lib/lp/soyuz/interfaces/binarypackagebuild.py
@@ -5,21 +5,17 @@
55
6__all__ = [6__all__ = [
7 'BuildSetStatus',7 'BuildSetStatus',
8 'CannotBeRescored',
9 'IBinaryPackageBuild',8 'IBinaryPackageBuild',
10 'IBuildRescoreForm',9 'IBuildRescoreForm',
11 'IBinaryPackageBuildSet',10 'IBinaryPackageBuildSet',
12 'UnparsableDependencies',11 'UnparsableDependencies',
13 ]12 ]
1413
15import http.client
16
17from lazr.enum import (14from lazr.enum import (
18 EnumeratedType,15 EnumeratedType,
19 Item,16 Item,
20 )17 )
21from lazr.restful.declarations import (18from lazr.restful.declarations import (
22 error_status,
23 export_read_operation,19 export_read_operation,
24 export_write_operation,20 export_write_operation,
25 exported,21 exported,
@@ -42,24 +38,25 @@ from zope.schema import (
4238
43from lp import _39from lp import _
44from lp.buildmaster.enums import BuildStatus40from lp.buildmaster.enums import BuildStatus
45from lp.buildmaster.interfaces.buildfarmjob import ISpecificBuildFarmJobSource41from lp.buildmaster.interfaces.buildfarmjob import (
46from lp.buildmaster.interfaces.packagebuild import IPackageBuild42 IBuildFarmJobAdmin,
43 IBuildFarmJobEdit,
44 ISpecificBuildFarmJobSource,
45 )
46from lp.buildmaster.interfaces.packagebuild import (
47 IPackageBuild,
48 IPackageBuildView,
49 )
47from lp.buildmaster.interfaces.processor import IProcessor50from lp.buildmaster.interfaces.processor import IProcessor
48from lp.soyuz.interfaces.publishing import ISourcePackagePublishingHistory51from lp.soyuz.interfaces.publishing import ISourcePackagePublishingHistory
49from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease52from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease
5053
5154
52@error_status(http.client.BAD_REQUEST)
53class CannotBeRescored(Exception):
54 """Raised when rescoring a build that cannot be rescored."""
55 _message_prefix = "Cannot rescore build"
56
57
58class UnparsableDependencies(Exception):55class UnparsableDependencies(Exception):
59 """Raised when parsing invalid dependencies on a binary package."""56 """Raised when parsing invalid dependencies on a binary package."""
6057
6158
62class IBinaryPackageBuildView(IPackageBuild):59class IBinaryPackageBuildView(IPackageBuildView):
63 """A Build interface for items requiring launchpad.View."""60 """A Build interface for items requiring launchpad.View."""
64 id = Int(title=_('ID'), required=True, readonly=True)61 id = Int(title=_('ID'), required=True, readonly=True)
6562
@@ -116,25 +113,6 @@ class IBinaryPackageBuildView(IPackageBuild):
116 "A list of distroarchseriesbinarypackages that resulted from this"113 "A list of distroarchseriesbinarypackages that resulted from this"
117 "build, ordered by name.")114 "build, ordered by name.")
118115
119 can_be_rescored = exported(
120 Bool(
121 title=_("Can Be Rescored"), required=False, readonly=True,
122 description=_(
123 "Whether or not this build record can be rescored "
124 "manually.")))
125
126 can_be_retried = exported(
127 Bool(
128 title=_("Can Be Retried"), required=False, readonly=True,
129 description=_(
130 "Whether or not this build record can be retried.")))
131
132 can_be_cancelled = exported(
133 Bool(
134 title=_("Can Be Cancelled"), required=False, readonly=True,
135 description=_(
136 "Whether or not this build record can be cancelled.")))
137
138 upload_changesfile = Attribute(116 upload_changesfile = Attribute(
139 "The `LibraryFileAlias` object containing the changes file which "117 "The `LibraryFileAlias` object containing the changes file which "
140 "was originally uploaded with the results of this build. It's "118 "was originally uploaded with the results of this build. It's "
@@ -243,10 +221,19 @@ class IBinaryPackageBuildView(IPackageBuild):
243 """221 """
244222
245223
246class IBinaryPackageBuildEdit(Interface):224class IBinaryPackageBuildEdit(IBuildFarmJobEdit):
247 """A Build interface for items requiring launchpad.Edit."""225 """A Build interface for items requiring launchpad.Edit."""
248226
227 def addBuildInfo(buildinfo):
228 """Add a buildinfo file to this build.
229
230 :param buildinfo: An `ILibraryFileAlias`.
231 """
232
233 # Redeclaring from IBuildFarmJobEdit.retry since this was available in
234 # the beta version.
249 @export_write_operation()235 @export_write_operation()
236 @operation_for_version("beta")
250 def retry():237 def retry():
251 """Restore the build record to its initial state.238 """Restore the build record to its initial state.
252239
@@ -254,28 +241,6 @@ class IBinaryPackageBuildEdit(Interface):
254 non-scored BuildQueue entry is created for it.241 non-scored BuildQueue entry is created for it.
255 """242 """
256243
257 @export_write_operation()
258 @operation_for_version("devel")
259 def cancel():
260 """Cancel the build if it is either pending or in progress.
261
262 Check the can_be_cancelled property prior to calling this method to
263 find out if cancelling the build is possible.
264
265 If the build is in progress, it is marked as CANCELLING until the
266 buildd manager terminates the build and marks it CANCELLED. If the
267 build is not in progress, it is marked CANCELLED immediately and is
268 removed from the build queue.
269
270 If the build is not in a cancellable state, this method is a no-op.
271 """
272
273 def addBuildInfo(buildinfo):
274 """Add a buildinfo file to this build.
275
276 :param buildinfo: An `ILibraryFileAlias`.
277 """
278
279244
280class IBinaryPackageBuildRestricted(Interface):245class IBinaryPackageBuildRestricted(Interface):
281 """Restricted `IBinaryPackageBuild` attributes.246 """Restricted `IBinaryPackageBuild` attributes.
@@ -297,11 +262,14 @@ class IBinaryPackageBuildRestricted(Interface):
297 exported_as="external_dependencies")262 exported_as="external_dependencies")
298263
299264
300class IBinaryPackageBuildAdmin(Interface):265class IBinaryPackageBuildAdmin(IBuildFarmJobAdmin):
301 """A Build interface for items requiring launchpad.Admin."""266 """A Build interface for items requiring launchpad.Admin."""
302267
268 # Redeclaring from IBuildFarmJobEdit.rescore since this was available in
269 # the beta version.
303 @operation_parameters(score=Int(title=_("Score"), required=True))270 @operation_parameters(score=Int(title=_("Score"), required=True))
304 @export_write_operation()271 @export_write_operation()
272 @operation_for_version("beta")
305 def rescore(score):273 def rescore(score):
306 """Change the build's score."""274 """Change the build's score."""
307275
@@ -309,7 +277,8 @@ class IBinaryPackageBuildAdmin(Interface):
309@exported_as_webservice_entry(singular_name='build', plural_name='builds')277@exported_as_webservice_entry(singular_name='build', plural_name='builds')
310class IBinaryPackageBuild(278class IBinaryPackageBuild(
311 IBinaryPackageBuildView, IBinaryPackageBuildEdit,279 IBinaryPackageBuildView, IBinaryPackageBuildEdit,
312 IBinaryPackageBuildRestricted, IBinaryPackageBuildAdmin):280 IBinaryPackageBuildRestricted, IBinaryPackageBuildAdmin,
281 IPackageBuild):
313 """A Build interface"""282 """A Build interface"""
314283
315284
diff --git a/lib/lp/soyuz/interfaces/livefsbuild.py b/lib/lp/soyuz/interfaces/livefsbuild.py
index cbba840..5ed7098 100644
--- a/lib/lp/soyuz/interfaces/livefsbuild.py
+++ b/lib/lp/soyuz/interfaces/livefsbuild.py
@@ -11,11 +11,9 @@ __all__ = [
1111
12from lazr.restful.declarations import (12from lazr.restful.declarations import (
13 export_read_operation,13 export_read_operation,
14 export_write_operation,
15 exported,14 exported,
16 exported_as_webservice_entry,15 exported_as_webservice_entry,
17 operation_for_version,16 operation_for_version,
18 operation_parameters,
19 )17 )
20from lazr.restful.fields import Reference18from lazr.restful.fields import Reference
21from zope.interface import Interface19from zope.interface import Interface
@@ -29,8 +27,15 @@ from zope.schema import (
2927
30from lp import _28from lp import _
31from lp.app.interfaces.launchpad import IPrivacy29from lp.app.interfaces.launchpad import IPrivacy
32from lp.buildmaster.interfaces.buildfarmjob import ISpecificBuildFarmJobSource30from lp.buildmaster.interfaces.buildfarmjob import (
33from lp.buildmaster.interfaces.packagebuild import IPackageBuild31 IBuildFarmJobAdmin,
32 IBuildFarmJobEdit,
33 ISpecificBuildFarmJobSource,
34 )
35from lp.buildmaster.interfaces.packagebuild import (
36 IPackageBuild,
37 IPackageBuildView,
38 )
34from lp.registry.interfaces.person import IPerson39from lp.registry.interfaces.person import IPerson
35from lp.registry.interfaces.pocket import PackagePublishingPocket40from lp.registry.interfaces.pocket import PackagePublishingPocket
36from lp.services.database.constants import DEFAULT41from lp.services.database.constants import DEFAULT
@@ -53,7 +58,7 @@ class ILiveFSFile(Interface):
53 required=True, readonly=True)58 required=True, readonly=True)
5459
5560
56class ILiveFSBuildView(IPackageBuild, IPrivacy):61class ILiveFSBuildView(IPackageBuildView, IPrivacy):
57 """`ILiveFSBuild` attributes that require launchpad.View permission."""62 """`ILiveFSBuild` attributes that require launchpad.View permission."""
5863
59 requester = exported(Reference(64 requester = exported(Reference(
@@ -103,16 +108,6 @@ class ILiveFSBuildView(IPackageBuild, IPrivacy):
103 title=_("Score of the related build farm job (if any)."),108 title=_("Score of the related build farm job (if any)."),
104 required=False, readonly=True))109 required=False, readonly=True))
105110
106 can_be_rescored = exported(Bool(
107 title=_("Can be rescored"),
108 required=True, readonly=True,
109 description=_("Whether this build record can be rescored manually.")))
110
111 can_be_cancelled = exported(Bool(
112 title=_("Can be cancelled"),
113 required=True, readonly=True,
114 description=_("Whether this build record can be cancelled.")))
115
116 def getFiles():111 def getFiles():
117 """Retrieve the build's `ILiveFSFile` records.112 """Retrieve the build's `ILiveFSFile` records.
118113
@@ -144,7 +139,7 @@ class ILiveFSBuildView(IPackageBuild, IPrivacy):
144 :return: A collection of URLs for this build."""139 :return: A collection of URLs for this build."""
145140
146141
147class ILiveFSBuildEdit(Interface):142class ILiveFSBuildEdit(IBuildFarmJobEdit):
148 """`ILiveFSBuild` attributes that require launchpad.Edit."""143 """`ILiveFSBuild` attributes that require launchpad.Edit."""
149144
150 def addFile(lfa):145 def addFile(lfa):
@@ -154,38 +149,17 @@ class ILiveFSBuildEdit(Interface):
154 :return: An `ILiveFSFile`.149 :return: An `ILiveFSFile`.
155 """150 """
156151
157 @export_write_operation()
158 @operation_for_version("devel")
159 def cancel():
160 """Cancel the build if it is either pending or in progress.
161
162 Check the can_be_cancelled property prior to calling this method to
163 find out if cancelling the build is possible.
164
165 If the build is in progress, it is marked as CANCELLING until the
166 buildd manager terminates the build and marks it CANCELLED. If the
167 build is not in progress, it is marked CANCELLED immediately and is
168 removed from the build queue.
169
170 If the build is not in a cancellable state, this method is a no-op.
171 """
172
173152
174class ILiveFSBuildAdmin(Interface):153class ILiveFSBuildAdmin(IBuildFarmJobAdmin):
175 """`ILiveFSBuild` attributes that require launchpad.Admin."""154 """`ILiveFSBuild` attributes that require launchpad.Admin."""
176155
177 @operation_parameters(score=Int(title=_("Score"), required=True))
178 @export_write_operation()
179 @operation_for_version("devel")
180 def rescore(score):
181 """Change the build's score."""
182
183156
184# XXX cjwatson 2014-05-06 bug=760849: "beta" is a lie to get WADL157# XXX cjwatson 2014-05-06 bug=760849: "beta" is a lie to get WADL
185# generation working. Individual attributes must set their version to158# generation working. Individual attributes must set their version to
186# "devel".159# "devel".
187@exported_as_webservice_entry(singular_name="livefs_build", as_of="beta")160@exported_as_webservice_entry(singular_name="livefs_build", as_of="beta")
188class ILiveFSBuild(ILiveFSBuildView, ILiveFSBuildEdit, ILiveFSBuildAdmin):161class ILiveFSBuild(
162 ILiveFSBuildView, ILiveFSBuildEdit, ILiveFSBuildAdmin, IPackageBuild):
189 """Build information for live filesystem builds."""163 """Build information for live filesystem builds."""
190164
191165
diff --git a/lib/lp/soyuz/interfaces/webservice.py b/lib/lp/soyuz/interfaces/webservice.py
index 90ef0e1..b8ecec2 100644
--- a/lib/lp/soyuz/interfaces/webservice.py
+++ b/lib/lp/soyuz/interfaces/webservice.py
@@ -13,7 +13,6 @@ __all__ = [
13 'AlreadySubscribed',13 'AlreadySubscribed',
14 'ArchiveDisabled',14 'ArchiveDisabled',
15 'ArchiveNotPrivate',15 'ArchiveNotPrivate',
16 'CannotBeRescored',
17 'CannotCopy',16 'CannotCopy',
18 'CannotSwitchPrivacy',17 'CannotSwitchPrivacy',
19 'CannotUploadToArchive',18 'CannotUploadToArchive',
@@ -81,10 +80,7 @@ from lp.soyuz.interfaces.archive import (
81from lp.soyuz.interfaces.archivedependency import IArchiveDependency80from lp.soyuz.interfaces.archivedependency import IArchiveDependency
82from lp.soyuz.interfaces.archivepermission import IArchivePermission81from lp.soyuz.interfaces.archivepermission import IArchivePermission
83from lp.soyuz.interfaces.archivesubscriber import IArchiveSubscriber82from lp.soyuz.interfaces.archivesubscriber import IArchiveSubscriber
84from lp.soyuz.interfaces.binarypackagebuild import (83from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuild
85 CannotBeRescored,
86 IBinaryPackageBuild,
87 )
88from lp.soyuz.interfaces.binarypackagerelease import (84from lp.soyuz.interfaces.binarypackagerelease import (
89 IBinaryPackageReleaseDownloadCount,85 IBinaryPackageReleaseDownloadCount,
90 )86 )
diff --git a/lib/lp/soyuz/model/binarypackagebuild.py b/lib/lp/soyuz/model/binarypackagebuild.py
index 511c975..f51f565 100644
--- a/lib/lp/soyuz/model/binarypackagebuild.py
+++ b/lib/lp/soyuz/model/binarypackagebuild.py
@@ -101,7 +101,6 @@ from lp.soyuz.interfaces.archive import (
101 )101 )
102from lp.soyuz.interfaces.binarypackagebuild import (102from lp.soyuz.interfaces.binarypackagebuild import (
103 BuildSetStatus,103 BuildSetStatus,
104 CannotBeRescored,
105 IBinaryPackageBuild,104 IBinaryPackageBuild,
106 IBinaryPackageBuildSet,105 IBinaryPackageBuildSet,
107 UnparsableDependencies,106 UnparsableDependencies,
@@ -475,64 +474,19 @@ class BinaryPackageBuild(PackageBuildMixin, SQLBase):
475474
476 @property475 @property
477 def can_be_retried(self):476 def can_be_retried(self):
478 """See `IBuild`."""477 """See `IBuildFarmJob`."""
479 # First check that the slave scanner would pick up the build record478 # First check that the slave scanner would pick up the build record
480 # if we reset it.479 # if we reset it.
481 if not self.archive.canModifySuite(self.distro_series, self.pocket):480 if not self.archive.canModifySuite(self.distro_series, self.pocket):
482 # The slave scanner would not pick this up, so it cannot be481 # The slave scanner would not pick this up, so it cannot be
483 # re-tried.482 # re-tried.
484 return False483 return False
484 return super().can_be_retried
485485
486 failed_statuses = [486 def resetBuild(self):
487 BuildStatus.FAILEDTOBUILD,487 """See `IBuildFarmJob`."""
488 BuildStatus.MANUALDEPWAIT,488 super().resetBuild()
489 BuildStatus.CHROOTWAIT,
490 BuildStatus.FAILEDTOUPLOAD,
491 BuildStatus.CANCELLED,
492 BuildStatus.SUPERSEDED,
493 ]
494
495 # If the build is currently in any of the failed states,
496 # it may be retried.
497 return self.status in failed_statuses
498
499 @property
500 def can_be_rescored(self):
501 """See `IBuild`."""
502 return self.status is BuildStatus.NEEDSBUILD
503
504 @property
505 def can_be_cancelled(self):
506 """See `IBuild`."""
507 if not self.buildqueue_record:
508 return False
509
510 cancellable_statuses = [
511 BuildStatus.BUILDING,
512 BuildStatus.NEEDSBUILD,
513 ]
514 return self.status in cancellable_statuses
515
516 def retry(self):
517 """See `IBuild`."""
518 assert self.can_be_retried, "Build %s cannot be retried" % self.id
519 self.build_farm_job.status = self.status = BuildStatus.NEEDSBUILD
520 self.build_farm_job.date_finished = self.date_finished = None
521 self.date_started = None
522 self.build_farm_job.builder = self.builder = None
523 self.log = None
524 self.upload_log = None
525 self.dependencies = None
526 self.failure_count = 0
527 self.virtualized = is_build_virtualized(self.archive, self.processor)489 self.virtualized = is_build_virtualized(self.archive, self.processor)
528 self.queueBuild()
529
530 def rescore(self, score):
531 """See `IBuild`."""
532 if not self.can_be_rescored:
533 raise CannotBeRescored("Build cannot be rescored.")
534
535 self.buildqueue_record.manualScore(score)
536490
537 @property491 @property
538 def api_score(self):492 def api_score(self):
@@ -543,15 +497,6 @@ class BinaryPackageBuild(PackageBuildMixin, SQLBase):
543 else:497 else:
544 return self.buildqueue_record.lastscore498 return self.buildqueue_record.lastscore
545499
546 def cancel(self):
547 """See `IBinaryPackageBuild`."""
548 if not self.can_be_cancelled:
549 return
550 # BuildQueue.cancel() will decide whether to go straight to
551 # CANCELLED, or go through CANCELLING to let buildd-manager
552 # clean up the slave.
553 self.buildqueue_record.cancel()
554
555 def _parseDependencyToken(self, token):500 def _parseDependencyToken(self, token):
556 """Parse the given token.501 """Parse the given token.
557502
diff --git a/lib/lp/soyuz/model/livefsbuild.py b/lib/lp/soyuz/model/livefsbuild.py
index f8312c2..7ec3e87 100644
--- a/lib/lp/soyuz/model/livefsbuild.py
+++ b/lib/lp/soyuz/model/livefsbuild.py
@@ -230,38 +230,7 @@ class LiveFSBuild(PackageBuildMixin, Storm):
230 else:230 else:
231 return self.buildqueue_record.lastscore231 return self.buildqueue_record.lastscore
232232
233 @property233 can_be_retried = False
234 def can_be_rescored(self):
235 """See `ILiveFSBuild`."""
236 return (
237 self.buildqueue_record is not None and
238 self.status is BuildStatus.NEEDSBUILD)
239
240 @property
241 def can_be_cancelled(self):
242 """See `ILiveFSBuild`."""
243 if not self.buildqueue_record:
244 return False
245
246 cancellable_statuses = [
247 BuildStatus.BUILDING,
248 BuildStatus.NEEDSBUILD,
249 ]
250 return self.status in cancellable_statuses
251
252 def rescore(self, score):
253 """See `ILiveFSBuild`."""
254 assert self.can_be_rescored, "Build %s cannot be rescored" % self.id
255 self.buildqueue_record.manualScore(score)
256
257 def cancel(self):
258 """See `ILiveFSBuild`."""
259 if not self.can_be_cancelled:
260 return
261 # BuildQueue.cancel() will decide whether to go straight to
262 # CANCELLED, or go through CANCELLING to let buildd-manager clean up
263 # the slave.
264 self.buildqueue_record.cancel()
265234
266 def calculateScore(self):235 def calculateScore(self):
267 return (236 return (
diff --git a/lib/lp/soyuz/stories/webservice/xx-builds.txt b/lib/lp/soyuz/stories/webservice/xx-builds.txt
index 1e7a44a..5148042 100644
--- a/lib/lp/soyuz/stories/webservice/xx-builds.txt
+++ b/lib/lp/soyuz/stories/webservice/xx-builds.txt
@@ -205,10 +205,9 @@ As can cprov who owns the PPA for the build:
205 ... cprov, permission=OAuthPermission.WRITE_PUBLIC)205 ... cprov, permission=OAuthPermission.WRITE_PUBLIC)
206 >>> print(cprov_webservice.named_post(206 >>> print(cprov_webservice.named_post(
207 ... a_build['self_link'], 'retry'))207 ... a_build['self_link'], 'retry'))
208 HTTP/1.1 500 Internal Server Error208 HTTP/1.1 400 Bad Request
209 ...209 ...
210 AssertionError: Build ... cannot be retried210 Build ... cannot be retried.
211 <BLANKLINE>
212211
213but in this case, although he has permission to retry the build, it212but in this case, although he has permission to retry the build, it
214failed because it was already retried by an admin. This is reflected in the213failed because it was already retried by an admin. This is reflected in the
@@ -265,4 +264,4 @@ alter the buildstate to one that cannot be retried:
265 ... a_build['self_link'], 'rescore', score=1000))264 ... a_build['self_link'], 'rescore', score=1000))
266 HTTP/1.1 400 Bad Request265 HTTP/1.1 400 Bad Request
267 ...266 ...
268 Build cannot be rescored.267 Build ... cannot be rescored.
diff --git a/lib/lp/soyuz/tests/test_build.py b/lib/lp/soyuz/tests/test_build.py
index 6e83cde..8181a2d 100644
--- a/lib/lp/soyuz/tests/test_build.py
+++ b/lib/lp/soyuz/tests/test_build.py
@@ -11,6 +11,7 @@ import transaction
11from zope.component import getUtility11from zope.component import getUtility
1212
13from lp.buildmaster.enums import BuildStatus13from lp.buildmaster.enums import BuildStatus
14from lp.buildmaster.interfaces.buildfarmjob import CannotBeRescored
14from lp.registry.interfaces.person import IPersonSet15from lp.registry.interfaces.person import IPersonSet
15from lp.registry.interfaces.pocket import PackagePublishingPocket16from lp.registry.interfaces.pocket import PackagePublishingPocket
16from lp.registry.interfaces.series import SeriesStatus17from lp.registry.interfaces.series import SeriesStatus
@@ -20,10 +21,7 @@ from lp.soyuz.enums import (
20 PackagePublishingPriority,21 PackagePublishingPriority,
21 PackageUploadStatus,22 PackageUploadStatus,
22 )23 )
23from lp.soyuz.interfaces.binarypackagebuild import (24from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuildSet
24 CannotBeRescored,
25 IBinaryPackageBuildSet,
26 )
27from lp.soyuz.interfaces.component import IComponentSet25from lp.soyuz.interfaces.component import IComponentSet
28from lp.soyuz.interfaces.publishing import PackagePublishingStatus26from lp.soyuz.interfaces.publishing import PackagePublishingStatus
29from lp.soyuz.tests.test_publishing import SoyuzTestPublisher27from lp.soyuz.tests.test_publishing import SoyuzTestPublisher

Subscribers

People subscribed via source and target branches

to status/vote changes: