Browse Source

[placement] use simple FaultWrapper

Prior to this patch placement was using the nova FaultWrapper
middleware as a final guard against unexpected exceptions. That
middleware does more than required and also imports stuff that
placement does not otherwise require.

The new middleware is only for exceptions that have fallen through
previous handling and need to be turned into 500 responses, formatted
to JSON, and logged.

While doing this it became clear that the limited Exception catching
in handler.py was redundant if FaultWrapper and the Microversion
middlewares (both required for the operation of the system) are
involved, so that has been removed, with a comment.

This should not require a microversion as the structure of successful
responses and expected error responses does not change. What does
change is that a 500 response will be formatted more as the api-wg
errors guideline[1] desires.

[1] http://specs.openstack.org/openstack/api-wg/guidelines/errors.html

Change-Id: Ib2c3250b964a30239f298cdf13ea2020f205f67d
Partial-Bug: #1743120
tags/18.0.0.0b1
Chris Dent 1 year ago
parent
commit
5f881ff030

+ 3
- 3
nova/api/openstack/placement/deploy.py View File

@@ -14,8 +14,8 @@
14 14
 import oslo_middleware
15 15
 from oslo_middleware import cors
16 16
 
17
-from nova.api import openstack as common_api
18 17
 from nova.api.openstack.placement import auth
18
+from nova.api.openstack.placement import fault_wrap
19 19
 from nova.api.openstack.placement import handler
20 20
 from nova.api.openstack.placement import microversion
21 21
 from nova.api.openstack.placement import requestlog
@@ -56,7 +56,7 @@ def deploy(conf):
56 56
     context_middleware = auth.PlacementKeystoneContext
57 57
     req_id_middleware = oslo_middleware.RequestId
58 58
     microversion_middleware = microversion.MicroversionMiddleware
59
-    fault_wrap = common_api.FaultWrapper
59
+    fault_middleware = fault_wrap.FaultWrapper
60 60
     request_log = requestlog.RequestLog
61 61
 
62 62
     application = handler.PlacementHandler()
@@ -70,7 +70,7 @@ def deploy(conf):
70 70
     # all see the same contextual information including request id and
71 71
     # authentication information.
