diff --git a/aodh/api/app.py b/aodh/api/app.py index cbd01cde..4dda7f97 100644 --- a/aodh/api/app.py +++ b/aodh/api/app.py @@ -15,6 +15,7 @@ # under the License. import os +import uuid from oslo_config import cfg from oslo_log import log @@ -29,59 +30,56 @@ from aodh import storage LOG = log.getLogger(__name__) -PECAN_CONFIG = { - 'app': { - 'root': 'aodh.api.controllers.root.RootController', - 'modules': ['aodh.api'], - }, -} + +# NOTE(sileht): pastedeploy uses ConfigParser to handle +# global_conf, since python 3 ConfigParser doesn't +# allow to store object as config value, only strings are +# permit, so to be able to pass an object created before paste load +# the app, we store them into a global var. But the each loaded app +# store it's configuration in unique key to be concurrency safe. +global APPCONFIGS +APPCONFIGS = {} -def setup_app(pecan_config=PECAN_CONFIG, conf=None): - if conf is None: - # NOTE(jd) That sucks but pecan forces us to use kwargs :( - raise RuntimeError("Config is actually mandatory") - # FIXME: Replace DBHook with a hooks.TransactionHook +def setup_app(root, conf): app_hooks = [hooks.ConfigHook(conf), hooks.DBHook( storage.get_connection_from_config(conf)), hooks.TranslationHook()] - - pecan.configuration.set_config(dict(pecan_config), overwrite=True) - - app = pecan.make_app( - pecan_config['app']['root'], + return pecan.make_app( + root, hooks=app_hooks, wrap_app=middleware.ParsableErrorMiddleware, guess_content_type_from_ext=False ) - return app +def load_app(conf, appname="aodh+keystone"): + global APPCONFIGS -def load_app(conf): # Build the WSGI app - cfg_file = None cfg_path = conf.api.paste_config if not os.path.isabs(cfg_path): - cfg_file = conf.find_file(cfg_path) - elif os.path.exists(cfg_path): - cfg_file = cfg_path + cfg_path = conf.find_file(cfg_path) - if not cfg_file: + if cfg_path is None or not os.path.exists(cfg_path): raise cfg.ConfigFilesNotFoundError([conf.api.paste_config]) - LOG.info(_LI("Full WSGI config used: %s"), cfg_file) - return deploy.loadapp("config:" + cfg_file) + + config = dict(conf=conf) + configkey = str(uuid.uuid4()) + APPCONFIGS[configkey] = config + + LOG.info(_LI("WSGI config used: %s"), cfg_path) + return deploy.loadapp("config:" + cfg_path, + name=appname, + global_conf={'configkey': configkey}) + + +def app_factory(global_config, **local_conf): + global APPCONFIGS + appconfig = APPCONFIGS.get(global_config.get('configkey')) + return setup_app(root=local_conf.get('root'), **appconfig) def build_wsgi_app(argv=None): return load_app(service.prepare_service(argv=argv)) - - -def _app(): - conf = service.prepare_service() - return setup_app(conf=conf) - - -def app_factory(global_config, **local_conf): - return _app() diff --git a/aodh/api/controllers/root.py b/aodh/api/controllers/root.py index 033140e6..b66d1770 100644 --- a/aodh/api/controllers/root.py +++ b/aodh/api/controllers/root.py @@ -15,15 +15,11 @@ import pecan -from aodh.api.controllers.v2 import root as v2 - MEDIA_TYPE_JSON = 'application/vnd.openstack.telemetry-%s+json' MEDIA_TYPE_XML = 'application/vnd.openstack.telemetry-%s+xml' -class RootController(object): - - v2 = v2.V2Controller() +class VersionsController(object): @pecan.expose('json') def index(self): diff --git a/aodh/api/controllers/v2/alarms.py b/aodh/api/controllers/v2/alarms.py index 9e5a68a5..8810b1ff 100644 --- a/aodh/api/controllers/v2/alarms.py +++ b/aodh/api/controllers/v2/alarms.py @@ -804,7 +804,7 @@ class AlarmsController(rest.RestController): alarm = conn.create_alarm(alarm_in) self._record_creation(conn, change, alarm.alarm_id, now) - v2_utils.set_resp_location_hdr("/v2/alarms/" + alarm.alarm_id) + v2_utils.set_resp_location_hdr("/alarms/" + alarm.alarm_id) return Alarm.from_db_model(alarm) @wsme_pecan.wsexpose([Alarm], [base.Query], [str], int, str) diff --git a/aodh/tests/functional/api/__init__.py b/aodh/tests/functional/api/__init__.py index c9da6968..ed38b6ed 100644 --- a/aodh/tests/functional/api/__init__.py +++ b/aodh/tests/functional/api/__init__.py @@ -19,9 +19,9 @@ import os from oslo_config import fixture as fixture_config -import pecan -import pecan.testing +import webtest +from aodh.api import app from aodh import service from aodh.tests.functional import db as db_test_base @@ -44,24 +44,11 @@ class FunctionalTest(db_test_base.TestBase): self.CONF.set_override('policy_file', os.path.abspath('etc/aodh/policy.json'), group='oslo_policy', enforce_type=True) - self.app = self._make_app() - - def _make_app(self): - self.config = { - 'app': { - 'root': 'aodh.api.controllers.root.RootController', - 'modules': ['aodh.api'], - }, - 'wsme': { - 'debug': True, - }, - } - - return pecan.testing.load_test_app(self.config, conf=self.CONF) - - def tearDown(self): - super(FunctionalTest, self).tearDown() - pecan.set_config({}, overwrite=True) + self.CONF.set_override('paste_config', + os.path.abspath('etc/aodh/api_paste.ini'), + group='api', enforce_type=True) + self.app = webtest.TestApp(app.load_app( + self.CONF, appname='aodh+noauth')) def put_json(self, path, params, expect_errors=False, headers=None, extra_environ=None, status=None): diff --git a/aodh/tests/functional/api/v2/test_acl_scenarios.py b/aodh/tests/functional/api/v2/test_acl_scenarios.py deleted file mode 100644 index b834e36e..00000000 --- a/aodh/tests/functional/api/v2/test_acl_scenarios.py +++ /dev/null @@ -1,49 +0,0 @@ -# -# Copyright 2012 New Dream Network, LLC (DreamHost) -# Copyright 2015 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. -"""Test ACL.""" - -import mock -import webtest - -from aodh.api import app -from aodh.tests.functional.api import v2 - - -class TestAPIACL(v2.FunctionalTest): - - def _make_app(self): - file_name = self.path_get('etc/aodh/api_paste.ini') - self.CONF.set_override("paste_config", file_name, "api") - # We need the other call to prepare_service in app.py to return the - # same tweaked conf object. - with mock.patch('aodh.service.prepare_service') as ps: - ps.return_value = self.CONF - return webtest.TestApp(app.load_app(conf=self.CONF)) - - def test_non_authenticated(self): - response = self.get_json('/alarms', expect_errors=True) - self.assertEqual(401, response.status_int) - - def test_authenticated_wrong_role(self): - response = self.get_json('/alarms', - expect_errors=True, - headers={ - "X-Roles": "Member", - "X-Tenant-Name": "admin", - "X-Project-Id": - "bc23a9d531064583ace8f67dad60f6bb", - }) - self.assertEqual(401, response.status_int) diff --git a/aodh/tests/functional/api/v2/test_alarm_scenarios.py b/aodh/tests/functional/api/v2/test_alarm_scenarios.py index 96634430..99ca4f4c 100644 --- a/aodh/tests/functional/api/v2/test_alarm_scenarios.py +++ b/aodh/tests/functional/api/v2/test_alarm_scenarios.py @@ -24,7 +24,9 @@ from oslo_serialization import jsonutils from oslo_utils import uuidutils import six from six import moves +import webtest +from aodh.api import app from aodh.cmd import alarm_conversion from aodh import messaging from aodh.storage import models @@ -390,7 +392,8 @@ class TestAlarms(TestAlarmsBase): pf = os.path.abspath('aodh/tests/functional/api/v2/policy.json-test') self.CONF.set_override('policy_file', pf, group='oslo_policy', enforce_type=True) - self.app = self._make_app() + self.app = webtest.TestApp(app.load_app( + self.CONF, appname='aodh+noauth')) response = self.get_json('/alarms', expect_errors=True, diff --git a/aodh/tests/functional/gabbi/fixtures.py b/aodh/tests/functional/gabbi/fixtures.py index 0e487f2b..206f6229 100644 --- a/aodh/tests/functional/gabbi/fixtures.py +++ b/aodh/tests/functional/gabbi/fixtures.py @@ -26,16 +26,31 @@ from oslo_utils import uuidutils from six.moves.urllib import parse as urlparse import sqlalchemy_utils +from aodh.api import app from aodh import service from aodh import storage +# NOTE(chdent): Hack to restore semblance of global configuration to +# pass to the WSGI app used per test suite. LOAD_APP_KWARGS are the olso +# configuration, and the pecan application configuration of +# which the critical part is a reference to the current indexer. +LOAD_APP_KWARGS = None + + +def setup_app(): + global LOAD_APP_KWARGS + return app.load_app(**LOAD_APP_KWARGS) + + class ConfigFixture(fixture.GabbiFixture): """Establish the relevant configuration for a test run.""" def start_fixture(self): """Set up config.""" + global LOAD_APP_KWARGS + self.conf = None self.conn = None @@ -68,7 +83,7 @@ class ConfigFixture(fixture.GabbiFixture): enforce_type=True) conf.set_override( 'paste_config', - os.path.abspath('aodh/tests/functional/gabbi/gabbi_paste.ini'), + os.path.abspath('etc/aodh/api_paste.ini'), group='api', ) @@ -88,6 +103,11 @@ class ConfigFixture(fixture.GabbiFixture): self.conn = storage.get_connection_from_config(self.conf) self.conn.upgrade() + LOAD_APP_KWARGS = { + 'conf': conf, + 'appname': 'aodh+noauth', + } + def stop_fixture(self): """Reset the config and remove data.""" if self.conn: diff --git a/aodh/tests/functional/gabbi/gabbi_paste.ini b/aodh/tests/functional/gabbi/gabbi_paste.ini deleted file mode 100644 index 77ee25ae..00000000 --- a/aodh/tests/functional/gabbi/gabbi_paste.ini +++ /dev/null @@ -1,21 +0,0 @@ -# aodh API WSGI Pipeline -# Define the filters that make up the pipeline for processing WSGI requests -# Note: This pipeline is PasteDeploy's term rather than aodh's pipeline -# used for processing samples -# -# This is the gabbi paste file, which creates the full pipeline without the -# auth middleware. - -# Remove authtoken from the pipeline if you don't want to use keystone authentication -[pipeline:main] -pipeline = cors request_id api-server - -[app:api-server] -paste.app_factory = aodh.api.app:app_factory - -[filter:request_id] -paste.filter_factory = oslo_middleware:RequestId.factory - -[filter:cors] -paste.filter_factory = oslo_middleware.cors:filter_factory -oslo_config_project = aodh diff --git a/aodh/tests/functional/gabbi/gabbits/healthcheck.yaml b/aodh/tests/functional/gabbi/gabbits/healthcheck.yaml new file mode 100644 index 00000000..a2cf6fd1 --- /dev/null +++ b/aodh/tests/functional/gabbi/gabbits/healthcheck.yaml @@ -0,0 +1,7 @@ +fixtures: + - ConfigFixture + +tests: + - name: healthcheck + GET: /healthcheck + status: 200 diff --git a/aodh/tests/functional/gabbi/test_gabbi.py b/aodh/tests/functional/gabbi/test_gabbi.py index 3605b585..67d96188 100644 --- a/aodh/tests/functional/gabbi/test_gabbi.py +++ b/aodh/tests/functional/gabbi/test_gabbi.py @@ -22,8 +22,6 @@ import os from gabbi import driver -from aodh.api import app -from aodh import service from aodh.tests.functional.gabbi import fixtures as fixture_module @@ -34,10 +32,5 @@ def load_tests(loader, tests, pattern): """Provide a TestSuite to the discovery process.""" test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) return driver.build_tests(test_dir, loader, host=None, - intercept=setup_app, + intercept=fixture_module.setup_app, fixture_module=fixture_module) - - -def setup_app(): - conf = service.prepare_service([]) - return app.load_app(conf) diff --git a/etc/aodh/aodh-config-generator.conf b/etc/aodh/aodh-config-generator.conf index 5b32e721..cf47d9a0 100644 --- a/etc/aodh/aodh-config-generator.conf +++ b/etc/aodh/aodh-config-generator.conf @@ -7,6 +7,7 @@ namespace = oslo.db namespace = oslo.log namespace = oslo.messaging namespace = oslo.middleware.cors +namespace = oslo.middleware.healthcheck namespace = oslo.middleware.http_proxy_to_wsgi namespace = oslo.policy namespace = keystonemiddleware.auth_token diff --git a/etc/aodh/api_paste.ini b/etc/aodh/api_paste.ini index 756db34f..bccbb26c 100644 --- a/etc/aodh/api_paste.ini +++ b/etc/aodh/api_paste.ini @@ -1,14 +1,35 @@ -# aodh API WSGI Pipeline -# Define the filters that make up the pipeline for processing WSGI requests -# Note: This pipeline is PasteDeploy's term rather than aodh's pipeline -# used for processing samples +[composite:aodh+noauth] +use = egg:Paste#urlmap +/ = aodhversions_pipeline +/v2 = aodhv2_noauth_pipeline +/healthcheck = healthcheck -# Remove authtoken from the pipeline if you don't want to use keystone authentication -[pipeline:main] -pipeline = cors http_proxy_to_wsgi request_id authtoken api-server +[composite:aodh+keystone] +use = egg:Paste#urlmap +/ = aodhversions_pipeline +/v2 = aodhv2_keystone_pipeline +/healthcheck = healthcheck -[app:api-server] +[app:healthcheck] +use = egg:oslo.middleware#healthcheck +oslo_config_project = aodh + +[pipeline:aodhversions_pipeline] +pipeline = cors http_proxy_to_wsgi aodhversions + +[app:aodhversions] paste.app_factory = aodh.api.app:app_factory +root = aodh.api.controllers.root.VersionsController + +[pipeline:aodhv2_keystone_pipeline] +pipeline = cors http_proxy_to_wsgi request_id authtoken aodhv2 + +[pipeline:aodhv2_noauth_pipeline] +pipeline = cors http_proxy_to_wsgi request_id aodhv2 + +[app:aodhv2] +paste.app_factory = aodh.api.app:app_factory +root = aodh.api.controllers.v2.root.V2Controller [filter:authtoken] paste.filter_factory = keystonemiddleware.auth_token:filter_factory diff --git a/releasenotes/notes/healthcheck-560700b72ae68e18.yaml b/releasenotes/notes/healthcheck-560700b72ae68e18.yaml new file mode 100644 index 00000000..5e28af9c --- /dev/null +++ b/releasenotes/notes/healthcheck-560700b72ae68e18.yaml @@ -0,0 +1,5 @@ +--- +features: + - A healthcheck endpoint is provided by default at /healthcheck. It leverages + oslo_middleware healthcheck middleware. It allows to retrieve information + about the health of the API service. diff --git a/requirements.txt b/requirements.txt index a6113528..4d6d6f8a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ PasteDeploy>=1.5.0 pbr<2.0,>=0.11 pecan>=0.8.0 oslo.messaging>=5.2.0 # Apache-2.0 -oslo.middleware>=3.0.0 # Apache-2.0 +oslo.middleware>=3.22.0 # Apache-2.0 oslo.serialization>=1.4.0 # Apache-2.0 oslo.utils>=3.5.0 # Apache-2.0 python-ceilometerclient>=1.5.0