Merge ~cjwatson/launchpad:snap-retry-build into launchpad:master
- Git
- lp:~cjwatson/launchpad
- snap-retry-build
- Merge into master
Proposed by
Colin Watson
Status: | Merged |
---|---|
Approved by: | Colin Watson |
Approved revision: | 3de2a4a93971e416e46015d1828ba383e5a3605f |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~cjwatson/launchpad:snap-retry-build |
Merge into: | launchpad:master |
Diff against target: |
354 lines (+198/-7) 8 files modified
lib/lp/snappy/browser/configure.zcml (+7/-1) lib/lp/snappy/browser/snapbuild.py (+34/-2) lib/lp/snappy/browser/tests/test_snapbuild.py (+33/-1) lib/lp/snappy/interfaces/snapbuild.py (+15/-1) lib/lp/snappy/model/snapbuild.py (+36/-1) lib/lp/snappy/templates/snapbuild-index.pt (+3/-0) lib/lp/snappy/templates/snapbuild-retry.pt (+28/-0) lib/lp/snappy/tests/test_snapbuild.py (+42/-1) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ioana Lasc (community) | Approve | ||
Review via email: mp+394713@code.launchpad.net |
Commit message
Add support for retrying snap builds
Description of the change
To post a comment you must log in.
Revision history for this message
Ioana Lasc (ilasc) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/snappy/browser/configure.zcml b/lib/lp/snappy/browser/configure.zcml |
2 | index ba3dcc4..9da248a 100644 |
3 | --- a/lib/lp/snappy/browser/configure.zcml |
4 | +++ b/lib/lp/snappy/browser/configure.zcml |
5 | @@ -1,4 +1,4 @@ |
6 | -<!-- Copyright 2015-2019 Canonical Ltd. This software is licensed under the |
7 | +<!-- Copyright 2015-2020 Canonical Ltd. This software is licensed under the |
8 | GNU Affero General Public License version 3 (see the file LICENSE). |
9 | --> |
10 | |
11 | @@ -112,6 +112,12 @@ |
12 | template="../templates/snapbuild-index.pt" /> |
13 | <browser:page |
14 | for="lp.snappy.interfaces.snapbuild.ISnapBuild" |
15 | + class="lp.snappy.browser.snapbuild.SnapBuildRetryView" |
16 | + permission="launchpad.Edit" |
17 | + name="+retry" |
18 | + template="../templates/snapbuild-retry.pt" /> |
19 | + <browser:page |
20 | + for="lp.snappy.interfaces.snapbuild.ISnapBuild" |
21 | class="lp.snappy.browser.snapbuild.SnapBuildCancelView" |
22 | permission="launchpad.Edit" |
23 | name="+cancel" |
24 | diff --git a/lib/lp/snappy/browser/snapbuild.py b/lib/lp/snappy/browser/snapbuild.py |
25 | index a485afd..7ad92b5 100644 |
26 | --- a/lib/lp/snappy/browser/snapbuild.py |
27 | +++ b/lib/lp/snappy/browser/snapbuild.py |
28 | @@ -1,4 +1,4 @@ |
29 | -# Copyright 2015-2017 Canonical Ltd. This software is licensed under the |
30 | +# Copyright 2015-2020 Canonical Ltd. This software is licensed under the |
31 | # GNU Affero General Public License version 3 (see the file LICENSE). |
32 | |
33 | """SnapBuild views.""" |
34 | @@ -48,7 +48,13 @@ class SnapBuildContextMenu(ContextMenu): |
35 | |
36 | facet = 'overview' |
37 | |
38 | - links = ('cancel', 'rescore') |
39 | + links = ('retry', 'cancel', 'rescore') |
40 | + |
41 | + @enabled_with_permission('launchpad.Edit') |
42 | + def retry(self): |
43 | + return Link( |
44 | + '+retry', 'Retry this build', icon='retry', |
45 | + enabled=self.context.can_be_retried) |
46 | |
47 | @enabled_with_permission('launchpad.Edit') |
48 | def cancel(self): |
49 | @@ -106,6 +112,32 @@ class SnapBuildView(LaunchpadFormView): |
50 | "possible.") |
51 | |
52 | |
53 | +class SnapBuildRetryView(LaunchpadFormView): |
54 | + """View for retrying a snap package build.""" |
55 | + |
56 | + class schema(Interface): |
57 | + """Schema for retrying a build.""" |
58 | + |
59 | + page_title = label = 'Retry build' |
60 | + |
61 | + @property |
62 | + def cancel_url(self): |
63 | + return canonical_url(self.context) |
64 | + next_url = cancel_url |
65 | + |
66 | + @action('Retry build', name='retry') |
67 | + def request_action(self, action, data): |
68 | + """Retry the build.""" |
69 | + if not self.context.can_be_retried: |
70 | + self.request.response.addErrorNotification( |
71 | + 'Build cannot be retried') |
72 | + else: |
73 | + self.context.retry() |
74 | + self.request.response.addInfoNotification('Build has been queued') |
75 | + |
76 | + self.request.response.redirect(self.next_url) |
77 | + |
78 | + |
79 | class SnapBuildCancelView(LaunchpadFormView): |
80 | """View for cancelling a snap package build.""" |
81 | |
82 | diff --git a/lib/lp/snappy/browser/tests/test_snapbuild.py b/lib/lp/snappy/browser/tests/test_snapbuild.py |
83 | index 3b34639..848aa0d 100644 |
84 | --- a/lib/lp/snappy/browser/tests/test_snapbuild.py |
85 | +++ b/lib/lp/snappy/browser/tests/test_snapbuild.py |
86 | @@ -1,4 +1,4 @@ |
87 | -# Copyright 2015-2019 Canonical Ltd. This software is licensed under the |
88 | +# Copyright 2015-2020 Canonical Ltd. This software is licensed under the |
89 | # GNU Affero General Public License version 3 (see the file LICENSE). |
90 | |
91 | """Test snap package build views.""" |
92 | @@ -248,6 +248,38 @@ class TestSnapBuildOperations(BrowserTestCase): |
93 | self.buildd_admin = self.factory.makePerson( |
94 | member_of=[getUtility(ILaunchpadCelebrities).buildd_admin]) |
95 | |
96 | + def test_retry_build(self): |
97 | + # The requester of a build can retry it. |
98 | + self.build.updateStatus(BuildStatus.FAILEDTOBUILD) |
99 | + transaction.commit() |
100 | + browser = self.getViewBrowser(self.build, user=self.requester) |
101 | + browser.getLink("Retry this build").click() |
102 | + self.assertEqual(self.build_url, browser.getLink("Cancel").url) |
103 | + browser.getControl("Retry build").click() |
104 | + self.assertEqual(self.build_url, browser.url) |
105 | + login(ANONYMOUS) |
106 | + self.assertEqual(BuildStatus.NEEDSBUILD, self.build.status) |
107 | + |
108 | + def test_retry_build_random_user(self): |
109 | + # An unrelated non-admin user cannot retry a build. |
110 | + self.build.updateStatus(BuildStatus.FAILEDTOBUILD) |
111 | + transaction.commit() |
112 | + user = self.factory.makePerson() |
113 | + browser = self.getViewBrowser(self.build, user=user) |
114 | + self.assertRaises( |
115 | + LinkNotFoundError, browser.getLink, "Retry this build") |
116 | + self.assertRaises( |
117 | + Unauthorized, self.getUserBrowser, self.build_url + "/+retry", |
118 | + user=user) |
119 | + |
120 | + def test_retry_build_wrong_state(self): |
121 | + # If the build isn't in an unsuccessful terminal state, you can't |
122 | + # retry it. |
123 | + self.build.updateStatus(BuildStatus.FULLYBUILT) |
124 | + browser = self.getViewBrowser(self.build, user=self.requester) |
125 | + self.assertRaises( |
126 | + LinkNotFoundError, browser.getLink, "Retry this build") |
127 | + |
128 | def test_cancel_build(self): |
129 | # The requester of a build can cancel it. |
130 | self.build.queueBuild() |
131 | diff --git a/lib/lp/snappy/interfaces/snapbuild.py b/lib/lp/snappy/interfaces/snapbuild.py |
132 | index c452092..e812f46 100644 |
133 | --- a/lib/lp/snappy/interfaces/snapbuild.py |
134 | +++ b/lib/lp/snappy/interfaces/snapbuild.py |
135 | @@ -1,4 +1,4 @@ |
136 | -# Copyright 2015-2019 Canonical Ltd. This software is licensed under the |
137 | +# Copyright 2015-2020 Canonical Ltd. This software is licensed under the |
138 | # GNU Affero General Public License version 3 (see the file LICENSE). |
139 | |
140 | """Snap package build interfaces.""" |
141 | @@ -184,6 +184,11 @@ class ISnapBuildView(IPackageBuild): |
142 | required=True, readonly=True, |
143 | description=_("Whether this build record can be rescored manually."))) |
144 | |
145 | + can_be_retried = exported(Bool( |
146 | + title=_("Can be retried"), |
147 | + required=False, readonly=True, |
148 | + description=_("Whether this build record can be retried."))) |
149 | + |
150 | can_be_cancelled = exported(Bool( |
151 | title=_("Can be cancelled"), |
152 | required=True, readonly=True, |
153 | @@ -304,6 +309,15 @@ class ISnapBuildEdit(Interface): |
154 | |
155 | @export_write_operation() |
156 | @operation_for_version("devel") |
157 | + def retry(): |
158 | + """Restore the build record to its initial state. |
159 | + |
160 | + Build record loses its history, is moved to NEEDSBUILD and a new |
161 | + non-scored BuildQueue entry is created for it. |
162 | + """ |
163 | + |
164 | + @export_write_operation() |
165 | + @operation_for_version("devel") |
166 | def cancel(): |
167 | """Cancel the build if it is either pending or in progress. |
168 | |
169 | diff --git a/lib/lp/snappy/model/snapbuild.py b/lib/lp/snappy/model/snapbuild.py |
170 | index 69cbcfe..3a9475b 100644 |
171 | --- a/lib/lp/snappy/model/snapbuild.py |
172 | +++ b/lib/lp/snappy/model/snapbuild.py |
173 | @@ -1,4 +1,4 @@ |
174 | -# Copyright 2015-2019 Canonical Ltd. This software is licensed under the |
175 | +# Copyright 2015-2020 Canonical Ltd. This software is licensed under the |
176 | # GNU Affero General Public License version 3 (see the file LICENSE). |
177 | |
178 | from __future__ import absolute_import, print_function, unicode_literals |
179 | @@ -51,6 +51,7 @@ from lp.buildmaster.model.buildfarmjob import SpecificBuildFarmJobSourceMixin |
180 | from lp.buildmaster.model.packagebuild import PackageBuildMixin |
181 | from lp.code.interfaces.gitrepository import IGitRepository |
182 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
183 | +from lp.registry.interfaces.series import SeriesStatus |
184 | from lp.registry.model.distribution import Distribution |
185 | from lp.registry.model.distroseries import DistroSeries |
186 | from lp.registry.model.person import Person |
187 | @@ -273,6 +274,27 @@ class SnapBuild(PackageBuildMixin, Storm): |
188 | return self.buildqueue_record.lastscore |
189 | |
190 | @property |
191 | + def can_be_retried(self): |
192 | + """See `ISnapBuild`.""" |
193 | + # First check that the behaviour would accept the build if it |
194 | + # succeeded. |
195 | + if self.distro_series.status == SeriesStatus.OBSOLETE: |
196 | + return False |
197 | + |
198 | + failed_statuses = [ |
199 | + BuildStatus.FAILEDTOBUILD, |
200 | + BuildStatus.MANUALDEPWAIT, |
201 | + BuildStatus.CHROOTWAIT, |
202 | + BuildStatus.FAILEDTOUPLOAD, |
203 | + BuildStatus.CANCELLED, |
204 | + BuildStatus.SUPERSEDED, |
205 | + ] |
206 | + |
207 | + # If the build is currently in any of the failed states, |
208 | + # it may be retried. |
209 | + return self.status in failed_statuses |
210 | + |
211 | + @property |
212 | def can_be_rescored(self): |
213 | """See `ISnapBuild`.""" |
214 | return ( |
215 | @@ -291,6 +313,19 @@ class SnapBuild(PackageBuildMixin, Storm): |
216 | ] |
217 | return self.status in cancellable_statuses |
218 | |
219 | + def retry(self): |
220 | + """See `ISnapBuild`.""" |
221 | + assert self.can_be_retried, "Build %s cannot be retried" % self.id |
222 | + self.build_farm_job.status = self.status = BuildStatus.NEEDSBUILD |
223 | + self.build_farm_job.date_finished = self.date_finished = None |
224 | + self.date_started = None |
225 | + self.build_farm_job.builder = self.builder = None |
226 | + self.log = None |
227 | + self.upload_log = None |
228 | + self.dependencies = None |
229 | + self.failure_count = 0 |
230 | + self.queueBuild() |
231 | + |
232 | def rescore(self, score): |
233 | """See `ISnapBuild`.""" |
234 | assert self.can_be_rescored, "Build %s cannot be rescored" % self.id |
235 | diff --git a/lib/lp/snappy/templates/snapbuild-index.pt b/lib/lp/snappy/templates/snapbuild-index.pt |
236 | index dd848c0..513a81e 100644 |
237 | --- a/lib/lp/snappy/templates/snapbuild-index.pt |
238 | +++ b/lib/lp/snappy/templates/snapbuild-index.pt |
239 | @@ -106,6 +106,9 @@ |
240 | on <a tal:content="context/builder/title" |
241 | tal:attributes="href context/builder/fmt:url"/> |
242 | </tal:built> |
243 | + <tal:retry define="link context/menu:context/retry" |
244 | + condition="link/enabled" |
245 | + replace="structure link/fmt:link" /> |
246 | <tal:cancel define="link context/menu:context/cancel" |
247 | condition="link/enabled" |
248 | replace="structure link/fmt:link" /> |
249 | diff --git a/lib/lp/snappy/templates/snapbuild-retry.pt b/lib/lp/snappy/templates/snapbuild-retry.pt |
250 | new file mode 100644 |
251 | index 0000000..ba08004 |
252 | --- /dev/null |
253 | +++ b/lib/lp/snappy/templates/snapbuild-retry.pt |
254 | @@ -0,0 +1,28 @@ |
255 | +<html |
256 | + xmlns="http://www.w3.org/1999/xhtml" |
257 | + xmlns:tal="http://xml.zope.org/namespaces/tal" |
258 | + xmlns:metal="http://xml.zope.org/namespaces/metal" |
259 | + xmlns:i18n="http://xml.zope.org/namespaces/i18n" |
260 | + metal:use-macro="view/macro:page/main_only" |
261 | + i18n:domain="launchpad"> |
262 | +<body> |
263 | + |
264 | + <div metal:fill-slot="main"> |
265 | + <div metal:use-macro="context/@@launchpad_form/form"> |
266 | + <div metal:fill-slot="extra_info"> |
267 | + <p> |
268 | + The status of <dfn tal:content="context/title" /> is |
269 | + <span tal:replace="context/status/title" />. |
270 | + </p> |
271 | + <p>Retrying this build will destroy its history and logs.</p> |
272 | + <p> |
273 | + By default, this build will be retried only after other pending |
274 | + builds; please contact a build daemon administrator if you need |
275 | + special treatment. |
276 | + </p> |
277 | + </div> |
278 | + </div> |
279 | + </div> |
280 | + |
281 | +</body> |
282 | +</html> |
283 | diff --git a/lib/lp/snappy/tests/test_snapbuild.py b/lib/lp/snappy/tests/test_snapbuild.py |
284 | index 4a6000e..9ff69d0 100644 |
285 | --- a/lib/lp/snappy/tests/test_snapbuild.py |
286 | +++ b/lib/lp/snappy/tests/test_snapbuild.py |
287 | @@ -1,4 +1,4 @@ |
288 | -# Copyright 2015-2019 Canonical Ltd. This software is licensed under the |
289 | +# Copyright 2015-2020 Canonical Ltd. This software is licensed under the |
290 | # GNU Affero General Public License version 3 (see the file LICENSE). |
291 | |
292 | """Test snap package build features.""" |
293 | @@ -36,6 +36,7 @@ from lp.buildmaster.interfaces.buildqueue import IBuildQueue |
294 | from lp.buildmaster.interfaces.packagebuild import IPackageBuild |
295 | from lp.buildmaster.interfaces.processor import IProcessorSet |
296 | from lp.registry.enums import PersonVisibility |
297 | +from lp.registry.interfaces.series import SeriesStatus |
298 | from lp.services.authserver.xmlrpc import AuthServerAPIView |
299 | from lp.services.config import config |
300 | from lp.services.features.testing import FeatureFixture |
301 | @@ -181,6 +182,30 @@ class TestSnapBuild(TestCaseWithFactory): |
302 | build = self.factory.makeSnapBuild(archive=private_archive) |
303 | self.assertTrue(build.is_private) |
304 | |
305 | + def test_can_be_retried(self): |
306 | + ok_cases = [ |
307 | + BuildStatus.FAILEDTOBUILD, |
308 | + BuildStatus.MANUALDEPWAIT, |
309 | + BuildStatus.CHROOTWAIT, |
310 | + BuildStatus.FAILEDTOUPLOAD, |
311 | + BuildStatus.CANCELLED, |
312 | + BuildStatus.SUPERSEDED, |
313 | + ] |
314 | + for status in BuildStatus.items: |
315 | + build = self.factory.makeSnapBuild(status=status) |
316 | + if status in ok_cases: |
317 | + self.assertTrue(build.can_be_retried) |
318 | + else: |
319 | + self.assertFalse(build.can_be_retried) |
320 | + |
321 | + def test_can_be_retried_obsolete_series(self): |
322 | + # Builds for obsolete series cannot be retried. |
323 | + distroseries = self.factory.makeDistroSeries( |
324 | + status=SeriesStatus.OBSOLETE) |
325 | + das = self.factory.makeDistroArchSeries(distroseries=distroseries) |
326 | + build = self.factory.makeSnapBuild(distroarchseries=das) |
327 | + self.assertFalse(build.can_be_retried) |
328 | + |
329 | def test_can_be_cancelled(self): |
330 | # For all states that can be cancelled, can_be_cancelled returns True. |
331 | ok_cases = [ |
332 | @@ -193,6 +218,22 @@ class TestSnapBuild(TestCaseWithFactory): |
333 | else: |
334 | self.assertFalse(self.build.can_be_cancelled) |
335 | |
336 | + def test_retry_resets_state(self): |
337 | + # Retrying a build resets most of the state attributes, but does |
338 | + # not modify the first dispatch time. |
339 | + now = datetime.now(pytz.UTC) |
340 | + build = self.factory.makeSnapBuild() |
341 | + build.updateStatus(BuildStatus.BUILDING, date_started=now) |
342 | + build.updateStatus(BuildStatus.FAILEDTOBUILD) |
343 | + build.gotFailure() |
344 | + with person_logged_in(build.snap.owner): |
345 | + build.retry() |
346 | + self.assertEqual(BuildStatus.NEEDSBUILD, build.status) |
347 | + self.assertEqual(now, build.date_first_dispatched) |
348 | + self.assertIsNone(build.log) |
349 | + self.assertIsNone(build.upload_log) |
350 | + self.assertEqual(0, build.failure_count) |
351 | + |
352 | def test_cancel_not_in_progress(self): |
353 | # The cancel() method for a pending build leaves it in the CANCELLED |
354 | # state. |