]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Return request-id in API response
authorAkihiro Motoki <motoki@da.jp.nec.com>
Thu, 5 Dec 2013 06:55:31 +0000 (15:55 +0900)
committerAkihiro Motoki <motoki@da.jp.nec.com>
Sun, 9 Feb 2014 19:58:38 +0000 (04:58 +0900)
Import RequestIdMiddleware from oslo which ensures to request-id
in API response. CatchErrorsMiddleware is also imported to ensure
all internal exceptions are caught outermost.
api-paste.ini is updated to use them.

KeystonAuthContext middleware is updated so that it uses
request-id generated by RequestIdMiddleware.

Add middleware to openstack.conf and import all modules
under middleware directory from oslo.

DocImpact UpgradeImpact
This patch adds new WSGI middlewares "request_id" and "catch_errors".
They needs to be added to api-paste.ini when upgrading.

Change-Id: Icf01b7de697ef50bef53212da2cf520d1ff78b88
Closes-Bug: #1239923

13 files changed:
etc/api-paste.ini
neutron/auth.py
neutron/openstack/common/middleware/__init__.py [new file with mode: 0644]
neutron/openstack/common/middleware/audit.py [new file with mode: 0644]
neutron/openstack/common/middleware/base.py [new file with mode: 0644]
neutron/openstack/common/middleware/catch_errors.py [new file with mode: 0644]
neutron/openstack/common/middleware/correlation_id.py [new file with mode: 0644]
neutron/openstack/common/middleware/debug.py [new file with mode: 0644]
neutron/openstack/common/middleware/notifier.py [new file with mode: 0644]
neutron/openstack/common/middleware/request_id.py [new file with mode: 0644]
neutron/openstack/common/middleware/sizelimit.py [new file with mode: 0644]
neutron/tests/unit/test_auth.py
openstack-common.conf

index 8c084953febe72dfd8da874be018f36148b144c4..be8aae17fc7e467fc646eadd41b7d27a7bd333fc 100644 (file)
@@ -5,8 +5,14 @@ use = egg:Paste#urlmap
 
 [composite:neutronapi_v2_0]
 use = call:neutron.auth:pipeline_factory
-noauth = extensions neutronapiapp_v2_0
-keystone = authtoken keystonecontext extensions neutronapiapp_v2_0
+noauth = request_id catch_errors extensions neutronapiapp_v2_0
+keystone = request_id catch_errors authtoken keystonecontext extensions neutronapiapp_v2_0
+
+[filter:request_id]
+paste.filter_factory = neutron.openstack.common.middleware.request_id:RequestIdMiddleware.factory
+
+[filter:catch_errors]
+paste.filter_factory = neutron.openstack.common.middleware.catch_errors:CatchErrorsMiddleware.factory
 
 [filter:keystonecontext]
 paste.filter_factory = neutron.auth:NeutronKeystoneContext.factory
index 220bf3e2a6ff789beaa8583ac2b30423beedf976..52b32f84755ac5fe7b8b93c681ecd4c73b368903 100644 (file)
@@ -20,6 +20,7 @@ import webob.exc
 
 from neutron import context
 from neutron.openstack.common import log as logging
+from neutron.openstack.common.middleware import request_id
 from neutron import wsgi
 
 LOG = logging.getLogger(__name__)
@@ -46,9 +47,13 @@ class NeutronKeystoneContext(wsgi.Middleware):
         tenant_name = req.headers.get('X_PROJECT_NAME')
         user_name = req.headers.get('X_USER_NAME')
 
+        # Use request_id if already set
+        req_id = req.environ.get(request_id.ENV_REQUEST_ID)
+
         # Create a context with the authentication data
         ctx = context.Context(user_id, tenant_id, roles=roles,
-                              user_name=user_name, tenant_name=tenant_name)
+                              user_name=user_name, tenant_name=tenant_name,
+                              request_id=req_id)
 
         # Inject the context...
         req.environ['neutron.context'] = ctx
