Merge ~cjwatson/launchpad:snap-retry-build into launchpad: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)
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

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
1diff --git a/lib/lp/snappy/browser/configure.zcml b/lib/lp/snappy/browser/configure.zcml
2index 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"
24diff --git a/lib/lp/snappy/browser/snapbuild.py b/lib/lp/snappy/browser/snapbuild.py
25index 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
82diff --git a/lib/lp/snappy/browser/tests/test_snapbuild.py b/lib/lp/snappy/browser/tests/test_snapbuild.py
83index 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()
131diff --git a/lib/lp/snappy/interfaces/snapbuild.py b/lib/lp/snappy/interfaces/snapbuild.py
132index 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
169diff --git a/lib/lp/snappy/model/snapbuild.py b/lib/lp/snappy/model/snapbuild.py
170index 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
235diff --git a/lib/lp/snappy/templates/snapbuild-index.pt b/lib/lp/snappy/templates/snapbuild-index.pt
236index 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" />
249diff --git a/lib/lp/snappy/templates/snapbuild-retry.pt b/lib/lp/snappy/templates/snapbuild-retry.pt
250new file mode 100644
251index 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>
283diff --git a/lib/lp/snappy/tests/test_snapbuild.py b/lib/lp/snappy/tests/test_snapbuild.py
284index 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.

Subscribers

People subscribed via source and target branches

to status/vote changes: