Enable healthcheck app to check API status

Change-Id: Ide2aa8613fe4767c9e93e1421dbf7ba7439bb3c7
This commit is contained in:
Julien Danjou 2016-12-15 17:26:56 +01:00
parent aa9f7df75d
commit e86f216aa2
14 changed files with 110 additions and 149 deletions

View File

@ -15,6 +15,7 @@
# under the License. # under the License.
import os import os
import uuid
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log from oslo_log import log
@ -29,59 +30,56 @@ from aodh import storage
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
PECAN_CONFIG = {
'app': { # NOTE(sileht): pastedeploy uses ConfigParser to handle
'root': 'aodh.api.controllers.root.RootController', # global_conf, since python 3 ConfigParser doesn't
'modules': ['aodh.api'], # 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): def setup_app(root, conf):
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
app_hooks = [hooks.ConfigHook(conf), app_hooks = [hooks.ConfigHook(conf),
hooks.DBHook( hooks.DBHook(
storage.get_connection_from_config(conf)), storage.get_connection_from_config(conf)),
hooks.TranslationHook()] hooks.TranslationHook()]
return pecan.make_app(
pecan.configuration.set_config(dict(pecan_config), overwrite=True) root,
app = pecan.make_app(
pecan_config['app']['root'],
hooks=app_hooks, hooks=app_hooks,
wrap_app=middleware.ParsableErrorMiddleware, wrap_app=middleware.ParsableErrorMiddleware,
guess_content_type_from_ext=False 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 # Build the WSGI app
cfg_file = None
cfg_path = conf.api.paste_config cfg_path = conf.api.paste_config
if not os.path.isabs(cfg_path): if not os.path.isabs(cfg_path):
cfg_file = conf.find_file(cfg_path) cfg_path = conf.find_file(cfg_path)
elif os.path.exists(cfg_path):
cfg_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]) 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): def build_wsgi_app(argv=None):
return load_app(service.prepare_service(argv=argv)) 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()

View File

@ -15,15 +15,11 @@
import pecan import pecan
from aodh.api.controllers.v2 import root as v2
MEDIA_TYPE_JSON = 'application/vnd.openstack.telemetry-%s+json' MEDIA_TYPE_JSON = 'application/vnd.openstack.telemetry-%s+json'
MEDIA_TYPE_XML = 'application/vnd.openstack.telemetry-%s+xml' MEDIA_TYPE_XML = 'application/vnd.openstack.telemetry-%s+xml'
class RootController(object): class VersionsController(object):
v2 = v2.V2Controller()
@pecan.expose('json') @pecan.expose('json')
def index(self): def index(self):

View File

@ -792,7 +792,7 @@ class AlarmsController(rest.RestController):
alarm = conn.create_alarm(alarm_in) alarm = conn.create_alarm(alarm_in)
self._record_creation(conn, change, alarm.alarm_id, now) 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) return Alarm.from_db_model(alarm)
@wsme_pecan.wsexpose([Alarm], [base.Query], [str], int, str) @wsme_pecan.wsexpose([Alarm], [base.Query], [str], int, str)

View File