diff --git a/neutron/openstack/common/middleware/__init__.py b/neutron/openstack/common/middleware/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/neutron/openstack/common/middleware/audit.py b/neutron/openstack/common/middleware/audit.py
new file mode 100644 (file)
index 0000000..5d8da52
--- /dev/null
@@ -0,0 +1,44 @@
+# Copyright (c) 2013 OpenStack Foundation
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""
+Attach open standard audit information to request.environ
+
+AuditMiddleware filter should be place after Keystone's auth_token middleware
+in the pipeline so that it can utilise the information Keystone provides.
+
+"""
+from pycadf.audit import api as cadf_api
+
+from neutron.openstack.common.middleware import notifier
+
+
+class AuditMiddleware(notifier.RequestNotifier):
+
+    def __init__(self, app, **conf):
+        super(AuditMiddleware, self).__init__(app, **conf)
+        self.cadf_audit = cadf_api.OpenStackAuditApi()
+
+    @notifier.log_and_ignore_error
+    def process_request(self, request):
+        self.cadf_audit.append_audit_event(request)
+        super(AuditMiddleware, self).process_request(request)
+
+    @notifier.log_and_ignore_error
+    def process_response(self, request, response,
+                         exception=None, traceback=None):
+        self.cadf_audit.mod_audit_event(request, response)
+        super(AuditMiddleware, self).process_response(request, response,
+                                                      exception, traceback)
diff --git a/neutron/openstack/common/middleware/base.py b/neutron/openstack/common/middleware/base.py
new file mode 100644 (file)
index 0000000..464a1cc
--- /dev/null
@@ -0,0 +1,56 @@
+# Copyright 2011 OpenStack Foundation.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""Base class(es) for WSGI Middleware."""
+
+import webob.dec
+
+
+class Middleware(object):
+    """Base WSGI middleware wrapper.
+
+    These classes require an application to be initialized that will be called
+    next.  By default the middleware will simply call its wrapped app, or you
+    can override __call__ to customize its behavior.
+    """
+
+    @classmethod
+    def factory(cls, global_conf, **local_conf):
+        """Factory method for paste.deploy."""
+        return cls
+
+    def __init__(self, application):
+        self.application = application
+
+    def process_request(self, req):
+        """Called on each request.
+
+        If this returns None, the next application down the stack will be
+        executed. If it returns a response then that response will be returned
+        and execution will stop here.
+        """
+        return None
+
+    def process_response(self, response):
+        """Do whatever you'd like to the response."""
+        return response
+
+    @webob.dec.wsgify
+    def __call__(self, req):
+        response = self.process_request(req)
+        if response:
+            return response
+        response = req.get_response(self.application)
+        return self.process_response(response)
diff --git a/neutron/openstack/common/middleware/catch_errors.py b/neutron/openstack/common/middleware/catch_errors.py
new file mode 100644 (file)
index 0000000..b692aee
--- /dev/null
@@ -0,0 +1,43 @@
+# Copyright (c) 2013 NEC Corporation
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""Middleware that provides high-level error handling.
+
+It catches all exceptions from subsequent applications in WSGI pipeline
+to hide internal errors from API response.
+"""
+
+import webob.dec
+import webob.exc
+
+from neutron.openstack.common.gettextutils import _  # noqa
+from neutron.openstack.common import log as logging
+from neutron.openstack.common.middleware import base
+
+
+LOG = logging.getLogger(__name__)
+
+
+class CatchErrorsMiddleware(base.Middleware):
+
+    @webob.dec.wsgify
+    def __call__(self, req):
+        try:
+            response = req.get_response(self.application)
+        except Exception:
+            LOG.exception(_('An error occurred during '
+                            'processing the request: %s'))
+            response = webob.exc.HTTPInternalServerError()
+        return response
diff --git a/neutron/openstack/common/middleware/correlation_id.py b/neutron/openstack/common/middleware/correlation_id.py
new file mode 100644 (file)
index 0000000..80ee63f
--- /dev/null
@@ -0,0 +1,28 @@
+# Copyright (c) 2013 Rackspace Hosting
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""Middleware that attaches a correlation id to WSGI request"""
+
+import uuid
+
+from neutron.openstack.common.middleware import base
+
+
+class CorrelationIdMiddleware(base.Middleware):
+
+    def process_request(self, req):
+        correlation_id = (req.headers.get("X_CORRELATION_ID") or
+                          str(uuid.uuid4()))
+        req.headers['X_CORRELATION_ID'] = correlation_id
diff --git a/neutron/openstack/common/middleware/debug.py b/neutron/openstack/common/middleware/debug.py
new file mode 100644 (file)
index 0000000..5ab9605
--- /dev/null
@@ -0,0 +1,60 @@
+# Copyright 2011 OpenStack Foundation.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""Debug middleware"""
+
+from __future__ import print_function
+
+import sys
+
+import six
+import webob.dec
+
+from neutron.openstack.common.middleware import base
+
+
+class Debug(base.Middleware):
+    """Helper class that returns debug information.
+
+    Can be inserted into any WSGI application chain to get information about
+    the request and response.
+    """
+
+    @webob.dec.wsgify
+    def __call__(self, req):
+        print(("*" * 40) + " REQUEST ENVIRON")
+        for key, value in req.environ.items():
+            print(key, "=", value)
+        print()
+        resp = req.get_response(self.application)
+
+        print(("*" * 40) + " RESPONSE HEADERS")
+        for (key, value) in six.iteritems(resp.headers):
+            print(key, "=", value)
+        print()
+
+        resp.app_iter = self.print_generator(resp.app_iter)
+
+        return resp
+
+    @staticmethod
+    def print_generator(app_iter):
+        """Prints the contents of a wrapper string iterator when iterated."""
+        print(("*" * 40) + " BODY")
+        for part in app_iter:
+            sys.stdout.write(part)
+            sys.stdout.flush()
+            yield part
+        print()
diff --git a/neutron/openstack/common/middleware/notifier.py b/neutron/openstack/common/middleware/notifier.py
new file mode 100644 (file)
index 0000000..1dce3f5
--- /dev/null
@@ -0,0 +1,126 @@
+# Copyright (c) 2013 eNovance
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""
+Send notifications on request
+
+"""
+import os.path
+import sys
+import traceback as tb
+
+import six
+import webob.dec
+
+from neutron.openstack.common import context
+from neutron.openstack.common.gettextutils import _
+from neutron.openstack.common import log as logging
+from neutron.openstack.common.middleware import base
+from neutron.openstack.common.notifier import api
+
+LOG = logging.getLogger(__name__)
+
+
+def log_and_ignore_error(fn):
+    def wrapped(*args, **kwargs):
+        try:
+            return fn(*args, **kwargs)
+        except Exception as e:
+            LOG.exception(_('An exception occurred processing '
+                            'the API call: %s ') % e)
+    return wrapped
+
+
+class RequestNotifier(base.Middleware):
+    """Send notification on request."""
+
+    @classmethod
+    def factory(cls, global_conf, **local_conf):
+        """Factory method for paste.deploy."""
+        conf = global_conf.copy()
+        conf.update(local_conf)
+
+        def _factory(app):
+            return cls(app, **conf)
+        return _factory
+
+    def __init__(self, app, **conf):
+        self.service_name = conf.get('service_name', None)
+        self.ignore_req_list = [x.upper().strip() for x in
+                                conf.get('ignore_req_list', '').split(',')]
+        super(RequestNotifier, self).__init__(app)
+
+    @staticmethod
+    def environ_to_dict(environ):
+        """Following PEP 333, server variables are lower case, so don't
+        include them.
+
+        """
+        return dict((k, v) for k, v in six.iteritems(environ)
+                    if k.isupper())
+
+    @log_and_ignore_error
+    def process_request(self, request):
+        request.environ['HTTP_X_SERVICE_NAME'] = \
+            self.service_name or request.host
+        payload = {
+            'request': self.environ_to_dict(request.environ),
+        }
+
+        api.notify(context.get_admin_context(),
+                   api.publisher_id(os.path.basename(sys.argv[0])),
+                   'http.request',
+                   api.INFO,
+                   payload)
+
+    @log_and_ignore_error
+    def process_response(self, request, response,
+                         exception=None, traceback=None):
+        payload = {
+            'request': self.environ_to_dict(request.environ),
+        }
+
+        if response:
+            payload['response'] = {
+                'status': response.status,
+                'headers': response.headers,
+            }
+
+        if exception:
+            payload['exception'] = {
+                'value': repr(exception),
+                'traceback': tb.format_tb(traceback)
+            }
+
+        api.notify(context.get_admin_context(),
+                   api.publisher_id(os.path.basename(sys.argv[0])),
+                   'http.response',
+                   api.INFO,
+                   payload)
+
+    @webob.dec.wsgify
+    def __call__(self, req):
+        if req.method in self.ignore_req_list:
+            return req.get_response(self.application)
+        else:
+            self.process_request(req)
+            try:
+                response = req.get_response(self.application)
+            except Exception:
+                exc_type, value, traceback = sys.exc_info()
+                self.process_response(req, None, value, traceback)
+                raise
+            else:
+                self.process_response(req, response)
+            return response
diff --git a/neutron/openstack/common/middleware/request_id.py b/neutron/openstack/common/middleware/request_id.py
new file mode 100644 (file)
index 0000000..d442faf
--- /dev/null
@@ -0,0 +1,38 @@
+# Copyright (c) 2013 NEC Corporation
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""Middleware that ensures request ID.
+
+It ensures to assign request ID for each API request and set it to
+request environment. The request ID is also added to API response.
+"""
+
+from neutron.openstack.common import context
+from neutron.openstack.common.middleware import base
+
+
+ENV_REQUEST_ID = 'openstack.request_id'
+HTTP_RESP_HEADER_REQUEST_ID = 'x-openstack-request-id'
+
+
+class RequestIdMiddleware(base.Middleware):
+
+    def process_request(self, req):
+        self.req_id = context.generate_request_id()
+        req.environ[ENV_REQUEST_ID] = self.req_id
+
+    def process_response(self, response):
+        response.headers.add(HTTP_RESP_HEADER_REQUEST_ID, self.req_id)
+        return response
diff --git a/neutron/openstack/common/middleware/sizelimit.py b/neutron/openstack/common/middleware/sizelimit.py
new file mode 100644 (file)
index 0000000..56b3200
--- /dev/null
@@ -0,0 +1,81 @@
+# Copyright (c) 2012 Red Hat, Inc.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""
+Request Body limiting middleware.
+
+"""
+
+from oslo.config import cfg
+import webob.dec
+import webob.exc
+
+from neutron.openstack.common.gettextutils import _
+from neutron.openstack.common.middleware import base
+
+
+#default request size is 112k
+max_req_body_size = cfg.IntOpt('max_request_body_size',
+                               deprecated_name='osapi_max_request_body_size',
+                               default=114688,
+                               help='the maximum body size '
+                                    'per each request(bytes)')
+
+CONF = cfg.CONF
+CONF.register_opt(max_req_body_size)
+
+
+class LimitingReader(object):
+    """Reader to limit the size of an incoming request."""
+    def __init__(self, data, limit):
+        """Initiates LimitingReader object.
+
+        :param data: Underlying data object
+        :param limit: maximum number of bytes the reader should allow
+        """
+        self.data = data
+        self.limit = limit
+        self.bytes_read = 0
+
+    def __iter__(self):
+        for chunk in self.data:
+            self.bytes_read += len(chunk)
+            if self.bytes_read > self.limit:
+                msg = _("Request is too large.")
+                raise webob.exc.HTTPRequestEntityTooLarge(explanation=msg)
+            else:
+                yield chunk
+
+    def read(self, i=None):
+        result = self.data.read(i)
+        self.bytes_read += len(result)
+        if self.bytes_read > self.limit:
+            msg = _("Request is too large.")
+            raise webob.exc.HTTPRequestEntityTooLarge(explanation=msg)
+        return result
+
+
+class RequestBodySizeLimiter(base.Middleware):
+    """Limit the size of incoming requests."""
+
+    @webob.dec.wsgify
+    def __call__(self, req):
+        if req.content_length > CONF.max_request_body_size:
+            msg = _("Request is too large.")
+            raise webob.exc.HTTPRequestEntityTooLarge(explanation=msg)
+        if req.content_length is None and req.is_body_readable:
+            limiter = LimitingReader(req.body_file,
+                                     CONF.max_request_body_size)
+            req.body_file = limiter
+        return self.application
index f650baa190e0605e3d5067e0ef52f60af3c63d67..aa5c06743ee1b85b28dcacf37e51dafcc33a327b 100644 (file)
@@ -18,6 +18,7 @@
 import webob
 
 from neutron import auth
+from neutron.openstack.common.middleware import request_id
 from neutron.tests import base
 
 
@@ -88,3 +89,11 @@ class NeutronKeystoneContextTestCase(base.BaseTestCase):
         self.assertEqual(self.context.user_name, 'testusername')
         self.assertEqual(self.context.tenant_id, 'testtenantid')
         self.assertEqual(self.context.tenant_name, 'testtenantname')
+
+    def test_request_id_extracted_from_env(self):
+        req_id = 'dummy-request-id'
+        self.request.headers['X_PROJECT_ID'] = 'testtenantid'
+        self.request.headers['X_USER_ID'] = 'testuserid'
+        self.request.environ[request_id.ENV_REQUEST_ID] = req_id
+        self.request.get_response(self.middleware)
+        self.assertEqual(req_id, self.context.request_id)
index 8e578748aace2b265b7acfde6dd4fd5e8b3731f1..b5d2bb01719c0dc2acc09ce02d5233d2166601b4 100644 (file)
@@ -16,6 +16,7 @@ module=lockutils
 module=log
 module=log_handler
 module=loopingcall
+module=middleware
 module=network_utils
 module=notifier
 module=periodic_task