72 72
     for middleware in (microversion_middleware,
73
-                       fault_wrap,
73
+                       fault_middleware,
74 74
                        request_log,
75 75
                        context_middleware,
76 76
                        auth_middleware,

+ 48
- 0
nova/api/openstack/placement/fault_wrap.py View File

@@ -0,0 +1,48 @@
1
+# Licensed under the Apache License, Version 2.0 (the "License");
2
+# you may not use this file except in compliance with the License.
3
+# You may obtain a copy of the License at
4
+#
5
+#    http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+# Unless required by applicable law or agreed to in writing, software
8
+# distributed under the License is distributed on an "AS IS" BASIS,
9
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
10
+# implied.
11
+# See the License for the specific language governing permissions and
12
+# limitations under the License.
13
+"""Simple middleware for safely catching unexpected exceptions."""
14
+
15
+# NOTE(cdent): This is a super simplified replacement for the nova
16
+# FaultWrapper, which does more than placement needs.
17
+
18
+from oslo_log import log as logging
19
+import six
20
+from webob import exc
21
+
22
+from nova.api.openstack.placement import util
23
+
24
+LOG = logging.getLogger(__name__)
25
+
26
+
27
+class FaultWrapper(object):
28
+    """Turn an uncaught exception into a status 500.
29
+
30
+    Uncaught exceptions usually shouldn't happen, if it does it
31
+    means there is a bug in the placement service, which should be
32
+    fixed.
33
+    """
34
+
35
+    def __init__(self, application):
36
+        self.application = application
37
+
38
+    def __call__(self, environ, start_response):
39
+        try:
40
+            return self.application(environ, start_response)
41
+        except Exception as unexpected_exception:
42
+            LOG.exception('Placement API unexpected error: %s',
43
+                          unexpected_exception)
44
+            formatted_exception = exc.HTTPInternalServerError(
45
+                six.text_type(unexpected_exception))
46
+            formatted_exception.json_formatter = util.json_error_formatter
47
+            return formatted_exception.generate_response(
48
+                environ, start_response)

+ 9
- 9
nova/api/openstack/placement/handler.py View File

@@ -223,12 +223,12 @@ class PlacementHandler(object):
223 223
         except exception.NotFound as exc:
224 224
             raise webob.exc.HTTPNotFound(
225 225
                 exc, json_formatter=util.json_error_formatter)
226
-        # Trap the HTTPNotFound that can be raised by dispatch()
227
-        # when no route is found. The exception is passed through to
228
-        # the FaultWrap middleware without causing an alarming log
229
-        # message.
230
-        except webob.exc.HTTPNotFound:
231
-            raise
232
-        except Exception as exc:
233
-            LOG.exception("Uncaught exception")
234
-            raise
226
+        # Remaining uncaught exceptions will rise first to the Microversion
227
+        # middleware, where any WebOb generated exceptions will be caught and
228
+        # transformed into legit HTTP error responses (with microversion
229
+        # headers added), and then to the FaultWrapper middleware which will
230
+        # catch anything else and transform them into 500 responses.
231
+        # NOTE(cdent): There should be very few uncaught exceptions which are
232
+        # not WebOb exceptions at this stage as the handlers are contained by
233
+        # the wsgify decorator which will transform those exceptions to
234
+        # responses itself.

+ 66
- 0
nova/tests/unit/api/openstack/placement/test_fault_wrap.py View File

@@ -0,0 +1,66 @@
1
+# Licensed under the Apache License, Version 2.0 (the "License");
2
+# you may not use this file except in compliance with the License.
3
+# You may obtain a copy of the License at
4
+#
5
+#    http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+# Unless required by applicable law or agreed to in writing, software
8
+# distributed under the License is distributed on an "AS IS" BASIS,
9
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
10
+# implied.
11
+# See the License for the specific language governing permissions and
12
+# limitations under the License.
13
+"""Tests for the placement fault wrap middleware."""
14
+
15
+import mock
16
+
17
+from oslo_serialization import jsonutils
18
+import webob
19
+
20
+from nova.api.openstack.placement import fault_wrap
21
+from nova import test
22
+
23
+
24
+ERROR_MESSAGE = 'that was not supposed to happen'
25
+
26
+
27
+class Fault(Exception):
28
+    pass
29
+
30
+
31
+class TestFaultWrapper(test.NoDBTestCase):
32
+
33
+    @staticmethod
34
+    @webob.dec.wsgify
35
+    def failing_application(req):
36
+        raise Fault(ERROR_MESSAGE)
37
+
38
+    def setUp(self):
39
+        super(TestFaultWrapper, self).setUp()
40
+        self.req = webob.Request.blank('/')
41
+        self.environ = self.req.environ
42
+        self.environ['HTTP_ACCEPT'] = 'application/json'
43
+        self.start_response_mock = mock.MagicMock()
44
+        self.fail_app = fault_wrap.FaultWrapper(self.failing_application)
45
+
46
+    def test_fault_is_wrapped(self):
47
+        response = self.fail_app(self.environ, self.start_response_mock)
48
+        # response is a single member list
49
+        error_struct = jsonutils.loads(response[0])
50
+        first_error = error_struct['errors'][0]
51
+
52
+        self.assertIn(ERROR_MESSAGE, first_error['detail'])
53
+        self.assertEqual(500, first_error['status'])
54
+        self.assertEqual('Internal Server Error', first_error['title'])
55
+
56
+    def test_fault_response_headers(self):
57
+        self.fail_app(self.environ, self.start_response_mock)
58
+        call_args = self.start_response_mock.call_args
59
+        self.assertEqual('500 Internal Server Error', call_args[0][0])
60
+
61
+    @mock.patch("nova.api.openstack.placement.fault_wrap.LOG")
62
+    def test_fault_log(self, mocked_log):
63
+        self.fail_app(self.environ, self.start_response_mock)
64
+        mocked_log.exception.assert_called_once_with(
65
+                'Placement API unexpected error: %s',
66
+                mock.ANY)

Loading…
Cancel
Save