@ -19,9 +19,9 @@
import os import os
from oslo_config import fixture as fixture_config from oslo_config import fixture as fixture_config
import pecan import webtest
import pecan.testing
from aodh.api import app
from aodh import service from aodh import service
from aodh.tests.functional import db as db_test_base 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', self.CONF.set_override('policy_file',
os.path.abspath('etc/aodh/policy.json'), os.path.abspath('etc/aodh/policy.json'),
group='oslo_policy', enforce_type=True) group='oslo_policy', enforce_type=True)
self.app = self._make_app() self.CONF.set_override('paste_config',
os.path.abspath('etc/aodh/api_paste.ini'),
def _make_app(self): group='api', enforce_type=True)
self.config = { self.app = webtest.TestApp(app.load_app(
'app': { self.CONF, appname='aodh+noauth'))
'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)
def put_json(self, path, params, expect_errors=False, headers=None, def put_json(self, path, params, expect_errors=False, headers=None,
extra_environ=None, status=None): extra_environ=None, status=None):

View File

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

View File

@ -24,7 +24,9 @@ import oslo_messaging.conffixture
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
import six import six
from six import moves from six import moves
import webtest
from aodh.api import app
from aodh.cmd import alarm_conversion from aodh.cmd import alarm_conversion
from aodh import messaging from aodh import messaging
from aodh.storage import models from aodh.storage import models
@ -390,7 +392,8 @@ class TestAlarms(TestAlarmsBase):
pf = os.path.abspath('aodh/tests/functional/api/v2/policy.json-test') pf = os.path.abspath('aodh/tests/functional/api/v2/policy.json-test')
self.CONF.set_override('policy_file', pf, group='oslo_policy', self.CONF.set_override('policy_file', pf, group='oslo_policy',
enforce_type=True) 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', response = self.get_json('/alarms',
expect_errors=True, expect_errors=True,

View File

@ -26,16 +26,31 @@ from oslo_policy import opts
from six.moves.urllib import parse as urlparse from six.moves.urllib import parse as urlparse
import sqlalchemy_utils import sqlalchemy_utils
from aodh.api import app
from aodh import service from aodh import service
from aodh import storage 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): class ConfigFixture(fixture.GabbiFixture):
"""Establish the relevant configuration for a test run.""" """Establish the relevant configuration for a test run."""
def start_fixture(self): def start_fixture(self):
"""Set up config.""" """Set up config."""
global LOAD_APP_KWARGS
self.conf = None self.conf = None
self.conn = None self.conn = None
@ -68,7 +83,7 @@ class ConfigFixture(fixture.GabbiFixture):
enforce_type=True) enforce_type=True)
conf.set_override( conf.set_override(
'paste_config', 'paste_config',
os.path.abspath('aodh/tests/functional/gabbi/gabbi_paste.ini'), os.path.abspath('etc/aodh/api_paste.ini'),
group='api', group='api',
) )
@ -88,6 +103,11 @@ class ConfigFixture(fixture.GabbiFixture):
self.conn = storage.get_connection_from_config(self.conf) self.conn = storage.get_connection_from_config(self.conf)
self.conn.upgrade() self.conn.upgrade()
LOAD_APP_KWARGS = {
'conf': conf,
'appname': 'aodh+noauth',
}
def stop_fixture(self): def stop_fixture(self):
"""Reset the config and remove data.""" """Reset the config and remove data."""
if self.conn: if self.conn:

View File

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

View File

@ -0,0 +1,7 @@
fixtures:
- ConfigFixture
tests:
- name: healthcheck
GET: /healthcheck
status: 200

View File

@ -22,8 +22,6 @@ import os
from gabbi import driver from gabbi import driver
from aodh.api import app
from aodh import service
from aodh.tests.functional.gabbi import fixtures as fixture_module 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.""" """Provide a TestSuite to the discovery process."""
test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR)
return driver.build_tests(test_dir, loader, host=None, return driver.build_tests(test_dir, loader, host=None,
intercept=setup_app, intercept=fixture_module.setup_app,
fixture_module=fixture_module) fixture_module=fixture_module)
def setup_app():
conf = service.prepare_service([])
return app.load_app(conf)

View File

@ -7,6 +7,7 @@ namespace = oslo.db
namespace = oslo.log namespace = oslo.log
namespace = oslo.messaging namespace = oslo.messaging
namespace = oslo.middleware.cors namespace = oslo.middleware.cors
namespace = oslo.middleware.healthcheck
namespace = oslo.middleware.http_proxy_to_wsgi namespace = oslo.middleware.http_proxy_to_wsgi
namespace = oslo.policy namespace = oslo.policy
namespace = keystonemiddleware.auth_token namespace = keystonemiddleware.auth_token

View File

@ -1,14 +1,35 @@
# aodh API WSGI Pipeline [composite:aodh+noauth]
# Define the filters that make up the pipeline for processing WSGI requests use = egg:Paste#urlmap
# Note: This pipeline is PasteDeploy's term rather than aodh's pipeline / = aodhversions_pipeline
# used for processing samples /v2 = aodhv2_noauth_pipeline
/healthcheck = healthcheck
# Remove authtoken from the pipeline if you don't want to use keystone authentication [composite:aodh+keystone]
[pipeline:main] use = egg:Paste#urlmap
pipeline = cors http_proxy_to_wsgi request_id authtoken api-server / = 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 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] [filter:authtoken]
paste.filter_factory = keystonemiddleware.auth_token:filter_factory paste.filter_factory = keystonemiddleware.auth_token:filter_factory

View File

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

View File

@ -19,7 +19,7 @@ PasteDeploy>=1.5.0
pbr<2.0,>=0.11 pbr<2.0,>=0.11
pecan>=0.8.0 pecan>=0.8.0
oslo.messaging>=5.2.0 # Apache-2.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.serialization>=1.4.0 # Apache-2.0
oslo.utils>=3.5.0 # Apache-2.0 oslo.utils>=3.5.0 # Apache-2.0
python-ceilometerclient>=1.5.0 python-ceilometerclient>=1.5.0