Merge lp:~cjwatson/launchpad/snap-store-upload-error-debug into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18358
Proposed branch: lp:~cjwatson/launchpad/snap-store-upload-error-debug
Merge into: lp:launchpad
Diff against target: 278 lines (+108/-46)
6 files modified
lib/lp/snappy/interfaces/snapbuildjob.py (+3/-0)
lib/lp/snappy/interfaces/snapstoreclient.py (+51/-42)
lib/lp/snappy/model/snapbuildjob.py (+19/-1)
lib/lp/snappy/model/snapstoreclient.py (+11/-2)
lib/lp/snappy/tests/test_snapbuildjob.py (+4/-1)
lib/lp/snappy/tests/test_snapstoreclient.py (+20/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/snap-store-upload-error-debug
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+322715@code.launchpad.net

Commit message

Include the full response from unscanned-upload as an error_detail variable in BadUploadResponse OOPSes.

Description of the change

I'm hoping to get something I can use to figure out bug 1683819.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) wrote :

Let's just hope it doesn't echo back the whole blob.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/snappy/interfaces/snapbuildjob.py'
2--- lib/lp/snappy/interfaces/snapbuildjob.py 2017-04-03 14:43:22 +0000
3+++ lib/lp/snappy/interfaces/snapbuildjob.py 2017-04-18 21:59:36 +0000
4@@ -57,6 +57,9 @@
5 error_message = TextLine(
6 title=_("Error message"), required=False, readonly=True)
7
8+ error_detail = TextLine(
9+ title=_("Error message detail"), required=False, readonly=True)
10+
11 store_url = TextLine(
12 title=_("The URL on the store corresponding to this build"),
13 required=False, readonly=True)
14
15=== modified file 'lib/lp/snappy/interfaces/snapstoreclient.py'
16--- lib/lp/snappy/interfaces/snapstoreclient.py 2016-12-08 17:39:19 +0000
17+++ lib/lp/snappy/interfaces/snapstoreclient.py 2017-04-18 21:59:36 +0000
18@@ -1,4 +1,4 @@
19-# Copyright 2016 Canonical Ltd. This software is licensed under the
20+# Copyright 2016-2017 Canonical Ltd. This software is licensed under the
21 # GNU Affero General Public License version 3 (see the file LICENSE).
22
23 """Interface for communication with the snap store."""
24@@ -17,6 +17,7 @@
25 'NeedsRefreshResponse',
26 'ReleaseFailedResponse',
27 'ScanFailedResponse',
28+ 'SnapStoreError',
29 'UnauthorizedUploadResponse',
30 'UploadNotScannedYetResponse',
31 ]
32@@ -27,48 +28,56 @@
33 from zope.interface import Interface
34
35
36+class SnapStoreError(Exception):
37+
38+ def __init__(self, message="", detail=None):
39+ super(SnapStoreError, self).__init__(message)
40+ self.message = message
41+ self.detail = detail
42+
43+
44 @error_status(httplib.INTERNAL_SERVER_ERROR)
45-class BadRequestPackageUploadResponse(Exception):
46- pass
47-
48-
49-class BadUploadResponse(Exception):
50- pass
51-
52-
53-class BadRefreshResponse(Exception):
54- pass
55-
56-
57-class NeedsRefreshResponse(Exception):
58- pass
59-
60-
61-class UnauthorizedUploadResponse(Exception):
62- pass
63-
64-
65-class BadScanStatusResponse(Exception):
66- pass
67-
68-
69-class UploadNotScannedYetResponse(Exception):
70- pass
71-
72-
73-class ScanFailedResponse(Exception):
74- pass
75-
76-
77-class BadSearchResponse(Exception):
78- pass
79-
80-
81-class BadReleaseResponse(Exception):
82- pass
83-
84-
85-class ReleaseFailedResponse(Exception):
86+class BadRequestPackageUploadResponse(SnapStoreError):
87+ pass
88+
89+
90+class BadUploadResponse(SnapStoreError):
91+ pass
92+
93+
94+class BadRefreshResponse(SnapStoreError):
95+ pass
96+
97+
98+class NeedsRefreshResponse(SnapStoreError):
99+ pass
100+
101+
102+class UnauthorizedUploadResponse(SnapStoreError):
103+ pass
104+
105+
106+class BadScanStatusResponse(SnapStoreError):
107+ pass
108+
109+
110+class UploadNotScannedYetResponse(SnapStoreError):
111+ pass
112+
113+
114+class ScanFailedResponse(SnapStoreError):
115+ pass
116+
117+
118+class BadSearchResponse(SnapStoreError):
119+ pass
120+
121+
122+class BadReleaseResponse(SnapStoreError):
123+ pass
124+
125+
126+class ReleaseFailedResponse(SnapStoreError):
127 pass
128
129
130
131=== modified file 'lib/lp/snappy/model/snapbuildjob.py'
132--- lib/lp/snappy/model/snapbuildjob.py 2017-04-03 14:43:22 +0000
133+++ lib/lp/snappy/model/snapbuildjob.py 2017-04-18 21:59:36 +0000
134@@ -61,6 +61,7 @@
135 ISnapStoreClient,
136 ReleaseFailedResponse,
137 ScanFailedResponse,
138+ SnapStoreError,
139 UnauthorizedUploadResponse,
140 UploadNotScannedYetResponse,
141 )
142@@ -165,7 +166,7 @@
143 return oops_vars
144
145
146-class ManualReview(Exception):
147+class ManualReview(SnapStoreError):
148 pass
149
150
151@@ -216,6 +217,16 @@
152 self.metadata["error_message"] = message
153
154 @property
155+ def error_detail(self):
156+ """See `ISnapStoreUploadJob`."""
157+ return self.metadata.get("error_detail")
158+
159+ @error_detail.setter
160+ def error_detail(self, detail):
161+ """See `ISnapStoreUploadJob`."""
162+ self.metadata["error_detail"] = detail
163+
164+ @property
165 def store_url(self):
166 """See `ISnapStoreUploadJob`."""
167 return self.metadata.get("store_url")
168@@ -262,6 +273,12 @@
169 def resume(self, *args, **kwargs):
170 self._do_lifecycle(self.job.resume, *args, **kwargs)
171
172+ def getOopsVars(self):
173+ """See `IRunnableJob`."""
174+ oops_vars = super(SnapStoreUploadJob, self).getOopsVars()
175+ oops_vars.append(('error_detail', self.error_detail))
176+ return oops_vars
177+
178 def run(self):
179 """See `IRunnableJob`."""
180 client = getUtility(ISnapStoreClient)
181@@ -282,6 +299,7 @@
182 raise
183 except Exception as e:
184 self.error_message = str(e)
185+ self.error_detail = getattr(e, "detail", None)
186 if isinstance(e, UnauthorizedUploadResponse):
187 mailer = SnapBuildMailer.forUnauthorizedUpload(self.snapbuild)
188 mailer.sendAll()
189
190=== modified file 'lib/lp/snappy/model/snapstoreclient.py'
191--- lib/lp/snappy/model/snapstoreclient.py 2017-02-27 12:01:08 +0000
192+++ lib/lp/snappy/model/snapstoreclient.py 2017-04-18 21:59:36 +0000
193@@ -179,13 +179,22 @@
194 try:
195 response = urlfetch(
196 unscanned_upload_url, method="POST", data=encoder,
197- headers={"Content-Type": encoder.content_type})
198+ headers={
199+ "Content-Type": encoder.content_type,
200+ "Accept": "application/json",
201+ })
202 response_data = response.json()
203 if not response_data.get("successful", False):
204 raise BadUploadResponse(response.text)
205 return {"upload_id": response_data["upload_id"]}
206 except requests.HTTPError as e:
207- raise BadUploadResponse(e.args[0])
208+ if e.response is not None:
209+ detail = e.response.content
210+ if isinstance(detail, bytes):
211+ detail = detail.decode("UTF-8", errors="replace")
212+ else:
213+ detail = None
214+ raise BadUploadResponse(e.args[0], detail=detail)
215 finally:
216 lfa.close()
217
218
219=== modified file 'lib/lp/snappy/tests/test_snapbuildjob.py'
220--- lib/lp/snappy/tests/test_snapbuildjob.py 2017-04-03 14:43:22 +0000
221+++ lib/lp/snappy/tests/test_snapbuildjob.py 2017-04-18 21:59:36 +0000
222@@ -239,7 +239,8 @@
223 self.assertContentEqual([], snapbuild.store_upload_jobs)
224 job = SnapStoreUploadJob.create(snapbuild)
225 client = FakeSnapStoreClient()
226- client.upload.failure = BadUploadResponse("Failed to upload")
227+ client.upload.failure = BadUploadResponse(
228+ "Failed to upload", detail="The proxy exploded.\n")
229 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
230 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
231 JobRunner([job]).runAll()
232@@ -277,6 +278,8 @@
233 build_url, footer)
234 self.assertWebhookDeliveries(
235 snapbuild, ["Pending", "Failed to upload"])
236+ self.assertIn(
237+ ("error_detail", "The proxy exploded.\n"), job.getOopsVars())
238
239 def test_run_scan_pending_retries(self):
240 # A run that finds that the store has not yet finished scanning the
241
242=== modified file 'lib/lp/snappy/tests/test_snapstoreclient.py'
243--- lib/lp/snappy/tests/test_snapstoreclient.py 2017-02-27 12:01:08 +0000
244+++ lib/lp/snappy/tests/test_snapstoreclient.py 2017-04-18 21:59:36 +0000
245@@ -48,6 +48,7 @@
246 BadRequestPackageUploadResponse,
247 BadScanStatusResponse,
248 BadSearchResponse,
249+ BadUploadResponse,
250 ISnapStoreClient,
251 ReleaseFailedResponse,
252 ScanFailedResponse,
253@@ -445,6 +446,25 @@
254 store_secrets["discharge"],
255 snapbuild.snap.store_secrets["discharge"])
256
257+ def test_upload_file_error(self):
258+ @urlmatch(path=r".*/unscanned-upload/$")
259+ def unscanned_upload_handler(url, request):
260+ return {
261+ "status_code": 502,
262+ "reason": "Proxy Error",
263+ "content": b"The proxy exploded.\n",
264+ }
265+
266+ store_secrets = self._make_store_secrets()
267+ snapbuild = self.makeUploadableSnapBuild(store_secrets=store_secrets)
268+ transaction.commit()
269+ with dbuser(config.ISnapStoreUploadJobSource.dbuser):
270+ with HTTMock(unscanned_upload_handler):
271+ err = self.assertRaises(
272+ BadUploadResponse, self.client.upload, snapbuild)
273+ self.assertEqual("502 Server Error: Proxy Error", str(err))
274+ self.assertEqual(b"The proxy exploded.\n", err.detail)
275+
276 def test_refresh_discharge_macaroon(self):
277 store_secrets = self._make_store_secrets()
278 snap = self.factory.makeSnap(