From 64583e4b839d96e2cbe7dcba7d0f08852b40bcca Mon Sep 17 00:00:00 2001 From: Sergey Reshetnyak Date: Wed, 7 Oct 2015 16:24:35 +0300 Subject: [PATCH] Use api-paste.ini for loading middleware Changes: * use api-paste.ini config for loading middleware * refactoring middlewares to support loading via pastedeploy * use debug middleware from oslo_middleware library instead log_exchange middleware Closes-bug: #1503983 Closes-bug: #1361360 Change-Id: I444c1799ef53dbb19a601e51dd95cd8509fb1c0c --- MANIFEST.in | 2 + devstack/plugin.sh | 2 + doc/source/userdoc/configuration.guide.rst | 9 +++ etc/sahara/api-paste.ini | 28 ++++++++ sahara/api/acl.py | 6 -- sahara/api/middleware/auth_valid.py | 33 ++++----- sahara/api/middleware/log_exchange.py | 67 ------------------ sahara/api/middleware/sahara_middleware.py | 79 ++++++++++++++++++++++ sahara/cli/sahara_engine.py | 6 -- sahara/config.py | 2 + sahara/main.py | 73 +------------------- 11 files changed, 137 insertions(+), 170 deletions(-) create mode 100644 etc/sahara/api-paste.ini delete mode 100644 sahara/api/middleware/log_exchange.py create mode 100644 sahara/api/middleware/sahara_middleware.py diff --git a/MANIFEST.in b/MANIFEST.in index 1322ceabb7..8adef55154 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -10,6 +10,8 @@ include sahara/db/migration/alembic_migrations/versions/*.py include sahara/db/migration/alembic_migrations/versions/README include sahara/db/templates/README.rst +include etc/sahara/api-paste.ini + recursive-include sahara/locale * recursive-include sahara/db/migration/alembic_migrations * diff --git a/devstack/plugin.sh b/devstack/plugin.sh index 043ab110fc..e2b09441b0 100644 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -76,6 +76,8 @@ function configure_sahara { cp -p $SAHARA_DIR/etc/sahara/policy.json $SAHARA_CONF_DIR fi + cp -p $SAHARA_DIR/etc/sahara/api-paste.ini $SAHARA_CONF_DIR + # Create auth cache dir sudo install -d -o $STACK_USER -m 700 $SAHARA_AUTH_CACHE_DIR rm -rf $SAHARA_AUTH_CACHE_DIR/* diff --git a/doc/source/userdoc/configuration.guide.rst b/doc/source/userdoc/configuration.guide.rst index 65c750016c..0bc1afad84 100644 --- a/doc/source/userdoc/configuration.guide.rst +++ b/doc/source/userdoc/configuration.guide.rst @@ -250,3 +250,12 @@ Example 2. Disallow image registry manipulations to non-admin users. "data-processing:images:add_tags": "role:admin", "data-processing:images:remove_tags": "role:admin" } + +API configuration +----------------- + +Sahara uses the ``api-paste.ini`` file to configure the data processing API +service. For middleware injection sahara uses pastedeploy library. The location +of the api-paste file is controlled by the ``api_paste_config`` parameter in +the ``[default]`` section. By default sahara will search for a +``api-paste.ini`` file in the same directory as the configuration file. diff --git a/etc/sahara/api-paste.ini b/etc/sahara/api-paste.ini new file mode 100644 index 0000000000..440c4f3af1 --- /dev/null +++ b/etc/sahara/api-paste.ini @@ -0,0 +1,28 @@ +[pipeline:sahara] +pipeline = request_id cors acl auth_validator sahara_api + +[composite:sahara_api] +use = egg:Paste#urlmap +/: sahara_apiv11 + +[app:sahara_apiv11] +paste.app_factory = sahara.api.middleware.sahara_middleware:Router.factory + +[filter:cors] +paste.filter_factory = oslo_middleware.cors:filter_factory +allow_headers=X-Auth-Token,X-Server-Management-Url +allow_methods=GET,PUT,POST,DELETE,PATCH +allowed_origin= +expose_headers=X-Auth-Token,X-Server-Management-Url + +[filter:request_id] +paste.filter_factory = oslo_middleware.request_id:RequestId.factory + +[filter:acl] +paste.filter_factory = keystonemiddleware.auth_token:filter_factory + +[filter:auth_validator] +paste.filter_factory = sahara.api.middleware.auth_valid:AuthValidator.factory + +[filter:debug] +paste.filter_factory = oslo_middleware.debug:Debug.factory diff --git a/sahara/api/acl.py b/sahara/api/acl.py index 76b2f755bb..57c18063a3 100644 --- a/sahara/api/acl.py +++ b/sahara/api/acl.py @@ -17,7 +17,6 @@ import functools -from keystonemiddleware import auth_token from oslo_config import cfg from oslo_policy import policy @@ -45,8 +44,3 @@ def enforce(rule): return handler return decorator - - -def wrap(app): - """Wrap wsgi application with ACL check.""" - return auth_token.AuthProtocol(app, {}) diff --git a/sahara/api/middleware/auth_valid.py b/sahara/api/middleware/auth_valid.py index 4dd72c0797..ffde2210d7 100644 --- a/sahara/api/middleware/auth_valid.py +++ b/sahara/api/middleware/auth_valid.py @@ -14,6 +14,8 @@ # limitations under the License. from oslo_log import log as logging +from oslo_middleware import base +import webob import webob.exc as ex from sahara.i18n import _ @@ -24,13 +26,12 @@ import sahara.openstack.commons as commons LOG = logging.getLogger(__name__) -class AuthValidator(object): +class AuthValidator(base.Middleware): + """Handles token auth results and tenants.""" - def __init__(self, app): - self.app = app - - def __call__(self, env, start_response): + @webob.dec.wsgify + def __call__(self, req): """Ensures that tenants in url and token are equal. Handle incoming request by checking tenant info prom the headers and @@ -40,30 +41,20 @@ class AuthValidator(object): Reject request if tenant_id from headers not equals to tenant_id from url. """ - token_tenant = env['HTTP_X_TENANT_ID'] + token_tenant = req.environ.get("HTTP_X_TENANT_ID") if not token_tenant: LOG.warning(_LW("Can't get tenant_id from env")) - resp = ex.HTTPServiceUnavailable() - return resp(env, start_response) + raise ex.HTTPServiceUnavailable() - path = env['PATH_INFO'] + path = req.environ['PATH_INFO'] if path != '/': version, url_tenant, rest = commons.split_path(path, 3, 3, True) if not version or not url_tenant or not rest: LOG.warning(_LW("Incorrect path: {path}").format(path=path)) - resp = ex.HTTPNotFound(_("Incorrect path")) - return resp(env, start_response) + raise ex.HTTPNotFound(_("Incorrect path")) if token_tenant != url_tenant: LOG.debug("Unauthorized: token tenant != requested tenant") - resp = ex.HTTPUnauthorized( + raise ex.HTTPUnauthorized( _('Token tenant != requested tenant')) - return resp(env, start_response) - - return self.app(env, start_response) - - -def wrap(app): - """Wrap wsgi application with auth validator check.""" - - return AuthValidator(app) + return self.application diff --git a/sahara/api/middleware/log_exchange.py b/sahara/api/middleware/log_exchange.py deleted file mode 100644 index 9ef57f9427..0000000000 --- a/sahara/api/middleware/log_exchange.py +++ /dev/null @@ -1,67 +0,0 @@ -# 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. - -# It's based on debug middleware from oslo-incubator - -"""Debug middleware""" - -from __future__ import print_function -import sys - -from oslo_middleware import base -import six -import webob.dec - - -class LogExchange(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) - if req.is_body_readable: - print(('*' * 40) + " REQUEST BODY") - if req.content_type == 'application/json': - print(req.json) - else: - print(req.body) - 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) + " RESPONSE BODY") - for part in app_iter: - sys.stdout.write(part) - sys.stdout.flush() - yield part - print() diff --git a/sahara/api/middleware/sahara_middleware.py b/sahara/api/middleware/sahara_middleware.py new file mode 100644 index 0000000000..8bda2eccab --- /dev/null +++ b/sahara/api/middleware/sahara_middleware.py @@ -0,0 +1,79 @@ +# Copyright (c) 2015 Mirantis 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. + +import flask +from oslo_config import cfg +import six +from werkzeug import exceptions as werkzeug_exceptions + +from sahara.api import v10 as api_v10 +from sahara.api import v11 as api_v11 +from sahara import context +from sahara.utils import api as api_utils + + +CONF = cfg.CONF + + +def build_app(): + """App builder (wsgi). + + Entry point for Sahara REST API server + """ + app = flask.Flask('sahara.api') + + @app.route('/', methods=['GET']) + def version_list(): + context.set_ctx(None) + return api_utils.render({ + "versions": [ + {"id": "v1.0", "status": "SUPPORTED"}, + {"id": "v1.1", "status": "CURRENT"} + ] + }) + + @app.teardown_request + def teardown_request(_ex=None): + context.set_ctx(None) + + app.register_blueprint(api_v10.rest, url_prefix='/v1.0') + app.register_blueprint(api_v10.rest, url_prefix='/v1.1') + app.register_blueprint(api_v11.rest, url_prefix='/v1.1') + + def make_json_error(ex): + status_code = (ex.code + if isinstance(ex, werkzeug_exceptions.HTTPException) + else 500) + description = (ex.description + if isinstance(ex, werkzeug_exceptions.HTTPException) + else str(ex)) + return api_utils.render({'error': status_code, + 'error_message': description}, + status=status_code) + + for code in six.iterkeys(werkzeug_exceptions.default_exceptions): + app.error_handler_spec[None][code] = make_json_error + + return app + + +class Router(object): + def __call__(self, environ, response): + return self.app(environ, response) + + @classmethod + def factory(cls, global_config, **local_config): + cls.app = build_app() + return cls(**local_config) diff --git a/sahara/cli/sahara_engine.py b/sahara/cli/sahara_engine.py index 9f6ae46993..1d54655886 100644 --- a/sahara/cli/sahara_engine.py +++ b/sahara/cli/sahara_engine.py @@ -41,7 +41,6 @@ if os.path.exists(os.path.join(possible_topdir, oslo_i18n.enable_lazy() -from sahara.api import acl import sahara.main as server from sahara.service import ops @@ -49,11 +48,6 @@ from sahara.service import ops def main(): server.setup_common(possible_topdir, 'engine') - # NOTE(apavlov): acl.wrap is called here to set up auth_uri value - # in context by using keystone functionality (mostly to avoid - # code duplication). - acl.wrap(None) - server.setup_sahara_engine() ops_server = ops.OpsServer() diff --git a/sahara/config.py b/sahara/config.py index 582e0a6bcd..d9c26c76a8 100644 --- a/sahara/config.py +++ b/sahara/config.py @@ -15,6 +15,8 @@ import itertools +# loading keystonemiddleware opts because sahara uses these options in code +from keystonemiddleware import opts # noqa from oslo_config import cfg from oslo_config import types from oslo_log import log diff --git a/sahara/main.py b/sahara/main.py index 69990f640f..28730b33a5 100644 --- a/sahara/main.py +++ b/sahara/main.py @@ -15,23 +15,14 @@ import os -import flask from oslo_config import cfg from oslo_log import log -import oslo_middleware.cors as cors_middleware -from oslo_middleware import request_id from oslo_service import systemd -import six +from oslo_service import wsgi as oslo_wsgi import stevedore -from werkzeug import exceptions as werkzeug_exceptions from sahara.api import acl -from sahara.api.middleware import auth_valid -from sahara.api.middleware import log_exchange -from sahara.api import v10 as api_v10 -from sahara.api import v11 as api_v11 from sahara import config -from sahara import context from sahara.i18n import _LI from sahara.i18n import _LW from sahara.plugins import base as plugins_base @@ -39,7 +30,6 @@ from sahara.service import api as service_api from sahara.service.edp import api as edp_api from sahara.service import ops as service_ops from sahara.service import periodic -from sahara.utils import api as api_utils from sahara.utils.openstack import cinder from sahara.utils import remote from sahara.utils import rpc as messaging @@ -113,65 +103,8 @@ def setup_auth_policy(): def make_app(): - """App builder (wsgi) - - Entry point for Sahara REST API server - """ - app = flask.Flask('sahara.api') - - @app.route('/', methods=['GET']) - def version_list(): - context.set_ctx(None) - return api_utils.render({ - "versions": [ - {"id": "v1.0", "status": "SUPPORTED"}, - {"id": "v1.1", "status": "CURRENT"} - ] - }) - - @app.teardown_request - def teardown_request(_ex=None): - context.set_ctx(None) - - app.register_blueprint(api_v10.rest, url_prefix='/v1.0') - app.register_blueprint(api_v10.rest, url_prefix='/v1.1') - app.register_blueprint(api_v11.rest, url_prefix='/v1.1') - - def make_json_error(ex): - status_code = (ex.code - if isinstance(ex, werkzeug_exceptions.HTTPException) - else 500) - description = (ex.description - if isinstance(ex, werkzeug_exceptions.HTTPException) - else str(ex)) - return api_utils.render({'error': status_code, - 'error_message': description}, - status=status_code) - - for code in six.iterkeys(werkzeug_exceptions.default_exceptions): - app.error_handler_spec[None][code] = make_json_error - - if CONF.debug and not CONF.log_exchange: - LOG.debug('Logging of request/response exchange could be enabled using' - ' flag --log-exchange') - - # Create a CORS wrapper, and attach sahara-specific defaults that must be - # included in all CORS responses. - app.wsgi_app = cors_middleware.CORS(app.wsgi_app, CONF) - app.wsgi_app.set_latent( - allow_headers=['X-Auth-Token', 'X-Server-Management-Url'], - allow_methods=['GET', 'PUT', 'POST', 'DELETE', 'PATCH'], - expose_headers=['X-Auth-Token', 'X-Server-Management-Url'] - ) - - if CONF.log_exchange: - app.wsgi_app = log_exchange.LogExchange.factory(CONF)(app.wsgi_app) - - app.wsgi_app = auth_valid.wrap(app.wsgi_app) - app.wsgi_app = acl.wrap(app.wsgi_app) - app.wsgi_app = request_id.RequestId(app.wsgi_app) - - return app + app_loader = oslo_wsgi.Loader(CONF) + return app_loader.load_app("sahara") def _load_driver(namespace, name):