Merge ~cjwatson/launchpad:refactor-build-retry-cancel-rescore into launchpad:master
- Git
- lp:~cjwatson/launchpad
- refactor-build-retry-cancel-rescore
- Merge into master
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) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jürgen Gmach | Approve | ||
Review via email: mp+413831@code.launchpad.net |
Commit message
Push retry/cancel/
Description of the change
Previously, each build type implemented retry/cancel/
I had to split `IBuildFarmJob` into `IBuildFarmJob{
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.
Colin Watson (cjwatson) : | # |
Preview Diff
1 | diff --git a/lib/lp/_schema_circular_imports.py b/lib/lp/_schema_circular_imports.py | |||
2 | index 09fe6e4..af12d9a 100644 | |||
3 | --- a/lib/lp/_schema_circular_imports.py | |||
4 | +++ b/lib/lp/_schema_circular_imports.py | |||
5 | @@ -742,8 +742,6 @@ patch_entry_explicit_version(IArchiveSubscriber, 'beta') | |||
6 | 742 | 742 | ||
7 | 743 | # IBinaryPackageBuild | 743 | # IBinaryPackageBuild |
8 | 744 | patch_entry_explicit_version(IBinaryPackageBuild, 'beta') | 744 | patch_entry_explicit_version(IBinaryPackageBuild, 'beta') |
9 | 745 | patch_operations_explicit_version( | ||
10 | 746 | IBinaryPackageBuild, 'beta', "rescore", "retry") | ||
11 | 747 | 745 | ||
12 | 748 | # IBinaryPackagePublishingHistory | 746 | # IBinaryPackagePublishingHistory |
13 | 749 | patch_entry_explicit_version(IBinaryPackagePublishingHistory, 'beta') | 747 | patch_entry_explicit_version(IBinaryPackagePublishingHistory, 'beta') |
14 | diff --git a/lib/lp/buildmaster/interfaces/buildfarmjob.py b/lib/lp/buildmaster/interfaces/buildfarmjob.py | |||
15 | index 86d62e5..9e8d461 100644 | |||
16 | --- a/lib/lp/buildmaster/interfaces/buildfarmjob.py | |||
17 | +++ b/lib/lp/buildmaster/interfaces/buildfarmjob.py | |||
18 | @@ -4,17 +4,28 @@ | |||
19 | 4 | """Interface for Soyuz build farm jobs.""" | 4 | """Interface for Soyuz build farm jobs.""" |
20 | 5 | 5 | ||
21 | 6 | __all__ = [ | 6 | __all__ = [ |
22 | 7 | 'CannotBeRescored', | ||
23 | 8 | 'CannotBeRetried', | ||
24 | 7 | 'IBuildFarmJob', | 9 | 'IBuildFarmJob', |
25 | 10 | 'IBuildFarmJobAdmin', | ||
26 | 8 | 'IBuildFarmJobDB', | 11 | 'IBuildFarmJobDB', |
27 | 12 | 'IBuildFarmJobEdit', | ||
28 | 9 | 'IBuildFarmJobSet', | 13 | 'IBuildFarmJobSet', |
29 | 10 | 'IBuildFarmJobSource', | 14 | 'IBuildFarmJobSource', |
30 | 15 | 'IBuildFarmJobView', | ||
31 | 11 | 'InconsistentBuildFarmJobError', | 16 | 'InconsistentBuildFarmJobError', |
32 | 12 | 'ISpecificBuildFarmJobSource', | 17 | 'ISpecificBuildFarmJobSource', |
33 | 13 | ] | 18 | ] |
34 | 14 | 19 | ||
35 | 20 | import http.client | ||
36 | 21 | |||
37 | 15 | from lazr.restful.declarations import ( | 22 | from lazr.restful.declarations import ( |
38 | 23 | error_status, | ||
39 | 24 | export_write_operation, | ||
40 | 16 | exported, | 25 | exported, |
41 | 17 | exported_as_webservice_entry, | 26 | exported_as_webservice_entry, |
42 | 27 | operation_for_version, | ||
43 | 28 | operation_parameters, | ||
44 | 18 | ) | 29 | ) |
45 | 19 | from lazr.restful.fields import Reference | 30 | from lazr.restful.fields import Reference |
46 | 20 | from zope.interface import ( | 31 | from zope.interface import ( |
47 | @@ -49,6 +60,22 @@ class InconsistentBuildFarmJobError(Exception): | |||
48 | 49 | """ | 60 | """ |
49 | 50 | 61 | ||
50 | 51 | 62 | ||
51 | 63 | @error_status(http.client.BAD_REQUEST) | ||
52 | 64 | class CannotBeRetried(Exception): | ||
53 | 65 | """Raised when retrying a build that cannot be retried.""" | ||
54 | 66 | |||
55 | 67 | def __init__(self, build_id): | ||
56 | 68 | super().__init__("Build %s cannot be retried." % build_id) | ||
57 | 69 | |||
58 | 70 | |||
59 | 71 | @error_status(http.client.BAD_REQUEST) | ||
60 | 72 | class CannotBeRescored(Exception): | ||
61 | 73 | """Raised when rescoring a build that cannot be rescored.""" | ||
62 | 74 | |||
63 | 75 | def __init__(self, build_id): | ||
64 | 76 | super().__init__("Build %s cannot be rescored." % build_id) | ||
65 | 77 | |||
66 | 78 | |||
67 | 52 | class IBuildFarmJobDB(Interface): | 79 | class IBuildFarmJobDB(Interface): |
68 | 53 | """Operations on a `BuildFarmJob` DB row. | 80 | """Operations on a `BuildFarmJob` DB row. |
69 | 54 | 81 | ||
70 | @@ -63,9 +90,8 @@ class IBuildFarmJobDB(Interface): | |||
71 | 63 | description=_("The specific type of job.")) | 90 | description=_("The specific type of job.")) |
72 | 64 | 91 | ||
73 | 65 | 92 | ||
77 | 66 | @exported_as_webservice_entry(as_of='beta') | 93 | class IBuildFarmJobView(Interface): |
78 | 67 | class IBuildFarmJob(Interface): | 94 | """`IBuildFarmJob` attributes that require launchpad.View.""" |
76 | 68 | """Operations that jobs for the build farm must implement.""" | ||
79 | 69 | 95 | ||
80 | 70 | id = Attribute('The build farm job ID.') | 96 | id = Attribute('The build farm job ID.') |
81 | 71 | 97 | ||
82 | @@ -238,6 +264,70 @@ class IBuildFarmJob(Interface): | |||
83 | 238 | "Newline-separated list of repositories to be used to retrieve any " | 264 | "Newline-separated list of repositories to be used to retrieve any " |
84 | 239 | "external build-dependencies when performing this build.") | 265 | "external build-dependencies when performing this build.") |
85 | 240 | 266 | ||
86 | 267 | can_be_rescored = exported(Bool( | ||
87 | 268 | title=_("Can be rescored"), required=True, readonly=True, | ||
88 | 269 | description=_( | ||
89 | 270 | "Whether this build record can be rescored manually."))) | ||
90 | 271 | |||
91 | 272 | can_be_retried = exported(Bool( | ||
92 | 273 | title=_("Can be retried"), required=True, readonly=True, | ||
93 | 274 | description=_("Whether this build record can be retried."))) | ||
94 | 275 | |||
95 | 276 | can_be_cancelled = exported(Bool( | ||
96 | 277 | title=_("Can be cancelled"), required=True, readonly=True, | ||
97 | 278 | description=_("Whether this build record can be cancelled."))) | ||
98 | 279 | |||
99 | 280 | |||
100 | 281 | class IBuildFarmJobEdit(Interface): | ||
101 | 282 | """`IBuildFarmJob` methods that require launchpad.Edit.""" | ||
102 | 283 | |||
103 | 284 | def resetBuild(): | ||
104 | 285 | """Reset this build record to a clean state. | ||
105 | 286 | |||
106 | 287 | This method should only be called by `BuildFarmJobMixin.retry`, but | ||
107 | 288 | subclasses may override it to reset additional state. | ||
108 | 289 | """ | ||
109 | 290 | |||
110 | 291 | @export_write_operation() | ||
111 | 292 | @operation_for_version("devel") | ||
112 | 293 | def retry(): | ||
113 | 294 | """Restore the build record to its initial state. | ||
114 | 295 | |||
115 | 296 | Build record loses its history, is moved to NEEDSBUILD and a new | ||
116 | 297 | non-scored BuildQueue entry is created for it. | ||
117 | 298 | """ | ||
118 | 299 | |||
119 | 300 | @export_write_operation() | ||
120 | 301 | @operation_for_version("devel") | ||
121 | 302 | def cancel(): | ||
122 | 303 | """Cancel the build if it is either pending or in progress. | ||
123 | 304 | |||
124 | 305 | Check the can_be_cancelled property prior to calling this method to | ||
125 | 306 | find out if cancelling the build is possible. | ||
126 | 307 | |||
127 | 308 | If the build is in progress, it is marked as CANCELLING until the | ||
128 | 309 | buildd manager terminates the build and marks it CANCELLED. If the | ||
129 | 310 | build is not in progress, it is marked CANCELLED immediately and is | ||
130 | 311 | removed from the build queue. | ||
131 | 312 | |||
132 | 313 | If the build is not in a cancellable state, this method is a no-op. | ||
133 | 314 | """ | ||
134 | 315 | |||
135 | 316 | |||
136 | 317 | class IBuildFarmJobAdmin(Interface): | ||
137 | 318 | """`IBuildFarmJob` methods that require launchpad.Admin.""" | ||
138 | 319 | |||
139 | 320 | @operation_parameters(score=Int(title=_("Score"), required=True)) | ||
140 | 321 | @export_write_operation() | ||
141 | 322 | @operation_for_version("devel") | ||
142 | 323 | def rescore(score): | ||
143 | 324 | """Change the build's score.""" | ||
144 | 325 | |||
145 | 326 | |||
146 | 327 | @exported_as_webservice_entry(as_of='beta') | ||
147 | 328 | class IBuildFarmJob(IBuildFarmJobView, IBuildFarmJobEdit, IBuildFarmJobAdmin): | ||
148 | 329 | """Operations that jobs for the build farm must implement.""" | ||
149 | 330 | |||
150 | 241 | 331 | ||
151 | 242 | class ISpecificBuildFarmJobSource(Interface): | 332 | class ISpecificBuildFarmJobSource(Interface): |
152 | 243 | """A utility for retrieving objects of a specific IBuildFarmJob type. | 333 | """A utility for retrieving objects of a specific IBuildFarmJob type. |
153 | diff --git a/lib/lp/buildmaster/interfaces/packagebuild.py b/lib/lp/buildmaster/interfaces/packagebuild.py | |||
154 | index 8d6b836..3af81f7 100644 | |||
155 | --- a/lib/lp/buildmaster/interfaces/packagebuild.py | |||
156 | +++ b/lib/lp/buildmaster/interfaces/packagebuild.py | |||
157 | @@ -5,6 +5,7 @@ | |||
158 | 5 | 5 | ||
159 | 6 | __all__ = [ | 6 | __all__ = [ |
160 | 7 | 'IPackageBuild', | 7 | 'IPackageBuild', |
161 | 8 | 'IPackageBuildView', | ||
162 | 8 | ] | 9 | ] |
163 | 9 | 10 | ||
164 | 10 | 11 | ||
165 | @@ -18,7 +19,10 @@ from zope.schema import ( | |||
166 | 18 | ) | 19 | ) |
167 | 19 | 20 | ||
168 | 20 | from lp import _ | 21 | from lp import _ |
170 | 21 | from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJob | 22 | from lp.buildmaster.interfaces.buildfarmjob import ( |
171 | 23 | IBuildFarmJob, | ||
172 | 24 | IBuildFarmJobView, | ||
173 | 25 | ) | ||
174 | 22 | from lp.registry.interfaces.distribution import IDistribution | 26 | from lp.registry.interfaces.distribution import IDistribution |
175 | 23 | from lp.registry.interfaces.distroseries import IDistroSeries | 27 | from lp.registry.interfaces.distroseries import IDistroSeries |
176 | 24 | from lp.registry.interfaces.pocket import PackagePublishingPocket | 28 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
177 | @@ -26,8 +30,8 @@ from lp.services.librarian.interfaces import ILibraryFileAlias | |||
178 | 26 | from lp.soyuz.interfaces.archive import IArchive | 30 | from lp.soyuz.interfaces.archive import IArchive |
179 | 27 | 31 | ||
180 | 28 | 32 | ||
183 | 29 | class IPackageBuild(IBuildFarmJob): | 33 | class IPackageBuildView(IBuildFarmJobView): |
184 | 30 | """Attributes and operations specific to package build jobs.""" | 34 | """`IPackageBuild` methods that require launchpad.View.""" |
185 | 31 | 35 | ||
186 | 32 | archive = exported( | 36 | archive = exported( |
187 | 33 | Reference( | 37 | Reference( |
188 | @@ -95,3 +99,7 @@ class IPackageBuild(IBuildFarmJob): | |||
189 | 95 | 99 | ||
190 | 96 | :param changes: Changes file from the upload. | 100 | :param changes: Changes file from the upload. |
191 | 97 | """ | 101 | """ |
192 | 102 | |||
193 | 103 | |||
194 | 104 | class IPackageBuild(IPackageBuildView, IBuildFarmJob): | ||
195 | 105 | """Attributes and operations specific to package build jobs.""" | ||
196 | diff --git a/lib/lp/buildmaster/interfaces/webservice.py b/lib/lp/buildmaster/interfaces/webservice.py | |||
197 | index b899a8f..e6ca23e 100644 | |||
198 | --- a/lib/lp/buildmaster/interfaces/webservice.py | |||
199 | +++ b/lib/lp/buildmaster/interfaces/webservice.py | |||
200 | @@ -10,6 +10,8 @@ which tells `lazr.restful` that it should look for webservice exports here. | |||
201 | 10 | """ | 10 | """ |
202 | 11 | 11 | ||
203 | 12 | __all__ = [ | 12 | __all__ = [ |
204 | 13 | 'CannotBeRescored', | ||
205 | 14 | 'CannotBeRetried', | ||
206 | 13 | 'IBuilder', | 15 | 'IBuilder', |
207 | 14 | 'IBuilderSet', | 16 | 'IBuilderSet', |
208 | 15 | 'IBuildFarmJob', | 17 | 'IBuildFarmJob', |
209 | @@ -21,7 +23,11 @@ from lp.buildmaster.interfaces.builder import ( | |||
210 | 21 | IBuilder, | 23 | IBuilder, |
211 | 22 | IBuilderSet, | 24 | IBuilderSet, |
212 | 23 | ) | 25 | ) |
214 | 24 | from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJob | 26 | from lp.buildmaster.interfaces.buildfarmjob import ( |
215 | 27 | CannotBeRescored, | ||
216 | 28 | CannotBeRetried, | ||
217 | 29 | IBuildFarmJob, | ||
218 | 30 | ) | ||
219 | 25 | from lp.buildmaster.interfaces.processor import ( | 31 | from lp.buildmaster.interfaces.processor import ( |
220 | 26 | IProcessor, | 32 | IProcessor, |
221 | 27 | IProcessorSet, | 33 | IProcessorSet, |
222 | diff --git a/lib/lp/buildmaster/model/buildfarmjob.py b/lib/lp/buildmaster/model/buildfarmjob.py | |||
223 | index 876546d..181be99 100644 | |||
224 | --- a/lib/lp/buildmaster/model/buildfarmjob.py | |||
225 | +++ b/lib/lp/buildmaster/model/buildfarmjob.py | |||
226 | @@ -34,6 +34,8 @@ from lp.buildmaster.enums import ( | |||
227 | 34 | BuildStatus, | 34 | BuildStatus, |
228 | 35 | ) | 35 | ) |
229 | 36 | from lp.buildmaster.interfaces.buildfarmjob import ( | 36 | from lp.buildmaster.interfaces.buildfarmjob import ( |
230 | 37 | CannotBeRescored, | ||
231 | 38 | CannotBeRetried, | ||
232 | 37 | IBuildFarmJob, | 39 | IBuildFarmJob, |
233 | 38 | IBuildFarmJobDB, | 40 | IBuildFarmJobDB, |
234 | 39 | IBuildFarmJobSet, | 41 | IBuildFarmJobSet, |
235 | @@ -262,6 +264,81 @@ class BuildFarmJobMixin: | |||
236 | 262 | del get_property_cache(self).buildqueue_record | 264 | del get_property_cache(self).buildqueue_record |
237 | 263 | return queue_entry | 265 | return queue_entry |
238 | 264 | 266 | ||
239 | 267 | @property | ||
240 | 268 | def can_be_retried(self): | ||
241 | 269 | """See `IBuildFarmJob`. | ||
242 | 270 | |||
243 | 271 | Implementations should override this method to first check whether | ||
244 | 272 | their associated build behaviour would accept the build if it | ||
245 | 273 | succeeded. | ||
246 | 274 | """ | ||
247 | 275 | failed_statuses = [ | ||
248 | 276 | BuildStatus.FAILEDTOBUILD, | ||
249 | 277 | BuildStatus.MANUALDEPWAIT, | ||
250 | 278 | BuildStatus.CHROOTWAIT, | ||
251 | 279 | BuildStatus.FAILEDTOUPLOAD, | ||
252 | 280 | BuildStatus.CANCELLED, | ||
253 | 281 | BuildStatus.SUPERSEDED, | ||
254 | 282 | ] | ||
255 | 283 | |||
256 | 284 | # If the build is currently in any of the failed states, | ||
257 | 285 | # it may be retried. | ||
258 | 286 | return self.status in failed_statuses | ||
259 | 287 | |||
260 | 288 | @property | ||
261 | 289 | def can_be_rescored(self): | ||
262 | 290 | """See `IBuildFarmJob`.""" | ||
263 | 291 | return ( | ||
264 | 292 | self.buildqueue_record is not None and | ||
265 | 293 | self.status is BuildStatus.NEEDSBUILD) | ||
266 | 294 | |||
267 | 295 | @property | ||
268 | 296 | def can_be_cancelled(self): | ||
269 | 297 | """See `IBuildFarmJob`.""" | ||
270 | 298 | if not self.buildqueue_record: | ||
271 | 299 | return False | ||
272 | 300 | |||
273 | 301 | cancellable_statuses = [ | ||
274 | 302 | BuildStatus.BUILDING, | ||
275 | 303 | BuildStatus.NEEDSBUILD, | ||
276 | 304 | ] | ||
277 | 305 | return self.status in cancellable_statuses | ||
278 | 306 | |||
279 | 307 | def resetBuild(self): | ||
280 | 308 | """See `IBuildFarmJob`.""" | ||
281 | 309 | self.build_farm_job.status = self.status = BuildStatus.NEEDSBUILD | ||
282 | 310 | self.build_farm_job.date_finished = self.date_finished = None | ||
283 | 311 | self.date_started = None | ||
284 | 312 | self.build_farm_job.builder = self.builder = None | ||
285 | 313 | self.log = None | ||
286 | 314 | self.upload_log = None | ||
287 | 315 | self.dependencies = None | ||
288 | 316 | self.failure_count = 0 | ||
289 | 317 | |||
290 | 318 | def retry(self): | ||
291 | 319 | """See `IBuildFarmJob`.""" | ||
292 | 320 | if not self.can_be_retried: | ||
293 | 321 | raise CannotBeRetried(self.id) | ||
294 | 322 | |||
295 | 323 | self.resetBuild() | ||
296 | 324 | self.queueBuild() | ||
297 | 325 | |||
298 | 326 | def rescore(self, score): | ||
299 | 327 | """See `IBuildFarmJob`.""" | ||
300 | 328 | if not self.can_be_rescored: | ||
301 | 329 | raise CannotBeRescored(self.id) | ||
302 | 330 | |||
303 | 331 | self.buildqueue_record.manualScore(score) | ||
304 | 332 | |||
305 | 333 | def cancel(self): | ||
306 | 334 | """See `IBuildFarmJob`.""" | ||
307 | 335 | if not self.can_be_cancelled: | ||
308 | 336 | return | ||
309 | 337 | # BuildQueue.cancel() will decide whether to go straight to | ||
310 | 338 | # CANCELLED, or go through CANCELLING to let buildd-manager clean up | ||
311 | 339 | # the builder. | ||
312 | 340 | self.buildqueue_record.cancel() | ||
313 | 341 | |||
314 | 265 | 342 | ||
315 | 266 | class SpecificBuildFarmJobSourceMixin: | 343 | class SpecificBuildFarmJobSourceMixin: |
316 | 267 | 344 | ||
317 | diff --git a/lib/lp/buildmaster/tests/test_buildfarmjob.py b/lib/lp/buildmaster/tests/test_buildfarmjob.py | |||
318 | index a10dfb0..911582f 100644 | |||
319 | --- a/lib/lp/buildmaster/tests/test_buildfarmjob.py | |||
320 | +++ b/lib/lp/buildmaster/tests/test_buildfarmjob.py | |||
321 | @@ -21,6 +21,7 @@ from lp.buildmaster.enums import ( | |||
322 | 21 | BuildStatus, | 21 | BuildStatus, |
323 | 22 | ) | 22 | ) |
324 | 23 | from lp.buildmaster.interfaces.buildfarmjob import ( | 23 | from lp.buildmaster.interfaces.buildfarmjob import ( |
325 | 24 | CannotBeRetried, | ||
326 | 24 | IBuildFarmJob, | 25 | IBuildFarmJob, |
327 | 25 | IBuildFarmJobSet, | 26 | IBuildFarmJobSet, |
328 | 26 | IBuildFarmJobSource, | 27 | IBuildFarmJobSource, |
329 | @@ -119,6 +120,7 @@ class TestBuildFarmJobMixin(TestCaseWithFactory): | |||
330 | 119 | 120 | ||
331 | 120 | def test_providesInterface(self): | 121 | def test_providesInterface(self): |
332 | 121 | # BuildFarmJobMixin derivatives provide IBuildFarmJob | 122 | # BuildFarmJobMixin derivatives provide IBuildFarmJob |
333 | 123 | login('admin@canonical.com') | ||
334 | 122 | self.assertProvides(self.build_farm_job, IBuildFarmJob) | 124 | self.assertProvides(self.build_farm_job, IBuildFarmJob) |
335 | 123 | 125 | ||
336 | 124 | def test_duration_none(self): | 126 | def test_duration_none(self): |
337 | @@ -149,7 +151,7 @@ class TestBuildFarmJobMixin(TestCaseWithFactory): | |||
338 | 149 | def test_edit_build_farm_job(self): | 151 | def test_edit_build_farm_job(self): |
339 | 150 | # Users with edit access can update attributes. | 152 | # Users with edit access can update attributes. |
340 | 151 | login('admin@canonical.com') | 153 | login('admin@canonical.com') |
342 | 152 | self.assertRaises(AssertionError, self.build_farm_job.retry) | 154 | self.assertRaises(CannotBeRetried, self.build_farm_job.retry) |
343 | 153 | 155 | ||
344 | 154 | def test_updateStatus_sets_status(self): | 156 | def test_updateStatus_sets_status(self): |
345 | 155 | # updateStatus always sets status. | 157 | # updateStatus always sets status. |
346 | diff --git a/lib/lp/buildmaster/tests/test_packagebuild.py b/lib/lp/buildmaster/tests/test_packagebuild.py | |||
347 | index 90d5adb..8a1b51c 100644 | |||
348 | --- a/lib/lp/buildmaster/tests/test_packagebuild.py | |||
349 | +++ b/lib/lp/buildmaster/tests/test_packagebuild.py | |||
350 | @@ -35,6 +35,7 @@ class TestPackageBuildMixin(TestCaseWithFactory): | |||
351 | 35 | 35 | ||
352 | 36 | def test_providesInterface(self): | 36 | def test_providesInterface(self): |
353 | 37 | # PackageBuild provides IPackageBuild | 37 | # PackageBuild provides IPackageBuild |
354 | 38 | login('admin@canonical.com') | ||
355 | 38 | self.assertProvides(self.package_build, IPackageBuild) | 39 | self.assertProvides(self.package_build, IPackageBuild) |
356 | 39 | 40 | ||
357 | 40 | def test_updateStatus_MANUALDEPWAIT_sets_dependencies(self): | 41 | def test_updateStatus_MANUALDEPWAIT_sets_dependencies(self): |
358 | diff --git a/lib/lp/charms/interfaces/charmrecipebuild.py b/lib/lp/charms/interfaces/charmrecipebuild.py | |||
359 | index 4a33e19..2bbfe04 100644 | |||
360 | --- a/lib/lp/charms/interfaces/charmrecipebuild.py | |||
361 | +++ b/lib/lp/charms/interfaces/charmrecipebuild.py | |||
362 | @@ -23,7 +23,6 @@ from lazr.restful.declarations import ( | |||
363 | 23 | exported, | 23 | exported, |
364 | 24 | exported_as_webservice_entry, | 24 | exported_as_webservice_entry, |
365 | 25 | operation_for_version, | 25 | operation_for_version, |
366 | 26 | operation_parameters, | ||
367 | 27 | ) | 26 | ) |
368 | 28 | from lazr.restful.fields import ( | 27 | from lazr.restful.fields import ( |
369 | 29 | CollectionField, | 28 | CollectionField, |
370 | @@ -43,8 +42,15 @@ from zope.schema import ( | |||
371 | 43 | ) | 42 | ) |
372 | 44 | 43 | ||
373 | 45 | from lp import _ | 44 | from lp import _ |
376 | 46 | from lp.buildmaster.interfaces.buildfarmjob import ISpecificBuildFarmJobSource | 45 | from lp.buildmaster.interfaces.buildfarmjob import ( |
377 | 47 | from lp.buildmaster.interfaces.packagebuild import IPackageBuild | 46 | IBuildFarmJobAdmin, |
378 | 47 | IBuildFarmJobEdit, | ||
379 | 48 | ISpecificBuildFarmJobSource, | ||
380 | 49 | ) | ||
381 | 50 | from lp.buildmaster.interfaces.packagebuild import ( | ||
382 | 51 | IPackageBuild, | ||
383 | 52 | IPackageBuildView, | ||
384 | 53 | ) | ||
385 | 48 | from lp.charms.interfaces.charmrecipe import ( | 54 | from lp.charms.interfaces.charmrecipe import ( |
386 | 49 | ICharmRecipe, | 55 | ICharmRecipe, |
387 | 50 | ICharmRecipeBuildRequest, | 56 | ICharmRecipeBuildRequest, |
388 | @@ -100,7 +106,7 @@ class CharmRecipeBuildStoreUploadStatus(EnumeratedType): | |||
389 | 100 | """) | 106 | """) |
390 | 101 | 107 | ||
391 | 102 | 108 | ||
393 | 103 | class ICharmRecipeBuildView(IPackageBuild): | 109 | class ICharmRecipeBuildView(IPackageBuildView): |
394 | 104 | """`ICharmRecipeBuild` attributes that require launchpad.View.""" | 110 | """`ICharmRecipeBuild` attributes that require launchpad.View.""" |
395 | 105 | 111 | ||
396 | 106 | build_request = Reference( | 112 | build_request = Reference( |
397 | @@ -141,21 +147,6 @@ class ICharmRecipeBuildView(IPackageBuild): | |||
398 | 141 | title=_("Score of the related build farm job (if any)."), | 147 | title=_("Score of the related build farm job (if any)."), |
399 | 142 | required=False, readonly=True)) | 148 | required=False, readonly=True)) |
400 | 143 | 149 | ||
401 | 144 | can_be_rescored = exported(Bool( | ||
402 | 145 | title=_("Can be rescored"), | ||
403 | 146 | required=True, readonly=True, | ||
404 | 147 | description=_("Whether this build record can be rescored manually."))) | ||
405 | 148 | |||
406 | 149 | can_be_retried = exported(Bool( | ||
407 | 150 | title=_("Can be retried"), | ||
408 | 151 | required=False, readonly=True, | ||
409 | 152 | description=_("Whether this build record can be retried."))) | ||
410 | 153 | |||
411 | 154 | can_be_cancelled = exported(Bool( | ||
412 | 155 | title=_("Can be cancelled"), | ||
413 | 156 | required=True, readonly=True, | ||
414 | 157 | description=_("Whether this build record can be cancelled."))) | ||
415 | 158 | |||
416 | 159 | eta = Datetime( | 150 | eta = Datetime( |
417 | 160 | title=_("The datetime when the build job is estimated to complete."), | 151 | title=_("The datetime when the build job is estimated to complete."), |
418 | 161 | readonly=True) | 152 | readonly=True) |
419 | @@ -229,7 +220,7 @@ class ICharmRecipeBuildView(IPackageBuild): | |||
420 | 229 | """ | 220 | """ |
421 | 230 | 221 | ||
422 | 231 | 222 | ||
424 | 232 | class ICharmRecipeBuildEdit(Interface): | 223 | class ICharmRecipeBuildEdit(IBuildFarmJobEdit): |
425 | 233 | """`ICharmRecipeBuild` methods that require launchpad.Edit.""" | 224 | """`ICharmRecipeBuild` methods that require launchpad.Edit.""" |
426 | 234 | 225 | ||
427 | 235 | def addFile(lfa): | 226 | def addFile(lfa): |
428 | @@ -248,48 +239,18 @@ class ICharmRecipeBuildEdit(Interface): | |||
429 | 248 | where an upload can be scheduled. | 239 | where an upload can be scheduled. |
430 | 249 | """ | 240 | """ |
431 | 250 | 241 | ||
432 | 251 | @export_write_operation() | ||
433 | 252 | @operation_for_version("devel") | ||
434 | 253 | def retry(): | ||
435 | 254 | """Restore the build record to its initial state. | ||
436 | 255 | |||
437 | 256 | Build record loses its history, is moved to NEEDSBUILD and a new | ||
438 | 257 | non-scored BuildQueue entry is created for it. | ||
439 | 258 | """ | ||
440 | 259 | |||
441 | 260 | @export_write_operation() | ||
442 | 261 | @operation_for_version("devel") | ||
443 | 262 | def cancel(): | ||
444 | 263 | """Cancel the build if it is either pending or in progress. | ||
445 | 264 | |||
446 | 265 | Check the can_be_cancelled property prior to calling this method to | ||
447 | 266 | find out if cancelling the build is possible. | ||
448 | 267 | |||
449 | 268 | If the build is in progress, it is marked as CANCELLING until the | ||
450 | 269 | buildd manager terminates the build and marks it CANCELLED. If the | ||
451 | 270 | build is not in progress, it is marked CANCELLED immediately and is | ||
452 | 271 | removed from the build queue. | ||
453 | 272 | |||
454 | 273 | If the build is not in a cancellable state, this method is a no-op. | ||
455 | 274 | """ | ||
456 | 275 | |||
457 | 276 | 242 | ||
459 | 277 | class ICharmRecipeBuildAdmin(Interface): | 243 | class ICharmRecipeBuildAdmin(IBuildFarmJobAdmin): |
460 | 278 | """`ICharmRecipeBuild` methods that require launchpad.Admin.""" | 244 | """`ICharmRecipeBuild` methods that require launchpad.Admin.""" |
461 | 279 | 245 | ||
462 | 280 | @operation_parameters(score=Int(title=_("Score"), required=True)) | ||
463 | 281 | @export_write_operation() | ||
464 | 282 | @operation_for_version("devel") | ||
465 | 283 | def rescore(score): | ||
466 | 284 | """Change the build's score.""" | ||
467 | 285 | |||
468 | 286 | 246 | ||
469 | 287 | # XXX cjwatson 2021-09-15 bug=760849: "beta" is a lie to get WADL | 247 | # XXX cjwatson 2021-09-15 bug=760849: "beta" is a lie to get WADL |
470 | 288 | # generation working. Individual attributes must set their version to | 248 | # generation working. Individual attributes must set their version to |
471 | 289 | # "devel". | 249 | # "devel". |
472 | 290 | @exported_as_webservice_entry(as_of="beta") | 250 | @exported_as_webservice_entry(as_of="beta") |
473 | 291 | class ICharmRecipeBuild( | 251 | class ICharmRecipeBuild( |
475 | 292 | ICharmRecipeBuildView, ICharmRecipeBuildEdit, ICharmRecipeBuildAdmin): | 252 | ICharmRecipeBuildView, ICharmRecipeBuildEdit, ICharmRecipeBuildAdmin, |
476 | 253 | IPackageBuild): | ||
477 | 293 | """A build record for a charm recipe.""" | 254 | """A build record for a charm recipe.""" |
478 | 294 | 255 | ||
479 | 295 | 256 | ||
480 | diff --git a/lib/lp/charms/model/charmrecipebuild.py b/lib/lp/charms/model/charmrecipebuild.py | |||
481 | index cbcdd9e..2374924 100644 | |||
482 | --- a/lib/lp/charms/model/charmrecipebuild.py | |||
483 | +++ b/lib/lp/charms/model/charmrecipebuild.py | |||
484 | @@ -220,70 +220,12 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase): | |||
485 | 220 | 220 | ||
486 | 221 | @property | 221 | @property |
487 | 222 | def can_be_retried(self): | 222 | def can_be_retried(self): |
489 | 223 | """See `ICharmRecipeBuild`.""" | 223 | """See `IBuildFarmJob`.""" |
490 | 224 | # First check that the behaviour would accept the build if it | 224 | # First check that the behaviour would accept the build if it |
491 | 225 | # succeeded. | 225 | # succeeded. |
492 | 226 | if self.distro_series.status == SeriesStatus.OBSOLETE: | 226 | if self.distro_series.status == SeriesStatus.OBSOLETE: |
493 | 227 | return False | 227 | return False |
553 | 228 | 228 | return super().can_be_retried | |
495 | 229 | failed_statuses = [ | ||
496 | 230 | BuildStatus.FAILEDTOBUILD, | ||
497 | 231 | BuildStatus.MANUALDEPWAIT, | ||
498 | 232 | BuildStatus.CHROOTWAIT, | ||
499 | 233 | BuildStatus.FAILEDTOUPLOAD, | ||
500 | 234 | BuildStatus.CANCELLED, | ||
501 | 235 | BuildStatus.SUPERSEDED, | ||
502 | 236 | ] | ||
503 | 237 | |||
504 | 238 | # If the build is currently in any of the failed states, | ||
505 | 239 | # it may be retried. | ||
506 | 240 | return self.status in failed_statuses | ||
507 | 241 | |||
508 | 242 | @property | ||
509 | 243 | def can_be_rescored(self): | ||
510 | 244 | """See `ICharmRecipeBuild`.""" | ||
511 | 245 | return ( | ||
512 | 246 | self.buildqueue_record is not None and | ||
513 | 247 | self.status is BuildStatus.NEEDSBUILD) | ||
514 | 248 | |||
515 | 249 | @property | ||
516 | 250 | def can_be_cancelled(self): | ||
517 | 251 | """See `ICharmRecipeBuild`.""" | ||
518 | 252 | if not self.buildqueue_record: | ||
519 | 253 | return False | ||
520 | 254 | |||
521 | 255 | cancellable_statuses = [ | ||
522 | 256 | BuildStatus.BUILDING, | ||
523 | 257 | BuildStatus.NEEDSBUILD, | ||
524 | 258 | ] | ||
525 | 259 | return self.status in cancellable_statuses | ||
526 | 260 | |||
527 | 261 | def retry(self): | ||
528 | 262 | """See `ICharmRecipeBuild`.""" | ||
529 | 263 | assert self.can_be_retried, "Build %s cannot be retried" % self.id | ||
530 | 264 | self.build_farm_job.status = self.status = BuildStatus.NEEDSBUILD | ||
531 | 265 | self.build_farm_job.date_finished = self.date_finished = None | ||
532 | 266 | self.date_started = None | ||
533 | 267 | self.build_farm_job.builder = self.builder = None | ||
534 | 268 | self.log = None | ||
535 | 269 | self.upload_log = None | ||
536 | 270 | self.dependencies = None | ||
537 | 271 | self.failure_count = 0 | ||
538 | 272 | self.queueBuild() | ||
539 | 273 | |||
540 | 274 | def rescore(self, score): | ||
541 | 275 | """See `ICharmRecipeBuild`.""" | ||
542 | 276 | assert self.can_be_rescored, "Build %s cannot be rescored" % self.id | ||
543 | 277 | self.buildqueue_record.manualScore(score) | ||
544 | 278 | |||
545 | 279 | def cancel(self): | ||
546 | 280 | """See `ICharmRecipeBuild`.""" | ||
547 | 281 | if not self.can_be_cancelled: | ||
548 | 282 | return | ||
549 | 283 | # BuildQueue.cancel() will decide whether to go straight to | ||
550 | 284 | # CANCELLED, or go through CANCELLING to let buildd-manager clean up | ||
551 | 285 | # the slave. | ||
552 | 286 | self.buildqueue_record.cancel() | ||
554 | 287 | 229 | ||
555 | 288 | def calculateScore(self): | 230 | def calculateScore(self): |
556 | 289 | """See `IBuildFarmJob`.""" | 231 | """See `IBuildFarmJob`.""" |
557 | diff --git a/lib/lp/code/configure.zcml b/lib/lp/code/configure.zcml | |||
558 | index fb8e0d5..b5399af 100644 | |||
559 | --- a/lib/lp/code/configure.zcml | |||
560 | +++ b/lib/lp/code/configure.zcml | |||
561 | @@ -1196,6 +1196,9 @@ | |||
562 | 1196 | <require | 1196 | <require |
563 | 1197 | permission="launchpad.Edit" | 1197 | permission="launchpad.Edit" |
564 | 1198 | interface="lp.code.interfaces.sourcepackagerecipebuild.ISourcePackageRecipeBuildEdit"/> | 1198 | interface="lp.code.interfaces.sourcepackagerecipebuild.ISourcePackageRecipeBuildEdit"/> |
565 | 1199 | <require | ||
566 | 1200 | permission="launchpad.Admin" | ||
567 | 1201 | interface="lp.code.interfaces.sourcepackagerecipebuild.ISourcePackageRecipeBuildAdmin"/> | ||
568 | 1199 | </class> | 1202 | </class> |
569 | 1200 | 1203 | ||
570 | 1201 | <securedutility | 1204 | <securedutility |
571 | diff --git a/lib/lp/code/interfaces/sourcepackagerecipebuild.py b/lib/lp/code/interfaces/sourcepackagerecipebuild.py | |||
572 | index 82375f7..12fe659 100644 | |||
573 | --- a/lib/lp/code/interfaces/sourcepackagerecipebuild.py | |||
574 | +++ b/lib/lp/code/interfaces/sourcepackagerecipebuild.py | |||
575 | @@ -8,26 +8,26 @@ __all__ = [ | |||
576 | 8 | 'ISourcePackageRecipeBuildSource', | 8 | 'ISourcePackageRecipeBuildSource', |
577 | 9 | ] | 9 | ] |
578 | 10 | 10 | ||
585 | 11 | from lazr.restful.declarations import ( | 11 | from lazr.restful.declarations import exported_as_webservice_entry |
580 | 12 | export_write_operation, | ||
581 | 13 | exported, | ||
582 | 14 | exported_as_webservice_entry, | ||
583 | 15 | operation_for_version, | ||
584 | 16 | ) | ||
586 | 17 | from lazr.restful.fields import ( | 12 | from lazr.restful.fields import ( |
587 | 18 | CollectionField, | 13 | CollectionField, |
588 | 19 | Reference, | 14 | Reference, |
589 | 20 | ) | 15 | ) |
590 | 21 | from zope.interface import Interface | ||
591 | 22 | from zope.schema import ( | 16 | from zope.schema import ( |
592 | 23 | Bool, | ||
593 | 24 | Int, | 17 | Int, |
594 | 25 | Object, | 18 | Object, |
595 | 26 | ) | 19 | ) |
596 | 27 | 20 | ||
597 | 28 | from lp import _ | 21 | from lp import _ |
600 | 29 | from lp.buildmaster.interfaces.buildfarmjob import ISpecificBuildFarmJobSource | 22 | from lp.buildmaster.interfaces.buildfarmjob import ( |
601 | 30 | from lp.buildmaster.interfaces.packagebuild import IPackageBuild | 23 | IBuildFarmJobAdmin, |
602 | 24 | IBuildFarmJobEdit, | ||
603 | 25 | ISpecificBuildFarmJobSource, | ||
604 | 26 | ) | ||
605 | 27 | from lp.buildmaster.interfaces.packagebuild import ( | ||
606 | 28 | IPackageBuild, | ||
607 | 29 | IPackageBuildView, | ||
608 | 30 | ) | ||
609 | 31 | from lp.code.interfaces.sourcepackagerecipe import ( | 31 | from lp.code.interfaces.sourcepackagerecipe import ( |
610 | 32 | ISourcePackageRecipe, | 32 | ISourcePackageRecipe, |
611 | 33 | ISourcePackageRecipeData, | 33 | ISourcePackageRecipeData, |
612 | @@ -38,7 +38,8 @@ from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuild | |||
613 | 38 | from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease | 38 | from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease |
614 | 39 | 39 | ||
615 | 40 | 40 | ||
617 | 41 | class ISourcePackageRecipeBuildView(IPackageBuild): | 41 | class ISourcePackageRecipeBuildView(IPackageBuildView): |
618 | 42 | """`ISourcePackageRecipeBuild` attributes that require launchpad.View.""" | ||
619 | 42 | 43 | ||
620 | 43 | id = Int(title=_("Identifier for this build.")) | 44 | id = Int(title=_("Identifier for this build.")) |
621 | 44 | 45 | ||
622 | @@ -57,16 +58,6 @@ class ISourcePackageRecipeBuildView(IPackageBuild): | |||
623 | 57 | recipe = Object( | 58 | recipe = Object( |
624 | 58 | schema=ISourcePackageRecipe, title=_("The recipe being built.")) | 59 | schema=ISourcePackageRecipe, title=_("The recipe being built.")) |
625 | 59 | 60 | ||
626 | 60 | can_be_rescored = exported(Bool( | ||
627 | 61 | title=_("Can be rescored"), | ||
628 | 62 | required=True, readonly=True, | ||
629 | 63 | description=_("Whether this build record can be rescored manually."))) | ||
630 | 64 | |||
631 | 65 | can_be_cancelled = exported(Bool( | ||
632 | 66 | title=_("Can be cancelled"), | ||
633 | 67 | required=True, readonly=True, | ||
634 | 68 | description=_("Whether this build record can be cancelled."))) | ||
635 | 69 | |||
636 | 70 | manifest = Object( | 61 | manifest = Object( |
637 | 71 | schema=ISourcePackageRecipeData, title=_( | 62 | schema=ISourcePackageRecipeData, title=_( |
638 | 72 | 'A snapshot of the recipe for this build.')) | 63 | 'A snapshot of the recipe for this build.')) |
639 | @@ -82,31 +73,22 @@ class ISourcePackageRecipeBuildView(IPackageBuild): | |||
640 | 82 | """Return the file under +files with specified name.""" | 73 | """Return the file under +files with specified name.""" |
641 | 83 | 74 | ||
642 | 84 | 75 | ||
660 | 85 | class ISourcePackageRecipeBuildEdit(Interface): | 76 | class ISourcePackageRecipeBuildEdit(IBuildFarmJobEdit): |
661 | 86 | 77 | """`ISourcePackageRecipeBuild` attributes that require launchpad.Edit.""" | |
645 | 87 | @export_write_operation() | ||
646 | 88 | @operation_for_version("devel") | ||
647 | 89 | def cancel(): | ||
648 | 90 | """Cancel the build if it is either pending or in progress. | ||
649 | 91 | |||
650 | 92 | Check the can_be_cancelled property prior to calling this method to | ||
651 | 93 | find out if cancelling the build is possible. | ||
652 | 94 | |||
653 | 95 | If the build is in progress, it is marked as CANCELLING until the | ||
654 | 96 | buildd manager terminates the build and marks it CANCELLED. If the | ||
655 | 97 | build is not in progress, it is marked CANCELLED immediately and is | ||
656 | 98 | removed from the build queue. | ||
657 | 99 | |||
658 | 100 | If the build is not in a cancellable state, this method is a no-op. | ||
659 | 101 | """ | ||
662 | 102 | 78 | ||
663 | 103 | def destroySelf(): | 79 | def destroySelf(): |
664 | 104 | """Delete the build itself.""" | 80 | """Delete the build itself.""" |
665 | 105 | 81 | ||
666 | 106 | 82 | ||
667 | 83 | class ISourcePackageRecipeBuildAdmin(IBuildFarmJobAdmin): | ||
668 | 84 | """`ISourcePackageRecipeBuild` attributes that require launchpad.Admin.""" | ||
669 | 85 | |||
670 | 86 | |||
671 | 107 | @exported_as_webservice_entry() | 87 | @exported_as_webservice_entry() |
672 | 108 | class ISourcePackageRecipeBuild(ISourcePackageRecipeBuildView, | 88 | class ISourcePackageRecipeBuild(ISourcePackageRecipeBuildView, |
674 | 109 | ISourcePackageRecipeBuildEdit): | 89 | ISourcePackageRecipeBuildEdit, |
675 | 90 | ISourcePackageRecipeBuildAdmin, | ||
676 | 91 | IPackageBuild): | ||
677 | 110 | """A build of a source package.""" | 92 | """A build of a source package.""" |
678 | 111 | 93 | ||
679 | 112 | 94 | ||
680 | diff --git a/lib/lp/code/model/sourcepackagerecipebuild.py b/lib/lp/code/model/sourcepackagerecipebuild.py | |||
681 | index 7be41ee..273d606 100644 | |||
682 | --- a/lib/lp/code/model/sourcepackagerecipebuild.py | |||
683 | +++ b/lib/lp/code/model/sourcepackagerecipebuild.py | |||
684 | @@ -263,31 +263,7 @@ class SourcePackageRecipeBuild(SpecificBuildFarmJobSourceMixin, | |||
685 | 263 | builds.append(build) | 263 | builds.append(build) |
686 | 264 | return builds | 264 | return builds |
687 | 265 | 265 | ||
713 | 266 | @property | 266 | can_be_retried = False |
689 | 267 | def can_be_rescored(self): | ||
690 | 268 | """See `IBuild`.""" | ||
691 | 269 | return self.status is BuildStatus.NEEDSBUILD | ||
692 | 270 | |||
693 | 271 | @property | ||
694 | 272 | def can_be_cancelled(self): | ||
695 | 273 | """See `ISourcePackageRecipeBuild`.""" | ||
696 | 274 | if not self.buildqueue_record: | ||
697 | 275 | return False | ||
698 | 276 | |||
699 | 277 | cancellable_statuses = [ | ||
700 | 278 | BuildStatus.BUILDING, | ||
701 | 279 | BuildStatus.NEEDSBUILD, | ||
702 | 280 | ] | ||
703 | 281 | return self.status in cancellable_statuses | ||
704 | 282 | |||
705 | 283 | def cancel(self): | ||
706 | 284 | """See `ISourcePackageRecipeBuild`.""" | ||
707 | 285 | if not self.can_be_cancelled: | ||
708 | 286 | return | ||
709 | 287 | # BuildQueue.cancel() will decide whether to go straight to | ||
710 | 288 | # CANCELLED, or go through CANCELLING to let buildd-manager | ||
711 | 289 | # clean up the slave. | ||
712 | 290 | self.buildqueue_record.cancel() | ||
714 | 291 | 267 | ||
715 | 292 | def destroySelf(self): | 268 | def destroySelf(self): |
716 | 293 | if self.buildqueue_record is not None: | 269 | if self.buildqueue_record is not None: |
717 | diff --git a/lib/lp/oci/interfaces/ocirecipebuild.py b/lib/lp/oci/interfaces/ocirecipebuild.py | |||
718 | index 32aabe0..b366f87 100644 | |||
719 | --- a/lib/lp/oci/interfaces/ocirecipebuild.py | |||
720 | +++ b/lib/lp/oci/interfaces/ocirecipebuild.py | |||
721 | @@ -25,7 +25,6 @@ from lazr.restful.declarations import ( | |||
722 | 25 | exported, | 25 | exported, |
723 | 26 | exported_as_webservice_entry, | 26 | exported_as_webservice_entry, |
724 | 27 | operation_for_version, | 27 | operation_for_version, |
725 | 28 | operation_parameters, | ||
726 | 29 | ) | 28 | ) |
727 | 30 | from lazr.restful.fields import ( | 29 | from lazr.restful.fields import ( |
728 | 31 | CollectionField, | 30 | CollectionField, |
729 | @@ -47,8 +46,15 @@ from zope.schema import ( | |||
730 | 47 | 46 | ||
731 | 48 | from lp import _ | 47 | from lp import _ |
732 | 49 | from lp.app.interfaces.launchpad import IPrivacy | 48 | from lp.app.interfaces.launchpad import IPrivacy |
735 | 50 | from lp.buildmaster.interfaces.buildfarmjob import ISpecificBuildFarmJobSource | 49 | from lp.buildmaster.interfaces.buildfarmjob import ( |
736 | 51 | from lp.buildmaster.interfaces.packagebuild import IPackageBuild | 50 | IBuildFarmJobAdmin, |
737 | 51 | IBuildFarmJobEdit, | ||
738 | 52 | ISpecificBuildFarmJobSource, | ||
739 | 53 | ) | ||
740 | 54 | from lp.buildmaster.interfaces.packagebuild import ( | ||
741 | 55 | IPackageBuild, | ||
742 | 56 | IPackageBuildView, | ||
743 | 57 | ) | ||
744 | 52 | from lp.oci.interfaces.ocirecipe import ( | 58 | from lp.oci.interfaces.ocirecipe import ( |
745 | 53 | IOCIRecipe, | 59 | IOCIRecipe, |
746 | 54 | IOCIRecipeBuildRequest, | 60 | IOCIRecipeBuildRequest, |
747 | @@ -141,7 +147,7 @@ class OCIRecipeBuildSetRegistryUploadStatus(EnumeratedType): | |||
748 | 141 | """) | 147 | """) |
749 | 142 | 148 | ||
750 | 143 | 149 | ||
752 | 144 | class IOCIRecipeBuildView(IPackageBuild, IPrivacy): | 150 | class IOCIRecipeBuildView(IPackageBuildView, IPrivacy): |
753 | 145 | """`IOCIRecipeBuild` attributes that require launchpad.View permission.""" | 151 | """`IOCIRecipeBuild` attributes that require launchpad.View permission.""" |
754 | 146 | 152 | ||
755 | 147 | build_request = Reference( | 153 | build_request = Reference( |
756 | @@ -207,21 +213,6 @@ class IOCIRecipeBuildView(IPackageBuild, IPrivacy): | |||
757 | 207 | title=_("Score of the related build farm job (if any)."), | 213 | title=_("Score of the related build farm job (if any)."), |
758 | 208 | required=False, readonly=True)) | 214 | required=False, readonly=True)) |
759 | 209 | 215 | ||
760 | 210 | can_be_rescored = exported(Bool( | ||
761 | 211 | title=_("Can be rescored"), | ||
762 | 212 | required=True, readonly=True, | ||
763 | 213 | description=_("Whether this build record can be rescored manually."))) | ||
764 | 214 | |||
765 | 215 | can_be_retried = exported(Bool( | ||
766 | 216 | title=_("Can be retried"), | ||
767 | 217 | required=True, readonly=True, | ||
768 | 218 | description=_("Whether this build record can be retried."))) | ||
769 | 219 | |||
770 | 220 | can_be_cancelled = exported(Bool( | ||
771 | 221 | title=_("Can be cancelled"), | ||
772 | 222 | required=True, readonly=True, | ||
773 | 223 | description=_("Whether this build record can be cancelled."))) | ||
774 | 224 | |||
775 | 225 | manifest = Attribute(_("The manifest of the image.")) | 216 | manifest = Attribute(_("The manifest of the image.")) |
776 | 226 | 217 | ||
777 | 227 | digests = Attribute(_("File containing the image digests.")) | 218 | digests = Attribute(_("File containing the image digests.")) |
778 | @@ -266,7 +257,7 @@ class IOCIRecipeBuildView(IPackageBuild, IPrivacy): | |||
779 | 266 | """ | 257 | """ |
780 | 267 | 258 | ||
781 | 268 | 259 | ||
783 | 269 | class IOCIRecipeBuildEdit(Interface): | 260 | class IOCIRecipeBuildEdit(IBuildFarmJobEdit): |
784 | 270 | """`IOCIRecipeBuild` attributes that require launchpad.Edit permission.""" | 261 | """`IOCIRecipeBuild` attributes that require launchpad.Edit permission.""" |
785 | 271 | 262 | ||
786 | 272 | def addFile(lfa, layer_file_digest): | 263 | def addFile(lfa, layer_file_digest): |
787 | @@ -286,46 +277,15 @@ class IOCIRecipeBuildEdit(Interface): | |||
788 | 286 | where an upload can be scheduled. | 277 | where an upload can be scheduled. |
789 | 287 | """ | 278 | """ |
790 | 288 | 279 | ||
791 | 289 | @export_write_operation() | ||
792 | 290 | @operation_for_version("devel") | ||
793 | 291 | def retry(): | ||
794 | 292 | """Restore the build record to its initial state. | ||
795 | 293 | |||
796 | 294 | Build record loses its history, is moved to NEEDSBUILD and a new | ||
797 | 295 | non-scored BuildQueue entry is created for it. | ||
798 | 296 | """ | ||
799 | 297 | |||
800 | 298 | @export_write_operation() | ||
801 | 299 | @operation_for_version("devel") | ||
802 | 300 | def cancel(): | ||
803 | 301 | """Cancel the build if it is either pending or in progress. | ||
804 | 302 | |||
805 | 303 | Check the can_be_cancelled property prior to calling this method to | ||
806 | 304 | find out if cancelling the build is possible. | ||
807 | 305 | |||
808 | 306 | If the build is in progress, it is marked as CANCELLING until the | ||
809 | 307 | buildd manager terminates the build and marks it CANCELLED. If the | ||
810 | 308 | build is not in progress, it is marked CANCELLED immediately and is | ||
811 | 309 | removed from the build queue. | ||
812 | 310 | |||
813 | 311 | If the build is not in a cancellable state, this method is a no-op. | ||
814 | 312 | """ | ||
815 | 313 | |||
816 | 314 | 280 | ||
818 | 315 | class IOCIRecipeBuildAdmin(Interface): | 281 | class IOCIRecipeBuildAdmin(IBuildFarmJobAdmin): |
819 | 316 | """`IOCIRecipeBuild` attributes that require launchpad.Admin permission.""" | 282 | """`IOCIRecipeBuild` attributes that require launchpad.Admin permission.""" |
820 | 317 | 283 | ||
821 | 318 | @operation_parameters(score=Int(title=_("Score"), required=True)) | ||
822 | 319 | @export_write_operation() | ||
823 | 320 | @operation_for_version("devel") | ||
824 | 321 | def rescore(score): | ||
825 | 322 | """Change the build's score.""" | ||
826 | 323 | |||
827 | 324 | 284 | ||
828 | 325 | @exported_as_webservice_entry( | 285 | @exported_as_webservice_entry( |
829 | 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") |
830 | 327 | class IOCIRecipeBuild(IOCIRecipeBuildAdmin, IOCIRecipeBuildEdit, | 287 | class IOCIRecipeBuild(IOCIRecipeBuildAdmin, IOCIRecipeBuildEdit, |
832 | 328 | IOCIRecipeBuildView): | 288 | IOCIRecipeBuildView, IPackageBuild): |
833 | 329 | """A build record for an OCI recipe.""" | 289 | """A build record for an OCI recipe.""" |
834 | 330 | 290 | ||
835 | 331 | 291 | ||
836 | diff --git a/lib/lp/oci/model/ocirecipebuild.py b/lib/lp/oci/model/ocirecipebuild.py | |||
837 | index fc4e3e2..13cc08a 100644 | |||
838 | --- a/lib/lp/oci/model/ocirecipebuild.py | |||
839 | +++ b/lib/lp/oci/model/ocirecipebuild.py | |||
840 | @@ -214,43 +214,12 @@ class OCIRecipeBuild(PackageBuildMixin, StormBase): | |||
841 | 214 | 214 | ||
842 | 215 | @property | 215 | @property |
843 | 216 | def can_be_retried(self): | 216 | def can_be_retried(self): |
845 | 217 | """See `IOCIRecipeBuild`.""" | 217 | """See `IBuildFarmJob`.""" |
846 | 218 | # First check that the behaviour would accept the build if it | 218 | # First check that the behaviour would accept the build if it |
847 | 219 | # succeeded. | 219 | # succeeded. |
848 | 220 | if self.distro_series.status == SeriesStatus.OBSOLETE: | 220 | if self.distro_series.status == SeriesStatus.OBSOLETE: |
849 | 221 | return False | 221 | return False |
882 | 222 | 222 | return super().can_be_retried | |
851 | 223 | failed_statuses = [ | ||
852 | 224 | BuildStatus.FAILEDTOBUILD, | ||
853 | 225 | BuildStatus.MANUALDEPWAIT, | ||
854 | 226 | BuildStatus.CHROOTWAIT, | ||
855 | 227 | BuildStatus.FAILEDTOUPLOAD, | ||
856 | 228 | BuildStatus.CANCELLED, | ||
857 | 229 | BuildStatus.SUPERSEDED, | ||
858 | 230 | ] | ||
859 | 231 | |||
860 | 232 | # If the build is currently in any of the failed states, | ||
861 | 233 | # it may be retried. | ||
862 | 234 | return self.status in failed_statuses | ||
863 | 235 | |||
864 | 236 | @property | ||
865 | 237 | def can_be_rescored(self): | ||
866 | 238 | """See `IOCIRecipeBuild`.""" | ||
867 | 239 | return ( | ||
868 | 240 | self.buildqueue_record is not None and | ||
869 | 241 | self.status is BuildStatus.NEEDSBUILD) | ||
870 | 242 | |||
871 | 243 | @property | ||
872 | 244 | def can_be_cancelled(self): | ||
873 | 245 | """See `IOCIRecipeBuild`.""" | ||
874 | 246 | if not self.buildqueue_record: | ||
875 | 247 | return False | ||
876 | 248 | |||
877 | 249 | cancellable_statuses = [ | ||
878 | 250 | BuildStatus.BUILDING, | ||
879 | 251 | BuildStatus.NEEDSBUILD, | ||
880 | 252 | ] | ||
881 | 253 | return self.status in cancellable_statuses | ||
883 | 254 | 223 | ||
884 | 255 | @property | 224 | @property |
885 | 256 | def is_private(self): | 225 | def is_private(self): |
886 | @@ -271,33 +240,6 @@ class OCIRecipeBuild(PackageBuildMixin, StormBase): | |||
887 | 271 | 240 | ||
888 | 272 | private = is_private | 241 | private = is_private |
889 | 273 | 242 | ||
890 | 274 | def retry(self): | ||
891 | 275 | """See `IOCIRecipeBuild`.""" | ||
892 | 276 | assert self.can_be_retried, "Build %s cannot be retried" % self.id | ||
893 | 277 | self.build_farm_job.status = self.status = BuildStatus.NEEDSBUILD | ||
894 | 278 | self.build_farm_job.date_finished = self.date_finished = None | ||
895 | 279 | self.date_started = None | ||
896 | 280 | self.build_farm_job.builder = self.builder = None | ||
897 | 281 | self.log = None | ||
898 | 282 | self.upload_log = None | ||
899 | 283 | self.dependencies = None | ||
900 | 284 | self.failure_count = 0 | ||
901 | 285 | self.queueBuild() | ||
902 | 286 | |||
903 | 287 | def rescore(self, score): | ||
904 | 288 | """See `IOCIRecipeBuild`.""" | ||
905 | 289 | assert self.can_be_rescored, "Build %s cannot be rescored" % self.id | ||
906 | 290 | self.buildqueue_record.manualScore(score) | ||
907 | 291 | |||
908 | 292 | def cancel(self): | ||
909 | 293 | """See `IOCIRecipeBuild`.""" | ||
910 | 294 | if not self.can_be_cancelled: | ||
911 | 295 | return | ||
912 | 296 | # BuildQueue.cancel() will decide whether to go straight to | ||
913 | 297 | # CANCELLED, or go through CANCELLING to let buildd-manager clean up | ||
914 | 298 | # the slave. | ||
915 | 299 | self.buildqueue_record.cancel() | ||
916 | 300 | |||
917 | 301 | def calculateScore(self): | 243 | def calculateScore(self): |
918 | 302 | # XXX twom 2020-02-11 - This might need an addition? | 244 | # XXX twom 2020-02-11 - This might need an addition? |
919 | 303 | return 2510 | 245 | return 2510 |
920 | diff --git a/lib/lp/security.py b/lib/lp/security.py | |||
921 | index 9f6690e..03c580f 100644 | |||
922 | --- a/lib/lp/security.py | |||
923 | +++ b/lib/lp/security.py | |||
924 | @@ -1666,18 +1666,6 @@ class EditCodeImportMachine(OnlyBazaarExpertsAndAdmins): | |||
925 | 1666 | usedfor = ICodeImportMachine | 1666 | usedfor = ICodeImportMachine |
926 | 1667 | 1667 | ||
927 | 1668 | 1668 | ||
928 | 1669 | class AdminSourcePackageRecipeBuilds(AuthorizationBase): | ||
929 | 1670 | """Control who can edit SourcePackageRecipeBuilds. | ||
930 | 1671 | |||
931 | 1672 | Access is restricted to Buildd Admins. | ||
932 | 1673 | """ | ||
933 | 1674 | permission = 'launchpad.Admin' | ||
934 | 1675 | usedfor = ISourcePackageRecipeBuild | ||
935 | 1676 | |||
936 | 1677 | def checkAuthenticated(self, user): | ||
937 | 1678 | return user.in_buildd_admin | ||
938 | 1679 | |||
939 | 1680 | |||
940 | 1681 | class AdminDistributionTranslations(AuthorizationBase): | 1669 | class AdminDistributionTranslations(AuthorizationBase): |
941 | 1682 | """Class for deciding who can administer distribution translations. | 1670 | """Class for deciding who can administer distribution translations. |
942 | 1683 | 1671 | ||
943 | diff --git a/lib/lp/snappy/interfaces/snapbuild.py b/lib/lp/snappy/interfaces/snapbuild.py | |||
944 | index 6d41fbe..4012391 100644 | |||
945 | --- a/lib/lp/snappy/interfaces/snapbuild.py | |||
946 | +++ b/lib/lp/snappy/interfaces/snapbuild.py | |||
947 | @@ -25,7 +25,6 @@ from lazr.restful.declarations import ( | |||
948 | 25 | exported, | 25 | exported, |
949 | 26 | exported_as_webservice_entry, | 26 | exported_as_webservice_entry, |
950 | 27 | operation_for_version, | 27 | operation_for_version, |
951 | 28 | operation_parameters, | ||
952 | 29 | ) | 28 | ) |
953 | 30 | from lazr.restful.fields import ( | 29 | from lazr.restful.fields import ( |
954 | 31 | CollectionField, | 30 | CollectionField, |
955 | @@ -48,8 +47,15 @@ from zope.schema import ( | |||
956 | 48 | 47 | ||
957 | 49 | from lp import _ | 48 | from lp import _ |
958 | 50 | from lp.app.interfaces.launchpad import IPrivacy | 49 | from lp.app.interfaces.launchpad import IPrivacy |
961 | 51 | from lp.buildmaster.interfaces.buildfarmjob import ISpecificBuildFarmJobSource | 50 | from lp.buildmaster.interfaces.buildfarmjob import ( |
962 | 52 | from lp.buildmaster.interfaces.packagebuild import IPackageBuild | 51 | IBuildFarmJobAdmin, |
963 | 52 | IBuildFarmJobEdit, | ||
964 | 53 | ISpecificBuildFarmJobSource, | ||
965 | 54 | ) | ||
966 | 55 | from lp.buildmaster.interfaces.packagebuild import ( | ||
967 | 56 | IPackageBuild, | ||
968 | 57 | IPackageBuildView, | ||
969 | 58 | ) | ||
970 | 53 | from lp.registry.interfaces.person import IPerson | 59 | from lp.registry.interfaces.person import IPerson |
971 | 54 | from lp.registry.interfaces.pocket import PackagePublishingPocket | 60 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
972 | 55 | from lp.services.database.constants import DEFAULT | 61 | from lp.services.database.constants import DEFAULT |
973 | @@ -128,7 +134,7 @@ class SnapBuildStoreUploadStatus(EnumeratedType): | |||
974 | 128 | """) | 134 | """) |
975 | 129 | 135 | ||
976 | 130 | 136 | ||
978 | 131 | class ISnapBuildView(IPackageBuild, IPrivacy): | 137 | class ISnapBuildView(IPackageBuildView, IPrivacy): |
979 | 132 | """`ISnapBuild` attributes that require launchpad.View permission.""" | 138 | """`ISnapBuild` attributes that require launchpad.View permission.""" |
980 | 133 | 139 | ||
981 | 134 | build_request = Reference( | 140 | build_request = Reference( |
982 | @@ -189,21 +195,6 @@ class ISnapBuildView(IPackageBuild, IPrivacy): | |||
983 | 189 | title=_("Score of the related build farm job (if any)."), | 195 | title=_("Score of the related build farm job (if any)."), |
984 | 190 | required=False, readonly=True)) | 196 | required=False, readonly=True)) |
985 | 191 | 197 | ||
986 | 192 | can_be_rescored = exported(Bool( | ||
987 | 193 | title=_("Can be rescored"), | ||
988 | 194 | required=True, readonly=True, | ||
989 | 195 | description=_("Whether this build record can be rescored manually."))) | ||
990 | 196 | |||
991 | 197 | can_be_retried = exported(Bool( | ||
992 | 198 | title=_("Can be retried"), | ||
993 | 199 | required=False, readonly=True, | ||
994 | 200 | description=_("Whether this build record can be retried."))) | ||
995 | 201 | |||
996 | 202 | can_be_cancelled = exported(Bool( | ||
997 | 203 | title=_("Can be cancelled"), | ||
998 | 204 | required=True, readonly=True, | ||
999 | 205 | description=_("Whether this build record can be cancelled."))) | ||
1000 | 206 | |||
1001 | 207 | eta = Datetime( | 198 | eta = Datetime( |
1002 | 208 | title=_("The datetime when the build job is estimated to complete."), | 199 | title=_("The datetime when the build job is estimated to complete."), |
1003 | 209 | readonly=True) | 200 | readonly=True) |
1004 | @@ -298,7 +289,7 @@ class ISnapBuildView(IPackageBuild, IPrivacy): | |||
1005 | 298 | :return: A collection of URLs for this build.""" | 289 | :return: A collection of URLs for this build.""" |
1006 | 299 | 290 | ||
1007 | 300 | 291 | ||
1009 | 301 | class ISnapBuildEdit(Interface): | 292 | class ISnapBuildEdit(IBuildFarmJobEdit): |
1010 | 302 | """`ISnapBuild` attributes that require launchpad.Edit.""" | 293 | """`ISnapBuild` attributes that require launchpad.Edit.""" |
1011 | 303 | 294 | ||
1012 | 304 | def addFile(lfa): | 295 | def addFile(lfa): |
1013 | @@ -317,47 +308,17 @@ class ISnapBuildEdit(Interface): | |||
1014 | 317 | where an upload can be scheduled. | 308 | where an upload can be scheduled. |
1015 | 318 | """ | 309 | """ |
1016 | 319 | 310 | ||
1017 | 320 | @export_write_operation() | ||
1018 | 321 | @operation_for_version("devel") | ||
1019 | 322 | def retry(): | ||
1020 | 323 | """Restore the build record to its initial state. | ||
1021 | 324 | |||
1022 | 325 | Build record loses its history, is moved to NEEDSBUILD and a new | ||
1023 | 326 | non-scored BuildQueue entry is created for it. | ||
1024 | 327 | """ | ||
1025 | 328 | |||
1026 | 329 | @export_write_operation() | ||
1027 | 330 | @operation_for_version("devel") | ||
1028 | 331 | def cancel(): | ||
1029 | 332 | """Cancel the build if it is either pending or in progress. | ||
1030 | 333 | |||
1031 | 334 | Check the can_be_cancelled property prior to calling this method to | ||
1032 | 335 | find out if cancelling the build is possible. | ||
1033 | 336 | |||
1034 | 337 | If the build is in progress, it is marked as CANCELLING until the | ||
1035 | 338 | buildd manager terminates the build and marks it CANCELLED. If the | ||
1036 | 339 | build is not in progress, it is marked CANCELLED immediately and is | ||
1037 | 340 | removed from the build queue. | ||
1038 | 341 | |||
1039 | 342 | If the build is not in a cancellable state, this method is a no-op. | ||
1040 | 343 | """ | ||
1041 | 344 | |||
1042 | 345 | 311 | ||
1044 | 346 | class ISnapBuildAdmin(Interface): | 312 | class ISnapBuildAdmin(IBuildFarmJobAdmin): |
1045 | 347 | """`ISnapBuild` attributes that require launchpad.Admin.""" | 313 | """`ISnapBuild` attributes that require launchpad.Admin.""" |
1046 | 348 | 314 | ||
1047 | 349 | @operation_parameters(score=Int(title=_("Score"), required=True)) | ||
1048 | 350 | @export_write_operation() | ||
1049 | 351 | @operation_for_version("devel") | ||
1050 | 352 | def rescore(score): | ||
1051 | 353 | """Change the build's score.""" | ||
1052 | 354 | |||
1053 | 355 | 315 | ||
1054 | 356 | # XXX cjwatson 2014-05-06 bug=760849: "beta" is a lie to get WADL | 316 | # XXX cjwatson 2014-05-06 bug=760849: "beta" is a lie to get WADL |
1055 | 357 | # generation working. Individual attributes must set their version to | 317 | # generation working. Individual attributes must set their version to |
1056 | 358 | # "devel". | 318 | # "devel". |
1057 | 359 | @exported_as_webservice_entry(as_of="beta") | 319 | @exported_as_webservice_entry(as_of="beta") |
1059 | 360 | class ISnapBuild(ISnapBuildView, ISnapBuildEdit, ISnapBuildAdmin): | 320 | class ISnapBuild( |
1060 | 321 | ISnapBuildView, ISnapBuildEdit, ISnapBuildAdmin, IPackageBuild): | ||
1061 | 361 | """Build information for snap package builds.""" | 322 | """Build information for snap package builds.""" |
1062 | 362 | 323 | ||
1063 | 363 | 324 | ||
1064 | diff --git a/lib/lp/snappy/model/snapbuild.py b/lib/lp/snappy/model/snapbuild.py | |||
1065 | index a4405de..24fa63f 100644 | |||
1066 | --- a/lib/lp/snappy/model/snapbuild.py | |||
1067 | +++ b/lib/lp/snappy/model/snapbuild.py | |||
1068 | @@ -284,70 +284,12 @@ class SnapBuild(PackageBuildMixin, Storm): | |||
1069 | 284 | 284 | ||
1070 | 285 | @property | 285 | @property |
1071 | 286 | def can_be_retried(self): | 286 | def can_be_retried(self): |
1073 | 287 | """See `ISnapBuild`.""" | 287 | """See `IBuildFarmJob`.""" |
1074 | 288 | # First check that the behaviour would accept the build if it | 288 | # First check that the behaviour would accept the build if it |
1075 | 289 | # succeeded. | 289 | # succeeded. |
1076 | 290 | if self.distro_series.status == SeriesStatus.OBSOLETE: | 290 | if self.distro_series.status == SeriesStatus.OBSOLETE: |
1077 | 291 | return False | 291 | return False |
1137 | 292 | 292 | return super().can_be_retried | |
1079 | 293 | failed_statuses = [ | ||
1080 | 294 | BuildStatus.FAILEDTOBUILD, | ||
1081 | 295 | BuildStatus.MANUALDEPWAIT, | ||
1082 | 296 | BuildStatus.CHROOTWAIT, | ||
1083 | 297 | BuildStatus.FAILEDTOUPLOAD, | ||
1084 | 298 | BuildStatus.CANCELLED, | ||
1085 | 299 | BuildStatus.SUPERSEDED, | ||
1086 | 300 | ] | ||
1087 | 301 | |||
1088 | 302 | # If the build is currently in any of the failed states, | ||
1089 | 303 | # it may be retried. | ||
1090 | 304 | return self.status in failed_statuses | ||
1091 | 305 | |||
1092 | 306 | @property | ||
1093 | 307 | def can_be_rescored(self): | ||
1094 | 308 | """See `ISnapBuild`.""" | ||
1095 | 309 | return ( | ||
1096 | 310 | self.buildqueue_record is not None and | ||
1097 | 311 | self.status is BuildStatus.NEEDSBUILD) | ||
1098 | 312 | |||
1099 | 313 | @property | ||
1100 | 314 | def can_be_cancelled(self): | ||
1101 | 315 | """See `ISnapBuild`.""" | ||
1102 | 316 | if not self.buildqueue_record: | ||
1103 | 317 | return False | ||
1104 | 318 | |||
1105 | 319 | cancellable_statuses = [ | ||
1106 | 320 | BuildStatus.BUILDING, | ||
1107 | 321 | BuildStatus.NEEDSBUILD, | ||
1108 | 322 | ] | ||
1109 | 323 | return self.status in cancellable_statuses | ||
1110 | 324 | |||
1111 | 325 | def retry(self): | ||
1112 | 326 | """See `ISnapBuild`.""" | ||
1113 | 327 | assert self.can_be_retried, "Build %s cannot be retried" % self.id | ||
1114 | 328 | self.build_farm_job.status = self.status = BuildStatus.NEEDSBUILD | ||
1115 | 329 | self.build_farm_job.date_finished = self.date_finished = None | ||
1116 | 330 | self.date_started = None | ||
1117 | 331 | self.build_farm_job.builder = self.builder = None | ||
1118 | 332 | self.log = None | ||
1119 | 333 | self.upload_log = None | ||
1120 | 334 | self.dependencies = None | ||
1121 | 335 | self.failure_count = 0 | ||
1122 | 336 | self.queueBuild() | ||
1123 | 337 | |||
1124 | 338 | def rescore(self, score): | ||
1125 | 339 | """See `ISnapBuild`.""" | ||
1126 | 340 | assert self.can_be_rescored, "Build %s cannot be rescored" % self.id | ||
1127 | 341 | self.buildqueue_record.manualScore(score) | ||
1128 | 342 | |||
1129 | 343 | def cancel(self): | ||
1130 | 344 | """See `ISnapBuild`.""" | ||
1131 | 345 | if not self.can_be_cancelled: | ||
1132 | 346 | return | ||
1133 | 347 | # BuildQueue.cancel() will decide whether to go straight to | ||
1134 | 348 | # CANCELLED, or go through CANCELLING to let buildd-manager clean up | ||
1135 | 349 | # the slave. | ||
1136 | 350 | self.buildqueue_record.cancel() | ||
1138 | 351 | 293 | ||
1139 | 352 | def calculateScore(self): | 294 | def calculateScore(self): |
1140 | 353 | return 2510 + self.archive.relative_build_score | 295 | return 2510 + self.archive.relative_build_score |
1141 | diff --git a/lib/lp/soyuz/interfaces/binarypackagebuild.py b/lib/lp/soyuz/interfaces/binarypackagebuild.py | |||
1142 | index 3771af8..edc245f 100644 | |||
1143 | --- a/lib/lp/soyuz/interfaces/binarypackagebuild.py | |||
1144 | +++ b/lib/lp/soyuz/interfaces/binarypackagebuild.py | |||
1145 | @@ -5,21 +5,17 @@ | |||
1146 | 5 | 5 | ||
1147 | 6 | __all__ = [ | 6 | __all__ = [ |
1148 | 7 | 'BuildSetStatus', | 7 | 'BuildSetStatus', |
1149 | 8 | 'CannotBeRescored', | ||
1150 | 9 | 'IBinaryPackageBuild', | 8 | 'IBinaryPackageBuild', |
1151 | 10 | 'IBuildRescoreForm', | 9 | 'IBuildRescoreForm', |
1152 | 11 | 'IBinaryPackageBuildSet', | 10 | 'IBinaryPackageBuildSet', |
1153 | 12 | 'UnparsableDependencies', | 11 | 'UnparsableDependencies', |
1154 | 13 | ] | 12 | ] |
1155 | 14 | 13 | ||
1156 | 15 | import http.client | ||
1157 | 16 | |||
1158 | 17 | from lazr.enum import ( | 14 | from lazr.enum import ( |
1159 | 18 | EnumeratedType, | 15 | EnumeratedType, |
1160 | 19 | Item, | 16 | Item, |
1161 | 20 | ) | 17 | ) |
1162 | 21 | from lazr.restful.declarations import ( | 18 | from lazr.restful.declarations import ( |
1163 | 22 | error_status, | ||
1164 | 23 | export_read_operation, | 19 | export_read_operation, |
1165 | 24 | export_write_operation, | 20 | export_write_operation, |
1166 | 25 | exported, | 21 | exported, |
1167 | @@ -42,24 +38,25 @@ from zope.schema import ( | |||
1168 | 42 | 38 | ||
1169 | 43 | from lp import _ | 39 | from lp import _ |
1170 | 44 | from lp.buildmaster.enums import BuildStatus | 40 | from lp.buildmaster.enums import BuildStatus |
1173 | 45 | from lp.buildmaster.interfaces.buildfarmjob import ISpecificBuildFarmJobSource | 41 | from lp.buildmaster.interfaces.buildfarmjob import ( |
1174 | 46 | from lp.buildmaster.interfaces.packagebuild import IPackageBuild | 42 | IBuildFarmJobAdmin, |
1175 | 43 | IBuildFarmJobEdit, | ||
1176 | 44 | ISpecificBuildFarmJobSource, | ||
1177 | 45 | ) | ||
1178 | 46 | from lp.buildmaster.interfaces.packagebuild import ( | ||
1179 | 47 | IPackageBuild, | ||
1180 | 48 | IPackageBuildView, | ||
1181 | 49 | ) | ||
1182 | 47 | from lp.buildmaster.interfaces.processor import IProcessor | 50 | from lp.buildmaster.interfaces.processor import IProcessor |
1183 | 48 | from lp.soyuz.interfaces.publishing import ISourcePackagePublishingHistory | 51 | from lp.soyuz.interfaces.publishing import ISourcePackagePublishingHistory |
1184 | 49 | from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease | 52 | from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease |
1185 | 50 | 53 | ||
1186 | 51 | 54 | ||
1187 | 52 | @error_status(http.client.BAD_REQUEST) | ||
1188 | 53 | class CannotBeRescored(Exception): | ||
1189 | 54 | """Raised when rescoring a build that cannot be rescored.""" | ||
1190 | 55 | _message_prefix = "Cannot rescore build" | ||
1191 | 56 | |||
1192 | 57 | |||
1193 | 58 | class UnparsableDependencies(Exception): | 55 | class UnparsableDependencies(Exception): |
1194 | 59 | """Raised when parsing invalid dependencies on a binary package.""" | 56 | """Raised when parsing invalid dependencies on a binary package.""" |
1195 | 60 | 57 | ||
1196 | 61 | 58 | ||
1198 | 62 | class IBinaryPackageBuildView(IPackageBuild): | 59 | class IBinaryPackageBuildView(IPackageBuildView): |
1199 | 63 | """A Build interface for items requiring launchpad.View.""" | 60 | """A Build interface for items requiring launchpad.View.""" |
1200 | 64 | id = Int(title=_('ID'), required=True, readonly=True) | 61 | id = Int(title=_('ID'), required=True, readonly=True) |
1201 | 65 | 62 | ||
1202 | @@ -116,25 +113,6 @@ class IBinaryPackageBuildView(IPackageBuild): | |||
1203 | 116 | "A list of distroarchseriesbinarypackages that resulted from this" | 113 | "A list of distroarchseriesbinarypackages that resulted from this" |
1204 | 117 | "build, ordered by name.") | 114 | "build, ordered by name.") |
1205 | 118 | 115 | ||
1206 | 119 | can_be_rescored = exported( | ||
1207 | 120 | Bool( | ||
1208 | 121 | title=_("Can Be Rescored"), required=False, readonly=True, | ||
1209 | 122 | description=_( | ||
1210 | 123 | "Whether or not this build record can be rescored " | ||
1211 | 124 | "manually."))) | ||
1212 | 125 | |||
1213 | 126 | can_be_retried = exported( | ||
1214 | 127 | Bool( | ||
1215 | 128 | title=_("Can Be Retried"), required=False, readonly=True, | ||
1216 | 129 | description=_( | ||
1217 | 130 | "Whether or not this build record can be retried."))) | ||
1218 | 131 | |||
1219 | 132 | can_be_cancelled = exported( | ||
1220 | 133 | Bool( | ||
1221 | 134 | title=_("Can Be Cancelled"), required=False, readonly=True, | ||
1222 | 135 | description=_( | ||
1223 | 136 | "Whether or not this build record can be cancelled."))) | ||
1224 | 137 | |||
1225 | 138 | upload_changesfile = Attribute( | 116 | upload_changesfile = Attribute( |
1226 | 139 | "The `LibraryFileAlias` object containing the changes file which " | 117 | "The `LibraryFileAlias` object containing the changes file which " |
1227 | 140 | "was originally uploaded with the results of this build. It's " | 118 | "was originally uploaded with the results of this build. It's " |
1228 | @@ -243,10 +221,19 @@ class IBinaryPackageBuildView(IPackageBuild): | |||
1229 | 243 | """ | 221 | """ |
1230 | 244 | 222 | ||
1231 | 245 | 223 | ||
1233 | 246 | class IBinaryPackageBuildEdit(Interface): | 224 | class IBinaryPackageBuildEdit(IBuildFarmJobEdit): |
1234 | 247 | """A Build interface for items requiring launchpad.Edit.""" | 225 | """A Build interface for items requiring launchpad.Edit.""" |
1235 | 248 | 226 | ||
1236 | 227 | def addBuildInfo(buildinfo): | ||
1237 | 228 | """Add a buildinfo file to this build. | ||
1238 | 229 | |||
1239 | 230 | :param buildinfo: An `ILibraryFileAlias`. | ||
1240 | 231 | """ | ||
1241 | 232 | |||
1242 | 233 | # Redeclaring from IBuildFarmJobEdit.retry since this was available in | ||
1243 | 234 | # the beta version. | ||
1244 | 249 | @export_write_operation() | 235 | @export_write_operation() |
1245 | 236 | @operation_for_version("beta") | ||
1246 | 250 | def retry(): | 237 | def retry(): |
1247 | 251 | """Restore the build record to its initial state. | 238 | """Restore the build record to its initial state. |
1248 | 252 | 239 | ||
1249 | @@ -254,28 +241,6 @@ class IBinaryPackageBuildEdit(Interface): | |||
1250 | 254 | non-scored BuildQueue entry is created for it. | 241 | non-scored BuildQueue entry is created for it. |
1251 | 255 | """ | 242 | """ |
1252 | 256 | 243 | ||
1253 | 257 | @export_write_operation() | ||
1254 | 258 | @operation_for_version("devel") | ||
1255 | 259 | def cancel(): | ||
1256 | 260 | """Cancel the build if it is either pending or in progress. | ||
1257 | 261 | |||
1258 | 262 | Check the can_be_cancelled property prior to calling this method to | ||
1259 | 263 | find out if cancelling the build is possible. | ||
1260 | 264 | |||
1261 | 265 | If the build is in progress, it is marked as CANCELLING until the | ||
1262 | 266 | buildd manager terminates the build and marks it CANCELLED. If the | ||
1263 | 267 | build is not in progress, it is marked CANCELLED immediately and is | ||
1264 | 268 | removed from the build queue. | ||
1265 | 269 | |||
1266 | 270 | If the build is not in a cancellable state, this method is a no-op. | ||
1267 | 271 | """ | ||
1268 | 272 | |||
1269 | 273 | def addBuildInfo(buildinfo): | ||
1270 | 274 | """Add a buildinfo file to this build. | ||
1271 | 275 | |||
1272 | 276 | :param buildinfo: An `ILibraryFileAlias`. | ||
1273 | 277 | """ | ||
1274 | 278 | |||
1275 | 279 | 244 | ||
1276 | 280 | class IBinaryPackageBuildRestricted(Interface): | 245 | class IBinaryPackageBuildRestricted(Interface): |
1277 | 281 | """Restricted `IBinaryPackageBuild` attributes. | 246 | """Restricted `IBinaryPackageBuild` attributes. |
1278 | @@ -297,11 +262,14 @@ class IBinaryPackageBuildRestricted(Interface): | |||
1279 | 297 | exported_as="external_dependencies") | 262 | exported_as="external_dependencies") |
1280 | 298 | 263 | ||
1281 | 299 | 264 | ||
1283 | 300 | class IBinaryPackageBuildAdmin(Interface): | 265 | class IBinaryPackageBuildAdmin(IBuildFarmJobAdmin): |
1284 | 301 | """A Build interface for items requiring launchpad.Admin.""" | 266 | """A Build interface for items requiring launchpad.Admin.""" |
1285 | 302 | 267 | ||
1286 | 268 | # Redeclaring from IBuildFarmJobEdit.rescore since this was available in | ||
1287 | 269 | # the beta version. | ||
1288 | 303 | @operation_parameters(score=Int(title=_("Score"), required=True)) | 270 | @operation_parameters(score=Int(title=_("Score"), required=True)) |
1289 | 304 | @export_write_operation() | 271 | @export_write_operation() |
1290 | 272 | @operation_for_version("beta") | ||
1291 | 305 | def rescore(score): | 273 | def rescore(score): |
1292 | 306 | """Change the build's score.""" | 274 | """Change the build's score.""" |
1293 | 307 | 275 | ||
1294 | @@ -309,7 +277,8 @@ class IBinaryPackageBuildAdmin(Interface): | |||
1295 | 309 | @exported_as_webservice_entry(singular_name='build', plural_name='builds') | 277 | @exported_as_webservice_entry(singular_name='build', plural_name='builds') |
1296 | 310 | class IBinaryPackageBuild( | 278 | class IBinaryPackageBuild( |
1297 | 311 | IBinaryPackageBuildView, IBinaryPackageBuildEdit, | 279 | IBinaryPackageBuildView, IBinaryPackageBuildEdit, |
1299 | 312 | IBinaryPackageBuildRestricted, IBinaryPackageBuildAdmin): | 280 | IBinaryPackageBuildRestricted, IBinaryPackageBuildAdmin, |
1300 | 281 | IPackageBuild): | ||
1301 | 313 | """A Build interface""" | 282 | """A Build interface""" |
1302 | 314 | 283 | ||
1303 | 315 | 284 | ||
1304 | diff --git a/lib/lp/soyuz/interfaces/livefsbuild.py b/lib/lp/soyuz/interfaces/livefsbuild.py | |||
1305 | index cbba840..5ed7098 100644 | |||
1306 | --- a/lib/lp/soyuz/interfaces/livefsbuild.py | |||
1307 | +++ b/lib/lp/soyuz/interfaces/livefsbuild.py | |||
1308 | @@ -11,11 +11,9 @@ __all__ = [ | |||
1309 | 11 | 11 | ||
1310 | 12 | from lazr.restful.declarations import ( | 12 | from lazr.restful.declarations import ( |
1311 | 13 | export_read_operation, | 13 | export_read_operation, |
1312 | 14 | export_write_operation, | ||
1313 | 15 | exported, | 14 | exported, |
1314 | 16 | exported_as_webservice_entry, | 15 | exported_as_webservice_entry, |
1315 | 17 | operation_for_version, | 16 | operation_for_version, |
1316 | 18 | operation_parameters, | ||
1317 | 19 | ) | 17 | ) |
1318 | 20 | from lazr.restful.fields import Reference | 18 | from lazr.restful.fields import Reference |
1319 | 21 | from zope.interface import Interface | 19 | from zope.interface import Interface |
1320 | @@ -29,8 +27,15 @@ from zope.schema import ( | |||
1321 | 29 | 27 | ||
1322 | 30 | from lp import _ | 28 | from lp import _ |
1323 | 31 | from lp.app.interfaces.launchpad import IPrivacy | 29 | from lp.app.interfaces.launchpad import IPrivacy |
1326 | 32 | from lp.buildmaster.interfaces.buildfarmjob import ISpecificBuildFarmJobSource | 30 | from lp.buildmaster.interfaces.buildfarmjob import ( |
1327 | 33 | from lp.buildmaster.interfaces.packagebuild import IPackageBuild | 31 | IBuildFarmJobAdmin, |
1328 | 32 | IBuildFarmJobEdit, | ||
1329 | 33 | ISpecificBuildFarmJobSource, | ||
1330 | 34 | ) | ||
1331 | 35 | from lp.buildmaster.interfaces.packagebuild import ( | ||
1332 | 36 | IPackageBuild, | ||
1333 | 37 | IPackageBuildView, | ||
1334 | 38 | ) | ||
1335 | 34 | from lp.registry.interfaces.person import IPerson | 39 | from lp.registry.interfaces.person import IPerson |
1336 | 35 | from lp.registry.interfaces.pocket import PackagePublishingPocket | 40 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
1337 | 36 | from lp.services.database.constants import DEFAULT | 41 | from lp.services.database.constants import DEFAULT |
1338 | @@ -53,7 +58,7 @@ class ILiveFSFile(Interface): | |||
1339 | 53 | required=True, readonly=True) | 58 | required=True, readonly=True) |
1340 | 54 | 59 | ||
1341 | 55 | 60 | ||
1343 | 56 | class ILiveFSBuildView(IPackageBuild, IPrivacy): | 61 | class ILiveFSBuildView(IPackageBuildView, IPrivacy): |
1344 | 57 | """`ILiveFSBuild` attributes that require launchpad.View permission.""" | 62 | """`ILiveFSBuild` attributes that require launchpad.View permission.""" |
1345 | 58 | 63 | ||
1346 | 59 | requester = exported(Reference( | 64 | requester = exported(Reference( |
1347 | @@ -103,16 +108,6 @@ class ILiveFSBuildView(IPackageBuild, IPrivacy): | |||
1348 | 103 | title=_("Score of the related build farm job (if any)."), | 108 | title=_("Score of the related build farm job (if any)."), |
1349 | 104 | required=False, readonly=True)) | 109 | required=False, readonly=True)) |
1350 | 105 | 110 | ||
1351 | 106 | can_be_rescored = exported(Bool( | ||
1352 | 107 | title=_("Can be rescored"), | ||
1353 | 108 | required=True, readonly=True, | ||
1354 | 109 | description=_("Whether this build record can be rescored manually."))) | ||
1355 | 110 | |||
1356 | 111 | can_be_cancelled = exported(Bool( | ||
1357 | 112 | title=_("Can be cancelled"), | ||
1358 | 113 | required=True, readonly=True, | ||
1359 | 114 | description=_("Whether this build record can be cancelled."))) | ||
1360 | 115 | |||
1361 | 116 | def getFiles(): | 111 | def getFiles(): |
1362 | 117 | """Retrieve the build's `ILiveFSFile` records. | 112 | """Retrieve the build's `ILiveFSFile` records. |
1363 | 118 | 113 | ||
1364 | @@ -144,7 +139,7 @@ class ILiveFSBuildView(IPackageBuild, IPrivacy): | |||
1365 | 144 | :return: A collection of URLs for this build.""" | 139 | :return: A collection of URLs for this build.""" |
1366 | 145 | 140 | ||
1367 | 146 | 141 | ||
1369 | 147 | class ILiveFSBuildEdit(Interface): | 142 | class ILiveFSBuildEdit(IBuildFarmJobEdit): |
1370 | 148 | """`ILiveFSBuild` attributes that require launchpad.Edit.""" | 143 | """`ILiveFSBuild` attributes that require launchpad.Edit.""" |
1371 | 149 | 144 | ||
1372 | 150 | def addFile(lfa): | 145 | def addFile(lfa): |
1373 | @@ -154,38 +149,17 @@ class ILiveFSBuildEdit(Interface): | |||
1374 | 154 | :return: An `ILiveFSFile`. | 149 | :return: An `ILiveFSFile`. |
1375 | 155 | """ | 150 | """ |
1376 | 156 | 151 | ||
1377 | 157 | @export_write_operation() | ||
1378 | 158 | @operation_for_version("devel") | ||
1379 | 159 | def cancel(): | ||
1380 | 160 | """Cancel the build if it is either pending or in progress. | ||
1381 | 161 | |||
1382 | 162 | Check the can_be_cancelled property prior to calling this method to | ||
1383 | 163 | find out if cancelling the build is possible. | ||
1384 | 164 | |||
1385 | 165 | If the build is in progress, it is marked as CANCELLING until the | ||
1386 | 166 | buildd manager terminates the build and marks it CANCELLED. If the | ||
1387 | 167 | build is not in progress, it is marked CANCELLED immediately and is | ||
1388 | 168 | removed from the build queue. | ||
1389 | 169 | |||
1390 | 170 | If the build is not in a cancellable state, this method is a no-op. | ||
1391 | 171 | """ | ||
1392 | 172 | |||
1393 | 173 | 152 | ||
1395 | 174 | class ILiveFSBuildAdmin(Interface): | 153 | class ILiveFSBuildAdmin(IBuildFarmJobAdmin): |
1396 | 175 | """`ILiveFSBuild` attributes that require launchpad.Admin.""" | 154 | """`ILiveFSBuild` attributes that require launchpad.Admin.""" |
1397 | 176 | 155 | ||
1398 | 177 | @operation_parameters(score=Int(title=_("Score"), required=True)) | ||
1399 | 178 | @export_write_operation() | ||
1400 | 179 | @operation_for_version("devel") | ||
1401 | 180 | def rescore(score): | ||
1402 | 181 | """Change the build's score.""" | ||
1403 | 182 | |||
1404 | 183 | 156 | ||
1405 | 184 | # XXX cjwatson 2014-05-06 bug=760849: "beta" is a lie to get WADL | 157 | # XXX cjwatson 2014-05-06 bug=760849: "beta" is a lie to get WADL |
1406 | 185 | # generation working. Individual attributes must set their version to | 158 | # generation working. Individual attributes must set their version to |
1407 | 186 | # "devel". | 159 | # "devel". |
1408 | 187 | @exported_as_webservice_entry(singular_name="livefs_build", as_of="beta") | 160 | @exported_as_webservice_entry(singular_name="livefs_build", as_of="beta") |
1410 | 188 | class ILiveFSBuild(ILiveFSBuildView, ILiveFSBuildEdit, ILiveFSBuildAdmin): | 161 | class ILiveFSBuild( |
1411 | 162 | ILiveFSBuildView, ILiveFSBuildEdit, ILiveFSBuildAdmin, IPackageBuild): | ||
1412 | 189 | """Build information for live filesystem builds.""" | 163 | """Build information for live filesystem builds.""" |
1413 | 190 | 164 | ||
1414 | 191 | 165 | ||
1415 | diff --git a/lib/lp/soyuz/interfaces/webservice.py b/lib/lp/soyuz/interfaces/webservice.py | |||
1416 | index 90ef0e1..b8ecec2 100644 | |||
1417 | --- a/lib/lp/soyuz/interfaces/webservice.py | |||
1418 | +++ b/lib/lp/soyuz/interfaces/webservice.py | |||
1419 | @@ -13,7 +13,6 @@ __all__ = [ | |||
1420 | 13 | 'AlreadySubscribed', | 13 | 'AlreadySubscribed', |
1421 | 14 | 'ArchiveDisabled', | 14 | 'ArchiveDisabled', |
1422 | 15 | 'ArchiveNotPrivate', | 15 | 'ArchiveNotPrivate', |
1423 | 16 | 'CannotBeRescored', | ||
1424 | 17 | 'CannotCopy', | 16 | 'CannotCopy', |
1425 | 18 | 'CannotSwitchPrivacy', | 17 | 'CannotSwitchPrivacy', |
1426 | 19 | 'CannotUploadToArchive', | 18 | 'CannotUploadToArchive', |
1427 | @@ -81,10 +80,7 @@ from lp.soyuz.interfaces.archive import ( | |||
1428 | 81 | from lp.soyuz.interfaces.archivedependency import IArchiveDependency | 80 | from lp.soyuz.interfaces.archivedependency import IArchiveDependency |
1429 | 82 | from lp.soyuz.interfaces.archivepermission import IArchivePermission | 81 | from lp.soyuz.interfaces.archivepermission import IArchivePermission |
1430 | 83 | from lp.soyuz.interfaces.archivesubscriber import IArchiveSubscriber | 82 | from lp.soyuz.interfaces.archivesubscriber import IArchiveSubscriber |
1435 | 84 | from lp.soyuz.interfaces.binarypackagebuild import ( | 83 | from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuild |
1432 | 85 | CannotBeRescored, | ||
1433 | 86 | IBinaryPackageBuild, | ||
1434 | 87 | ) | ||
1436 | 88 | from lp.soyuz.interfaces.binarypackagerelease import ( | 84 | from lp.soyuz.interfaces.binarypackagerelease import ( |
1437 | 89 | IBinaryPackageReleaseDownloadCount, | 85 | IBinaryPackageReleaseDownloadCount, |
1438 | 90 | ) | 86 | ) |
1439 | diff --git a/lib/lp/soyuz/model/binarypackagebuild.py b/lib/lp/soyuz/model/binarypackagebuild.py | |||
1440 | index 511c975..f51f565 100644 | |||
1441 | --- a/lib/lp/soyuz/model/binarypackagebuild.py | |||
1442 | +++ b/lib/lp/soyuz/model/binarypackagebuild.py | |||
1443 | @@ -101,7 +101,6 @@ from lp.soyuz.interfaces.archive import ( | |||
1444 | 101 | ) | 101 | ) |
1445 | 102 | from lp.soyuz.interfaces.binarypackagebuild import ( | 102 | from lp.soyuz.interfaces.binarypackagebuild import ( |
1446 | 103 | BuildSetStatus, | 103 | BuildSetStatus, |
1447 | 104 | CannotBeRescored, | ||
1448 | 105 | IBinaryPackageBuild, | 104 | IBinaryPackageBuild, |
1449 | 106 | IBinaryPackageBuildSet, | 105 | IBinaryPackageBuildSet, |
1450 | 107 | UnparsableDependencies, | 106 | UnparsableDependencies, |
1451 | @@ -475,64 +474,19 @@ class BinaryPackageBuild(PackageBuildMixin, SQLBase): | |||
1452 | 475 | 474 | ||
1453 | 476 | @property | 475 | @property |
1454 | 477 | def can_be_retried(self): | 476 | def can_be_retried(self): |
1456 | 478 | """See `IBuild`.""" | 477 | """See `IBuildFarmJob`.""" |
1457 | 479 | # First check that the slave scanner would pick up the build record | 478 | # First check that the slave scanner would pick up the build record |
1458 | 480 | # if we reset it. | 479 | # if we reset it. |
1459 | 481 | if not self.archive.canModifySuite(self.distro_series, self.pocket): | 480 | if not self.archive.canModifySuite(self.distro_series, self.pocket): |
1460 | 482 | # The slave scanner would not pick this up, so it cannot be | 481 | # The slave scanner would not pick this up, so it cannot be |
1461 | 483 | # re-tried. | 482 | # re-tried. |
1462 | 484 | return False | 483 | return False |
1463 | 484 | return super().can_be_retried | ||
1464 | 485 | 485 | ||
1506 | 486 | failed_statuses = [ | 486 | def resetBuild(self): |
1507 | 487 | BuildStatus.FAILEDTOBUILD, | 487 | """See `IBuildFarmJob`.""" |
1508 | 488 | BuildStatus.MANUALDEPWAIT, | 488 | super().resetBuild() |
1468 | 489 | BuildStatus.CHROOTWAIT, | ||
1469 | 490 | BuildStatus.FAILEDTOUPLOAD, | ||
1470 | 491 | BuildStatus.CANCELLED, | ||
1471 | 492 | BuildStatus.SUPERSEDED, | ||
1472 | 493 | ] | ||
1473 | 494 | |||
1474 | 495 | # If the build is currently in any of the failed states, | ||
1475 | 496 | # it may be retried. | ||
1476 | 497 | return self.status in failed_statuses | ||
1477 | 498 | |||
1478 | 499 | @property | ||
1479 | 500 | def can_be_rescored(self): | ||
1480 | 501 | """See `IBuild`.""" | ||
1481 | 502 | return self.status is BuildStatus.NEEDSBUILD | ||
1482 | 503 | |||
1483 | 504 | @property | ||
1484 | 505 | def can_be_cancelled(self): | ||
1485 | 506 | """See `IBuild`.""" | ||
1486 | 507 | if not self.buildqueue_record: | ||
1487 | 508 | return False | ||
1488 | 509 | |||
1489 | 510 | cancellable_statuses = [ | ||
1490 | 511 | BuildStatus.BUILDING, | ||
1491 | 512 | BuildStatus.NEEDSBUILD, | ||
1492 | 513 | ] | ||
1493 | 514 | return self.status in cancellable_statuses | ||
1494 | 515 | |||
1495 | 516 | def retry(self): | ||
1496 | 517 | """See `IBuild`.""" | ||
1497 | 518 | assert self.can_be_retried, "Build %s cannot be retried" % self.id | ||
1498 | 519 | self.build_farm_job.status = self.status = BuildStatus.NEEDSBUILD | ||
1499 | 520 | self.build_farm_job.date_finished = self.date_finished = None | ||
1500 | 521 | self.date_started = None | ||
1501 | 522 | self.build_farm_job.builder = self.builder = None | ||
1502 | 523 | self.log = None | ||
1503 | 524 | self.upload_log = None | ||
1504 | 525 | self.dependencies = None | ||
1505 | 526 | self.failure_count = 0 | ||
1509 | 527 | self.virtualized = is_build_virtualized(self.archive, self.processor) | 489 | self.virtualized = is_build_virtualized(self.archive, self.processor) |
1510 | 528 | self.queueBuild() | ||
1511 | 529 | |||
1512 | 530 | def rescore(self, score): | ||
1513 | 531 | """See `IBuild`.""" | ||
1514 | 532 | if not self.can_be_rescored: | ||
1515 | 533 | raise CannotBeRescored("Build cannot be rescored.") | ||
1516 | 534 | |||
1517 | 535 | self.buildqueue_record.manualScore(score) | ||
1518 | 536 | 490 | ||
1519 | 537 | @property | 491 | @property |
1520 | 538 | def api_score(self): | 492 | def api_score(self): |
1521 | @@ -543,15 +497,6 @@ class BinaryPackageBuild(PackageBuildMixin, SQLBase): | |||
1522 | 543 | else: | 497 | else: |
1523 | 544 | return self.buildqueue_record.lastscore | 498 | return self.buildqueue_record.lastscore |
1524 | 545 | 499 | ||
1525 | 546 | def cancel(self): | ||
1526 | 547 | """See `IBinaryPackageBuild`.""" | ||
1527 | 548 | if not self.can_be_cancelled: | ||
1528 | 549 | return | ||
1529 | 550 | # BuildQueue.cancel() will decide whether to go straight to | ||
1530 | 551 | # CANCELLED, or go through CANCELLING to let buildd-manager | ||
1531 | 552 | # clean up the slave. | ||
1532 | 553 | self.buildqueue_record.cancel() | ||
1533 | 554 | |||
1534 | 555 | def _parseDependencyToken(self, token): | 500 | def _parseDependencyToken(self, token): |
1535 | 556 | """Parse the given token. | 501 | """Parse the given token. |
1536 | 557 | 502 | ||
1537 | diff --git a/lib/lp/soyuz/model/livefsbuild.py b/lib/lp/soyuz/model/livefsbuild.py | |||
1538 | index f8312c2..7ec3e87 100644 | |||
1539 | --- a/lib/lp/soyuz/model/livefsbuild.py | |||
1540 | +++ b/lib/lp/soyuz/model/livefsbuild.py | |||
1541 | @@ -230,38 +230,7 @@ class LiveFSBuild(PackageBuildMixin, Storm): | |||
1542 | 230 | else: | 230 | else: |
1543 | 231 | return self.buildqueue_record.lastscore | 231 | return self.buildqueue_record.lastscore |
1544 | 232 | 232 | ||
1577 | 233 | @property | 233 | can_be_retried = False |
1546 | 234 | def can_be_rescored(self): | ||
1547 | 235 | """See `ILiveFSBuild`.""" | ||
1548 | 236 | return ( | ||
1549 | 237 | self.buildqueue_record is not None and | ||
1550 | 238 | self.status is BuildStatus.NEEDSBUILD) | ||
1551 | 239 | |||
1552 | 240 | @property | ||
1553 | 241 | def can_be_cancelled(self): | ||
1554 | 242 | """See `ILiveFSBuild`.""" | ||
1555 | 243 | if not self.buildqueue_record: | ||
1556 | 244 | return False | ||
1557 | 245 | |||
1558 | 246 | cancellable_statuses = [ | ||
1559 | 247 | BuildStatus.BUILDING, | ||
1560 | 248 | BuildStatus.NEEDSBUILD, | ||
1561 | 249 | ] | ||
1562 | 250 | return self.status in cancellable_statuses | ||
1563 | 251 | |||
1564 | 252 | def rescore(self, score): | ||
1565 | 253 | """See `ILiveFSBuild`.""" | ||
1566 | 254 | assert self.can_be_rescored, "Build %s cannot be rescored" % self.id | ||
1567 | 255 | self.buildqueue_record.manualScore(score) | ||
1568 | 256 | |||
1569 | 257 | def cancel(self): | ||
1570 | 258 | """See `ILiveFSBuild`.""" | ||
1571 | 259 | if not self.can_be_cancelled: | ||
1572 | 260 | return | ||
1573 | 261 | # BuildQueue.cancel() will decide whether to go straight to | ||
1574 | 262 | # CANCELLED, or go through CANCELLING to let buildd-manager clean up | ||
1575 | 263 | # the slave. | ||
1576 | 264 | self.buildqueue_record.cancel() | ||
1578 | 265 | 234 | ||
1579 | 266 | def calculateScore(self): | 235 | def calculateScore(self): |
1580 | 267 | return ( | 236 | return ( |
1581 | diff --git a/lib/lp/soyuz/stories/webservice/xx-builds.txt b/lib/lp/soyuz/stories/webservice/xx-builds.txt | |||
1582 | index 1e7a44a..5148042 100644 | |||
1583 | --- a/lib/lp/soyuz/stories/webservice/xx-builds.txt | |||
1584 | +++ b/lib/lp/soyuz/stories/webservice/xx-builds.txt | |||
1585 | @@ -205,10 +205,9 @@ As can cprov who owns the PPA for the build: | |||
1586 | 205 | ... cprov, permission=OAuthPermission.WRITE_PUBLIC) | 205 | ... cprov, permission=OAuthPermission.WRITE_PUBLIC) |
1587 | 206 | >>> print(cprov_webservice.named_post( | 206 | >>> print(cprov_webservice.named_post( |
1588 | 207 | ... a_build['self_link'], 'retry')) | 207 | ... a_build['self_link'], 'retry')) |
1590 | 208 | HTTP/1.1 500 Internal Server Error | 208 | HTTP/1.1 400 Bad Request |
1591 | 209 | ... | 209 | ... |
1594 | 210 | AssertionError: Build ... cannot be retried | 210 | Build ... cannot be retried. |
1593 | 211 | <BLANKLINE> | ||
1595 | 212 | 211 | ||
1596 | 213 | but in this case, although he has permission to retry the build, it | 212 | but in this case, although he has permission to retry the build, it |
1597 | 214 | failed because it was already retried by an admin. This is reflected in the | 213 | failed because it was already retried by an admin. This is reflected in the |
1598 | @@ -265,4 +264,4 @@ alter the buildstate to one that cannot be retried: | |||
1599 | 265 | ... a_build['self_link'], 'rescore', score=1000)) | 264 | ... a_build['self_link'], 'rescore', score=1000)) |
1600 | 266 | HTTP/1.1 400 Bad Request | 265 | HTTP/1.1 400 Bad Request |
1601 | 267 | ... | 266 | ... |
1603 | 268 | Build cannot be rescored. | 267 | Build ... cannot be rescored. |
1604 | diff --git a/lib/lp/soyuz/tests/test_build.py b/lib/lp/soyuz/tests/test_build.py | |||
1605 | index 6e83cde..8181a2d 100644 | |||
1606 | --- a/lib/lp/soyuz/tests/test_build.py | |||
1607 | +++ b/lib/lp/soyuz/tests/test_build.py | |||
1608 | @@ -11,6 +11,7 @@ import transaction | |||
1609 | 11 | from zope.component import getUtility | 11 | from zope.component import getUtility |
1610 | 12 | 12 | ||
1611 | 13 | from lp.buildmaster.enums import BuildStatus | 13 | from lp.buildmaster.enums import BuildStatus |
1612 | 14 | from lp.buildmaster.interfaces.buildfarmjob import CannotBeRescored | ||
1613 | 14 | from lp.registry.interfaces.person import IPersonSet | 15 | from lp.registry.interfaces.person import IPersonSet |
1614 | 15 | from lp.registry.interfaces.pocket import PackagePublishingPocket | 16 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
1615 | 16 | from lp.registry.interfaces.series import SeriesStatus | 17 | from lp.registry.interfaces.series import SeriesStatus |
1616 | @@ -20,10 +21,7 @@ from lp.soyuz.enums import ( | |||
1617 | 20 | PackagePublishingPriority, | 21 | PackagePublishingPriority, |
1618 | 21 | PackageUploadStatus, | 22 | PackageUploadStatus, |
1619 | 22 | ) | 23 | ) |
1624 | 23 | from lp.soyuz.interfaces.binarypackagebuild import ( | 24 | from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuildSet |
1621 | 24 | CannotBeRescored, | ||
1622 | 25 | IBinaryPackageBuildSet, | ||
1623 | 26 | ) | ||
1625 | 27 | from lp.soyuz.interfaces.component import IComponentSet | 25 | from lp.soyuz.interfaces.component import IComponentSet |
1626 | 28 | from lp.soyuz.interfaces.publishing import PackagePublishingStatus | 26 | from lp.soyuz.interfaces.publishing import PackagePublishingStatus |
1627 | 29 | from lp.soyuz.tests.test_publishing import SoyuzTestPublisher | 27 | from lp.soyuz.tests.test_publishing import SoyuzTestPublisher |
LGTM + tests pass