Merge lp:~gundlach/nova/rsapi-faults into lp:~hudson-openstack/nova/trunk

Proposed by Michael Gundlach
Status: Merged
Approved by: Michael Gundlach
Approved revision: 297
Merged at revision: 307
Proposed branch: lp:~gundlach/nova/rsapi-faults
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 315 lines (+120/-21)
8 files modified
nova/api/rackspace/__init__.py (+6/-3)
nova/api/rackspace/auth.py (+4/-3)
nova/api/rackspace/backup_schedules.py (+4/-3)
nova/api/rackspace/faults.py (+61/-0)
nova/api/rackspace/flavors.py (+2/-1)
nova/api/rackspace/images.py (+4/-3)
nova/api/rackspace/servers.py (+9/-8)
nova/tests/api/rackspace/testfaults.py (+30/-0)
To merge this branch: bzr merge lp:~gundlach/nova/rsapi-faults
Reviewer Review Type Date Requested Status
Matt Dietz (community) Approve
Review via email: mp+36984@code.launchpad.net

Description of the change

Support fault notation in error messages in the RS API.

To post a comment you must log in.
lp:~gundlach/nova/rsapi-faults updated
296. By Michael Gundlach

Merge from trunk

297. By Michael Gundlach

After update from trunk, a few more exceptions that need to be converted to Faults

Revision history for this message
Matt Dietz (cerberus) wrote :

lgtm.

Not for now, but the API also supports more granular information per exception. In the future, we should modify the faults to include an optional extended_description field.

review: Approve
Revision history for this message
Michael Gundlach (gundlach) wrote :

