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
This commit is contained in:
Sergey Reshetnyak 2015-10-07 16:24:35 +03:00
parent f8e6907299
commit 64583e4b83
11 changed files with 137 additions and 170 deletions

View File

@ -10,6 +10,8 @@ include sahara/db/migration/alembic_migrations/versions/*.py
include sahara/db/migration/alembic_migrations/versions/README include sahara/db/migration/alembic_migrations/versions/README
include sahara/db/templates/README.rst include sahara/db/templates/README.rst
include etc/sahara/api-paste.ini
recursive-include sahara/locale * recursive-include sahara/locale *
recursive-include sahara/db/migration/alembic_migrations * recursive-include sahara/db/migration/alembic_migrations *

View File

@ -76,6 +76,8 @@ function configure_sahara {
cp -p $SAHARA_DIR/etc/sahara/policy.json $SAHARA_CONF_DIR cp -p $SAHARA_DIR/etc/sahara/policy.json $SAHARA_CONF_DIR
fi fi
cp -p $SAHARA_DIR/etc/sahara/api-paste.ini $SAHARA_CONF_DIR
# Create auth cache dir # Create auth cache dir
sudo install -d -o $STACK_USER -m 700 $SAHARA_AUTH_CACHE_DIR sudo install -d -o $STACK_USER -m 700 $SAHARA_AUTH_CACHE_DIR
rm -rf $SAHARA_AUTH_CACHE_DIR/* rm -rf $SAHARA_AUTH_CACHE_DIR/*

View File

@ -250,3 +250,12 @@ Example 2. Disallow image registry manipulations to non-admin users.
"data-processing:images:add_tags": "role:admin", "data-processing:images:add_tags": "role:admin",
"data-processing:images:remove_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.

28
etc/sahara/api-paste.ini Normal file
View File

@ -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

View File

@ -17,7 +17,6 @@
import functools import functools
from keystonemiddleware import auth_token
from oslo_config import cfg from oslo_config import cfg
from oslo_policy import policy from oslo_policy import policy
@ -45,8 +44,3 @@ def enforce(rule):
return handler return handler
return decorator return decorator
def wrap(app):
"""Wrap wsgi application with ACL check."""
return auth_token.AuthProtocol(app, {})

View File

@ -14,6 +14,8 @@
# limitations under the License. # limitations under the License.
from oslo_log import log as logging from oslo_log import log as logging
from oslo_middleware import base
import webob
import webob.exc as ex import webob.exc as ex
from sahara.i18n import _ from sahara.i18n import _
@ -24,13 +26,12 @@ import sahara.openstack.commons as commons
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class AuthValidator(object): class AuthValidator(base.Middleware):
"""Handles token auth results and tenants.""" """Handles token auth results and tenants."""
def __init__(self, app): @webob.dec.wsgify
self.app = app def __call__(self, req):
def __call__(self, env, start_response):
"""Ensures that tenants in url and token are equal. """Ensures that tenants in url and token are equal.
Handle incoming request by checking tenant info prom the headers and 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 Reject request if tenant_id from headers not equals to tenant_id from
url. url.
""" """
token_tenant = env['HTTP_X_TENANT_ID'] token_tenant = req.environ.get("HTTP_X_TENANT_ID")
if not token_tenant: if not token_tenant:
LOG.warning(_LW("Can't get tenant_id from env")) LOG.warning(_LW("Can't get tenant_id from env"))
resp = ex.HTTPServiceUnavailable() raise ex.HTTPServiceUnavailable()
return resp(env, start_response)
path = env['PATH_INFO'] path = req.environ['PATH_INFO']
if path != '/': if path != '/':
version, url_tenant, rest = commons.split_path(path, 3, 3, True) version, url_tenant, rest = commons.split_path(path, 3, 3, True)
if not version or not url_tenant or not rest: if not version or not url_tenant or not rest:
LOG.warning(_LW("Incorrect path: {path}").format(path=path)) LOG.warning(_LW("Incorrect path: {path}").format(path=path))
resp = ex.HTTPNotFound(_("Incorrect path")) raise ex.HTTPNotFound(_("Incorrect path"))
return resp(env, start_response)
if token_tenant != url_tenant: if token_tenant != url_tenant:
LOG.debug("Unauthorized: token tenant != requested tenant") LOG.debug("Unauthorized: token tenant != requested tenant")
resp = ex.HTTPUnauthorized( raise ex.HTTPUnauthorized(
_('Token tenant != requested tenant')) _('Token tenant != requested tenant'))
return resp(env, start_response) return self.application
return self.app(env, start_response)
def wrap(app):
"""Wrap wsgi application with auth validator check."""
return AuthValidator(app)

View File

@ -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()

View File

@ -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)

View File

@ -41,7 +41,6 @@ if os.path.exists(os.path.join(possible_topdir,
oslo_i18n.enable_lazy() oslo_i18n.enable_lazy()
from sahara.api import acl
import sahara.main as server import sahara.main as server
from sahara.service import ops from sahara.service import ops
@ -49,11 +48,6 @@ from sahara.service import ops
def main(): def main():
server.setup_common(possible_topdir, 'engine') 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() server.setup_sahara_engine()
ops_server = ops.OpsServer() ops_server = ops.OpsServer()

View File

@ -15,6 +15,8 @@
import itertools 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 cfg
from oslo_config import types from oslo_config import types
from oslo_log import log from oslo_log import log

View File

@ -15,23 +15,14 @@
import os import os
import flask
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log from oslo_log import log
import oslo_middleware.cors as cors_middleware
from oslo_middleware import request_id
from oslo_service import systemd from oslo_service import systemd
import six from oslo_service import wsgi as oslo_wsgi
import stevedore import stevedore
from werkzeug import exceptions as werkzeug_exceptions
from sahara.api import acl 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 config
from sahara import context
from sahara.i18n import _LI from sahara.i18n import _LI
from sahara.i18n import _LW from sahara.i18n import _LW
from sahara.plugins import base as plugins_base 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.edp import api as edp_api
from sahara.service import ops as service_ops from sahara.service import ops as service_ops
from sahara.service import periodic from sahara.service import periodic
from sahara.utils import api as api_utils
from sahara.utils.openstack import cinder from sahara.utils.openstack import cinder
from sahara.utils import remote from sahara.utils import remote
from sahara.utils import rpc as messaging from sahara.utils import rpc as messaging
@ -113,65 +103,8 @@ def setup_auth_policy():
def make_app(): def make_app():
"""App builder (wsgi) app_loader = oslo_wsgi.Loader(CONF)
return app_loader.load_app("sahara")
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
def _load_driver(namespace, name): def _load_driver(namespace, name):