From b0f98cb767d4e0190dbf5ac3c5636523f88cf8f8 Mon Sep 17 00:00:00 2001 From: Mehdi Abaakouk Date: Fri, 17 Jul 2015 10:00:28 +0200 Subject: [PATCH] api: return 410 if only Gnocchi is enabled If gnocchi dispatcher is enabled and not the database dispatcher. All the ceilometer API calls (except alarms) will return an empty result. To avoid misunderstanding of why a user got its data ingested by ceilometer without error but he can't retrieve them via ceilometer-api and should use the Gnocchi API. We explicitly tell that through the API by returning a 410. APIImpact DocImpact Change-Id: Ia947f354da73a9a98feaa8cc6c39a1791208613e --- ceilometer/api/controllers/root.py | 3 +- ceilometer/api/controllers/v2/query.py | 6 -- ceilometer/api/controllers/v2/root.py | 96 +++++++++++++++++++-- ceilometer/tests/api/__init__.py | 4 + ceilometer/tests/api/v2/test_api_upgrade.py | 65 ++++++++++++++ ceilometer/tests/gabbi/fixtures.py | 1 + 6 files changed, 163 insertions(+), 12 deletions(-) create mode 100644 ceilometer/tests/api/v2/test_api_upgrade.py diff --git a/ceilometer/api/controllers/root.py b/ceilometer/api/controllers/root.py index 2198c406..90abbca8 100644 --- a/ceilometer/api/controllers/root.py +++ b/ceilometer/api/controllers/root.py @@ -23,7 +23,8 @@ MEDIA_TYPE_XML = 'application/vnd.openstack.telemetry-%s+xml' class RootController(object): - v2 = v2.V2Controller() + def __init__(self): + self.v2 = v2.V2Controller() @pecan.expose('json') def index(self): diff --git a/ceilometer/api/controllers/v2/query.py b/ceilometer/api/controllers/v2/query.py index 3826294f..77b5ff09 100644 --- a/ceilometer/api/controllers/v2/query.py +++ b/ceilometer/api/controllers/v2/query.py @@ -401,9 +401,3 @@ class QueryAlarmsController(rest.RestController): for s in conn.query_alarms(query.filter_expr, query.orderby, query.limit)] - - -class QueryController(rest.RestController): - - samples = QuerySamplesController() - alarms = QueryAlarmsController() diff --git a/ceilometer/api/controllers/v2/root.py b/ceilometer/api/controllers/v2/root.py index 6379f9ae..fcaeea38 100644 --- a/ceilometer/api/controllers/v2/root.py +++ b/ceilometer/api/controllers/v2/root.py @@ -18,6 +18,11 @@ # License for the specific language governing permissions and limitations # under the License. +from keystoneclient.openstack.common.apiclient import exceptions +from oslo_config import cfg +from oslo_log import log +import pecan + from ceilometer.api.controllers.v2 import alarms from ceilometer.api.controllers.v2 import capabilities from ceilometer.api.controllers.v2 import events @@ -25,16 +30,97 @@ from ceilometer.api.controllers.v2 import meters from ceilometer.api.controllers.v2 import query from ceilometer.api.controllers.v2 import resources from ceilometer.api.controllers.v2 import samples +from ceilometer.i18n import _LW +from ceilometer import keystone_client + + +API_OPTS = [ + cfg.BoolOpt('gnocchi_is_enabled', + default=None, + help=('Set True to disable resource/meter/sample URLs. ' + 'Default autodetection by querying keystone.')), +] + +cfg.CONF.register_opts(API_OPTS, group='api') +cfg.CONF.import_opt('dispatcher', 'ceilometer.dispatcher') + +LOG = log.getLogger(__name__) + + +def gnocchi_abort(): + pecan.abort(410, ("This telemetry installation is configured to use " + "Gnocchi. Please use the Gnocchi API available on " + "the metric endpoint to retreive data.")) + + +class QueryController(object): + def __init__(self, gnocchi_is_enabled=False): + self.gnocchi_is_enabled = gnocchi_is_enabled + + @pecan.expose() + def _lookup(self, kind, *remainder): + if kind == 'alarms': + return query.QueryAlarmsController(), remainder + elif kind == 'samples' and self.gnocchi_is_enabled: + gnocchi_abort() + elif kind == 'samples': + return query.QuerySamplesController(), remainder + else: + pecan.abort(404) class V2Controller(object): """Version 2 API controller root.""" - resources = resources.ResourcesController() - meters = meters.MetersController() - samples = samples.SamplesController() - alarms = alarms.AlarmsController() event_types = events.EventTypesController() events = events.EventsController() - query = query.QueryController() capabilities = capabilities.CapabilitiesController() + + def __init__(self): + self._gnocchi_is_enabled = None + + @property + def gnocchi_is_enabled(self): + if self._gnocchi_is_enabled is None: + if cfg.CONF.api.gnocchi_is_enabled is not None: + self._gnocchi_is_enabled = cfg.CONF.api.gnocchi_is_enabled + + elif ("gnocchi" not in cfg.CONF.dispatcher + or "database" in cfg.CONF.dispatcher): + self._gnocchi_is_enabled = False + else: + try: + ks = keystone_client.get_client() + ks.service_catalog.url_for(service_type='metric') + except exceptions.EndpointNotFound: + self._gnocchi_is_enabled = False + except exceptions.ClientException: + LOG.warn(_LW("Can't connect to keystone, assuming gnocchi " + "is disabled and retry later")) + return False + else: + self._gnocchi_is_enabled = True + LOG.warn(_LW("ceilometer-api started with gnocchi " + "enabled. The resources/meters/samples " + "URLs are disabled.")) + return self._gnocchi_is_enabled + + @pecan.expose() + def _lookup(self, kind, *remainder): + if (kind in ['meters', 'resources', 'samples'] + and self.gnocchi_is_enabled): + gnocchi_abort() + elif kind == 'meters': + return meters.MetersController(), remainder + elif kind == 'resources': + return resources.ResourcesController(), remainder + elif kind == 'samples': + return samples.SamplesController(), remainder + elif kind == 'query': + return QueryController( + gnocchi_is_enabled=self.gnocchi_is_enabled, + ), remainder + elif kind == 'alarms': + return alarms.AlarmsController(), remainder + else: + pecan.abort(404) diff --git a/ceilometer/tests/api/__init__.py b/ceilometer/tests/api/__init__.py index 037cd390..97b0890c 100644 --- a/ceilometer/tests/api/__init__.py +++ b/ceilometer/tests/api/__init__.py @@ -25,6 +25,7 @@ from ceilometer.tests import db as db_test_base OPT_GROUP_NAME = 'keystone_authtoken' cfg.CONF.import_group(OPT_GROUP_NAME, "keystonemiddleware.auth_token") +cfg.CONF.import_group('api', 'ceilometer.api.controllers.v2.root') class FunctionalTest(db_test_base.TestBase): @@ -47,6 +48,9 @@ class FunctionalTest(db_test_base.TestBase): self.CONF.set_override("policy_file", self.path_get('etc/ceilometer/policy.json'), group='oslo_policy') + + self.CONF.set_override('gnocchi_is_enabled', False, group='api') + self.app = self._make_app() def _make_app(self, enable_acl=False): diff --git a/ceilometer/tests/api/v2/test_api_upgrade.py b/ceilometer/tests/api/v2/test_api_upgrade.py new file mode 100644 index 00000000..eeaa2839 --- /dev/null +++ b/ceilometer/tests/api/v2/test_api_upgrade.py @@ -0,0 +1,65 @@ +# +# 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. + +from keystoneclient.openstack.common.apiclient import exceptions +import mock +from oslotest import mockpatch + +from ceilometer.tests.api import v2 + + +class TestAPIUpgradePath(v2.FunctionalTest): + def _setup_osloconfig_options(self): + self.CONF.set_override('gnocchi_is_enabled', True, group='api') + + def _setup_keystone_mock(self): + self.CONF.set_override('gnocchi_is_enabled', None, group='api') + self.ks = mock.Mock() + self.ks.service_catalog.url_for.side_effect = self._url_for + self.useFixture(mockpatch.Patch( + 'ceilometer.keystone_client.get_client', return_value=self.ks)) + + @staticmethod + def _url_for(service_type=None): + if service_type == 'metric': + return 'http://gnocchi/' + raise exceptions.EndpointNotFound() + + def _do_test_gnocchi_enabled_without_database_backend(self): + self.CONF.set_override('dispatcher', 'gnocchi') + for endpoint in ['meters', 'samples', 'resources']: + response = self.app.get(self.PATH_PREFIX + '/' + endpoint, + status=410) + self.assertIn('Gnocchi API', response.body) + + for endpoint in ['events', 'event_types']: + self.app.get(self.PATH_PREFIX + '/' + endpoint, + status=200) + + response = self.post_json('/query/samples', + params={ + "filter": '{"=": {"type": "creation"}}', + "orderby": '[{"timestamp": "DESC"}]', + "limit": 3 + }, status=410) + self.assertIn('Gnocchi API', response.body) + + def test_gnocchi_enabled_without_database_backend_keystone(self): + self._setup_keystone_mock() + self._do_test_gnocchi_enabled_without_database_backend() + self.ks.service_catalog.url_for.assert_called_once_with( + service_type="metric") + + def test_gnocchi_enabled_without_database_backend_configoptions(self): + self._setup_osloconfig_options() + self._do_test_gnocchi_enabled_without_database_backend() diff --git a/ceilometer/tests/gabbi/fixtures.py b/ceilometer/tests/gabbi/fixtures.py index 35e89695..f3f3b333 100644 --- a/ceilometer/tests/gabbi/fixtures.py +++ b/ceilometer/tests/gabbi/fixtures.py @@ -77,6 +77,7 @@ class ConfigFixture(fixture.GabbiFixture): conf.set_override('alarm_connection', '', group='database') conf.set_override('pecan_debug', True, group='api') + conf.set_override('gnocchi_is_enabled', False, group='api') conf.set_override('store_events', True, group='notification')