Cool -- webob.exc.HTTPException's ctor supports 'detail=', which I imagine we could map into the <details> of the Fault response body.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'nova/api/rackspace/__init__.py'
2--- nova/api/rackspace/__init__.py 2010-09-28 21:46:21 +0000
3+++ nova/api/rackspace/__init__.py 2010-09-29 14:20:56 +0000
4@@ -31,6 +31,7 @@
5 from nova import flags
6 from nova import utils
7 from nova import wsgi
8+from nova.api.rackspace import faults
9 from nova.api.rackspace import backup_schedules
10 from nova.api.rackspace import flavors
11 from nova.api.rackspace import images
12@@ -67,7 +68,7 @@
13 user = self.auth_driver.authorize_token(req.headers["X-Auth-Token"])
14
15 if not user:
16- return webob.exc.HTTPUnauthorized()
17+ return faults.Fault(webob.exc.HTTPUnauthorized())
18
19 if not req.environ.has_key('nova.context'):
20 req.environ['nova.context'] = {}
21@@ -112,8 +113,10 @@
22 delay = self.get_delay(action_name, username)
23 if delay:
24 # TODO(gundlach): Get the retry-after format correct.
25- raise webob.exc.HTTPRequestEntityTooLarge(headers={
26- 'Retry-After': time.time() + delay})
27+ exc = webob.exc.HTTPRequestEntityTooLarge(
28+ explanation='Too many requests.',
29+ headers={'Retry-After': time.time() + delay})
30+ raise faults.Fault(exc)
31 return self.application
32
33 def get_delay(self, action_name, username):
34
35=== modified file 'nova/api/rackspace/auth.py'
36--- nova/api/rackspace/auth.py 2010-09-28 21:46:21 +0000
37+++ nova/api/rackspace/auth.py 2010-09-29 14:20:56 +0000
38@@ -11,6 +11,7 @@
39 from nova import flags
40 from nova import manager
41 from nova import utils
42+from nova.api.rackspace import faults
43
44 FLAGS = flags.FLAGS
45
46@@ -36,13 +37,13 @@
47 # honor it
48 path_info = req.path_info
49 if len(path_info) > 1:
50- return webob.exc.HTTPUnauthorized()
51+ return faults.Fault(webob.exc.HTTPUnauthorized())
52
53 try:
54 username, key = req.headers['X-Auth-User'], \
55 req.headers['X-Auth-Key']
56 except KeyError:
57- return webob.exc.HTTPUnauthorized()
58+ return faults.Fault(webob.exc.HTTPUnauthorized())
59
60 username, key = req.headers['X-Auth-User'], req.headers['X-Auth-Key']
61 token, user = self._authorize_user(username, key)
62@@ -57,7 +58,7 @@
63 res.status = '204'
64 return res
65 else:
66- return webob.exc.HTTPUnauthorized()
67+ return faults.Fault(webob.exc.HTTPUnauthorized())
68
69 def authorize_token(self, token_hash):
70 """ retrieves user information from the datastore given a token
71
72=== modified file 'nova/api/rackspace/backup_schedules.py'
73--- nova/api/rackspace/backup_schedules.py 2010-09-28 21:46:21 +0000
74+++ nova/api/rackspace/backup_schedules.py 2010-09-29 14:20:56 +0000
75@@ -20,6 +20,7 @@
76
77 from nova import wsgi
78 from nova.api.rackspace import _id_translator
79+from nova.api.rackspace import faults
80 import nova.image.service
81
82 class Controller(wsgi.Controller):
83@@ -27,12 +28,12 @@
84 pass
85
86 def index(self, req, server_id):
87- return exc.HTTPNotFound()
88+ return faults.Fault(exc.HTTPNotFound())
89
90 def create(self, req, server_id):
91 """ No actual update method required, since the existing API allows
92 both create and update through a POST """
93- return exc.HTTPNotFound()
94+ return faults.Fault(exc.HTTPNotFound())
95
96 def delete(self, req, server_id):
97- return exc.HTTPNotFound()
98+ return faults.Fault(exc.HTTPNotFound())
99
100=== added file 'nova/api/rackspace/faults.py'
101--- nova/api/rackspace/faults.py 1970-01-01 00:00:00 +0000
102+++ nova/api/rackspace/faults.py 2010-09-29 14:20:56 +0000
103@@ -0,0 +1,61 @@
104+# vim: tabstop=4 shiftwidth=4 softtabstop=4
105+
106+# Copyright 2010 OpenStack LLC.
107+# All Rights Reserved.
108+#
109+# Licensed under the Apache License, Version 2.0 (the "License"); you may
110+# not use this file except in compliance with the License. You may obtain
111+# a copy of the License at
112+#
113+# http://www.apache.org/licenses/LICENSE-2.0
114+#
115+# Unless required by applicable law or agreed to in writing, software
116+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
117+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
118+# License for the specific language governing permissions and limitations
119+# under the License.
120+
121+
122+import webob.dec
123+
124+from nova import wsgi
125+
126+
127+class Fault(wsgi.Application):
128+
129+ """An RS API fault response."""
130+
131+ _fault_names = {
132+ 400: "badRequest",
133+ 401: "unauthorized",
134+ 403: "resizeNotAllowed",
135+ 404: "itemNotFound",
136+ 405: "badMethod",
137+ 409: "inProgress",
138+ 413: "overLimit",
139+ 415: "badMediaType",
140+ 501: "notImplemented",
141+ 503: "serviceUnavailable"}
142+
143+ def __init__(self, exception):
144+ """Create a Fault for the given webob.exc.exception."""
145+ self.exception = exception
146+
147+ @webob.dec.wsgify
148+ def __call__(self, req):
149+ """Generate a WSGI response based on self.exception."""
150+ # Replace the body with fault details.
151+ code = self.exception.status_int
152+ fault_name = self._fault_names.get(code, "cloudServersFault")
153+ fault_data = {
154+ fault_name: {
155+ 'code': code,
156+ 'message': self.exception.explanation}}
157+ if code == 413:
158+ retry = self.exception.headers['Retry-After']
159+ fault_data[fault_name]['retryAfter'] = retry
160+ # 'code' is an attribute on the fault tag itself
161+ metadata = {'application/xml': {'attributes': {fault_name: 'code'}}}
162+ serializer = wsgi.Serializer(req.environ, metadata)
163+ self.exception.body = serializer.to_content_type(fault_data)
164+ return self.exception
165
166=== modified file 'nova/api/rackspace/flavors.py'
167--- nova/api/rackspace/flavors.py 2010-09-28 05:23:49 +0000
168+++ nova/api/rackspace/flavors.py 2010-09-29 14:20:56 +0000
169@@ -15,6 +15,7 @@
170 # License for the specific language governing permissions and limitations
171 # under the License.
172
173+from nova.api.rackspace import faults
174 from nova.compute import instance_types
175 from nova import wsgi
176 from webob import exc
177@@ -47,7 +48,7 @@
178 item = dict(ram=val['memory_mb'], disk=val['local_gb'],
179 id=val['flavorid'], name=name)
180 return dict(flavor=item)
181- raise exc.HTTPNotFound()
182+ raise faults.Fault(exc.HTTPNotFound())
183
184 def _all_ids(self):
185 """Return the list of all flavorids."""
186
187=== modified file 'nova/api/rackspace/images.py'
188--- nova/api/rackspace/images.py 2010-09-28 21:46:21 +0000
189+++ nova/api/rackspace/images.py 2010-09-29 14:20:56 +0000
190@@ -20,6 +20,7 @@
191 from nova import wsgi
192 from nova.api.rackspace import _id_translator
193 import nova.image.service
194+from nova.api.rackspace import faults
195
196 class Controller(wsgi.Controller):
197
198@@ -58,14 +59,14 @@
199
200 def delete(self, req, id):
201 # Only public images are supported for now.
202- raise exc.HTTPNotFound()
203+ raise faults.Fault(exc.HTTPNotFound())
204
205 def create(self, req):
206 # Only public images are supported for now, so a request to
207 # make a backup of a server cannot be supproted.
208- raise exc.HTTPNotFound()
209+ raise faults.Fault(exc.HTTPNotFound())
210
211 def update(self, req, id):
212 # Users may not modify public images, and that's all that
213 # we support for now.
214- raise exc.HTTPNotFound()
215+ raise faults.Fault(exc.HTTPNotFound())
216
217=== modified file 'nova/api/rackspace/servers.py'
218--- nova/api/rackspace/servers.py 2010-09-28 21:46:21 +0000
219+++ nova/api/rackspace/servers.py 2010-09-29 14:20:56 +0000
220@@ -24,6 +24,7 @@
221 from nova import utils
222 from nova import wsgi
223 from nova.api.rackspace import _id_translator
224+from nova.api.rackspace import faults
225 from nova.compute import power_state
226 import nova.image.service
227
228@@ -120,7 +121,7 @@
229 if inst:
230 if inst.user_id == user_id:
231 return _entity_detail(inst)
232- raise exc.HTTPNotFound()
233+ raise faults.Fault(exc.HTTPNotFound())
234
235 def delete(self, req, id):
236 """ Destroys a server """
237@@ -128,13 +129,13 @@
238 instance = self.db_driver.instance_get(None, id)
239 if instance and instance['user_id'] == user_id:
240 self.db_driver.instance_destroy(None, id)
241- return exc.HTTPAccepted()
242- return exc.HTTPNotFound()
243+ return faults.Fault(exc.HTTPAccepted())
244+ return faults.Fault(exc.HTTPNotFound())
245
246 def create(self, req):
247 """ Creates a new server for a given user """
248 if not req.environ.has_key('inst_dict'):
249- return exc.HTTPUnprocessableEntity()
250+ return faults.Fault(exc.HTTPUnprocessableEntity())
251
252 inst = self._build_server_instance(req)
253
254@@ -147,22 +148,22 @@
255 def update(self, req, id):
256 """ Updates the server name or password """
257 if not req.environ.has_key('inst_dict'):
258- return exc.HTTPUnprocessableEntity()
259+ return faults.Fault(exc.HTTPUnprocessableEntity())
260
261 instance = self.db_driver.instance_get(None, id)
262 if not instance:
263- return exc.HTTPNotFound()
264+ return faults.Fault(exc.HTTPNotFound())
265
266 attrs = req.environ['nova.context'].get('model_attributes', None)
267 if attrs:
268 self.db_driver.instance_update(None, id, _filter_params(attrs))
269- return exc.HTTPNoContent()
270+ return faults.Fault(exc.HTTPNoContent())
271
272 def action(self, req, id):
273 """ multi-purpose method used to reboot, rebuild, and
274 resize a server """
275 if not req.environ.has_key('inst_dict'):
276- return exc.HTTPUnprocessableEntity()
277+ return faults.Fault(exc.HTTPUnprocessableEntity())
278
279 def _build_server_instance(self, req):
280 """Build instance data structure and save it to the data store."""
281
282=== added file 'nova/tests/api/rackspace/testfaults.py'
283--- nova/tests/api/rackspace/testfaults.py 1970-01-01 00:00:00 +0000
284+++ nova/tests/api/rackspace/testfaults.py 2010-09-29 14:20:56 +0000
285@@ -0,0 +1,30 @@
286+import unittest
287+import webob
288+import webob.exc
289+
290+from nova.api.rackspace import faults
291+
292+class TestFaults(unittest.TestCase):
293+
294+ def test_fault_parts(self):
295+ req = webob.Request.blank('/.xml')
296+ f = faults.Fault(webob.exc.HTTPBadRequest(explanation='scram'))
297+ resp = req.get_response(f)
298+
299+ first_two_words = resp.body.strip().split()[:2]
300+ self.assertEqual(first_two_words, ['<badRequest', 'code="400">'])
301+ body_without_spaces = ''.join(resp.body.split())
302+ self.assertTrue('<message>scram</message>' in body_without_spaces)
303+
304+ def test_retry_header(self):
305+ req = webob.Request.blank('/.xml')
306+ exc = webob.exc.HTTPRequestEntityTooLarge(explanation='sorry',
307+ headers={'Retry-After': 4})
308+ f = faults.Fault(exc)
309+ resp = req.get_response(f)
310+ first_two_words = resp.body.strip().split()[:2]
311+ self.assertEqual(first_two_words, ['<overLimit', 'code="413">'])
312+ body_sans_spaces = ''.join(resp.body.split())
313+ self.assertTrue('<message>sorry</message>' in body_sans_spaces)
314+ self.assertTrue('<retryAfter>4</retryAfter>' in body_sans_spaces)
315+ self.assertEqual(resp.headers['Retry-After'], 4)