From d10718458b0e04f02971976c1f5ca8def0cfde5f Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Thu, 29 Aug 2013 10:52:31 +0200 Subject: [PATCH] Import middleware from Oslo Change-Id: I90babb2c1f2b4f875214d38ddad0ccb644adfd6c Blueprint: count-api-requests --- .../openstack/common/middleware/__init__.py | 0 .../openstack/common/middleware/audit.py | 45 +++++++ .../openstack/common/middleware/base.py | 55 ++++++++ .../openstack/common/middleware/context.py | 61 +++++++++ .../common/middleware/correlation_id.py | 29 +++++ .../openstack/common/middleware/debug.py | 58 +++++++++ .../openstack/common/middleware/notifier.py | 121 ++++++++++++++++++ .../openstack/common/middleware/sizelimit.py | 83 ++++++++++++ openstack-common.conf | 1 + 9 files changed, 453 insertions(+) create mode 100644 ceilometer/openstack/common/middleware/__init__.py create mode 100644 ceilometer/openstack/common/middleware/audit.py create mode 100644 ceilometer/openstack/common/middleware/base.py create mode 100644 ceilometer/openstack/common/middleware/context.py create mode 100644 ceilometer/openstack/common/middleware/correlation_id.py create mode 100644 ceilometer/openstack/common/middleware/debug.py create mode 100644 ceilometer/openstack/common/middleware/notifier.py create mode 100644 ceilometer/openstack/common/middleware/sizelimit.py diff --git a/ceilometer/openstack/common/middleware/__init__.py b/ceilometer/openstack/common/middleware/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ceilometer/openstack/common/middleware/audit.py b/ceilometer/openstack/common/middleware/audit.py new file mode 100644 index 0000000000..1bda8d1172 --- /dev/null +++ b/ceilometer/openstack/common/middleware/audit.py @@ -0,0 +1,45 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2013 OpenStack LLC. +# 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 ceilometer.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/ceilometer/openstack/common/middleware/base.py b/ceilometer/openstack/common/middleware/base.py new file mode 100644 index 0000000000..20995498a3 --- /dev/null +++ b/ceilometer/openstack/common/middleware/base.py @@ -0,0 +1,55 @@ +# 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/ceilometer/openstack/common/middleware/context.py b/ceilometer/openstack/common/middleware/context.py new file mode 100644 index 0000000000..da664942f4 --- /dev/null +++ b/ceilometer/openstack/common/middleware/context.py @@ -0,0 +1,61 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. + +""" +Middleware that attaches a context to the WSGI request +""" + +from ceilometer.openstack.common import context +from ceilometer.openstack.common import importutils +from ceilometer.openstack.common.middleware import base + + +class ContextMiddleware(base.Middleware): + def __init__(self, app, options): + self.options = options + super(ContextMiddleware, self).__init__(app) + + def make_context(self, *args, **kwargs): + """Create a context with the given arguments.""" + + # Determine the context class to use + ctxcls = context.RequestContext + if 'context_class' in self.options: + ctxcls = importutils.import_class(self.options['context_class']) + + return ctxcls(*args, **kwargs) + + def process_request(self, req): + """Process the request. + + Extract any authentication information in the request and + construct an appropriate context from it. + """ + # Use the default empty context, with admin turned on for + # backwards compatibility + req.context = self.make_context(is_admin=True) + + +def filter_factory(global_conf, **local_conf): + """Factory method for paste.deploy.""" + conf = global_conf.copy() + conf.update(local_conf) + + def filter(app): + return ContextMiddleware(app, conf) + + return filter diff --git a/ceilometer/openstack/common/middleware/correlation_id.py b/ceilometer/openstack/common/middleware/correlation_id.py new file mode 100644 index 0000000000..af35f8d7aa --- /dev/null +++ b/ceilometer/openstack/common/middleware/correlation_id.py @@ -0,0 +1,29 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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""" + +from ceilometer.openstack.common.middleware import base +from ceilometer.openstack.common import uuidutils + + +class CorrelationIdMiddleware(base.Middleware): + + def process_request(self, req): + correlation_id = (req.headers.get("X_CORRELATION_ID") or + uuidutils.generate_uuid()) + req.headers['X_CORRELATION_ID'] = correlation_id diff --git a/ceilometer/openstack/common/middleware/debug.py b/ceilometer/openstack/common/middleware/debug.py new file mode 100644 index 0000000000..dd5d6ed4b4 --- /dev/null +++ b/ceilometer/openstack/common/middleware/debug.py @@ -0,0 +1,58 @@ +# 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 webob.dec + +from ceilometer.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 resp.headers.iteritems(): + 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/ceilometer/openstack/common/middleware/notifier.py b/ceilometer/openstack/common/middleware/notifier.py new file mode 100644 index 0000000000..ab744ff0e6 --- /dev/null +++ b/ceilometer/openstack/common/middleware/notifier.py @@ -0,0 +1,121 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 webob.dec + +from ceilometer.openstack.common import context +from ceilometer.openstack.common.gettextutils import _ # noqa +from ceilometer.openstack.common import log as logging +from ceilometer.openstack.common.middleware import base +from ceilometer.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) + 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 environ.iteritems() + 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): + self.process_request(req) + try: + response = req.get_response(self.application) + except Exception: + 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/ceilometer/openstack/common/middleware/sizelimit.py b/ceilometer/openstack/common/middleware/sizelimit.py new file mode 100644 index 0000000000..3fba438428 --- /dev/null +++ b/ceilometer/openstack/common/middleware/sizelimit.py @@ -0,0 +1,83 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 ceilometer.openstack.common.deprecated import wsgi +from ceilometer.openstack.common.gettextutils import _ # noqa +from ceilometer.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(RequestClass=wsgi.Request) + 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 diff --git a/openstack-common.conf b/openstack-common.conf index e15ba6dc57..d54ca60a58 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -10,6 +10,7 @@ module=jsonutils module=local module=log module=loopingcall +module=middleware module=network_utils module=notifier module=policy