From 6bc86f75ea881e5660413a9e2d67d91c1a16f8c7 Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Tue, 30 Jun 2015 18:05:19 +0200 Subject: [PATCH] Remove alarming code Since we moved all the alarming code and subsystem to the Aodh project, remove it from Ceilometer. Depends-On: I3983128d2d964b0f1f3326948b27f5d94df65a04 Depends-On: I99c9f2be0bbc70f289da5c2ba22698b8b7dc4495 Change-Id: Id169a914c1d1f2f5ad03ebb515d3d052204d5c5c --- MAINTAINERS | 7 - ceilometer/alarm/__init__.py | 26 - ceilometer/alarm/evaluator/__init__.py | 134 - ceilometer/alarm/evaluator/combination.py | 114 - ceilometer/alarm/evaluator/gnocchi.py | 233 -- ceilometer/alarm/evaluator/threshold.py | 203 -- ceilometer/alarm/evaluator/utils.py | 58 - ceilometer/alarm/notifier/__init__.py | 38 - ceilometer/alarm/notifier/log.py | 40 - ceilometer/alarm/notifier/rest.py | 104 - ceilometer/alarm/notifier/test.py | 35 - ceilometer/alarm/notifier/trust.py | 56 - ceilometer/alarm/rpc.py | 65 - ceilometer/alarm/service.py | 231 -- ceilometer/alarm/storage/__init__.py | 0 ceilometer/alarm/storage/base.py | 166 - ceilometer/alarm/storage/impl_db2.py | 73 - ceilometer/alarm/storage/impl_hbase.py | 181 - ceilometer/alarm/storage/impl_log.py | 61 - ceilometer/alarm/storage/impl_mongodb.py | 87 - ceilometer/alarm/storage/impl_sqlalchemy.py | 343 -- ceilometer/alarm/storage/models.py | 134 - ceilometer/alarm/storage/pymongo_base.py | 307 -- .../controllers/v2/alarm_rules/__init__.py | 0 .../controllers/v2/alarm_rules/combination.py | 76 - .../api/controllers/v2/alarm_rules/gnocchi.py | 194 -- .../controllers/v2/alarm_rules/threshold.py | 120 - ceilometer/api/controllers/v2/alarms.py | 793 ----- ceilometer/api/controllers/v2/base.py | 26 - ceilometer/api/controllers/v2/capabilities.py | 13 - ceilometer/api/controllers/v2/query.py | 45 +- ceilometer/api/controllers/v2/root.py | 3 - ceilometer/api/controllers/v2/utils.py | 17 +- ceilometer/api/hooks.py | 9 +- ceilometer/cmd/eventlet/alarm.py | 33 - ceilometer/cmd/eventlet/storage.py | 10 - ceilometer/opts.py | 10 - ceilometer/storage/__init__.py | 11 - ceilometer/storage/base.py | 5 +- ceilometer/storage/hbase/migration.py | 16 - ceilometer/storage/sqlalchemy/models.py | 47 - ceilometer/tests/constants.py | 17 - ceilometer/tests/db.py | 19 +- .../functional/api/v2/test_alarm_scenarios.py | 2916 ----------------- .../tests/functional/api/v2/test_app.py | 84 - .../api/v2/test_complex_query_scenarios.py | 298 -- ceilometer/tests/functional/gabbi/fixtures.py | 1 - .../functional/gabbi/gabbits/alarms.yaml | 139 - .../gabbi/gabbits/capabilities.yaml | 1 - .../tests/functional/storage/test_impl_db2.py | 12 - .../functional/storage/test_impl_hbase.py | 12 - .../functional/storage/test_impl_mongodb.py | 20 - .../storage/test_impl_sqlalchemy.py | 12 - .../functional/storage/test_pymongo_base.py | 74 - .../storage/test_storage_scenarios.py | 483 --- ceilometer/tests/functional/test_bin.py | 38 +- ceilometer/tests/unit/alarm/__init__.py | 0 .../tests/unit/alarm/evaluator/__init__.py | 0 ceilometer/tests/unit/alarm/evaluator/base.py | 43 - .../tests/unit/alarm/evaluator/test_base.py | 156 - .../unit/alarm/evaluator/test_combination.py | 408 --- .../unit/alarm/evaluator/test_gnocchi.py | 438 --- .../unit/alarm/evaluator/test_threshold.py | 540 --- ceilometer/tests/unit/alarm/test_alarm_svc.py | 158 - ceilometer/tests/unit/alarm/test_notifier.py | 266 -- ceilometer/tests/unit/alarm/test_rpc.py | 170 - .../tests/unit/api/v2/test_complex_query.py | 39 - ceilometer/tests/unit/api/v2/test_query.py | 31 - ceilometer/tests/unit/storage/test_base.py | 3 - .../tests/unit/storage/test_get_connection.py | 18 - ceilometer/tests/unit/storage/test_models.py | 63 - devstack/plugin.sh | 15 +- devstack/settings | 2 - devstack/upgrade/settings | 4 +- devstack/upgrade/shutdown.sh | 2 +- devstack/upgrade/upgrade.sh | 2 - doc/source/architecture.rst | 53 +- doc/source/glossary.rst | 12 - doc/source/install/manual.rst | 3 +- doc/source/overview.rst | 7 - doc/source/webapi/v2.rst | 47 +- etc/ceilometer/policy.json.sample | 16 - rally-jobs/ceilometer.yaml | 144 - requirements.txt | 4 +- setup.cfg | 33 - tools/test_hbase_table_utils.py | 3 - 86 files changed, 26 insertions(+), 10904 deletions(-) delete mode 100644 ceilometer/alarm/__init__.py delete mode 100644 ceilometer/alarm/evaluator/__init__.py delete mode 100644 ceilometer/alarm/evaluator/combination.py delete mode 100644 ceilometer/alarm/evaluator/gnocchi.py delete mode 100644 ceilometer/alarm/evaluator/threshold.py delete mode 100644 ceilometer/alarm/evaluator/utils.py delete mode 100644 ceilometer/alarm/notifier/__init__.py delete mode 100644 ceilometer/alarm/notifier/log.py delete mode 100644 ceilometer/alarm/notifier/rest.py delete mode 100644 ceilometer/alarm/notifier/test.py delete mode 100644 ceilometer/alarm/notifier/trust.py delete mode 100644 ceilometer/alarm/rpc.py delete mode 100644 ceilometer/alarm/service.py delete mode 100644 ceilometer/alarm/storage/__init__.py delete mode 100644 ceilometer/alarm/storage/base.py delete mode 100644 ceilometer/alarm/storage/impl_db2.py delete mode 100644 ceilometer/alarm/storage/impl_hbase.py delete mode 100644 ceilometer/alarm/storage/impl_log.py delete mode 100644 ceilometer/alarm/storage/impl_mongodb.py delete mode 100644 ceilometer/alarm/storage/impl_sqlalchemy.py delete mode 100644 ceilometer/alarm/storage/models.py delete mode 100644 ceilometer/alarm/storage/pymongo_base.py delete mode 100644 ceilometer/api/controllers/v2/alarm_rules/__init__.py delete mode 100644 ceilometer/api/controllers/v2/alarm_rules/combination.py delete mode 100644 ceilometer/api/controllers/v2/alarm_rules/gnocchi.py delete mode 100644 ceilometer/api/controllers/v2/alarm_rules/threshold.py delete mode 100644 ceilometer/api/controllers/v2/alarms.py delete mode 100644 ceilometer/cmd/eventlet/alarm.py delete mode 100644 ceilometer/tests/constants.py delete mode 100644 ceilometer/tests/functional/api/v2/test_alarm_scenarios.py delete mode 100644 ceilometer/tests/functional/gabbi/gabbits/alarms.yaml delete mode 100644 ceilometer/tests/unit/alarm/__init__.py delete mode 100644 ceilometer/tests/unit/alarm/evaluator/__init__.py delete mode 100644 ceilometer/tests/unit/alarm/evaluator/base.py delete mode 100644 ceilometer/tests/unit/alarm/evaluator/test_base.py delete mode 100644 ceilometer/tests/unit/alarm/evaluator/test_combination.py delete mode 100644 ceilometer/tests/unit/alarm/evaluator/test_gnocchi.py delete mode 100644 ceilometer/tests/unit/alarm/evaluator/test_threshold.py delete mode 100644 ceilometer/tests/unit/alarm/test_alarm_svc.py delete mode 100644 ceilometer/tests/unit/alarm/test_notifier.py delete mode 100644 ceilometer/tests/unit/alarm/test_rpc.py diff --git a/MAINTAINERS b/MAINTAINERS index 8f2c5d11..39dc120a 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -37,13 +37,6 @@ Each has an entry with the following keys: F: Wildcard patterns, relative to ceilometer/ -== alarms == - -M: Eoghan Glynn (eglynn) -M: Mehdi Abaakouk (sileht) -S: Maintained -F: alarm/ - == api == M: Doug Hellmann (dhellmann) diff --git a/ceilometer/alarm/__init__.py b/ceilometer/alarm/__init__.py deleted file mode 100644 index bbccacb2..00000000 --- a/ceilometer/alarm/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -# -# Copyright 2015 Red Hat, Inc -# -# Authors: Eoghan Glynn -# -# 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 stevedore import extension - - -EVALUATOR_EXTENSIONS_NAMESPACE = "ceilometer.alarm.evaluator" -NOTIFIER_EXTENSIONS_NAMESPACE = "ceilometer.alarm.notifier" - -NOTIFIERS = extension.ExtensionManager(NOTIFIER_EXTENSIONS_NAMESPACE, - invoke_on_load=True) -NOTIFIER_SCHEMAS = NOTIFIERS.map(lambda x: x.name) diff --git a/ceilometer/alarm/evaluator/__init__.py b/ceilometer/alarm/evaluator/__init__.py deleted file mode 100644 index c10505c0..00000000 --- a/ceilometer/alarm/evaluator/__init__.py +++ /dev/null @@ -1,134 +0,0 @@ -# -# Copyright 2013 eNovance -# -# Authors: Mehdi Abaakouk -# -# 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 abc -import datetime - -from ceilometerclient import client as ceiloclient -import croniter -from oslo_config import cfg -from oslo_log import log -from oslo_utils import timeutils -import pytz -import six - -from ceilometer.i18n import _, _LI - - -LOG = log.getLogger(__name__) - -UNKNOWN = 'insufficient data' -OK = 'ok' -ALARM = 'alarm' - -cfg.CONF.import_opt('http_timeout', 'ceilometer.service') -cfg.CONF.import_group('service_credentials', 'ceilometer.service') - - -@six.add_metaclass(abc.ABCMeta) -class Evaluator(object): - """Base class for alarm rule evaluator plugins.""" - - def __init__(self, notifier): - self.notifier = notifier - self.api_client = None - - @property - def _client(self): - """Construct or reuse an authenticated API client.""" - if not self.api_client: - auth_config = cfg.CONF.service_credentials - creds = dict( - os_auth_url=auth_config.os_auth_url, - os_region_name=auth_config.os_region_name, - os_tenant_name=auth_config.os_tenant_name, - os_password=auth_config.os_password, - os_username=auth_config.os_username, - os_cacert=auth_config.os_cacert, - os_endpoint_type=auth_config.os_endpoint_type, - insecure=auth_config.insecure, - timeout=cfg.CONF.http_timeout, - ) - self.api_client = ceiloclient.get_client(2, **creds) - return self.api_client - - def _refresh(self, alarm, state, reason, reason_data): - """Refresh alarm state.""" - try: - previous = alarm.state - if previous != state: - LOG.info(_LI('alarm %(id)s transitioning to %(state)s because ' - '%(reason)s') % {'id': alarm.alarm_id, - 'state': state, - 'reason': reason}) - - self._client.alarms.set_state(alarm.alarm_id, state=state) - alarm.state = state - if self.notifier: - self.notifier.notify(alarm, previous, reason, reason_data) - except Exception: - # retry will occur naturally on the next evaluation - # cycle (unless alarm state reverts in the meantime) - LOG.exception(_('alarm state update failed')) - - @classmethod - def within_time_constraint(cls, alarm): - """Check whether the alarm is within at least one of its time limits. - - If there are none, then the answer is yes. - """ - if not alarm.time_constraints: - return True - - now_utc = timeutils.utcnow().replace(tzinfo=pytz.utc) - for tc in alarm.time_constraints: - tz = pytz.timezone(tc['timezone']) if tc['timezone'] else None - now_tz = now_utc.astimezone(tz) if tz else now_utc - start_cron = croniter.croniter(tc['start'], now_tz) - if cls._is_exact_match(start_cron, now_tz): - return True - # start_cron.cur has changed in _is_exact_match(), - # croniter cannot recover properly in some corner case. - start_cron = croniter.croniter(tc['start'], now_tz) - latest_start = start_cron.get_prev(datetime.datetime) - duration = datetime.timedelta(seconds=tc['duration']) - if latest_start <= now_tz <= latest_start + duration: - return True - return False - - @staticmethod - def _is_exact_match(cron, ts): - """Handle edge in case when both parameters are equal. - - Handle edge case where if the timestamp is the same as the - cron point in time to the minute, croniter returns the previous - start, not the current. We can check this by first going one - step back and then one step forward and check if we are - at the original point in time. - """ - cron.get_prev() - diff = timeutils.total_seconds(ts - cron.get_next(datetime.datetime)) - return abs(diff) < 60 # minute precision - - @abc.abstractmethod - def evaluate(self, alarm): - """Interface definition. - - evaluate an alarm - alarm Alarm: an instance of the Alarm - """ diff --git a/ceilometer/alarm/evaluator/combination.py b/ceilometer/alarm/evaluator/combination.py deleted file mode 100644 index b25499a5..00000000 --- a/ceilometer/alarm/evaluator/combination.py +++ /dev/null @@ -1,114 +0,0 @@ -# -# Copyright 2013 eNovance -# -# Authors: Mehdi Abaakouk -# -# 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 oslo_log import log -from six import moves - -from ceilometer.alarm import evaluator -from ceilometer.i18n import _ - -LOG = log.getLogger(__name__) - -COMPARATORS = {'and': all, 'or': any} - - -class CombinationEvaluator(evaluator.Evaluator): - - def _get_alarm_state(self, alarm_id): - try: - alarm = self._client.alarms.get(alarm_id) - except Exception: - LOG.exception(_('alarm retrieval failed')) - return None - return alarm.state - - def _sufficient_states(self, alarm, states): - """Check for the sufficiency of the data for evaluation. - - Ensure that there is sufficient data for evaluation, - transitioning to unknown otherwise. - """ - # note(sileht): alarm can be evaluated only with - # stable state of other alarm - alarms_missing_states = [alarm_id for alarm_id, state in states - if not state or state == evaluator.UNKNOWN] - sufficient = len(alarms_missing_states) == 0 - if not sufficient and alarm.rule['operator'] == 'or': - # if operator is 'or' and there is one alarm, then the combinated - # alarm's state should be 'alarm' - sufficient = bool([alarm_id for alarm_id, state in states - if state == evaluator.ALARM]) - if not sufficient and alarm.state != evaluator.UNKNOWN: - reason = (_('Alarms %(alarm_ids)s' - ' are in unknown state') % - {'alarm_ids': ",".join(alarms_missing_states)}) - reason_data = self._reason_data(alarms_missing_states) - self._refresh(alarm, evaluator.UNKNOWN, reason, reason_data) - return sufficient - - @staticmethod - def _reason_data(alarm_ids): - """Create a reason data dictionary for this evaluator type.""" - return {'type': 'combination', 'alarm_ids': alarm_ids} - - @classmethod - def _reason(cls, alarm, state, underlying_states): - """Fabricate reason string.""" - transition = alarm.state != state - - alarms_to_report = [alarm_id for alarm_id, alarm_state - in underlying_states - if alarm_state == state] - reason_data = cls._reason_data(alarms_to_report) - if transition: - return (_('Transition to %(state)s due to alarms' - ' %(alarm_ids)s in state %(state)s') % - {'state': state, - 'alarm_ids': ",".join(alarms_to_report)}), reason_data - return (_('Remaining as %(state)s due to alarms' - ' %(alarm_ids)s in state %(state)s') % - {'state': state, - 'alarm_ids': ",".join(alarms_to_report)}), reason_data - - def _transition(self, alarm, underlying_states): - """Transition alarm state if necessary.""" - op = alarm.rule['operator'] - if COMPARATORS[op](s == evaluator.ALARM - for __, s in underlying_states): - state = evaluator.ALARM - else: - state = evaluator.OK - - continuous = alarm.repeat_actions - reason, reason_data = self._reason(alarm, state, underlying_states) - if alarm.state != state or continuous: - self._refresh(alarm, state, reason, reason_data) - - def evaluate(self, alarm): - if not self.within_time_constraint(alarm): - LOG.debug('Attempted to evaluate alarm %s, but it is not ' - 'within its time constraint.', alarm.alarm_id) - return - - states = zip(alarm.rule['alarm_ids'], - moves.map(self._get_alarm_state, alarm.rule['alarm_ids'])) - # states is consumed more than once, we need a list - states = list(states) - - if self._sufficient_states(alarm, states): - self._transition(alarm, states) diff --git a/ceilometer/alarm/evaluator/gnocchi.py b/ceilometer/alarm/evaluator/gnocchi.py deleted file mode 100644 index 809cb4f0..00000000 --- a/ceilometer/alarm/evaluator/gnocchi.py +++ /dev/null @@ -1,233 +0,0 @@ -# -# Copyright 2015 eNovance -# -# 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 datetime -import operator - -from oslo_config import cfg -from oslo_log import log -from oslo_serialization import jsonutils -from oslo_utils import timeutils -import requests -import six.moves - -from ceilometer.alarm import evaluator -from ceilometer.i18n import _ -from ceilometer import keystone_client - -LOG = log.getLogger(__name__) - -COMPARATORS = { - 'gt': operator.gt, - 'lt': operator.lt, - 'ge': operator.ge, - 'le': operator.le, - 'eq': operator.eq, - 'ne': operator.ne, -} - -OPTS = [ - cfg.StrOpt('gnocchi_url', - default="http://localhost:8041", - deprecated_for_removal=True, - help='URL to Gnocchi.'), -] - -cfg.CONF.register_opts(OPTS, group="alarms") -cfg.CONF.import_opt('http_timeout', 'ceilometer.service') - - -class GnocchiThresholdEvaluator(evaluator.Evaluator): - - # the sliding evaluation window is extended to allow - # for reporting/ingestion lag - look_back = 1 - - # minimum number of datapoints within sliding window to - # avoid unknown state - quorum = 1 - - def __init__(self, notifier): - super(GnocchiThresholdEvaluator, self).__init__(notifier) - self.gnocchi_url = cfg.CONF.alarms.gnocchi_url - self._ks_client = None - - @property - def ks_client(self): - if self._ks_client is None: - self._ks_client = keystone_client.get_client() - return self._ks_client - - def _get_headers(self, content_type="application/json"): - return { - 'Content-Type': content_type, - 'X-Auth-Token': self.ks_client.auth_token, - } - - def _statistics(self, alarm, start, end): - """Retrieve statistics over the current window.""" - method = 'get' - req = { - 'url': self.gnocchi_url + "/v1", - 'headers': self._get_headers(), - 'params': { - 'aggregation': alarm.rule['aggregation_method'], - 'start': start, - 'end': end, - } - } - - if alarm.type == 'gnocchi_aggregation_by_resources_threshold': - method = 'post' - req['url'] += "/aggregation/resource/%s/metric/%s" % ( - alarm.rule['resource_type'], alarm.rule['metric']) - req['data'] = alarm.rule['query'] - - elif alarm.type == 'gnocchi_aggregation_by_metrics_threshold': - req['url'] += "/aggregation/metric" - req['params']['metric[]'] = alarm.rule['metrics'] - - elif alarm.type == 'gnocchi_resources_threshold': - req['url'] += "/resource/%s/%s/metric/%s/measures" % ( - alarm.rule['resource_type'], - alarm.rule['resource_id'], alarm.rule['metric']) - - LOG.debug('stats query %s', req['url']) - try: - r = getattr(requests, method)(**req) - except Exception: - LOG.exception(_('alarm stats retrieval failed')) - return [] - if r.status_code // 100 != 2: - LOG.exception(_('alarm stats retrieval failed: %s') % r.text) - return [] - else: - return jsonutils.loads(r.text) - - @classmethod - def _bound_duration(cls, alarm): - """Bound the duration of the statistics query.""" - now = timeutils.utcnow() - # when exclusion of weak datapoints is enabled, we extend - # the look-back period so as to allow a clearer sample count - # trend to be established - window = (alarm.rule['granularity'] * - (alarm.rule['evaluation_periods'] + cls.look_back)) - start = now - datetime.timedelta(seconds=window) - LOG.debug('query stats from %(start)s to ' - '%(now)s', {'start': start, 'now': now}) - return start.isoformat(), now.isoformat() - - def _sufficient(self, alarm, statistics): - """Check for the sufficiency of the data for evaluation. - - Ensure there is sufficient data for evaluation, transitioning to - unknown otherwise. - """ - sufficient = len(statistics) >= self.quorum - if not sufficient and alarm.state != evaluator.UNKNOWN: - reason = _('%d datapoints are unknown') % alarm.rule[ - 'evaluation_periods'] - reason_data = self._reason_data('unknown', - alarm.rule['evaluation_periods'], - None) - self._refresh(alarm, evaluator.UNKNOWN, reason, reason_data) - return sufficient - - @staticmethod - def _reason_data(disposition, count, most_recent): - """Create a reason data dictionary for this evaluator type.""" - return {'type': 'threshold', 'disposition': disposition, - 'count': count, 'most_recent': most_recent} - - @classmethod - def _reason(cls, alarm, statistics, distilled, state): - """Fabricate reason string.""" - count = len(statistics) - disposition = 'inside' if state == evaluator.OK else 'outside' - last = statistics[-1] - transition = alarm.state != state - reason_data = cls._reason_data(disposition, count, last) - if transition: - return (_('Transition to %(state)s due to %(count)d samples' - ' %(disposition)s threshold, most recent:' - ' %(most_recent)s') - % dict(reason_data, state=state)), reason_data - return (_('Remaining as %(state)s due to %(count)d samples' - ' %(disposition)s threshold, most recent: %(most_recent)s') - % dict(reason_data, state=state)), reason_data - - def _transition(self, alarm, statistics, compared): - """Transition alarm state if necessary. - - The transition rules are currently hardcoded as: - - - transitioning from a known state requires an unequivocal - set of datapoints - - - transitioning from unknown is on the basis of the most - recent datapoint if equivocal - - Ultimately this will be policy-driven. - """ - distilled = all(compared) - unequivocal = distilled or not any(compared) - unknown = alarm.state == evaluator.UNKNOWN - continuous = alarm.repeat_actions - - if unequivocal: - state = evaluator.ALARM if distilled else evaluator.OK - reason, reason_data = self._reason(alarm, statistics, - distilled, state) - if alarm.state != state or continuous: - self._refresh(alarm, state, reason, reason_data) - elif unknown or continuous: - trending_state = evaluator.ALARM if compared[-1] else evaluator.OK - state = trending_state if unknown else alarm.state - reason, reason_data = self._reason(alarm, statistics, - distilled, state) - self._refresh(alarm, state, reason, reason_data) - - @staticmethod - def _select_best_granularity(alarm, statistics): - """Return the datapoints that correspond to the alarm granularity""" - # TODO(sileht): if there's no direct match, but there is an archive - # policy with granularity that's an even divisor or the period, - # we could potentially do a mean-of-means (or max-of-maxes or whatever, - # but not a stddev-of-stddevs). - return [stats[2] for stats in statistics - if stats[1] == alarm.rule['granularity']] - - def evaluate(self, alarm): - if not self.within_time_constraint(alarm): - LOG.debug('Attempted to evaluate alarm %s, but it is not ' - 'within its time constraint.', alarm.alarm_id) - return - - start, end = self._bound_duration(alarm) - statistics = self._statistics(alarm, start, end) - statistics = self._select_best_granularity(alarm, statistics) - - if self._sufficient(alarm, statistics): - def _compare(value): - op = COMPARATORS[alarm.rule['comparison_operator']] - limit = alarm.rule['threshold'] - LOG.debug('comparing value %(value)s against threshold' - ' %(limit)s', {'value': value, 'limit': limit}) - return op(value, limit) - - self._transition(alarm, - statistics, - list(six.moves.map(_compare, statistics))) diff --git a/ceilometer/alarm/evaluator/threshold.py b/ceilometer/alarm/evaluator/threshold.py deleted file mode 100644 index 6e92a1d1..00000000 --- a/ceilometer/alarm/evaluator/threshold.py +++ /dev/null @@ -1,203 +0,0 @@ -# -# Copyright 2013 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. - -import datetime -import operator - -from oslo_log import log -from oslo_utils import timeutils - -from ceilometer.alarm import evaluator -from ceilometer.alarm.evaluator import utils -from ceilometer.i18n import _, _LW - -LOG = log.getLogger(__name__) - -COMPARATORS = { - 'gt': operator.gt, - 'lt': operator.lt, - 'ge': operator.ge, - 'le': operator.le, - 'eq': operator.eq, - 'ne': operator.ne, -} - - -class ThresholdEvaluator(evaluator.Evaluator): - - # the sliding evaluation window is extended to allow - # for reporting/ingestion lag - look_back = 1 - - @classmethod - def _bound_duration(cls, alarm, constraints): - """Bound the duration of the statistics query.""" - now = timeutils.utcnow() - # when exclusion of weak datapoints is enabled, we extend - # the look-back period so as to allow a clearer sample count - # trend to be established - look_back = (cls.look_back if not alarm.rule.get('exclude_outliers') - else alarm.rule['evaluation_periods']) - window = (alarm.rule['period'] * - (alarm.rule['evaluation_periods'] + look_back)) - start = now - datetime.timedelta(seconds=window) - LOG.debug('query stats from %(start)s to ' - '%(now)s', {'start': start, 'now': now}) - after = dict(field='timestamp', op='ge', value=start.isoformat()) - before = dict(field='timestamp', op='le', value=now.isoformat()) - constraints.extend([before, after]) - return constraints - - @staticmethod - def _sanitize(alarm, statistics): - """Sanitize statistics.""" - LOG.debug('sanitize stats %s', statistics) - if alarm.rule.get('exclude_outliers'): - key = operator.attrgetter('count') - mean = utils.mean(statistics, key) - stddev = utils.stddev(statistics, key, mean) - lower = mean - 2 * stddev - upper = mean + 2 * stddev - inliers, outliers = utils.anomalies(statistics, key, lower, upper) - if outliers: - LOG.debug('excluded weak datapoints with sample counts %s', - [s.count for s in outliers]) - statistics = inliers - else: - LOG.debug('no excluded weak datapoints') - - # in practice statistics are always sorted by period start, not - # strictly required by the API though - statistics = statistics[-alarm.rule['evaluation_periods']:] - LOG.debug('pruned statistics to %d', len(statistics)) - return statistics - - def _statistics(self, alarm, query): - """Retrieve statistics over the current window.""" - LOG.debug('stats query %s', query) - try: - return self._client.statistics.list( - meter_name=alarm.rule['meter_name'], q=query, - period=alarm.rule['period']) - except Exception: - LOG.exception(_('alarm stats retrieval failed')) - return [] - - def _sufficient(self, alarm, statistics): - """Check for the sufficiency of the data for evaluation. - - Ensure there is sufficient data for evaluation, transitioning to - unknown otherwise. - """ - sufficient = len(statistics) >= alarm.rule['evaluation_periods'] - if not sufficient and alarm.state != evaluator.UNKNOWN: - LOG.warning(_LW('Expecting %(expected)d datapoints but only get ' - '%(actual)d') % { - 'expected': alarm.rule['evaluation_periods'], - 'actual': len(statistics)}) - # Reason is not same as log message because we want to keep - # consistent since thirdparty software may depend on old format. - reason = _('%d datapoints are unknown') % alarm.rule[ - 'evaluation_periods'] - last = None if not statistics else ( - getattr(statistics[-1], alarm.rule['statistic'])) - reason_data = self._reason_data('unknown', - alarm.rule['evaluation_periods'], - last) - self._refresh(alarm, evaluator.UNKNOWN, reason, reason_data) - return sufficient - - @staticmethod - def _reason_data(disposition, count, most_recent): - """Create a reason data dictionary for this evaluator type.""" - return {'type': 'threshold', 'disposition': disposition, - 'count': count, 'most_recent': most_recent} - - @classmethod - def _reason(cls, alarm, statistics, distilled, state): - """Fabricate reason string.""" - count = len(statistics) - disposition = 'inside' if state == evaluator.OK else 'outside' - last = getattr(statistics[-1], alarm.rule['statistic']) - transition = alarm.state != state - reason_data = cls._reason_data(disposition, count, last) - if transition: - return (_('Transition to %(state)s due to %(count)d samples' - ' %(disposition)s threshold, most recent:' - ' %(most_recent)s') - % dict(reason_data, state=state)), reason_data - return (_('Remaining as %(state)s due to %(count)d samples' - ' %(disposition)s threshold, most recent: %(most_recent)s') - % dict(reason_data, state=state)), reason_data - - def _transition(self, alarm, statistics, compared): - """Transition alarm state if necessary. - - The transition rules are currently hardcoded as: - - - transitioning from a known state requires an unequivocal - set of datapoints - - - transitioning from unknown is on the basis of the most - recent datapoint if equivocal - - Ultimately this will be policy-driven. - """ - distilled = all(compared) - unequivocal = distilled or not any(compared) - unknown = alarm.state == evaluator.UNKNOWN - continuous = alarm.repeat_actions - - if unequivocal: - state = evaluator.ALARM if distilled else evaluator.OK - reason, reason_data = self._reason(alarm, statistics, - distilled, state) - if alarm.state != state or continuous: - self._refresh(alarm, state, reason, reason_data) - elif unknown or continuous: - trending_state = evaluator.ALARM if compared[-1] else evaluator.OK - state = trending_state if unknown else alarm.state - reason, reason_data = self._reason(alarm, statistics, - distilled, state) - self._refresh(alarm, state, reason, reason_data) - - def evaluate(self, alarm): - if not self.within_time_constraint(alarm): - LOG.debug('Attempted to evaluate alarm %s, but it is not ' - 'within its time constraint.', alarm.alarm_id) - return - - query = self._bound_duration( - alarm, - alarm.rule['query'] - ) - - statistics = self._sanitize( - alarm, - self._statistics(alarm, query) - ) - - if self._sufficient(alarm, statistics): - def _compare(stat): - op = COMPARATORS[alarm.rule['comparison_operator']] - value = getattr(stat, alarm.rule['statistic']) - limit = alarm.rule['threshold'] - LOG.debug('comparing value %(value)s against threshold' - ' %(limit)s', {'value': value, 'limit': limit}) - return op(value, limit) - - self._transition(alarm, - statistics, - [_compare(statistic) for statistic in statistics]) diff --git a/ceilometer/alarm/evaluator/utils.py b/ceilometer/alarm/evaluator/utils.py deleted file mode 100644 index 07c16bdb..00000000 --- a/ceilometer/alarm/evaluator/utils.py +++ /dev/null @@ -1,58 +0,0 @@ -# -# Copyright 2014 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. - -import math - - -def mean(s, key=lambda x: x): - """Calculate the mean of a numeric list.""" - count = float(len(s)) - if count: - return math.fsum(map(key, s)) / count - return 0.0 - - -def deltas(s, key, m=None): - """Calculate the squared distances from mean for a numeric list.""" - m = m or mean(s, key) - return [(key(i) - m) ** 2 for i in s] - - -def variance(s, key, m=None): - """Calculate the variance of a numeric list.""" - return mean(deltas(s, key, m)) - - -def stddev(s, key, m=None): - """Calculate the standard deviation of a numeric list.""" - return math.sqrt(variance(s, key, m)) - - -def outside(s, key, lower=0.0, upper=0.0): - """Determine if value falls outside upper and lower bounds.""" - v = key(s) - return v < lower or v > upper - - -def anomalies(s, key, lower=0.0, upper=0.0): - """Separate anomalous data points from the in-liers.""" - inliers = [] - outliers = [] - for i in s: - if outside(i, key, lower, upper): - outliers.append(i) - else: - inliers.append(i) - return inliers, outliers diff --git a/ceilometer/alarm/notifier/__init__.py b/ceilometer/alarm/notifier/__init__.py deleted file mode 100644 index 0e34669a..00000000 --- a/ceilometer/alarm/notifier/__init__.py +++ /dev/null @@ -1,38 +0,0 @@ -# -# Copyright 2013 eNovance -# -# 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 abc - -import six - - -@six.add_metaclass(abc.ABCMeta) -class AlarmNotifier(object): - """Base class for alarm notifier plugins.""" - - @abc.abstractmethod - def notify(self, action, alarm_id, alarm_name, severity, previous, - current, reason, reason_data): - """Notify that an alarm has been triggered. - - :param action: The action that is being attended, as a parsed URL. - :param alarm_id: The triggered alarm. - :param alarm_name: The name of triggered alarm. - :param severity: The level of triggered alarm - :param previous: The previous state of the alarm. - :param current: The current state of the alarm. - :param reason: The reason the alarm changed its state. - :param reason_data: A dict representation of the reason. - """ diff --git a/ceilometer/alarm/notifier/log.py b/ceilometer/alarm/notifier/log.py deleted file mode 100644 index a9309312..00000000 --- a/ceilometer/alarm/notifier/log.py +++ /dev/null @@ -1,40 +0,0 @@ -# -# Copyright 2013 eNovance -# -# 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. -"""Log alarm notifier.""" - -from oslo_log import log - -from ceilometer.alarm import notifier -from ceilometer.i18n import _LI - -LOG = log.getLogger(__name__) - - -class LogAlarmNotifier(notifier.AlarmNotifier): - "Log alarm notifier.""" - - @staticmethod - def notify(action, alarm_id, alarm_name, severity, previous, current, - reason, reason_data): - LOG.info(_LI( - "Notifying alarm %(alarm_name)s %(alarm_id)s of %(severity)s " - "priority from %(previous)s to %(current)s with action %(action)s" - " because %(reason)s.") % ({'alarm_name': alarm_name, - 'alarm_id': alarm_id, - 'severity': severity, - 'previous': previous, - 'current': current, - 'action': action, - 'reason': reason})) diff --git a/ceilometer/alarm/notifier/rest.py b/ceilometer/alarm/notifier/rest.py deleted file mode 100644 index b702246a..00000000 --- a/ceilometer/alarm/notifier/rest.py +++ /dev/null @@ -1,104 +0,0 @@ -# -# Copyright 2013-2014 eNovance -# -# 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. -"""Rest alarm notifier.""" - -import eventlet -from oslo_config import cfg -from oslo_context import context -from oslo_log import log -from oslo_serialization import jsonutils -import requests -import six.moves.urllib.parse as urlparse - -from ceilometer.alarm import notifier -from ceilometer.i18n import _LI - -LOG = log.getLogger(__name__) - -OPTS = [ - cfg.StrOpt('rest_notifier_certificate_file', - default='', - deprecated_for_removal=True, - help='SSL Client certificate for REST notifier.' - ), - cfg.StrOpt('rest_notifier_certificate_key', - default='', - deprecated_for_removal=True, - help='SSL Client private key for REST notifier.' - ), - cfg.BoolOpt('rest_notifier_ssl_verify', - default=True, - deprecated_for_removal=True, - help='Whether to verify the SSL Server certificate when ' - 'calling alarm action.' - ), - cfg.IntOpt('rest_notifier_max_retries', - default=0, - deprecated_for_removal=True, - help='Number of retries for REST notifier', - ), - -] - -cfg.CONF.register_opts(OPTS, group="alarm") - - -class RestAlarmNotifier(notifier.AlarmNotifier): - """Rest alarm notifier.""" - - @staticmethod - def notify(action, alarm_id, alarm_name, severity, previous, - current, reason, reason_data, headers=None): - headers = headers or {} - if not headers.get('x-openstack-request-id'): - headers['x-openstack-request-id'] = context.generate_request_id() - - LOG.info(_LI( - "Notifying alarm %(alarm_name)s %(alarm_id)s with severity" - " %(severity)s from %(previous)s to %(current)s with action " - "%(action)s because %(reason)s. request-id: %(request_id)s ") % - ({'alarm_name': alarm_name, 'alarm_id': alarm_id, - 'severity': severity, 'previous': previous, - 'current': current, 'action': action, 'reason': reason, - 'request_id': headers['x-openstack-request-id']})) - body = {'alarm_name': alarm_name, 'alarm_id': alarm_id, - 'severity': severity, 'previous': previous, - 'current': current, 'reason': reason, - 'reason_data': reason_data} - headers['content-type'] = 'application/json' - kwargs = {'data': jsonutils.dumps(body), - 'headers': headers} - - if action.scheme == 'https': - default_verify = int(cfg.CONF.alarm.rest_notifier_ssl_verify) - options = urlparse.parse_qs(action.query) - verify = bool(int(options.get('ceilometer-alarm-ssl-verify', - [default_verify])[-1])) - kwargs['verify'] = verify - - cert = cfg.CONF.alarm.rest_notifier_certificate_file - key = cfg.CONF.alarm.rest_notifier_certificate_key - if cert: - kwargs['cert'] = (cert, key) if key else cert - - # FIXME(rhonjo): Retries are automatically done by urllib3 in requests - # library. However, there's no interval between retries in urllib3 - # implementation. It will be better to put some interval between - # retries (future work). - max_retries = cfg.CONF.alarm.rest_notifier_max_retries - session = requests.Session() - session.mount(action.geturl(), - requests.adapters.HTTPAdapter(max_retries=max_retries)) - eventlet.spawn_n(session.post, action.geturl(), **kwargs) diff --git a/ceilometer/alarm/notifier/test.py b/ceilometer/alarm/notifier/test.py deleted file mode 100644 index a7d74eb1..00000000 --- a/ceilometer/alarm/notifier/test.py +++ /dev/null @@ -1,35 +0,0 @@ -# -# Copyright 2013 eNovance -# -# 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 alarm notifier.""" - -from ceilometer.alarm import notifier - - -class TestAlarmNotifier(notifier.AlarmNotifier): - "Test alarm notifier.""" - - def __init__(self): - self.notifications = [] - - def notify(self, action, alarm_id, alarm_name, severity, - previous, current, reason, reason_data): - self.notifications.append((action, - alarm_id, - alarm_name, - severity, - previous, - current, - reason, - reason_data)) diff --git a/ceilometer/alarm/notifier/trust.py b/ceilometer/alarm/notifier/trust.py deleted file mode 100644 index fcdc982f..00000000 --- a/ceilometer/alarm/notifier/trust.py +++ /dev/null @@ -1,56 +0,0 @@ -# -# Copyright 2014 eNovance -# -# 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. -"""Rest alarm notifier with trusted authentication.""" - -from oslo_config import cfg -from six.moves.urllib import parse - -from ceilometer.alarm.notifier import rest -from ceilometer import keystone_client - - -cfg.CONF.import_opt('http_timeout', 'ceilometer.service') -cfg.CONF.import_group('service_credentials', 'ceilometer.service') - - -class TrustRestAlarmNotifier(rest.RestAlarmNotifier): - """Notifier supporting keystone trust authentication. - - This alarm notifier is intended to be used to call an endpoint using - keystone authentication. It uses the ceilometer service user to - authenticate using the trust ID provided. - - The URL must be in the form trust+http://trust-id@host/action. - """ - - @staticmethod - def notify(action, alarm_id, alarm_name, severity, previous, current, - reason, reason_data): - trust_id = action.username - - client = keystone_client.get_v3_client(trust_id) - - # Remove the fake user - netloc = action.netloc.split("@")[1] - # Remove the trust prefix - scheme = action.scheme[6:] - - action = parse.SplitResult(scheme, netloc, action.path, action.query, - action.fragment) - - headers = {'X-Auth-Token': client.auth_token} - rest.RestAlarmNotifier.notify( - action, alarm_id, alarm_name, severity, previous, current, reason, - reason_data, headers) diff --git a/ceilometer/alarm/rpc.py b/ceilometer/alarm/rpc.py deleted file mode 100644 index a7d1a854..00000000 --- a/ceilometer/alarm/rpc.py +++ /dev/null @@ -1,65 +0,0 @@ -# -# Copyright 2013 eNovance -# -# Authors: Mehdi Abaakouk -# -# 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 oslo_config import cfg -from oslo_context import context -from oslo_log import log -import six - -from ceilometer.alarm.storage import models -from ceilometer import messaging - -OPTS = [ - cfg.StrOpt('notifier_rpc_topic', - default='alarm_notifier', - deprecated_for_removal=True, - help='The topic that ceilometer uses for alarm notifier ' - 'messages.'), -] - -cfg.CONF.register_opts(OPTS, group='alarm') - -LOG = log.getLogger(__name__) - - -class RPCAlarmNotifier(object): - def __init__(self): - transport = messaging.get_transport() - self.client = messaging.get_rpc_client( - transport, topic=cfg.CONF.alarm.notifier_rpc_topic, - version="1.0") - - def notify(self, alarm, previous, reason, reason_data): - actions = getattr(alarm, models.Alarm.ALARM_ACTIONS_MAP[alarm.state]) - if not actions: - LOG.debug('alarm %(alarm_id)s has no action configured ' - 'for state transition from %(previous)s to ' - 'state %(state)s, skipping the notification.', - {'alarm_id': alarm.alarm_id, - 'previous': previous, - 'state': alarm.state}) - return - self.client.cast(context.get_admin_context(), - 'notify_alarm', data={ - 'actions': actions, - 'alarm_id': alarm.alarm_id, - 'alarm_name': alarm.name, - 'severity': alarm.severity, - 'previous': previous, - 'current': alarm.state, - 'reason': six.text_type(reason), - 'reason_data': reason_data}) diff --git a/ceilometer/alarm/service.py b/ceilometer/alarm/service.py deleted file mode 100644 index 03e3d88a..00000000 --- a/ceilometer/alarm/service.py +++ /dev/null @@ -1,231 +0,0 @@ -# -# Copyright 2013 Red Hat, Inc -# Copyright 2013 eNovance -# -# Authors: Eoghan Glynn -# Julien Danjou -# -# 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 abc - -from ceilometerclient import client as ceiloclient -from oslo_config import cfg -from oslo_log import log -from oslo_service import service as os_service -from oslo_utils import netutils -import six -from stevedore import extension - -from ceilometer import alarm as ceilometer_alarm -from ceilometer.alarm import rpc as rpc_alarm -from ceilometer import coordination as coordination -from ceilometer.i18n import _, _LI -from ceilometer import messaging - - -OPTS = [ - cfg.IntOpt('evaluation_interval', - default=60, - deprecated_for_removal=True, - help='Period of evaluation cycle, should' - ' be >= than configured pipeline interval for' - ' collection of underlying meters.', - deprecated_opts=[cfg.DeprecatedOpt( - 'threshold_evaluation_interval', group='alarm')]), -] - -cfg.CONF.register_opts(OPTS, group='alarm') -cfg.CONF.import_opt('http_timeout', 'ceilometer.service') -cfg.CONF.import_group('service_credentials', 'ceilometer.service') - -LOG = log.getLogger(__name__) - - -@six.add_metaclass(abc.ABCMeta) -class AlarmService(object): - - def __init__(self): - super(AlarmService, self).__init__() - self._load_evaluators() - self.api_client = None - - def _load_evaluators(self): - self.evaluators = extension.ExtensionManager( - namespace=ceilometer_alarm.EVALUATOR_EXTENSIONS_NAMESPACE, - invoke_on_load=True, - invoke_args=(rpc_alarm.RPCAlarmNotifier(),) - ) - self.supported_evaluators = [ext.name for ext in - self.evaluators.extensions] - - @property - def _client(self): - """Construct or reuse an authenticated API client.""" - if not self.api_client: - auth_config = cfg.CONF.service_credentials - creds = dict( - os_auth_url=auth_config.os_auth_url, - os_region_name=auth_config.os_region_name, - os_tenant_name=auth_config.os_tenant_name, - os_password=auth_config.os_password, - os_username=auth_config.os_username, - os_cacert=auth_config.os_cacert, - os_endpoint_type=auth_config.os_endpoint_type, - insecure=auth_config.insecure, - timeout=cfg.CONF.http_timeout, - ) - self.api_client = ceiloclient.get_client(2, **creds) - return self.api_client - - def _evaluate_assigned_alarms(self): - try: - alarms = self._assigned_alarms() - LOG.info(_LI('initiating evaluation cycle on %d alarms') % - len(alarms)) - for alarm in alarms: - self._evaluate_alarm(alarm) - except Exception: - LOG.exception(_('alarm evaluation cycle failed')) - - def _evaluate_alarm(self, alarm): - """Evaluate the alarms assigned to this evaluator.""" - if alarm.type not in self.supported_evaluators: - LOG.debug('skipping alarm %s: type unsupported', alarm.alarm_id) - return - - LOG.debug('evaluating alarm %s', alarm.alarm_id) - try: - self.evaluators[alarm.type].obj.evaluate(alarm) - except Exception: - LOG.exception(_('Failed to evaluate alarm %s'), alarm.alarm_id) - - @abc.abstractmethod - def _assigned_alarms(self): - pass - - -class AlarmEvaluationService(AlarmService, os_service.Service): - - PARTITIONING_GROUP_NAME = "alarm_evaluator" - - def __init__(self): - super(AlarmEvaluationService, self).__init__() - self.partition_coordinator = coordination.PartitionCoordinator() - - def start(self): - super(AlarmEvaluationService, self).start() - self.partition_coordinator.start() - self.partition_coordinator.join_group(self.PARTITIONING_GROUP_NAME) - - # allow time for coordination if necessary - delay_start = self.partition_coordinator.is_active() - - if self.evaluators: - interval = cfg.CONF.alarm.evaluation_interval - self.tg.add_timer( - interval, - self._evaluate_assigned_alarms, - initial_delay=interval if delay_start else None) - if self.partition_coordinator.is_active(): - heartbeat_interval = min(cfg.CONF.coordination.heartbeat, - cfg.CONF.alarm.evaluation_interval / 4) - self.tg.add_timer(heartbeat_interval, - self.partition_coordinator.heartbeat) - # Add a dummy thread to have wait() working - self.tg.add_timer(604800, lambda: None) - - def _assigned_alarms(self): - all_alarms = self._client.alarms.list(q=[{'field': 'enabled', - 'value': True}]) - return self.partition_coordinator.extract_my_subset( - self.PARTITIONING_GROUP_NAME, all_alarms) - - -class AlarmNotifierService(os_service.Service): - - def __init__(self): - super(AlarmNotifierService, self).__init__() - transport = messaging.get_transport() - self.rpc_server = messaging.get_rpc_server( - transport, cfg.CONF.alarm.notifier_rpc_topic, self) - - def start(self): - super(AlarmNotifierService, self).start() - self.rpc_server.start() - # Add a dummy thread to have wait() working - self.tg.add_timer(604800, lambda: None) - - def stop(self): - self.rpc_server.stop() - super(AlarmNotifierService, self).stop() - - def _handle_action(self, action, alarm_id, alarm_name, severity, - previous, current, reason, reason_data): - try: - action = netutils.urlsplit(action) - except Exception: - LOG.error( - _("Unable to parse action %(action)s for alarm %(alarm_id)s"), - {'action': action, 'alarm_id': alarm_id}) - return - - try: - notifier = ceilometer_alarm.NOTIFIERS[action.scheme].obj - except KeyError: - scheme = action.scheme - LOG.error( - _("Action %(scheme)s for alarm %(alarm_id)s is unknown, " - "cannot notify"), - {'scheme': scheme, 'alarm_id': alarm_id}) - return - - try: - LOG.debug("Notifying alarm %(id)s with action %(act)s", - {'id': alarm_id, 'act': action}) - notifier.notify(action, alarm_id, alarm_name, severity, - previous, current, reason, reason_data) - except Exception: - LOG.exception(_("Unable to notify alarm %s"), alarm_id) - return - - def notify_alarm(self, context, data): - """Notify that alarm has been triggered. - - :param context: Request context. - :param data: (dict): - - - actions, the URL of the action to run; this is mapped to - extensions automatically - - alarm_id, the ID of the alarm that has been triggered - - alarm_name, the name of the alarm that has been triggered - - severity, the level of the alarm that has been triggered - - previous, the previous state of the alarm - - current, the new state the alarm has transitioned to - - reason, the reason the alarm changed its state - - reason_data, a dict representation of the reason - """ - actions = data.get('actions') - if not actions: - LOG.error(_("Unable to notify for an alarm with no action")) - return - - for action in actions: - self._handle_action(action, - data.get('alarm_id'), - data.get('alarm_name'), - data.get('severity'), - data.get('previous'), - data.get('current'), - data.get('reason'), - data.get('reason_data')) diff --git a/ceilometer/alarm/storage/__init__.py b/ceilometer/alarm/storage/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/ceilometer/alarm/storage/base.py b/ceilometer/alarm/storage/base.py deleted file mode 100644 index 6aaa82f8..00000000 --- a/ceilometer/alarm/storage/base.py +++ /dev/null @@ -1,166 +0,0 @@ -# -# Copyright 2012 New Dream Network, LLC (DreamHost) -# -# 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. -"""Base classes for storage engines -""" -import ceilometer - - -class Connection(object): - """Base class for alarm storage system connections.""" - - # A dictionary representing the capabilities of this driver. - CAPABILITIES = { - 'alarms': {'query': {'simple': False, - 'complex': False}, - 'history': {'query': {'simple': False, - 'complex': False}}}, - } - - STORAGE_CAPABILITIES = { - 'storage': {'production_ready': False}, - } - - def __init__(self, url): - pass - - @staticmethod - def upgrade(): - """Migrate the database to `version` or the most recent version.""" - - @staticmethod - def get_alarms(name=None, user=None, state=None, meter=None, - project=None, enabled=None, alarm_id=None, - alarm_type=None, severity=None): - """Yields a lists of alarms that match filters. - - :param name: Optional name for alarm. - :param user: Optional ID for user that owns the resource. - :param state: Optional string for alarm state. - :param meter: Optional string for alarms associated with meter. - :param project: Optional ID for project that owns the resource. - :param enabled: Optional boolean to list disable alarm. - :param alarm_id: Optional alarm_id to return one alarm. - :param alarm_type: Optional alarm type. - :parmr severity: Optional alarm severity - """ - raise ceilometer.NotImplementedError('Alarms not implemented') - - @staticmethod - def create_alarm(alarm): - """Create an alarm. Returns the alarm as created. - - :param alarm: The alarm to create. - """ - raise ceilometer.NotImplementedError('Alarms not implemented') - - @staticmethod - def update_alarm(alarm): - """Update alarm.""" - raise ceilometer.NotImplementedError('Alarms not implemented') - - @staticmethod - def delete_alarm(alarm_id): - """Delete an alarm and its history data.""" - raise ceilometer.NotImplementedError('Alarms not implemented') - - @staticmethod - def get_alarm_changes(alarm_id, on_behalf_of, - user=None, project=None, alarm_type=None, - severity=None, start_timestamp=None, - start_timestamp_op=None, end_timestamp=None, - end_timestamp_op=None): - """Yields list of AlarmChanges describing alarm history - - Changes are always sorted in reverse order of occurrence, given - the importance of currency. - - Segregation for non-administrative users is done on the basis - of the on_behalf_of parameter. This allows such users to have - visibility on both the changes initiated by themselves directly - (generally creation, rule changes, or deletion) and also on those - changes initiated on their behalf by the alarming service (state - transitions after alarm thresholds are crossed). - - :param alarm_id: ID of alarm to return changes for - :param on_behalf_of: ID of tenant to scope changes query (None for - administrative user, indicating all projects) - :param user: Optional ID of user to return changes for - :param project: Optional ID of project to return changes for - :param alarm_type: Optional change type - :param severity: Optional change severity - :param start_timestamp: Optional modified timestamp start range - :param start_timestamp_op: Optional timestamp start range operation - :param end_timestamp: Optional modified timestamp end range - :param end_timestamp_op: Optional timestamp end range operation - """ - raise ceilometer.NotImplementedError('Alarm history not implemented') - - @staticmethod - def record_alarm_change(alarm_change): - """Record alarm change event.""" - raise ceilometer.NotImplementedError('Alarm history not implemented') - - @staticmethod - def clear(): - """Clear database.""" - - @staticmethod - def query_alarms(filter_expr=None, orderby=None, limit=None): - """Return an iterable of model.Alarm objects. - - :param filter_expr: Filter expression for query. - :param orderby: List of field name and direction pairs for order by. - :param limit: Maximum number of results to return. - """ - - raise ceilometer.NotImplementedError('Complex query for alarms ' - 'is not implemented.') - - @staticmethod - def query_alarm_history(filter_expr=None, orderby=None, limit=None): - """Return an iterable of model.AlarmChange objects. - - :param filter_expr: Filter expression for query. - :param orderby: List of field name and direction pairs for order by. - :param limit: Maximum number of results to return. - """ - - raise ceilometer.NotImplementedError('Complex query for alarms ' - 'history is not implemented.') - - @classmethod - def get_capabilities(cls): - """Return an dictionary with the capabilities of each driver.""" - return cls.CAPABILITIES - - @classmethod - def get_storage_capabilities(cls): - """Return a dictionary representing the performance capabilities. - - This is needed to evaluate the performance of each driver. - """ - return cls.STORAGE_CAPABILITIES - - @staticmethod - def clear_expired_alarm_history_data(alarm_history_ttl): - """Clear expired alarm history data from the backend storage system. - - Clearing occurs according to the time-to-live. - - :param alarm_history_ttl: Number of seconds to keep alarm history - records for. - """ - raise ceilometer.NotImplementedError('Clearing alarm history ' - 'not implemented') diff --git a/ceilometer/alarm/storage/impl_db2.py b/ceilometer/alarm/storage/impl_db2.py deleted file mode 100644 index 291dafd2..00000000 --- a/ceilometer/alarm/storage/impl_db2.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright 2012 New Dream Network, LLC (DreamHost) -# Copyright 2013 eNovance -# Copyright 2013 IBM Corp -# -# 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. -"""DB2 storage backend -""" - -from __future__ import division - -from oslo_log import log -import pymongo - -from ceilometer.alarm.storage import pymongo_base -from ceilometer import storage -from ceilometer.storage.mongo import utils as pymongo_utils - -LOG = log.getLogger(__name__) - - -class Connection(pymongo_base.Connection): - """The db2 alarm storage for Ceilometer.""" - - CONNECTION_POOL = pymongo_utils.ConnectionPool() - - def __init__(self, url): - - # Since we are using pymongo, even though we are connecting to DB2 - # we still have to make sure that the scheme which used to distinguish - # db2 driver from mongodb driver be replaced so that pymongo will not - # produce an exception on the scheme. - url = url.replace('db2:', 'mongodb:', 1) - self.conn = self.CONNECTION_POOL.connect(url) - - # Require MongoDB 2.2 to use aggregate(), since we are using mongodb - # as backend for test, the following code is necessary to make sure - # that the test wont try aggregate on older mongodb during the test. - # For db2, the versionArray won't be part of the server_info, so there - # will not be exception when real db2 gets used as backend. - server_info = self.conn.server_info() - if server_info.get('sysInfo'): - self._using_mongodb = True - else: - self._using_mongodb = False - - if self._using_mongodb and server_info.get('versionArray') < [2, 2]: - raise storage.StorageBadVersion("Need at least MongoDB 2.2") - - connection_options = pymongo.uri_parser.parse_uri(url) - self.db = getattr(self.conn, connection_options['database']) - if connection_options.get('username'): - self.db.authenticate(connection_options['username'], - connection_options['password']) - - self.upgrade() - - def clear(self): - # drop_database command does nothing on db2 database since this has - # not been implemented. However calling this method is important for - # removal of all the empty dbs created during the test runs since - # test run is against mongodb on Jenkins - self.conn.drop_database(self.db.name) - self.conn.close() diff --git a/ceilometer/alarm/storage/impl_hbase.py b/ceilometer/alarm/storage/impl_hbase.py deleted file mode 100644 index bd612665..00000000 --- a/ceilometer/alarm/storage/impl_hbase.py +++ /dev/null @@ -1,181 +0,0 @@ -# -# 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 datetime -import operator - -from oslo_log import log - -import ceilometer -from ceilometer.alarm.storage import base -from ceilometer.alarm.storage import models -from ceilometer.storage.hbase import base as hbase_base -from ceilometer.storage.hbase import migration as hbase_migration -from ceilometer.storage.hbase import utils as hbase_utils -from ceilometer import utils - -LOG = log.getLogger(__name__) - - -AVAILABLE_CAPABILITIES = { - 'alarms': {'query': {'simple': True, - 'complex': False}, - 'history': {'query': {'simple': True, - 'complex': False}}}, -} - - -AVAILABLE_STORAGE_CAPABILITIES = { - 'storage': {'production_ready': True}, -} - - -class Connection(hbase_base.Connection, base.Connection): - """Put the alarm data into a HBase database - - Collections: - - - alarm: - - - row_key: uuid of alarm - - Column Families: - - f: contains the raw incoming alarm data - - - alarm_h: - - - row_key: uuid of alarm + ":" + reversed timestamp - - Column Families: - - f: raw incoming alarm_history data. Timestamp becomes now() - if not determined - """ - - CAPABILITIES = utils.update_nested(base.Connection.CAPABILITIES, - AVAILABLE_CAPABILITIES) - STORAGE_CAPABILITIES = utils.update_nested( - base.Connection.STORAGE_CAPABILITIES, - AVAILABLE_STORAGE_CAPABILITIES, - ) - _memory_instance = None - - ALARM_TABLE = "alarm" - ALARM_HISTORY_TABLE = "alarm_h" - - def __init__(self, url): - super(Connection, self).__init__(url) - - def upgrade(self): - tables = [self.ALARM_HISTORY_TABLE, self.ALARM_TABLE] - column_families = {'f': dict()} - with self.conn_pool.connection() as conn: - hbase_utils.create_tables(conn, tables, column_families) - hbase_migration.migrate_tables(conn, tables) - - def clear(self): - LOG.debug('Dropping HBase schema...') - with self.conn_pool.connection() as conn: - for table in [self.ALARM_TABLE, - self.ALARM_HISTORY_TABLE]: - try: - conn.disable_table(table) - except Exception: - LOG.debug('Cannot disable table but ignoring error') - try: - conn.delete_table(table) - except Exception: - LOG.debug('Cannot delete table but ignoring error') - - def update_alarm(self, alarm): - """Create an alarm. - - :param alarm: The alarm to create. It is Alarm object, so we need to - call as_dict() - """ - _id = alarm.alarm_id - alarm_to_store = hbase_utils.serialize_entry(alarm.as_dict()) - with self.conn_pool.connection() as conn: - alarm_table = conn.table(self.ALARM_TABLE) - alarm_table.put(_id, alarm_to_store) - stored_alarm = hbase_utils.deserialize_entry( - alarm_table.row(_id))[0] - return models.Alarm(**stored_alarm) - - create_alarm = update_alarm - - def delete_alarm(self, alarm_id): - """Delete an alarm and its history data.""" - with self.conn_pool.connection() as conn: - alarm_table = conn.table(self.ALARM_TABLE) - alarm_table.delete(alarm_id) - q = hbase_utils.make_query(alarm_id=alarm_id) - alarm_history_table = conn.table(self.ALARM_HISTORY_TABLE) - for alarm_id, ignored in alarm_history_table.scan(filter=q): - alarm_history_table.delete(alarm_id) - - def get_alarms(self, name=None, user=None, state=None, meter=None, - project=None, enabled=None, alarm_id=None, - alarm_type=None, severity=None): - - if meter: - raise ceilometer.NotImplementedError( - 'Filter by meter not implemented') - - q = hbase_utils.make_query(alarm_id=alarm_id, name=name, - enabled=enabled, user_id=user, - project_id=project, state=state, - type=alarm_type, severity=severity) - - with self.conn_pool.connection() as conn: - alarm_table = conn.table(self.ALARM_TABLE) - gen = alarm_table.scan(filter=q) - alarms = [hbase_utils.deserialize_entry(data)[0] - for ignored, data in gen] - for alarm in sorted( - alarms, - key=operator.itemgetter('timestamp'), - reverse=True): - yield models.Alarm(**alarm) - - def get_alarm_changes(self, alarm_id, on_behalf_of, - user=None, project=None, alarm_type=None, - severity=None, start_timestamp=None, - start_timestamp_op=None, end_timestamp=None, - end_timestamp_op=None): - q = hbase_utils.make_query(alarm_id=alarm_id, - on_behalf_of=on_behalf_of, type=alarm_type, - user_id=user, project_id=project, - severity=severity) - start_row, end_row = hbase_utils.make_timestamp_query( - hbase_utils.make_general_rowkey_scan, - start=start_timestamp, start_op=start_timestamp_op, - end=end_timestamp, end_op=end_timestamp_op, bounds_only=True, - some_id=alarm_id) - with self.conn_pool.connection() as conn: - alarm_history_table = conn.table(self.ALARM_HISTORY_TABLE) - gen = alarm_history_table.scan(filter=q, row_start=start_row, - row_stop=end_row) - for ignored, data in gen: - stored_entry = hbase_utils.deserialize_entry(data)[0] - yield models.AlarmChange(**stored_entry) - - def record_alarm_change(self, alarm_change): - """Record alarm change event.""" - alarm_change_dict = hbase_utils.serialize_entry(alarm_change) - ts = alarm_change.get('timestamp') or datetime.datetime.now() - rts = hbase_utils.timestamp(ts) - with self.conn_pool.connection() as conn: - alarm_history_table = conn.table(self.ALARM_HISTORY_TABLE) - alarm_history_table.put( - hbase_utils.prepare_key(alarm_change.get('alarm_id'), rts), - alarm_change_dict) diff --git a/ceilometer/alarm/storage/impl_log.py b/ceilometer/alarm/storage/impl_log.py deleted file mode 100644 index 0dc02aea..00000000 --- a/ceilometer/alarm/storage/impl_log.py +++ /dev/null @@ -1,61 +0,0 @@ -# -# Copyright 2012 New Dream Network, LLC (DreamHost) -# -# 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. -"""Simple logging storage backend. -""" - -from oslo_log import log - -from ceilometer.alarm.storage import base -from ceilometer.i18n import _LI - -LOG = log.getLogger(__name__) - - -class Connection(base.Connection): - """Log the data.""" - - def upgrade(self): - pass - - def clear(self): - pass - - def get_alarms(self, name=None, user=None, state=None, meter=None, - project=None, enabled=None, alarm_id=None, - alarm_type=None, severity=None): - """Yields a lists of alarms that match filters.""" - return [] - - def create_alarm(self, alarm): - """Create alarm.""" - return alarm - - def update_alarm(self, alarm): - """Update alarm.""" - return alarm - - def delete_alarm(self, alarm_id): - """Delete an alarm and its history data.""" - - def clear_expired_alarm_history_data(self, alarm_history_ttl): - """Clear expired alarm history data from the backend storage system. - - Clearing occurs according to the time-to-live. - - :param alarm_history_ttl: Number of seconds to keep alarm history - records for. - """ - LOG.info(_LI('Dropping alarm history data with TTL %d'), - alarm_history_ttl) diff --git a/ceilometer/alarm/storage/impl_mongodb.py b/ceilometer/alarm/storage/impl_mongodb.py deleted file mode 100644 index e2d13629..00000000 --- a/ceilometer/alarm/storage/impl_mongodb.py +++ /dev/null @@ -1,87 +0,0 @@ -# -# Copyright 2012 New Dream Network, LLC (DreamHost) -# Copyright 2013 eNovance -# Copyright 2014 Red Hat, Inc -# -# Authors: Doug Hellmann -# Julien Danjou -# Eoghan Glynn -# -# 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. -"""MongoDB storage backend""" - -from oslo_config import cfg -from oslo_log import log -import pymongo - -from ceilometer.alarm.storage import pymongo_base -from ceilometer import storage -from ceilometer.storage import impl_mongodb -from ceilometer.storage.mongo import utils as pymongo_utils - -cfg.CONF.import_opt('alarm_history_time_to_live', 'ceilometer.alarm.storage', - group="database") - -LOG = log.getLogger(__name__) - - -class Connection(pymongo_base.Connection): - """Put the alarm data into a MongoDB database.""" - - CONNECTION_POOL = pymongo_utils.ConnectionPool() - - def __init__(self, url): - - # NOTE(jd) Use our own connection pooling on top of the Pymongo one. - # We need that otherwise we overflow the MongoDB instance with new - # connection since we instantiate a Pymongo client each time someone - # requires a new storage connection. - self.conn = self.CONNECTION_POOL.connect(url) - - # Require MongoDB 2.4 to use $setOnInsert - if self.conn.server_info()['versionArray'] < [2, 4]: - raise storage.StorageBadVersion("Need at least MongoDB 2.4") - - connection_options = pymongo.uri_parser.parse_uri(url) - self.db = getattr(self.conn, connection_options['database']) - if connection_options.get('username'): - self.db.authenticate(connection_options['username'], - connection_options['password']) - - # NOTE(jd) Upgrading is just about creating index, so let's do this - # on connection to be sure at least the TTL is correctly updated if - # needed. - self.upgrade() - - def upgrade(self): - super(Connection, self).upgrade() - # Establish indexes - ttl = cfg.CONF.database.alarm_history_time_to_live - impl_mongodb.Connection.update_ttl( - ttl, 'alarm_history_ttl', 'timestamp', self.db.alarm_history) - - def clear(self): - self.conn.drop_database(self.db.name) - # Connection will be reopened automatically if needed - self.conn.close() - - def clear_expired_alarm_history_data(self, alarm_history_ttl): - """Clear expired alarm history data from the backend storage system. - - Clearing occurs according to the time-to-live. - - :param alarm_history_ttl: Number of seconds to keep alarm history - records for. - """ - LOG.debug("Clearing expired alarm history data is based on native " - "MongoDB time to live feature and going in background.") diff --git a/ceilometer/alarm/storage/impl_sqlalchemy.py b/ceilometer/alarm/storage/impl_sqlalchemy.py deleted file mode 100644 index 3efd74bb..00000000 --- a/ceilometer/alarm/storage/impl_sqlalchemy.py +++ /dev/null @@ -1,343 +0,0 @@ -# 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. - -"""SQLAlchemy storage backend.""" - -from __future__ import absolute_import -import datetime -import os - -from oslo_config import cfg -from oslo_db.sqlalchemy import session as db_session -from oslo_log import log -from oslo_utils import timeutils -from sqlalchemy import desc - -from ceilometer.alarm.storage import base -from ceilometer.alarm.storage import models as alarm_api_models -from ceilometer.i18n import _LI -from ceilometer.storage.sqlalchemy import models -from ceilometer.storage.sqlalchemy import utils as sql_utils -from ceilometer import utils - -LOG = log.getLogger(__name__) - -AVAILABLE_CAPABILITIES = { - 'alarms': {'query': {'simple': True, - 'complex': True}, - 'history': {'query': {'simple': True, - 'complex': True}}}, -} - - -AVAILABLE_STORAGE_CAPABILITIES = { - 'storage': {'production_ready': True}, -} - - -class Connection(base.Connection): - """Put the data into a SQLAlchemy database. - - Tables:: - - - meter - - meter definition - - { id: meter def id - name: meter name - type: meter type - unit: meter unit - } - - sample - - the raw incoming data - - { id: sample id - meter_id: meter id (->meter.id) - user_id: user uuid - project_id: project uuid - resource_id: resource uuid - source_id: source id - resource_metadata: metadata dictionaries - volume: sample volume - timestamp: datetime - message_signature: message signature - message_id: message uuid - } - """ - CAPABILITIES = utils.update_nested(base.Connection.CAPABILITIES, - AVAILABLE_CAPABILITIES) - STORAGE_CAPABILITIES = utils.update_nested( - base.Connection.STORAGE_CAPABILITIES, - AVAILABLE_STORAGE_CAPABILITIES, - ) - - def __init__(self, url): - # Set max_retries to 0, since oslo.db in certain cases may attempt - # to retry making the db connection retried max_retries ^ 2 times - # in failure case and db reconnection has already been implemented - # in storage.__init__.get_connection_from_config function - options = dict(cfg.CONF.database.items()) - options['max_retries'] = 0 - self._engine_facade = db_session.EngineFacade(url, **options) - - def upgrade(self): - # NOTE(gordc): to minimise memory, only import migration when needed - from oslo_db.sqlalchemy import migration - path = os.path.join(os.path.abspath(os.path.dirname(__file__)), - '..', '..', 'storage', 'sqlalchemy', - 'migrate_repo') - migration.db_sync(self._engine_facade.get_engine(), path) - - def clear(self): - engine = self._engine_facade.get_engine() - for table in reversed(models.Base.metadata.sorted_tables): - engine.execute(table.delete()) - engine.dispose() - - def _retrieve_data(self, filter_expr, orderby, limit, table): - if limit == 0: - return [] - - session = self._engine_facade.get_session() - engine = self._engine_facade.get_engine() - query = session.query(table) - transformer = sql_utils.QueryTransformer(table, query, - dialect=engine.dialect.name) - if filter_expr is not None: - transformer.apply_filter(filter_expr) - - transformer.apply_options(orderby, - limit) - - retrieve = {models.Alarm: self._retrieve_alarms, - models.AlarmChange: self._retrieve_alarm_history} - return retrieve[table](transformer.get_query()) - - @staticmethod - def _row_to_alarm_model(row): - return alarm_api_models.Alarm(alarm_id=row.alarm_id, - enabled=row.enabled, - type=row.type, - name=row.name, - description=row.description, - timestamp=row.timestamp, - user_id=row.user_id, - project_id=row.project_id, - state=row.state, - state_timestamp=row.state_timestamp, - ok_actions=row.ok_actions, - alarm_actions=row.alarm_actions, - insufficient_data_actions=( - row.insufficient_data_actions), - rule=row.rule, - time_constraints=row.time_constraints, - repeat_actions=row.repeat_actions, - severity=row.severity) - - def _retrieve_alarms(self, query): - return (self._row_to_alarm_model(x) for x in query.all()) - - def get_alarms(self, name=None, user=None, state=None, meter=None, - project=None, enabled=None, alarm_id=None, - alarm_type=None, severity=None): - """Yields a lists of alarms that match filters. - - :param name: Optional name for alarm. - :param user: Optional ID for user that owns the resource. - :param state: Optional string for alarm state. - :param meter: Optional string for alarms associated with meter. - :param project: Optional ID for project that owns the resource. - :param enabled: Optional boolean to list disable alarm. - :param alarm_id: Optional alarm_id to return one alarm. - :param alarm_type: Optional alarm type. - :param severity: Optional alarm severity - """ - - session = self._engine_facade.get_session() - query = session.query(models.Alarm) - if name is not None: - query = query.filter(models.Alarm.name == name) - if enabled is not None: - query = query.filter(models.Alarm.enabled == enabled) - if user is not None: - query = query.filter(models.Alarm.user_id == user) - if project is not None: - query = query.filter(models.Alarm.project_id == project) - if alarm_id is not None: - query = query.filter(models.Alarm.alarm_id == alarm_id) - if state is not None: - query = query.filter(models.Alarm.state == state) - if alarm_type is not None: - query = query.filter(models.Alarm.type == alarm_type) - if severity is not None: - query = query.filter(models.Alarm.severity == severity) - - query = query.order_by(desc(models.Alarm.timestamp)) - alarms = self._retrieve_alarms(query) - - # TODO(cmart): improve this by using sqlalchemy.func factory - if meter is not None: - alarms = filter(lambda row: - row.rule.get('meter_name', None) == meter, - alarms) - - return alarms - - def create_alarm(self, alarm): - """Create an alarm. - - :param alarm: The alarm to create. - """ - session = self._engine_facade.get_session() - with session.begin(): - alarm_row = models.Alarm(alarm_id=alarm.alarm_id) - alarm_row.update(alarm.as_dict()) - session.add(alarm_row) - - return self._row_to_alarm_model(alarm_row) - - def update_alarm(self, alarm): - """Update an alarm. - - :param alarm: the new Alarm to update - """ - session = self._engine_facade.get_session() - with session.begin(): - alarm_row = session.merge(models.Alarm(alarm_id=alarm.alarm_id)) - alarm_row.update(alarm.as_dict()) - - return self._row_to_alarm_model(alarm_row) - - def delete_alarm(self, alarm_id): - """Delete an alarm and its history data. - - :param alarm_id: ID of the alarm to delete - """ - session = self._engine_facade.get_session() - with session.begin(): - session.query(models.Alarm).filter( - models.Alarm.alarm_id == alarm_id).delete() - # FIXME(liusheng): we should use delete cascade - session.query(models.AlarmChange).filter( - models.AlarmChange.alarm_id == alarm_id).delete() - - @staticmethod - def _row_to_alarm_change_model(row): - return alarm_api_models.AlarmChange(event_id=row.event_id, - alarm_id=row.alarm_id, - type=row.type, - detail=row.detail, - user_id=row.user_id, - project_id=row.project_id, - on_behalf_of=row.on_behalf_of, - timestamp=row.timestamp) - - def query_alarms(self, filter_expr=None, orderby=None, limit=None): - """Yields a lists of alarms that match filter.""" - return self._retrieve_data(filter_expr, orderby, limit, models.Alarm) - - def _retrieve_alarm_history(self, query): - return (self._row_to_alarm_change_model(x) for x in query.all()) - - def query_alarm_history(self, filter_expr=None, orderby=None, limit=None): - """Return an iterable of model.AlarmChange objects.""" - return self._retrieve_data(filter_expr, - orderby, - limit, - models.AlarmChange) - - def get_alarm_changes(self, alarm_id, on_behalf_of, - user=None, project=None, alarm_type=None, - severity=None, start_timestamp=None, - start_timestamp_op=None, end_timestamp=None, - end_timestamp_op=None): - """Yields list of AlarmChanges describing alarm history - - Changes are always sorted in reverse order of occurrence, given - the importance of currency. - - Segregation for non-administrative users is done on the basis - of the on_behalf_of parameter. This allows such users to have - visibility on both the changes initiated by themselves directly - (generally creation, rule changes, or deletion) and also on those - changes initiated on their behalf by the alarming service (state - transitions after alarm thresholds are crossed). - - :param alarm_id: ID of alarm to return changes for - :param on_behalf_of: ID of tenant to scope changes query (None for - administrative user, indicating all projects) - :param user: Optional ID of user to return changes for - :param project: Optional ID of project to return changes for - :param alarm_type: Optional change type - :param severity: Optional alarm severity - :param start_timestamp: Optional modified timestamp start range - :param start_timestamp_op: Optional timestamp start range operation - :param end_timestamp: Optional modified timestamp end range - :param end_timestamp_op: Optional timestamp end range operation - """ - session = self._engine_facade.get_session() - query = session.query(models.AlarmChange) - query = query.filter(models.AlarmChange.alarm_id == alarm_id) - - if on_behalf_of is not None: - query = query.filter( - models.AlarmChange.on_behalf_of == on_behalf_of) - if user is not None: - query = query.filter(models.AlarmChange.user_id == user) - if project is not None: - query = query.filter(models.AlarmChange.project_id == project) - if alarm_type is not None: - query = query.filter(models.AlarmChange.type == alarm_type) - if severity is not None: - query = query.filter(models.AlarmChange.severity == severity) - if start_timestamp: - if start_timestamp_op == 'gt': - query = query.filter( - models.AlarmChange.timestamp > start_timestamp) - else: - query = query.filter( - models.AlarmChange.timestamp >= start_timestamp) - if end_timestamp: - if end_timestamp_op == 'le': - query = query.filter( - models.AlarmChange.timestamp <= end_timestamp) - else: - query = query.filter( - models.AlarmChange.timestamp < end_timestamp) - - query = query.order_by(desc(models.AlarmChange.timestamp)) - return self._retrieve_alarm_history(query) - - def record_alarm_change(self, alarm_change): - """Record alarm change event.""" - session = self._engine_facade.get_session() - with session.begin(): - alarm_change_row = models.AlarmChange( - event_id=alarm_change['event_id']) - alarm_change_row.update(alarm_change) - session.add(alarm_change_row) - - def clear_expired_alarm_history_data(self, alarm_history_ttl): - """Clear expired alarm history data from the backend storage system. - - Clearing occurs according to the time-to-live. - - :param alarm_history_ttl: Number of seconds to keep alarm history - records for. - """ - session = self._engine_facade.get_session() - with session.begin(): - valid_start = (timeutils.utcnow() - - datetime.timedelta(seconds=alarm_history_ttl)) - deleted_rows = (session.query(models.AlarmChange) - .filter(models.AlarmChange.timestamp < valid_start) - .delete()) - LOG.info(_LI("%d alarm histories are removed from database"), - deleted_rows) diff --git a/ceilometer/alarm/storage/models.py b/ceilometer/alarm/storage/models.py deleted file mode 100644 index 97be8798..00000000 --- a/ceilometer/alarm/storage/models.py +++ /dev/null @@ -1,134 +0,0 @@ -# -# Copyright 2013 New Dream Network, LLC (DreamHost) -# -# 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. -"""Model classes for use in the storage API. -""" - -import datetime - -from ceilometer.i18n import _ -from ceilometer.storage import base - - -class Alarm(base.Model): - ALARM_INSUFFICIENT_DATA = 'insufficient data' - ALARM_OK = 'ok' - ALARM_ALARM = 'alarm' - - ALARM_ACTIONS_MAP = { - ALARM_INSUFFICIENT_DATA: 'insufficient_data_actions', - ALARM_OK: 'ok_actions', - ALARM_ALARM: 'alarm_actions', - } - - ALARM_LEVEL_LOW = 'low' - ALARM_LEVEL_MODERATE = 'moderate' - ALARM_LEVEL_CRITICAL = 'critical' - - """ - An alarm to monitor. - - :param alarm_id: UUID of the alarm - :param type: type of the alarm - :param name: The Alarm name - :param description: User friendly description of the alarm - :param enabled: Is the alarm enabled - :param state: Alarm state (ok/alarm/insufficient data) - :param rule: A rule that defines when the alarm fires - :param user_id: the owner/creator of the alarm - :param project_id: the project_id of the creator - :param evaluation_periods: the number of periods - :param period: the time period in seconds - :param time_constraints: the list of the alarm's time constraints, if any - :param timestamp: the timestamp when the alarm was last updated - :param state_timestamp: the timestamp of the last state change - :param ok_actions: the list of webhooks to call when entering the ok state - :param alarm_actions: the list of webhooks to call when entering the - alarm state - :param insufficient_data_actions: the list of webhooks to call when - entering the insufficient data state - :param repeat_actions: Is the actions should be triggered on each - alarm evaluation. - :param severity: Alarm level (low/moderate/critical) - """ - def __init__(self, alarm_id, type, enabled, name, description, - timestamp, user_id, project_id, state, state_timestamp, - ok_actions, alarm_actions, insufficient_data_actions, - repeat_actions, rule, time_constraints, severity=None): - if not isinstance(timestamp, datetime.datetime): - raise TypeError(_("timestamp should be datetime object")) - if not isinstance(state_timestamp, datetime.datetime): - raise TypeError(_("state_timestamp should be datetime object")) - base.Model.__init__( - self, - alarm_id=alarm_id, - type=type, - enabled=enabled, - name=name, - description=description, - timestamp=timestamp, - user_id=user_id, - project_id=project_id, - state=state, - state_timestamp=state_timestamp, - ok_actions=ok_actions, - alarm_actions=alarm_actions, - insufficient_data_actions=insufficient_data_actions, - repeat_actions=repeat_actions, - rule=rule, - time_constraints=time_constraints, - severity=severity) - - -class AlarmChange(base.Model): - """Record of an alarm change. - - :param event_id: UUID of the change event - :param alarm_id: UUID of the alarm - :param type: The type of change - :param severity: The severity of alarm - :param detail: JSON fragment describing change - :param user_id: the user ID of the initiating identity - :param project_id: the project ID of the initiating identity - :param on_behalf_of: the tenant on behalf of which the change - is being made - :param timestamp: the timestamp of the change - """ - - CREATION = 'creation' - RULE_CHANGE = 'rule change' - STATE_TRANSITION = 'state transition' - - def __init__(self, - event_id, - alarm_id, - type, - detail, - user_id, - project_id, - on_behalf_of, - severity=None, - timestamp=None - ): - base.Model.__init__( - self, - event_id=event_id, - alarm_id=alarm_id, - type=type, - severity=severity, - detail=detail, - user_id=user_id, - project_id=project_id, - on_behalf_of=on_behalf_of, - timestamp=timestamp) diff --git a/ceilometer/alarm/storage/pymongo_base.py b/ceilometer/alarm/storage/pymongo_base.py deleted file mode 100644 index 7173720b..00000000 --- a/ceilometer/alarm/storage/pymongo_base.py +++ /dev/null @@ -1,307 +0,0 @@ -# -# Copyright Ericsson AB 2013. All rights reserved -# -# Authors: Ildiko Vancsa -# Balazs Gibizer -# -# 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. -"""Common functions for MongoDB and DB2 backends -""" - -from oslo_log import log -import pymongo - -from ceilometer.alarm.storage import base -from ceilometer.alarm.storage import models -from ceilometer.storage.mongo import utils as pymongo_utils -from ceilometer import utils - -LOG = log.getLogger(__name__) - - -COMMON_AVAILABLE_CAPABILITIES = { - 'alarms': {'query': {'simple': True, - 'complex': True}, - 'history': {'query': {'simple': True, - 'complex': True}}}, -} - - -AVAILABLE_STORAGE_CAPABILITIES = { - 'storage': {'production_ready': True}, -} - - -class Connection(base.Connection): - """Base Alarm Connection class for MongoDB and DB2 drivers.""" - CAPABILITIES = utils.update_nested(base.Connection.CAPABILITIES, - COMMON_AVAILABLE_CAPABILITIES) - - STORAGE_CAPABILITIES = utils.update_nested( - base.Connection.STORAGE_CAPABILITIES, - AVAILABLE_STORAGE_CAPABILITIES, - ) - - def upgrade(self): - # create collection if not present - if 'alarm' not in self.db.conn.collection_names(): - self.db.conn.create_collection('alarm') - if 'alarm_history' not in self.db.conn.collection_names(): - self.db.conn.create_collection('alarm_history') - - def update_alarm(self, alarm): - """Update alarm.""" - data = alarm.as_dict() - - self.db.alarm.update( - {'alarm_id': alarm.alarm_id}, - {'$set': data}, - upsert=True) - - stored_alarm = self.db.alarm.find({'alarm_id': alarm.alarm_id})[0] - del stored_alarm['_id'] - self._ensure_encapsulated_rule_format(stored_alarm) - self._ensure_time_constraints(stored_alarm) - return models.Alarm(**stored_alarm) - - create_alarm = update_alarm - - def delete_alarm(self, alarm_id): - """Delete an alarm and its history data.""" - self.db.alarm.remove({'alarm_id': alarm_id}) - self.db.alarm_history.remove({'alarm_id': alarm_id}) - - def record_alarm_change(self, alarm_change): - """Record alarm change event.""" - self.db.alarm_history.insert(alarm_change.copy()) - - def get_alarms(self, name=None, user=None, state=None, meter=None, - project=None, enabled=None, alarm_id=None, - alarm_type=None, severity=None): - """Yields a lists of alarms that match filters. - - :param name: Optional name for alarm. - :param user: Optional ID for user that owns the resource. - :param state: Optional string for alarm state. - :param meter: Optional string for alarms associated with meter. - :param project: Optional ID for project that owns the resource. - :param enabled: Optional boolean to list disable alarm. - :param alarm_id: Optional alarm_id to return one alarm. - :param alarm_type: Optional alarm type. - :param severity: Optional alarm severity. - """ - q = {} - if user is not None: - q['user_id'] = user - if project is not None: - q['project_id'] = project - if name is not None: - q['name'] = name - if enabled is not None: - q['enabled'] = enabled - if alarm_id is not None: - q['alarm_id'] = alarm_id - if state is not None: - q['state'] = state - if meter is not None: - q['rule.meter_name'] = meter - if alarm_type is not None: - q['type'] = alarm_type - if severity is not None: - q['severity'] = severity - - return self._retrieve_alarms(q, - [("timestamp", - pymongo.DESCENDING)], - None) - - def get_alarm_changes(self, alarm_id, on_behalf_of, - user=None, project=None, alarm_type=None, - severity=None, start_timestamp=None, - start_timestamp_op=None, end_timestamp=None, - end_timestamp_op=None): - """Yields list of AlarmChanges describing alarm history - - Changes are always sorted in reverse order of occurrence, given - the importance of currency. - - Segregation for non-administrative users is done on the basis - of the on_behalf_of parameter. This allows such users to have - visibility on both the changes initiated by themselves directly - (generally creation, rule changes, or deletion) and also on those - changes initiated on their behalf by the alarming service (state - transitions after alarm thresholds are crossed). - - :param alarm_id: ID of alarm to return changes for - :param on_behalf_of: ID of tenant to scope changes query (None for - administrative user, indicating all projects) - :param user: Optional ID of user to return changes for - :param project: Optional ID of project to return changes for - :param alarm_type: Optional change type - :param severity: Optional change severity - :param start_timestamp: Optional modified timestamp start range - :param start_timestamp_op: Optional timestamp start range operation - :param end_timestamp: Optional modified timestamp end range - :param end_timestamp_op: Optional timestamp end range operation - """ - q = dict(alarm_id=alarm_id) - if on_behalf_of is not None: - q['on_behalf_of'] = on_behalf_of - if user is not None: - q['user_id'] = user - if project is not None: - q['project_id'] = project - if alarm_type is not None: - q['type'] = alarm_type - if severity is not None: - q['severity'] = severity - if start_timestamp or end_timestamp: - ts_range = pymongo_utils.make_timestamp_range(start_timestamp, - end_timestamp, - start_timestamp_op, - end_timestamp_op) - if ts_range: - q['timestamp'] = ts_range - - return self._retrieve_alarm_changes(q, - [("timestamp", - pymongo.DESCENDING)], - None) - - def query_alarms(self, filter_expr=None, orderby=None, limit=None): - """Return an iterable of model.Alarm objects.""" - return self._retrieve_data(filter_expr, orderby, limit, - models.Alarm) - - def query_alarm_history(self, filter_expr=None, orderby=None, limit=None): - """Return an iterable of model.AlarmChange objects.""" - return self._retrieve_data(filter_expr, - orderby, - limit, - models.AlarmChange) - - def _retrieve_data(self, filter_expr, orderby, limit, model): - if limit == 0: - return [] - query_filter = {} - orderby_filter = [("timestamp", pymongo.DESCENDING)] - transformer = pymongo_utils.QueryTransformer() - if orderby is not None: - orderby_filter = transformer.transform_orderby(orderby) - if filter_expr is not None: - query_filter = transformer.transform_filter(filter_expr) - - retrieve = {models.Alarm: self._retrieve_alarms, - models.AlarmChange: self._retrieve_alarm_changes} - return retrieve[model](query_filter, orderby_filter, limit) - - def _retrieve_alarms(self, query_filter, orderby, limit): - if limit is not None: - alarms = self.db.alarm.find(query_filter, - limit=limit, - sort=orderby) - else: - alarms = self.db.alarm.find(query_filter, sort=orderby) - - for alarm in alarms: - a = {} - a.update(alarm) - del a['_id'] - self._ensure_encapsulated_rule_format(a) - self._ensure_time_constraints(a) - yield models.Alarm(**a) - - def _retrieve_alarm_changes(self, query_filter, orderby, limit): - if limit is not None: - alarms_history = self.db.alarm_history.find(query_filter, - limit=limit, - sort=orderby) - else: - alarms_history = self.db.alarm_history.find( - query_filter, sort=orderby) - - for alarm_history in alarms_history: - ah = {} - ah.update(alarm_history) - del ah['_id'] - yield models.AlarmChange(**ah) - - @classmethod - def _ensure_encapsulated_rule_format(cls, alarm): - """Ensure the alarm returned by the storage have the correct format. - - The previous format looks like: - { - 'alarm_id': '0ld-4l3rt', - 'enabled': True, - 'name': 'old-alert', - 'description': 'old-alert', - 'timestamp': None, - 'meter_name': 'cpu', - 'user_id': 'me', - 'project_id': 'and-da-boys', - 'comparison_operator': 'lt', - 'threshold': 36, - 'statistic': 'count', - 'evaluation_periods': 1, - 'period': 60, - 'state': "insufficient data", - 'state_timestamp': None, - 'ok_actions': [], - 'alarm_actions': ['http://nowhere/alarms'], - 'insufficient_data_actions': [], - 'repeat_actions': False, - 'matching_metadata': {'key': 'value'} - # or 'matching_metadata': [{'key': 'key', 'value': 'value'}] - } - """ - - if isinstance(alarm.get('rule'), dict): - return - - alarm['type'] = 'threshold' - alarm['rule'] = {} - alarm['matching_metadata'] = cls._decode_matching_metadata( - alarm['matching_metadata']) - for field in ['period', 'evaluation_periods', 'threshold', - 'statistic', 'comparison_operator', 'meter_name']: - if field in alarm: - alarm['rule'][field] = alarm[field] - del alarm[field] - - query = [] - for key in alarm['matching_metadata']: - query.append({'field': key, - 'op': 'eq', - 'value': alarm['matching_metadata'][key], - 'type': 'string'}) - del alarm['matching_metadata'] - alarm['rule']['query'] = query - - @staticmethod - def _decode_matching_metadata(matching_metadata): - if isinstance(matching_metadata, dict): - # note(sileht): keep compatibility with alarm - # with matching_metadata as a dict - return matching_metadata - else: - new_matching_metadata = {} - for elem in matching_metadata: - new_matching_metadata[elem['key']] = elem['value'] - return new_matching_metadata - - @staticmethod - def _ensure_time_constraints(alarm): - """Ensures the alarm has a time constraints field.""" - if 'time_constraints' not in alarm: - alarm['time_constraints'] = [] diff --git a/ceilometer/api/controllers/v2/alarm_rules/__init__.py b/ceilometer/api/controllers/v2/alarm_rules/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/ceilometer/api/controllers/v2/alarm_rules/combination.py b/ceilometer/api/controllers/v2/alarm_rules/combination.py deleted file mode 100644 index ffa5844f..00000000 --- a/ceilometer/api/controllers/v2/alarm_rules/combination.py +++ /dev/null @@ -1,76 +0,0 @@ -# -# 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 pecan -import wsme -from wsme import types as wtypes - -from ceilometer.api.controllers.v2 import base -from ceilometer.api.controllers.v2 import utils as v2_utils -from ceilometer.i18n import _ - - -class AlarmCombinationRule(base.AlarmRule): - """Alarm Combinarion Rule - - Describe when to trigger the alarm based on combining the state of - other alarms. - """ - - operator = base.AdvEnum('operator', str, 'or', 'and', default='and') - "How to combine the sub-alarms" - - alarm_ids = wsme.wsattr([wtypes.text], mandatory=True) - "List of alarm identifiers to combine" - - @property - def default_description(self): - joiner = ' %s ' % self.operator - return _('Combined state of alarms %s') % joiner.join(self.alarm_ids) - - def as_dict(self): - return self.as_dict_from_keys(['operator', 'alarm_ids']) - - @staticmethod - def validate(rule): - rule.alarm_ids = sorted(set(rule.alarm_ids), key=rule.alarm_ids.index) - if len(rule.alarm_ids) <= 1: - raise base.ClientSideError(_('Alarm combination rule should ' - 'contain at least two different ' - 'alarm ids.')) - return rule - - @staticmethod - def validate_alarm(alarm): - project = v2_utils.get_auth_project( - alarm.project_id if alarm.project_id != wtypes.Unset else None) - for id in alarm.combination_rule.alarm_ids: - alarms = list(pecan.request.alarm_storage_conn.get_alarms( - alarm_id=id, project=project)) - if not alarms: - raise base.AlarmNotFound(id, project) - - @staticmethod - def update_hook(alarm): - # should check if there is any circle in the dependency, but for - # efficiency reason, here only check alarm cannot depend on itself - if alarm.alarm_id in alarm.combination_rule.alarm_ids: - raise base.ClientSideError( - _('Cannot specify alarm %s itself in combination rule') % - alarm.alarm_id) - - @classmethod - def sample(cls): - return cls(operator='or', - alarm_ids=['739e99cb-c2ec-4718-b900-332502355f38', - '153462d0-a9b8-4b5b-8175-9e4b05e9b856']) diff --git a/ceilometer/api/controllers/v2/alarm_rules/gnocchi.py b/ceilometer/api/controllers/v2/alarm_rules/gnocchi.py deleted file mode 100644 index 423dea2e..00000000 --- a/ceilometer/api/controllers/v2/alarm_rules/gnocchi.py +++ /dev/null @@ -1,194 +0,0 @@ -# -# Copyright 2015 eNovance -# -# 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 oslo_config import cfg -from oslo_serialization import jsonutils -import requests -import wsme -from wsme import types as wtypes - -from ceilometer.api.controllers.v2 import base -from ceilometer.api.controllers.v2 import utils as v2_utils -from ceilometer import keystone_client - - -cfg.CONF.import_opt('gnocchi_url', 'ceilometer.alarm.evaluator.gnocchi', - group="alarms") - - -class GnocchiUnavailable(Exception): - code = 503 - - -class AlarmGnocchiThresholdRule(base.AlarmRule): - comparison_operator = base.AdvEnum('comparison_operator', str, - 'lt', 'le', 'eq', 'ne', 'ge', 'gt', - default='eq') - "The comparison against the alarm threshold" - - threshold = wsme.wsattr(float, mandatory=True) - "The threshold of the alarm" - - aggregation_method = wsme.wsattr(wtypes.text, mandatory=True) - "The aggregation_method to compare to the threshold" - - evaluation_periods = wsme.wsattr(wtypes.IntegerType(minimum=1), default=1) - "The number of historical periods to evaluate the threshold" - - granularity = wsme.wsattr(wtypes.IntegerType(minimum=1), default=60) - "The time range in seconds over which query" - - @classmethod - def validate_alarm(cls, alarm): - alarm_rule = getattr(alarm, "%s_rule" % alarm.type) - aggregation_method = alarm_rule.aggregation_method - if aggregation_method not in cls._get_aggregation_methods(): - raise base.ClientSideError( - 'aggregation_method should be in %s not %s' % ( - cls._get_aggregation_methods(), aggregation_method)) - - # NOTE(sileht): once cachetools is in the requirements - # enable it - # @cachetools.ttl_cache(maxsize=1, ttl=600) - @staticmethod - def _get_aggregation_methods(): - ks_client = keystone_client.get_client() - gnocchi_url = cfg.CONF.alarms.gnocchi_url - headers = {'Content-Type': "application/json", - 'X-Auth-Token': ks_client.auth_token} - try: - r = requests.get("%s/v1/capabilities" % gnocchi_url, - headers=headers) - except requests.ConnectionError as e: - raise GnocchiUnavailable(e) - if r.status_code // 200 != 1: - raise GnocchiUnavailable(r.text) - - return jsonutils.loads(r.text).get('aggregation_methods', []) - - -class MetricOfResourceRule(AlarmGnocchiThresholdRule): - metric = wsme.wsattr(wtypes.text, mandatory=True) - "The name of the metric" - - resource_id = wsme.wsattr(wtypes.text, mandatory=True) - "The id of a resource" - - resource_type = wsme.wsattr(wtypes.text, mandatory=True) - "The resource type" - - def as_dict(self): - rule = self.as_dict_from_keys(['granularity', 'comparison_operator', - 'threshold', 'aggregation_method', - 'evaluation_periods', - 'metric', - 'resource_id', - 'resource_type']) - return rule - - @classmethod - def validate_alarm(cls, alarm): - super(MetricOfResourceRule, - cls).validate_alarm(alarm) - - rule = alarm.gnocchi_resources_threshold_rule - ks_client = keystone_client.get_client() - gnocchi_url = cfg.CONF.alarms.gnocchi_url - headers = {'Content-Type': "application/json", - 'X-Auth-Token': ks_client.auth_token} - try: - r = requests.get("%s/v1/resource/%s/%s" % ( - gnocchi_url, rule.resource_type, - rule.resource_id), - headers=headers) - except requests.ConnectionError as e: - raise GnocchiUnavailable(e) - if r.status_code == 404: - raise base.EntityNotFound('gnocchi resource', - rule.resource_id) - elif r.status_code // 200 != 1: - raise base.ClientSideError(r.content, status_code=r.status_code) - - -class AggregationMetricByResourcesLookupRule(AlarmGnocchiThresholdRule): - metric = wsme.wsattr(wtypes.text, mandatory=True) - "The name of the metric" - - query = wsme.wsattr(wtypes.text, mandatory=True) - "The query to filter the metric" - - resource_type = wsme.wsattr(wtypes.text, mandatory=True) - "The resource type" - - def as_dict(self): - rule = self.as_dict_from_keys(['granularity', 'comparison_operator', - 'threshold', 'aggregation_method', - 'evaluation_periods', - 'metric', - 'query', - 'resource_type']) - return rule - - @classmethod - def validate_alarm(cls, alarm): - super(AggregationMetricByResourcesLookupRule, - cls).validate_alarm(alarm) - - rule = alarm.gnocchi_aggregation_by_resources_threshold_rule - - # check the query string is a valid json - try: - query = jsonutils.loads(rule.query) - except ValueError: - raise wsme.exc.InvalidInput('rule/query', rule.query) - - # Scope the alarm to the project id if needed - auth_project = v2_utils.get_auth_project(alarm.project_id) - if auth_project: - rule.query = jsonutils.dumps({ - "and": [{"=": {"created_by_project_id": auth_project}}, - query]}) - - # Delegate the query validation to gnocchi - ks_client = keystone_client.get_client() - request = { - 'url': "%s/v1/aggregation/resource/%s/metric/%s" % ( - cfg.CONF.alarms.gnocchi_url, - rule.resource_type, - rule.metric), - 'headers': {'Content-Type': "application/json", - 'X-Auth-Token': ks_client.auth_token}, - 'params': {'aggregation': rule.aggregation_method}, - 'data': rule.query, - } - - try: - r = requests.post(**request) - except requests.ConnectionError as e: - raise GnocchiUnavailable(e) - if r.status_code // 200 != 1: - raise base.ClientSideError(r.content, status_code=r.status_code) - - -class AggregationMetricsByIdLookupRule(AlarmGnocchiThresholdRule): - metrics = wsme.wsattr([wtypes.text], mandatory=True) - "A list of metric Ids" - - def as_dict(self): - rule = self.as_dict_from_keys(['granularity', 'comparison_operator', - 'threshold', 'aggregation_method', - 'evaluation_periods', - 'metrics']) - return rule diff --git a/ceilometer/api/controllers/v2/alarm_rules/threshold.py b/ceilometer/api/controllers/v2/alarm_rules/threshold.py deleted file mode 100644 index 0445c73d..00000000 --- a/ceilometer/api/controllers/v2/alarm_rules/threshold.py +++ /dev/null @@ -1,120 +0,0 @@ -# -# 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 wsme -from wsme import types as wtypes - -from ceilometer.api.controllers.v2 import base -from ceilometer.api.controllers.v2 import utils as v2_utils -from ceilometer.i18n import _ -from ceilometer import storage - - -class AlarmThresholdRule(base.AlarmRule): - """Alarm Threshold Rule - - Describe when to trigger the alarm based on computed statistics - """ - - meter_name = wsme.wsattr(wtypes.text, mandatory=True) - "The name of the meter" - - # FIXME(sileht): default doesn't work - # workaround: default is set in validate method - query = wsme.wsattr([base.Query], default=[]) - """The query to find the data for computing statistics. - Ownership settings are automatically included based on the Alarm owner. - """ - - period = wsme.wsattr(wtypes.IntegerType(minimum=1), default=60) - "The time range in seconds over which query" - - comparison_operator = base.AdvEnum('comparison_operator', str, - 'lt', 'le', 'eq', 'ne', 'ge', 'gt', - default='eq') - "The comparison against the alarm threshold" - - threshold = wsme.wsattr(float, mandatory=True) - "The threshold of the alarm" - - statistic = base.AdvEnum('statistic', str, 'max', 'min', 'avg', 'sum', - 'count', default='avg') - "The statistic to compare to the threshold" - - evaluation_periods = wsme.wsattr(wtypes.IntegerType(minimum=1), default=1) - "The number of historical periods to evaluate the threshold" - - exclude_outliers = wsme.wsattr(bool, default=False) - "Whether datapoints with anomalously low sample counts are excluded" - - def __init__(self, query=None, **kwargs): - if query: - query = [base.Query(**q) for q in query] - super(AlarmThresholdRule, self).__init__(query=query, **kwargs) - - @staticmethod - def validate(threshold_rule): - # note(sileht): wsme default doesn't work in some case - # workaround for https://bugs.launchpad.net/wsme/+bug/1227039 - if not threshold_rule.query: - threshold_rule.query = [] - - # Timestamp is not allowed for AlarmThresholdRule query, as the alarm - # evaluator will construct timestamp bounds for the sequence of - # statistics queries as the sliding evaluation window advances - # over time. - v2_utils.validate_query(threshold_rule.query, - storage.SampleFilter.__init__, - allow_timestamps=False) - return threshold_rule - - @staticmethod - def validate_alarm(alarm): - # ensure an implicit constraint on project_id is added to - # the query if not already present - alarm.threshold_rule.query = v2_utils.sanitize_query( - alarm.threshold_rule.query, - storage.SampleFilter.__init__, - on_behalf_of=alarm.project_id - ) - - @property - def default_description(self): - return (_('Alarm when %(meter_name)s is %(comparison_operator)s a ' - '%(statistic)s of %(threshold)s over %(period)s seconds') % - dict(comparison_operator=self.comparison_operator, - statistic=self.statistic, - threshold=self.threshold, - meter_name=self.meter_name, - period=self.period)) - - def as_dict(self): - rule = self.as_dict_from_keys(['period', 'comparison_operator', - 'threshold', 'statistic', - 'evaluation_periods', 'meter_name', - 'exclude_outliers']) - rule['query'] = [q.as_dict() for q in self.query] - return rule - - @classmethod - def sample(cls): - return cls(meter_name='cpu_util', - period=60, - evaluation_periods=1, - threshold=300.0, - statistic='avg', - comparison_operator='gt', - query=[{'field': 'resource_id', - 'value': '2a4d689b-f0b8-49c1-9eef-87cae58d80db', - 'op': 'eq', - 'type': 'string'}]) diff --git a/ceilometer/api/controllers/v2/alarms.py b/ceilometer/api/controllers/v2/alarms.py deleted file mode 100644 index 5963ff24..00000000 --- a/ceilometer/api/controllers/v2/alarms.py +++ /dev/null @@ -1,793 +0,0 @@ -# -# Copyright 2012 New Dream Network, LLC (DreamHost) -# Copyright 2013 IBM Corp. -# Copyright 2013 eNovance -# Copyright Ericsson AB 2013. All rights reserved -# Copyright 2014 Hewlett-Packard Company -# Copyright 2015 Huawei Technologies Co., Ltd. -# -# 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 datetime -import itertools -import json -import uuid - -import croniter -from oslo_config import cfg -from oslo_context import context -from oslo_log import log -from oslo_utils import netutils -from oslo_utils import timeutils -import pecan -from pecan import rest -import pytz -import six -from six.moves.urllib import parse as urlparse -from stevedore import extension -import wsme -from wsme import types as wtypes -import wsmeext.pecan as wsme_pecan - -import ceilometer -from ceilometer import alarm as ceilometer_alarm -from ceilometer.alarm.storage import models as alarm_models -from ceilometer.api.controllers.v2.alarm_rules import combination -from ceilometer.api.controllers.v2 import base -from ceilometer.api.controllers.v2 import utils as v2_utils -from ceilometer.api import rbac -from ceilometer.i18n import _, _LI -from ceilometer import keystone_client -from ceilometer import messaging -from ceilometer import utils - -LOG = log.getLogger(__name__) - - -ALARM_API_OPTS = [ - cfg.BoolOpt('record_history', - default=True, - deprecated_for_removal=True, - help='Record alarm change events.' - ), - cfg.IntOpt('user_alarm_quota', - default=None, - deprecated_for_removal=True, - help='Maximum number of alarms defined for a user.' - ), - cfg.IntOpt('project_alarm_quota', - default=None, - deprecated_for_removal=True, - help='Maximum number of alarms defined for a project.' - ), - cfg.IntOpt('alarm_max_actions', - default=-1, - deprecated_for_removal=True, - help='Maximum count of actions for each state of an alarm, ' - 'non-positive number means no limit.'), -] - -cfg.CONF.register_opts(ALARM_API_OPTS, group='alarm') - -state_kind = ["ok", "alarm", "insufficient data"] -state_kind_enum = wtypes.Enum(str, *state_kind) -severity_kind = ["low", "moderate", "critical"] -severity_kind_enum = wtypes.Enum(str, *severity_kind) - - -class OverQuota(base.ClientSideError): - def __init__(self, data): - d = { - 'u': data.user_id, - 'p': data.project_id - } - super(OverQuota, self).__init__( - _("Alarm quota exceeded for user %(u)s on project %(p)s") % d, - status_code=403) - - -def is_over_quota(conn, project_id, user_id): - """Returns False if an alarm is within the set quotas, True otherwise. - - :param conn: a backend connection object - :param project_id: the ID of the project setting the alarm - :param user_id: the ID of the user setting the alarm - """ - - over_quota = False - - # Start by checking for user quota - user_alarm_quota = cfg.CONF.alarm.user_alarm_quota - if user_alarm_quota is not None: - user_alarms = list(conn.get_alarms(user=user_id)) - over_quota = len(user_alarms) >= user_alarm_quota - - # If the user quota isn't reached, we check for the project quota - if not over_quota: - project_alarm_quota = cfg.CONF.alarm.project_alarm_quota - if project_alarm_quota is not None: - project_alarms = list(conn.get_alarms(project=project_id)) - over_quota = len(project_alarms) >= project_alarm_quota - - return over_quota - - -class CronType(wtypes.UserType): - """A user type that represents a cron format.""" - basetype = six.string_types - name = 'cron' - - @staticmethod - def validate(value): - # raises ValueError if invalid - croniter.croniter(value) - return value - - -class AlarmTimeConstraint(base.Base): - """Representation of a time constraint on an alarm.""" - - name = wsme.wsattr(wtypes.text, mandatory=True) - "The name of the constraint" - - _description = None # provide a default - - def get_description(self): - if not self._description: - return ('Time constraint at %s lasting for %s seconds' - % (self.start, self.duration)) - return self._description - - def set_description(self, value): - self._description = value - - description = wsme.wsproperty(wtypes.text, get_description, - set_description) - "The description of the constraint" - - start = wsme.wsattr(CronType(), mandatory=True) - "Start point of the time constraint, in cron format" - - duration = wsme.wsattr(wtypes.IntegerType(minimum=0), mandatory=True) - "How long the constraint should last, in seconds" - - timezone = wsme.wsattr(wtypes.text, default="") - "Timezone of the constraint" - - def as_dict(self): - return self.as_dict_from_keys(['name', 'description', 'start', - 'duration', 'timezone']) - - @staticmethod - def validate(tc): - if tc.timezone: - try: - pytz.timezone(tc.timezone) - except Exception: - raise base.ClientSideError(_("Timezone %s is not valid") - % tc.timezone) - return tc - - @classmethod - def sample(cls): - return cls(name='SampleConstraint', - description='nightly build every night at 23h for 3 hours', - start='0 23 * * *', - duration=10800, - timezone='Europe/Ljubljana') - - -ALARMS_RULES = extension.ExtensionManager("ceilometer.alarm.rule") -LOG.debug("alarm rules plugin loaded: %s" % ",".join(ALARMS_RULES.names())) - - -class Alarm(base.Base): - """Representation of an alarm. - - .. note:: - combination_rule and threshold_rule are mutually exclusive. The *type* - of the alarm should be set to *threshold* or *combination* and the - appropriate rule should be filled. - """ - - alarm_id = wtypes.text - "The UUID of the alarm" - - name = wsme.wsattr(wtypes.text, mandatory=True) - "The name for the alarm" - - _description = None # provide a default - - def get_description(self): - rule = getattr(self, '%s_rule' % self.type, None) - if not self._description: - if hasattr(rule, 'default_description'): - return six.text_type(rule.default_description) - return "%s alarm rule" % self.type - return self._description - - def set_description(self, value): - self._description = value - - description = wsme.wsproperty(wtypes.text, get_description, - set_description) - "The description of the alarm" - - enabled = wsme.wsattr(bool, default=True) - "This alarm is enabled?" - - ok_actions = wsme.wsattr([wtypes.text], default=[]) - "The actions to do when alarm state change to ok" - - alarm_actions = wsme.wsattr([wtypes.text], default=[]) - "The actions to do when alarm state change to alarm" - - insufficient_data_actions = wsme.wsattr([wtypes.text], default=[]) - "The actions to do when alarm state change to insufficient data" - - repeat_actions = wsme.wsattr(bool, default=False) - "The actions should be re-triggered on each evaluation cycle" - - type = base.AdvEnum('type', str, *ALARMS_RULES.names(), - mandatory=True) - "Explicit type specifier to select which rule to follow below." - - time_constraints = wtypes.wsattr([AlarmTimeConstraint], default=[]) - """Describe time constraints for the alarm""" - - # These settings are ignored in the PUT or POST operations, but are - # filled in for GET - project_id = wtypes.text - "The ID of the project or tenant that owns the alarm" - - user_id = wtypes.text - "The ID of the user who created the alarm" - - timestamp = datetime.datetime - "The date of the last alarm definition update" - - state = base.AdvEnum('state', str, *state_kind, - default='insufficient data') - "The state offset the alarm" - - state_timestamp = datetime.datetime - "The date of the last alarm state changed" - - severity = base.AdvEnum('severity', str, *severity_kind, - default='low') - "The severity of the alarm" - - def __init__(self, rule=None, time_constraints=None, **kwargs): - super(Alarm, self).__init__(**kwargs) - - if rule: - setattr(self, '%s_rule' % self.type, - ALARMS_RULES[self.type].plugin(**rule)) - - if time_constraints: - self.time_constraints = [AlarmTimeConstraint(**tc) - for tc in time_constraints] - - @staticmethod - def validate(alarm): - - Alarm.check_rule(alarm) - Alarm.check_alarm_actions(alarm) - - ALARMS_RULES[alarm.type].plugin.validate_alarm(alarm) - - if alarm.time_constraints: - tc_names = [tc.name for tc in alarm.time_constraints] - if len(tc_names) > len(set(tc_names)): - error = _("Time constraint names must be " - "unique for a given alarm.") - raise base.ClientSideError(error) - - return alarm - - @staticmethod - def check_rule(alarm): - rule = '%s_rule' % alarm.type - if getattr(alarm, rule) in (wtypes.Unset, None): - error = _("%(rule)s must be set for %(type)s" - " type alarm") % {"rule": rule, "type": alarm.type} - raise base.ClientSideError(error) - - rule_set = None - for ext in ALARMS_RULES: - name = "%s_rule" % ext.name - if getattr(alarm, name): - if rule_set is None: - rule_set = name - else: - error = _("%(rule1)s and %(rule2)s cannot be set at the " - "same time") % {'rule1': rule_set, 'rule2': name} - raise base.ClientSideError(error) - - @staticmethod - def check_alarm_actions(alarm): - actions_schema = ceilometer_alarm.NOTIFIER_SCHEMAS - max_actions = cfg.CONF.alarm.alarm_max_actions - for state in state_kind: - actions_name = state.replace(" ", "_") + '_actions' - actions = getattr(alarm, actions_name) - if not actions: - continue - - action_set = set(actions) - if len(actions) != len(action_set): - LOG.info(_LI('duplicate actions are found: %s, ' - 'remove duplicate ones') % actions) - actions = list(action_set) - setattr(alarm, actions_name, actions) - - if 0 < max_actions < len(actions): - error = _('%(name)s count exceeds maximum value ' - '%(maximum)d') % {"name": actions_name, - "maximum": max_actions} - raise base.ClientSideError(error) - - limited = rbac.get_limited_to_project(pecan.request.headers) - - for action in actions: - try: - url = netutils.urlsplit(action) - except Exception: - error = _("Unable to parse action %s") % action - raise base.ClientSideError(error) - if url.scheme not in actions_schema: - error = _("Unsupported action %s") % action - raise base.ClientSideError(error) - if limited and url.scheme in ('log', 'test'): - error = _('You are not authorized to create ' - 'action: %s') % action - raise base.ClientSideError(error, status_code=401) - - @classmethod - def sample(cls): - return cls(alarm_id=None, - name="SwiftObjectAlarm", - description="An alarm", - type='combination', - time_constraints=[AlarmTimeConstraint.sample().as_dict()], - user_id="c96c887c216949acbdfbd8b494863567", - project_id="c96c887c216949acbdfbd8b494863567", - enabled=True, - timestamp=datetime.datetime.utcnow(), - state="ok", - severity="moderate", - state_timestamp=datetime.datetime.utcnow(), - ok_actions=["http://site:8000/ok"], - alarm_actions=["http://site:8000/alarm"], - insufficient_data_actions=["http://site:8000/nodata"], - repeat_actions=False, - combination_rule=combination.AlarmCombinationRule.sample(), - ) - - def as_dict(self, db_model): - d = super(Alarm, self).as_dict(db_model) - for k in d: - if k.endswith('_rule'): - del d[k] - d['rule'] = getattr(self, "%s_rule" % self.type).as_dict() - if self.time_constraints: - d['time_constraints'] = [tc.as_dict() - for tc in self.time_constraints] - return d - - @staticmethod - def _is_trust_url(url): - return url.scheme in ('trust+http', 'trust+https') - - def update_actions(self, old_alarm=None): - trustor_user_id = pecan.request.headers.get('X-User-Id') - trustor_project_id = pecan.request.headers.get('X-Project-Id') - roles = pecan.request.headers.get('X-Roles', '') - if roles: - roles = roles.split(',') - else: - roles = [] - auth_plugin = pecan.request.environ.get('keystone.token_auth') - for actions in (self.ok_actions, self.alarm_actions, - self.insufficient_data_actions): - if actions is not None: - for index, action in enumerate(actions[:]): - url = netutils.urlsplit(action) - if self._is_trust_url(url): - if '@' not in url.netloc: - # We have a trust action without a trust ID, - # create it - trust_id = keystone_client.create_trust_id( - trustor_user_id, trustor_project_id, roles, - auth_plugin) - netloc = '%s:delete@%s' % (trust_id, url.netloc) - url = list(url) - url[1] = netloc - actions[index] = urlparse.urlunsplit(url) - if old_alarm: - new_actions = list(itertools.chain( - self.ok_actions or [], - self.alarm_actions or [], - self.insufficient_data_actions or [])) - for action in itertools.chain( - old_alarm.ok_actions or [], - old_alarm.alarm_actions or [], - old_alarm.insufficient_data_actions or []): - if action not in new_actions: - self.delete_trust(action) - - def delete_actions(self): - for action in itertools.chain(self.ok_actions or [], - self.alarm_actions or [], - self.insufficient_data_actions or []): - self.delete_trust(action) - - def delete_trust(self, action): - auth_plugin = pecan.request.environ.get('keystone.token_auth') - url = netutils.urlsplit(action) - if self._is_trust_url(url) and url.password: - keystone_client.delete_trust_id(url.username, auth_plugin) - - -Alarm.add_attributes(**{"%s_rule" % ext.name: ext.plugin - for ext in ALARMS_RULES}) - - -class AlarmChange(base.Base): - """Representation of an event in an alarm's history.""" - - event_id = wtypes.text - "The UUID of the change event" - - alarm_id = wtypes.text - "The UUID of the alarm" - - type = wtypes.Enum(str, - 'creation', - 'rule change', - 'state transition', - 'deletion') - "The type of change" - - detail = wtypes.text - "JSON fragment describing change" - - project_id = wtypes.text - "The project ID of the initiating identity" - - user_id = wtypes.text - "The user ID of the initiating identity" - - on_behalf_of = wtypes.text - "The tenant on behalf of which the change is being made" - - timestamp = datetime.datetime - "The time/date of the alarm change" - - @classmethod - def sample(cls): - return cls(alarm_id='e8ff32f772a44a478182c3fe1f7cad6a', - type='rule change', - detail='{"threshold": 42.0, "evaluation_periods": 4}', - user_id="3e5d11fda79448ac99ccefb20be187ca", - project_id="b6f16144010811e387e4de429e99ee8c", - on_behalf_of="92159030020611e3b26dde429e99ee8c", - timestamp=datetime.datetime.utcnow(), - ) - - -def _send_notification(event, payload): - notification = event.replace(" ", "_") - notification = "alarm.%s" % notification - transport = messaging.get_transport() - notifier = messaging.get_notifier(transport, publisher_id="ceilometer.api") - # FIXME(sileht): perhaps we need to copy some infos from the - # pecan request headers like nova does - notifier.info(context.RequestContext(), notification, payload) - - -class AlarmController(rest.RestController): - """Manages operations on a single alarm.""" - - _custom_actions = { - 'history': ['GET'], - 'state': ['PUT', 'GET'], - } - - def __init__(self, alarm_id): - pecan.request.context['alarm_id'] = alarm_id - self._id = alarm_id - - def _alarm(self): - self.conn = pecan.request.alarm_storage_conn - auth_project = rbac.get_limited_to_project(pecan.request.headers) - alarms = list(self.conn.get_alarms(alarm_id=self._id, - project=auth_project)) - if not alarms: - raise base.AlarmNotFound(alarm=self._id, auth_project=auth_project) - return alarms[0] - - def _record_change(self, data, now, on_behalf_of=None, type=None): - if not cfg.CONF.alarm.record_history: - return - if not data: - return - type = type or alarm_models.AlarmChange.RULE_CHANGE - scrubbed_data = utils.stringify_timestamps(data) - detail = json.dumps(scrubbed_data) - user_id = pecan.request.headers.get('X-User-Id') - project_id = pecan.request.headers.get('X-Project-Id') - on_behalf_of = on_behalf_of or project_id - payload = dict(event_id=str(uuid.uuid4()), - alarm_id=self._id, - type=type, - detail=detail, - user_id=user_id, - project_id=project_id, - on_behalf_of=on_behalf_of, - timestamp=now) - - try: - self.conn.record_alarm_change(payload) - except ceilometer.NotImplementedError: - pass - - # Revert to the pre-json'ed details ... - payload['detail'] = scrubbed_data - _send_notification(type, payload) - - @wsme_pecan.wsexpose(Alarm) - def get(self): - """Return this alarm.""" - - rbac.enforce('get_alarm', pecan.request) - - return Alarm.from_db_model(self._alarm()) - - @wsme_pecan.wsexpose(Alarm, body=Alarm) - def put(self, data): - """Modify this alarm. - - :param data: an alarm within the request body. - """ - - rbac.enforce('change_alarm', pecan.request) - - # Ensure alarm exists - alarm_in = self._alarm() - - now = timeutils.utcnow() - - data.alarm_id = self._id - - user, project = rbac.get_limited_to(pecan.request.headers) - if user: - data.user_id = user - elif data.user_id == wtypes.Unset: - data.user_id = alarm_in.user_id - if project: - data.project_id = project - elif data.project_id == wtypes.Unset: - data.project_id = alarm_in.project_id - data.timestamp = now - if alarm_in.state != data.state: - data.state_timestamp = now - else: - data.state_timestamp = alarm_in.state_timestamp - - # make sure alarms are unique by name per project. - if alarm_in.name != data.name: - alarms = list(self.conn.get_alarms(name=data.name, - project=data.project_id)) - if alarms: - raise base.ClientSideError( - _("Alarm with name=%s exists") % data.name, - status_code=409) - - ALARMS_RULES[data.type].plugin.update_hook(data) - - old_data = Alarm.from_db_model(alarm_in) - old_alarm = old_data.as_dict(alarm_models.Alarm) - data.update_actions(old_data) - updated_alarm = data.as_dict(alarm_models.Alarm) - try: - alarm_in = alarm_models.Alarm(**updated_alarm) - except Exception: - LOG.exception(_("Error while putting alarm: %s") % updated_alarm) - raise base.ClientSideError(_("Alarm incorrect")) - - alarm = self.conn.update_alarm(alarm_in) - - change = dict((k, v) for k, v in updated_alarm.items() - if v != old_alarm[k] and k not in - ['timestamp', 'state_timestamp']) - self._record_change(change, now, on_behalf_of=alarm.project_id) - return Alarm.from_db_model(alarm) - - @wsme_pecan.wsexpose(None, status_code=204) - def delete(self): - """Delete this alarm.""" - - rbac.enforce('delete_alarm', pecan.request) - - # ensure alarm exists before deleting - alarm = self._alarm() - self.conn.delete_alarm(alarm.alarm_id) - alarm_object = Alarm.from_db_model(alarm) - alarm_object.delete_actions() - - @wsme_pecan.wsexpose([AlarmChange], [base.Query]) - def history(self, q=None): - """Assembles the alarm history requested. - - :param q: Filter rules for the changes to be described. - """ - - rbac.enforce('alarm_history', pecan.request) - - q = q or [] - # allow history to be returned for deleted alarms, but scope changes - # returned to those carried out on behalf of the auth'd tenant, to - # avoid inappropriate cross-tenant visibility of alarm history - auth_project = rbac.get_limited_to_project(pecan.request.headers) - conn = pecan.request.alarm_storage_conn - kwargs = v2_utils.query_to_kwargs( - q, conn.get_alarm_changes, ['on_behalf_of', 'alarm_id']) - return [AlarmChange.from_db_model(ac) - for ac in conn.get_alarm_changes(self._id, auth_project, - **kwargs)] - - @wsme.validate(state_kind_enum) - @wsme_pecan.wsexpose(state_kind_enum, body=state_kind_enum) - def put_state(self, state): - """Set the state of this alarm. - - :param state: an alarm state within the request body. - """ - - rbac.enforce('change_alarm_state', pecan.request) - - # note(sileht): body are not validated by wsme - # Workaround for https://bugs.launchpad.net/wsme/+bug/1227229 - if state not in state_kind: - raise base.ClientSideError(_("state invalid")) - now = timeutils.utcnow() - alarm = self._alarm() - alarm.state = state - alarm.state_timestamp = now - alarm = self.conn.update_alarm(alarm) - change = {'state': alarm.state} - self._record_change(change, now, on_behalf_of=alarm.project_id, - type=alarm_models.AlarmChange.STATE_TRANSITION) - return alarm.state - - @wsme_pecan.wsexpose(state_kind_enum) - def get_state(self): - """Get the state of this alarm.""" - - rbac.enforce('get_alarm_state', pecan.request) - - alarm = self._alarm() - return alarm.state - - -class AlarmsController(rest.RestController): - """Manages operations on the alarms collection.""" - - @pecan.expose() - def _lookup(self, alarm_id, *remainder): - return AlarmController(alarm_id), remainder - - @staticmethod - def _record_creation(conn, data, alarm_id, now): - if not cfg.CONF.alarm.record_history: - return - type = alarm_models.AlarmChange.CREATION - scrubbed_data = utils.stringify_timestamps(data) - detail = json.dumps(scrubbed_data) - user_id = pecan.request.headers.get('X-User-Id') - project_id = pecan.request.headers.get('X-Project-Id') - payload = dict(event_id=str(uuid.uuid4()), - alarm_id=alarm_id, - type=type, - detail=detail, - user_id=user_id, - project_id=project_id, - on_behalf_of=project_id, - timestamp=now) - - try: - conn.record_alarm_change(payload) - except ceilometer.NotImplementedError: - pass - - # Revert to the pre-json'ed details ... - payload['detail'] = scrubbed_data - _send_notification(type, payload) - - @wsme_pecan.wsexpose(Alarm, body=Alarm, status_code=201) - def post(self, data): - """Create a new alarm. - - :param data: an alarm within the request body. - """ - rbac.enforce('create_alarm', pecan.request) - - conn = pecan.request.alarm_storage_conn - now = timeutils.utcnow() - - data.alarm_id = str(uuid.uuid4()) - user_limit, project_limit = rbac.get_limited_to(pecan.request.headers) - - def _set_ownership(aspect, owner_limitation, header): - attr = '%s_id' % aspect - requested_owner = getattr(data, attr) - explicit_owner = requested_owner != wtypes.Unset - caller = pecan.request.headers.get(header) - if (owner_limitation and explicit_owner - and requested_owner != caller): - raise base.ProjectNotAuthorized(requested_owner, aspect) - - actual_owner = (owner_limitation or - requested_owner if explicit_owner else caller) - setattr(data, attr, actual_owner) - - _set_ownership('user', user_limit, 'X-User-Id') - _set_ownership('project', project_limit, 'X-Project-Id') - - # Check if there's room for one more alarm - if is_over_quota(conn, data.project_id, data.user_id): - raise OverQuota(data) - - data.timestamp = now - data.state_timestamp = now - - ALARMS_RULES[data.type].plugin.create_hook(data) - - data.update_actions() - change = data.as_dict(alarm_models.Alarm) - - # make sure alarms are unique by name per project. - alarms = list(conn.get_alarms(name=data.name, - project=data.project_id)) - if alarms: - raise base.ClientSideError( - _("Alarm with name='%s' exists") % data.name, - status_code=409) - - try: - alarm_in = alarm_models.Alarm(**change) - except Exception: - LOG.exception(_("Error while posting alarm: %s") % change) - raise base.ClientSideError(_("Alarm incorrect")) - - alarm = conn.create_alarm(alarm_in) - self._record_creation(conn, change, alarm.alarm_id, now) - return Alarm.from_db_model(alarm) - - @wsme_pecan.wsexpose([Alarm], [base.Query]) - def get_all(self, q=None): - """Return all alarms, based on the query provided. - - :param q: Filter rules for the alarms to be returned. - """ - - rbac.enforce('get_alarms', pecan.request) - - q = q or [] - # Timestamp is not supported field for Simple Alarm queries - kwargs = v2_utils.query_to_kwargs( - q, pecan.request.alarm_storage_conn.get_alarms, - allow_timestamps=False) - return [Alarm.from_db_model(m) - for m in pecan.request.alarm_storage_conn.get_alarms(**kwargs)] diff --git a/ceilometer/api/controllers/v2/base.py b/ceilometer/api/controllers/v2/base.py index efb1af33..e3e5f9db 100644 --- a/ceilometer/api/controllers/v2/base.py +++ b/ceilometer/api/controllers/v2/base.py @@ -234,32 +234,6 @@ class Query(Base): return converted_value -class AlarmNotFound(ClientSideError): - def __init__(self, alarm, auth_project): - if not auth_project: - msg = _('Alarm %s not found') % alarm - else: - msg = _('Alarm %(alarm_id)s not found in project %' - '(project)s') % { - 'alarm_id': alarm, 'project': auth_project} - super(AlarmNotFound, self).__init__(msg, status_code=404) - - -class AlarmRule(Base): - """Base class Alarm Rule extension and wsme.types.""" - @staticmethod - def validate_alarm(alarm): - pass - - @staticmethod - def create_hook(alarm): - pass - - @staticmethod - def update_hook(alarm): - pass - - class JsonType(wtypes.UserType): """A simple JSON type.""" diff --git a/ceilometer/api/controllers/v2/capabilities.py b/ceilometer/api/controllers/v2/capabilities.py index 85da8e57..4fc5396b 100644 --- a/ceilometer/api/controllers/v2/capabilities.py +++ b/ceilometer/api/controllers/v2/capabilities.py @@ -41,8 +41,6 @@ class Capabilities(base.Base): "A flattened dictionary of API capabilities" storage = {wtypes.text: bool} "A flattened dictionary of storage capabilities" - alarm_storage = {wtypes.text: bool} - "A flattened dictionary of alarm storage capabilities" event_storage = {wtypes.text: bool} "A flattened dictionary of event storage capabilities" @@ -73,16 +71,10 @@ class Capabilities(base.Base): 'stddev': True, 'cardinality': True, 'quartile': False}}}, - 'alarms': {'query': {'simple': True, - 'complex': True}, - 'history': {'query': {'simple': True, - 'complex': True}}}, 'events': {'query': {'simple': True}}, }), storage=_flatten_capabilities( {'storage': {'production_ready': True}}), - alarm_storage=_flatten_capabilities( - {'storage': {'production_ready': True}}), event_storage=_flatten_capabilities( {'storage': {'production_ready': True}}), ) @@ -100,17 +92,12 @@ class CapabilitiesController(rest.RestController): # variation in API capabilities is effectively determined by # the lack of strict feature parity across storage drivers conn = pecan.request.storage_conn - alarm_conn = pecan.request.alarm_storage_conn event_conn = pecan.request.event_storage_conn driver_capabilities = conn.get_capabilities().copy() - driver_capabilities['alarms'] = alarm_conn.get_capabilities()['alarms'] driver_capabilities['events'] = event_conn.get_capabilities()['events'] driver_perf = conn.get_storage_capabilities() - alarm_driver_perf = alarm_conn.get_storage_capabilities() event_driver_perf = event_conn.get_storage_capabilities() return Capabilities(api=_flatten_capabilities(driver_capabilities), storage=_flatten_capabilities(driver_perf), - alarm_storage=_flatten_capabilities( - alarm_driver_perf), event_storage=_flatten_capabilities( event_driver_perf)) diff --git a/ceilometer/api/controllers/v2/query.py b/ceilometer/api/controllers/v2/query.py index e6d10f4c..1c5af060 100644 --- a/ceilometer/api/controllers/v2/query.py +++ b/ceilometer/api/controllers/v2/query.py @@ -28,8 +28,6 @@ from pecan import rest from wsme import types as wtypes import wsmeext.pecan as wsme_pecan -from ceilometer.alarm.storage import models as alarm_models -from ceilometer.api.controllers.v2 import alarms from ceilometer.api.controllers.v2 import base from ceilometer.api.controllers.v2 import samples from ceilometer.api.controllers.v2 import utils as v2_utils @@ -356,45 +354,6 @@ class QuerySamplesController(rest.RestController): query.limit)] -class QueryAlarmHistoryController(rest.RestController): - """Provides complex query possibilities for alarm history.""" - @wsme_pecan.wsexpose([alarms.AlarmChange], body=ComplexQuery) - def post(self, body): - """Define query for retrieving AlarmChange data. +class QueryController(rest.RestController): - :param body: Query rules for the alarm history to be returned. - """ - - rbac.enforce('query_alarm_history', pecan.request) - - query = ValidatedComplexQuery(body, - alarm_models.AlarmChange) - query.validate(visibility_field="on_behalf_of") - conn = pecan.request.alarm_storage_conn - return [alarms.AlarmChange.from_db_model(s) - for s in conn.query_alarm_history(query.filter_expr, - query.orderby, - query.limit)] - - -class QueryAlarmsController(rest.RestController): - """Provides complex query possibilities for alarms.""" - history = QueryAlarmHistoryController() - - @wsme_pecan.wsexpose([alarms.Alarm], body=ComplexQuery) - def post(self, body): - """Define query for retrieving Alarm data. - - :param body: Query rules for the alarms to be returned. - """ - - rbac.enforce('query_alarm', pecan.request) - - query = ValidatedComplexQuery(body, - alarm_models.Alarm) - query.validate(visibility_field="project_id") - conn = pecan.request.alarm_storage_conn - return [alarms.Alarm.from_db_model(s) - for s in conn.query_alarms(query.filter_expr, - query.orderby, - query.limit)] + samples = QuerySamplesController() diff --git a/ceilometer/api/controllers/v2/root.py b/ceilometer/api/controllers/v2/root.py index 5108cc1d..561f992c 100644 --- a/ceilometer/api/controllers/v2/root.py +++ b/ceilometer/api/controllers/v2/root.py @@ -24,7 +24,6 @@ from oslo_log import log from oslo_utils import strutils import pecan -from ceilometer.api.controllers.v2 import alarms from ceilometer.api.controllers.v2 import capabilities from ceilometer.api.controllers.v2 import events from ceilometer.api.controllers.v2 import meters @@ -177,8 +176,6 @@ class V2Controller(object): ), remainder elif kind == 'alarms' and self.aodh_url: aodh_redirect(self.aodh_url) - elif kind == 'alarms': - return alarms.AlarmsController(), remainder else: pecan.abort(404) diff --git a/ceilometer/api/controllers/v2/utils.py b/ceilometer/api/controllers/v2/utils.py index 2c33897a..88142cbd 100644 --- a/ceilometer/api/controllers/v2/utils.py +++ b/ceilometer/api/controllers/v2/utils.py @@ -52,15 +52,6 @@ def enforce_limit(limit): def get_auth_project(on_behalf_of=None): - # when an alarm is created by an admin on behalf of another tenant - # we must ensure for: - # - threshold alarm, that an implicit query constraint on project_id is - # added so that admin-level visibility on statistics is not leaked - # - combination alarm, that alarm ids verification is scoped to - # alarms owned by the alarm project. - # hence for null auth_project (indicating admin-ness) we check if - # the creating tenant differs from the tenant on whose behalf the - # alarm is being created auth_project = rbac.get_limited_to_project(pecan.request.headers) created_by = pecan.request.headers.get('X-Project-Id') is_admin = auth_project is None @@ -133,9 +124,6 @@ def validate_query(query, db_func, internal_keys=None, _verify_query_segregation(query) valid_keys = inspect.getargspec(db_func)[0] - if 'alarm_type' in valid_keys: - valid_keys.remove('alarm_type') - valid_keys.append('type') internal_timestamp_keys = ['end_timestamp', 'start_timestamp', 'end_timestamp_op', 'start_timestamp_op'] @@ -235,8 +223,7 @@ def query_to_kwargs(query, db_func, internal_keys=None, query = sanitize_query(query, db_func) translation = {'user_id': 'user', 'project_id': 'project', - 'resource_id': 'resource', - 'type': 'alarm_type'} + 'resource_id': 'resource'} stamp = {} metaquery = {} kwargs = {} @@ -336,7 +323,7 @@ def flatten_metadata(metadata): # TODO(fabiog): this decorator should disappear and have a more unified # way of controlling access and scope. Before messing with this, though # I feel this file should be re-factored in smaller chunks one for each -# controller (e.g. meters, alarms and so on ...). Right now its size is +# controller (e.g. meters and so on ...). Right now its size is # overwhelming. def requires_admin(func): diff --git a/ceilometer/api/hooks.py b/ceilometer/api/hooks.py index 44278855..003a2363 100644 --- a/ceilometer/api/hooks.py +++ b/ceilometer/api/hooks.py @@ -45,19 +45,16 @@ class DBHook(hooks.PecanHook): def __init__(self): self.storage_connection = DBHook.get_connection('metering') self.event_storage_connection = DBHook.get_connection('event') - self.alarm_storage_connection = DBHook.get_connection('alarm') - if (not self.storage_connection and - not self.event_storage_connection and - not self.alarm_storage_connection): + if (not self.storage_connection + and not self.event_storage_connection): raise Exception("Api failed to start. Failed to connect to " "databases, purpose: %s" % - ', '.join(['metering', 'event', 'alarm'])) + ', '.join(['metering', 'event'])) def before(self, state): state.request.storage_conn = self.storage_connection state.request.event_storage_conn = self.event_storage_connection - state.request.alarm_storage_conn = self.alarm_storage_connection @staticmethod def get_connection(purpose): diff --git a/ceilometer/cmd/eventlet/alarm.py b/ceilometer/cmd/eventlet/alarm.py deleted file mode 100644 index b3df8b00..00000000 --- a/ceilometer/cmd/eventlet/alarm.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- encoding: utf-8 -*- -# -# Copyright 2014 OpenStack Foundation -# -# 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 oslo_config import cfg -from oslo_service import service as os_service - -from ceilometer.alarm import service as alarm_service -from ceilometer import service - -CONF = cfg.CONF - - -def notifier(): - service.prepare_service() - os_service.launch(CONF, alarm_service.AlarmNotifierService()).wait() - - -def evaluator(): - service.prepare_service() - os_service.launch(CONF, alarm_service.AlarmEvaluationService()).wait() diff --git a/ceilometer/cmd/eventlet/storage.py b/ceilometer/cmd/eventlet/storage.py index a38e39d9..752ea63f 100644 --- a/ceilometer/cmd/eventlet/storage.py +++ b/ceilometer/cmd/eventlet/storage.py @@ -29,7 +29,6 @@ LOG = logging.getLogger(__name__) def dbsync(): service.prepare_service() storage.get_connection_from_config(cfg.CONF, 'metering').upgrade() - storage.get_connection_from_config(cfg.CONF, 'alarm').upgrade() storage.get_connection_from_config(cfg.CONF, 'event').upgrade() @@ -53,12 +52,3 @@ def expirer(): else: LOG.info(_LI("Nothing to clean, database event time to live " "is disabled")) - - if cfg.CONF.database.alarm_history_time_to_live > 0: - LOG.debug("Clearing expired alarm history data") - storage_conn = storage.get_connection_from_config(cfg.CONF, 'alarm') - storage_conn.clear_expired_alarm_history_data( - cfg.CONF.database.alarm_history_time_to_live) - else: - LOG.info(_LI("Nothing to clean, database alarm history time to live " - "is disabled")) diff --git a/ceilometer/opts.py b/ceilometer/opts.py index b4a175ec..971ea83e 100644 --- a/ceilometer/opts.py +++ b/ceilometer/opts.py @@ -14,12 +14,8 @@ import itertools import ceilometer.agent.manager -import ceilometer.alarm.notifier.rest -import ceilometer.alarm.rpc -import ceilometer.alarm.service import ceilometer.api import ceilometer.api.app -import ceilometer.api.controllers.v2.alarms import ceilometer.cmd.eventlet.polling import ceilometer.collector import ceilometer.compute.discovery @@ -79,12 +75,6 @@ def list_opts(): ceilometer.storage.OLD_OPTS, ceilometer.storage.CLI_OPTS, ceilometer.utils.OPTS,)), - ('alarm', - itertools.chain(ceilometer.alarm.notifier.rest.OPTS, - ceilometer.alarm.service.OPTS, - ceilometer.alarm.rpc.OPTS, - ceilometer.alarm.evaluator.gnocchi.OPTS, - ceilometer.api.controllers.v2.alarms.ALARM_API_OPTS)), ('api', itertools.chain(ceilometer.api.OPTS, ceilometer.api.app.API_OPTS, diff --git a/ceilometer/storage/__init__.py b/ceilometer/storage/__init__.py index 6f40402b..7f49ce07 100644 --- a/ceilometer/storage/__init__.py +++ b/ceilometer/storage/__init__.py @@ -54,17 +54,6 @@ OPTS = [ default=None, help='The connection string used to connect to the metering ' 'database. (if unset, connection is used)'), - cfg.StrOpt('alarm_connection', - secret=True, - default=None, - deprecated_for_removal=True, - help='The connection string used to connect to the alarm ' - 'database. (if unset, connection is used)'), - cfg.IntOpt('alarm_history_time_to_live', - default=-1, - deprecated_for_removal=True, - help=("Number of seconds that alarm histories are kept " - "in the database for (<= 0 means forever).")), cfg.StrOpt('event_connection', secret=True, default=None, diff --git a/ceilometer/storage/base.py b/ceilometer/storage/base.py index e056b6ba..fb038b3f 100644 --- a/ceilometer/storage/base.py +++ b/ceilometer/storage/base.py @@ -49,12 +49,11 @@ def iter_period(start, end, period): def _handle_sort_key(model_name, sort_key=None): """Generate sort keys according to the passed in sort key from user. - :param model_name: Database model name be query.(alarm, meter, etc.) + :param model_name: Database model name be query.(meter, etc.) :param sort_key: sort key passed from user. return: sort keys list """ - sort_keys_extra = {'alarm': ['name', 'user_id', 'project_id'], - 'meter': ['user_id', 'project_id'], + sort_keys_extra = {'meter': ['user_id', 'project_id'], 'resource': ['user_id', 'project_id', 'timestamp'], } diff --git a/ceilometer/storage/hbase/migration.py b/ceilometer/storage/hbase/migration.py index 3bdadf83..9cc3df93 100644 --- a/ceilometer/storage/hbase/migration.py +++ b/ceilometer/storage/hbase/migration.py @@ -90,23 +90,7 @@ def migrate_event_table(conn, table): event_table.delete(row) -def migrate_alarm_history_table(conn, table): - """Migrate table 'alarm_h' in HBase. - - Change row format from ""%s_%s" % alarm_id, rts, - to new separator format "%s:%s" % alarm_id, rts - """ - alarm_h_table = conn.table(table) - alarm_h_filter = "RowFilter(=, 'regexstring:\\w*_\\d{19}')" - gen = alarm_h_table.scan(filter=alarm_h_filter) - for row, data in gen: - row_parts = row.rsplit('_', 1) - alarm_h_table.put(hbase_utils.prepare_key(*row_parts), data) - alarm_h_table.delete(row) - - TABLE_MIGRATION_FUNCS = {'resource': migrate_resource_table, - 'alarm_h': migrate_alarm_history_table, 'meter': migrate_meter_table, 'event': migrate_event_table} diff --git a/ceilometer/storage/sqlalchemy/models.py b/ceilometer/storage/sqlalchemy/models.py index 163ffeb4..c457443c 100644 --- a/ceilometer/storage/sqlalchemy/models.py +++ b/ceilometer/storage/sqlalchemy/models.py @@ -241,53 +241,6 @@ class FullSample(object): internal_id = Resource.internal_id -class Alarm(Base): - """Define Alarm data.""" - __tablename__ = 'alarm' - __table_args__ = ( - Index('ix_alarm_user_id', 'user_id'), - Index('ix_alarm_project_id', 'project_id'), - ) - alarm_id = Column(String(128), primary_key=True) - enabled = Column(Boolean) - name = Column(Text) - type = Column(String(50)) - severity = Column(String(50)) - description = Column(Text) - timestamp = Column(PreciseTimestamp, default=lambda: timeutils.utcnow()) - - user_id = Column(String(255)) - project_id = Column(String(255)) - - state = Column(String(255)) - state_timestamp = Column(PreciseTimestamp, - default=lambda: timeutils.utcnow()) - - ok_actions = Column(JSONEncodedDict) - alarm_actions = Column(JSONEncodedDict) - insufficient_data_actions = Column(JSONEncodedDict) - repeat_actions = Column(Boolean) - - rule = Column(JSONEncodedDict) - time_constraints = Column(JSONEncodedDict) - - -class AlarmChange(Base): - """Define AlarmChange data.""" - __tablename__ = 'alarm_history' - __table_args__ = ( - Index('ix_alarm_history_alarm_id', 'alarm_id'), - ) - event_id = Column(String(128), primary_key=True) - alarm_id = Column(String(128)) - on_behalf_of = Column(String(255)) - project_id = Column(String(255)) - user_id = Column(String(255)) - type = Column(String(20)) - detail = Column(Text) - timestamp = Column(PreciseTimestamp, default=lambda: timeutils.utcnow()) - - class EventType(Base): """Types of event records.""" __tablename__ = 'event_type' diff --git a/ceilometer/tests/constants.py b/ceilometer/tests/constants.py deleted file mode 100644 index 50678fc9..00000000 --- a/ceilometer/tests/constants.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2014 Huawei Technologies Co., Ltd. -# -# 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 datetime - -MIN_DATETIME = datetime.datetime(datetime.MINYEAR, 1, 1) diff --git a/ceilometer/tests/db.py b/ceilometer/tests/db.py index 607dfaa5..a7bce5b5 100644 --- a/ceilometer/tests/db.py +++ b/ceilometer/tests/db.py @@ -51,8 +51,6 @@ class MongoDbManager(fixtures.Fixture): try: self.connection = storage.get_connection( self.url, 'ceilometer.metering.storage') - self.alarm_connection = storage.get_connection( - self.url, 'ceilometer.alarm.storage') self.event_connection = storage.get_connection( self.url, 'ceilometer.event.storage') except storage.StorageBadVersion as e: @@ -71,8 +69,6 @@ class SQLManager(fixtures.Fixture): super(SQLManager, self).setUp() self.connection = storage.get_connection( self.url, 'ceilometer.metering.storage') - self.alarm_connection = storage.get_connection( - self.url, 'ceilometer.alarm.storage') self.event_connection = storage.get_connection( self.url, 'ceilometer.event.storage') @@ -117,8 +113,6 @@ class ElasticSearchManager(fixtures.Fixture): super(ElasticSearchManager, self).setUp() self.connection = storage.get_connection( 'sqlite://', 'ceilometer.metering.storage') - self.alarm_connection = storage.get_connection( - 'sqlite://', 'ceilometer.alarm.storage') self.event_connection = storage.get_connection( self.url, 'ceilometer.event.storage') # prefix each test with unique index name @@ -135,8 +129,6 @@ class HBaseManager(fixtures.Fixture): super(HBaseManager, self).setUp() self.connection = storage.get_connection( self.url, 'ceilometer.metering.storage') - self.alarm_connection = storage.get_connection( - self.url, 'ceilometer.alarm.storage') self.event_connection = storage.get_connection( self.url, 'ceilometer.event.storage') # Unique prefix for each test to keep data is distinguished because @@ -177,8 +169,6 @@ class SQLiteManager(fixtures.Fixture): super(SQLiteManager, self).setUp() self.connection = storage.get_connection( self.url, 'ceilometer.metering.storage') - self.alarm_connection = storage.get_connection( - self.url, 'ceilometer.alarm.storage') self.event_connection = storage.get_connection( self.url, 'ceilometer.event.storage') @@ -224,9 +214,6 @@ class TestBase(testscenarios.testcase.WithScenarios, test_base.BaseTestCase): self.conn = self.db_manager.connection self.conn.upgrade() - self.alarm_conn = self.db_manager.alarm_connection - self.alarm_conn.upgrade() - self.event_conn = self.db_manager.event_connection self.event_conn.upgrade() @@ -245,16 +232,12 @@ class TestBase(testscenarios.testcase.WithScenarios, test_base.BaseTestCase): def tearDown(self): self.event_conn.clear() self.event_conn = None - self.alarm_conn.clear() - self.alarm_conn = None self.conn.clear() self.conn = None super(TestBase, self).tearDown() def _get_connection(self, url, namespace): - if namespace == "ceilometer.alarm.storage": - return self.alarm_conn - elif namespace == "ceilometer.event.storage": + if namespace == "ceilometer.event.storage": return self.event_conn return self.conn diff --git a/ceilometer/tests/functional/api/v2/test_alarm_scenarios.py b/ceilometer/tests/functional/api/v2/test_alarm_scenarios.py deleted file mode 100644 index 7a9b0f7e..00000000 --- a/ceilometer/tests/functional/api/v2/test_alarm_scenarios.py +++ /dev/null @@ -1,2916 +0,0 @@ -# -# Copyright 2013 eNovance -# -# 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. -"""Tests alarm operation.""" - -import datetime -import uuid - -import mock -import oslo_messaging.conffixture -from oslo_serialization import jsonutils -import requests - -import six -from six import moves -import six.moves.urllib.parse as urlparse - -from ceilometer.alarm.storage import models -from ceilometer import messaging -from ceilometer.tests import constants -from ceilometer.tests import db as tests_db -from ceilometer.tests.functional.api import v2 - - -class TestListEmptyAlarms(v2.FunctionalTest, - tests_db.MixinTestsWithBackendScenarios): - - def test_empty(self): - data = self.get_json('/alarms') - self.assertEqual([], data) - - -class TestAlarms(v2.FunctionalTest, - tests_db.MixinTestsWithBackendScenarios): - - def setUp(self): - super(TestAlarms, self).setUp() - self.auth_headers = {'X-User-Id': str(uuid.uuid4()), - 'X-Project-Id': str(uuid.uuid4())} - for alarm in [ - models.Alarm(name='name1', - type='threshold', - enabled=True, - alarm_id='a', - description='a', - state='insufficient data', - severity='critical', - state_timestamp=constants.MIN_DATETIME, - timestamp=constants.MIN_DATETIME, - ok_actions=[], - insufficient_data_actions=[], - alarm_actions=[], - repeat_actions=True, - user_id=self.auth_headers['X-User-Id'], - project_id=self.auth_headers['X-Project-Id'], - time_constraints=[dict(name='testcons', - start='0 11 * * *', - duration=300)], - rule=dict(comparison_operator='gt', - threshold=2.0, - statistic='avg', - evaluation_periods=60, - period=1, - meter_name='meter.test', - query=[{'field': 'project_id', - 'op': 'eq', 'value': - self.auth_headers['X-Project-Id']} - ]), - ), - models.Alarm(name='name2', - type='threshold', - enabled=True, - alarm_id='b', - description='b', - state='insufficient data', - severity='critical', - state_timestamp=constants.MIN_DATETIME, - timestamp=constants.MIN_DATETIME, - ok_actions=[], - insufficient_data_actions=[], - alarm_actions=[], - repeat_actions=False, - user_id=self.auth_headers['X-User-Id'], - project_id=self.auth_headers['X-Project-Id'], - time_constraints=[], - rule=dict(comparison_operator='gt', - threshold=4.0, - statistic='avg', - evaluation_periods=60, - period=1, - meter_name='meter.test', - query=[{'field': 'project_id', - 'op': 'eq', 'value': - self.auth_headers['X-Project-Id']} - ]), - ), - models.Alarm(name='name3', - type='threshold', - enabled=True, - alarm_id='c', - description='c', - state='insufficient data', - severity='moderate', - state_timestamp=constants.MIN_DATETIME, - timestamp=constants.MIN_DATETIME, - ok_actions=[], - insufficient_data_actions=[], - alarm_actions=[], - repeat_actions=False, - user_id=self.auth_headers['X-User-Id'], - project_id=self.auth_headers['X-Project-Id'], - time_constraints=[], - rule=dict(comparison_operator='gt', - threshold=3.0, - statistic='avg', - evaluation_periods=60, - period=1, - meter_name='meter.mine', - query=[{'field': 'project_id', - 'op': 'eq', 'value': - self.auth_headers['X-Project-Id']} - ]), - ), - models.Alarm(name='name4', - type='combination', - enabled=True, - alarm_id='d', - description='d', - state='insufficient data', - severity='low', - state_timestamp=constants.MIN_DATETIME, - timestamp=constants.MIN_DATETIME, - ok_actions=[], - insufficient_data_actions=[], - alarm_actions=[], - repeat_actions=False, - user_id=self.auth_headers['X-User-Id'], - project_id=self.auth_headers['X-Project-Id'], - time_constraints=[], - rule=dict(alarm_ids=['a', 'b'], - operator='or'), - ), - models.Alarm(name='name5', - type='gnocchi_resources_threshold', - enabled=True, - alarm_id='e', - description='e', - state='insufficient data', - severity='critical', - state_timestamp=constants.MIN_DATETIME, - timestamp=constants.MIN_DATETIME, - ok_actions=[], - insufficient_data_actions=[], - alarm_actions=[], - repeat_actions=True, - user_id=self.auth_headers['X-User-Id'], - project_id=self.auth_headers['X-Project-Id'], - time_constraints=[], - rule=dict(comparison_operator='gt', - threshold=2.0, - aggregation_method='mean', - granularity=60, - evaluation_periods=1, - metric='meter.test', - resource_type='instance', - resource_id=( - '6841c175-d7c4-4bc2-bc7a-1c7832271b8f'), - ) - ), - models.Alarm(name='name6', - type='gnocchi_aggregation_by_metrics_threshold', - enabled=True, - alarm_id='f', - description='f', - state='insufficient data', - severity='critical', - state_timestamp=constants.MIN_DATETIME, - timestamp=constants.MIN_DATETIME, - ok_actions=[], - insufficient_data_actions=[], - alarm_actions=[], - repeat_actions=True, - user_id=self.auth_headers['X-User-Id'], - project_id=self.auth_headers['X-Project-Id'], - time_constraints=[], - rule=dict(comparison_operator='gt', - threshold=2.0, - aggregation_method='mean', - evaluation_periods=1, - granularity=60, - metrics=[ - '41869681-5776-46d6-91ed-cccc43b6e4e3', - 'a1fb80f4-c242-4f57-87c6-68f47521059e'] - ), - ), - models.Alarm(name='name7', - type='gnocchi_aggregation_by_resources_threshold', - enabled=True, - alarm_id='g', - description='f', - state='insufficient data', - severity='critical', - state_timestamp=constants.MIN_DATETIME, - timestamp=constants.MIN_DATETIME, - ok_actions=[], - insufficient_data_actions=[], - alarm_actions=[], - repeat_actions=True, - user_id=self.auth_headers['X-User-Id'], - project_id=self.auth_headers['X-Project-Id'], - time_constraints=[], - rule=dict(comparison_operator='gt', - threshold=2.0, - aggregation_method='mean', - granularity=60, - evaluation_periods=1, - metric='meter.test', - resource_type='instance', - query='{"=": {"server_group": ' - '"my_autoscaling_group"}}') - ), - - ]: - - self.alarm_conn.update_alarm(alarm) - - @staticmethod - def _add_default_threshold_rule(alarm): - if (alarm['type'] == 'threshold' and - 'exclude_outliers' not in alarm['threshold_rule']): - alarm['threshold_rule']['exclude_outliers'] = False - - def _verify_alarm(self, json, alarm, expected_name=None): - if expected_name and alarm.name != expected_name: - self.fail("Alarm not found") - self._add_default_threshold_rule(json) - for key in json: - if key.endswith('_rule'): - storage_key = 'rule' - else: - storage_key = key - self.assertEqual(json[key], getattr(alarm, storage_key)) - - def test_list_alarms(self): - data = self.get_json('/alarms') - self.assertEqual(7, len(data)) - self.assertEqual(set(['name1', 'name2', 'name3', 'name4', 'name5', - 'name6', 'name7']), - set(r['name'] for r in data)) - self.assertEqual(set(['meter.test', 'meter.mine']), - set(r['threshold_rule']['meter_name'] - for r in data if 'threshold_rule' in r)) - self.assertEqual(set(['or']), - set(r['combination_rule']['operator'] - for r in data if 'combination_rule' in r)) - self.assertEqual(set(['meter.test']), - set(r['gnocchi_resources_threshold_rule']['metric'] - for r in data - if 'gnocchi_resources_threshold_rule' in r)) - - def test_alarms_query_with_timestamp(self): - date_time = datetime.datetime(2012, 7, 2, 10, 41) - isotime = date_time.isoformat() - resp = self.get_json('/alarms', - q=[{'field': 'timestamp', - 'op': 'gt', - 'value': isotime}], - expect_errors=True) - self.assertEqual(resp.status_code, 400) - self.assertEqual(jsonutils.loads(resp.body)['error_message'] - ['faultstring'], - 'Unknown argument: "timestamp": ' - 'not valid for this resource') - - def test_alarms_query_with_meter(self): - resp = self.get_json('/alarms', - q=[{'field': 'meter', - 'op': 'eq', - 'value': 'meter.mine'}], - ) - self.assertEqual(1, len(resp)) - self.assertEqual('c', - resp[0]['alarm_id']) - self.assertEqual('meter.mine', - resp[0] - ['threshold_rule'] - ['meter_name']) - - def test_alarms_query_with_state(self): - alarm = models.Alarm(name='disabled', - type='combination', - enabled=False, - alarm_id='d', - description='d', - state='ok', - state_timestamp=constants.MIN_DATETIME, - timestamp=constants.MIN_DATETIME, - ok_actions=[], - insufficient_data_actions=[], - alarm_actions=[], - repeat_actions=False, - user_id=self.auth_headers['X-User-Id'], - project_id=self.auth_headers['X-Project-Id'], - time_constraints=[], - rule=dict(alarm_ids=['a', 'b'], operator='or'), - severity='critical') - self.alarm_conn.update_alarm(alarm) - resp = self.get_json('/alarms', - q=[{'field': 'state', - 'op': 'eq', - 'value': 'ok'}], - ) - self.assertEqual(1, len(resp)) - self.assertEqual('ok', resp[0]['state']) - - def test_list_alarms_by_type(self): - alarms = self.get_json('/alarms', - q=[{'field': 'type', - 'op': 'eq', - 'value': 'threshold'}]) - self.assertEqual(3, len(alarms)) - self.assertEqual(set(['threshold']), - set(alarm['type'] for alarm in alarms)) - - def test_get_not_existing_alarm(self): - resp = self.get_json('/alarms/alarm-id-3', expect_errors=True) - self.assertEqual(404, resp.status_code) - self.assertEqual('Alarm alarm-id-3 not found', - jsonutils.loads(resp.body)['error_message'] - ['faultstring']) - - def test_get_alarm(self): - alarms = self.get_json('/alarms', - q=[{'field': 'name', - 'value': 'name1', - }]) - self.assertEqual('name1', alarms[0]['name']) - self.assertEqual('meter.test', - alarms[0]['threshold_rule']['meter_name']) - - one = self.get_json('/alarms/%s' % alarms[0]['alarm_id']) - self.assertEqual('name1', one['name']) - self.assertEqual('meter.test', one['threshold_rule']['meter_name']) - self.assertEqual(alarms[0]['alarm_id'], one['alarm_id']) - self.assertEqual(alarms[0]['repeat_actions'], one['repeat_actions']) - self.assertEqual(alarms[0]['time_constraints'], - one['time_constraints']) - - def test_get_alarm_disabled(self): - alarm = models.Alarm(name='disabled', - type='combination', - enabled=False, - alarm_id='d', - description='d', - state='insufficient data', - state_timestamp=constants.MIN_DATETIME, - timestamp=constants.MIN_DATETIME, - ok_actions=[], - insufficient_data_actions=[], - alarm_actions=[], - repeat_actions=False, - user_id=self.auth_headers['X-User-Id'], - project_id=self.auth_headers['X-Project-Id'], - time_constraints=[], - rule=dict(alarm_ids=['a', 'b'], operator='or'), - severity='critical') - self.alarm_conn.update_alarm(alarm) - - alarms = self.get_json('/alarms', - q=[{'field': 'enabled', - 'value': 'False'}]) - self.assertEqual(1, len(alarms)) - self.assertEqual('disabled', alarms[0]['name']) - - one = self.get_json('/alarms/%s' % alarms[0]['alarm_id']) - self.assertEqual('disabled', one['name']) - - def test_get_alarm_combination(self): - alarms = self.get_json('/alarms', - q=[{'field': 'name', - 'value': 'name4', - }]) - self.assertEqual('name4', alarms[0]['name']) - self.assertEqual(['a', 'b'], - alarms[0]['combination_rule']['alarm_ids']) - self.assertEqual('or', alarms[0]['combination_rule']['operator']) - - one = self.get_json('/alarms/%s' % alarms[0]['alarm_id']) - self.assertEqual('name4', one['name']) - self.assertEqual(['a', 'b'], - alarms[0]['combination_rule']['alarm_ids']) - self.assertEqual('or', alarms[0]['combination_rule']['operator']) - self.assertEqual(alarms[0]['alarm_id'], one['alarm_id']) - self.assertEqual(alarms[0]['repeat_actions'], one['repeat_actions']) - - def test_get_alarm_project_filter_wrong_op_normal_user(self): - project = self.auth_headers['X-Project-Id'] - - def _test(field, op): - response = self.get_json('/alarms', - q=[{'field': field, - 'op': op, - 'value': project}], - expect_errors=True, - status=400, - headers=self.auth_headers) - faultstring = ('Invalid input for field/attribute op. ' - 'Value: \'%(op)s\'. unimplemented operator ' - 'for %(field)s' % {'field': field, 'op': op}) - self.assertEqual(faultstring, - response.json['error_message']['faultstring']) - - _test('project', 'ne') - _test('project_id', 'ne') - - def test_get_alarm_project_filter_normal_user(self): - project = self.auth_headers['X-Project-Id'] - - def _test(field): - alarms = self.get_json('/alarms', - q=[{'field': field, - 'op': 'eq', - 'value': project}]) - self.assertEqual(7, len(alarms)) - - _test('project') - _test('project_id') - - def test_get_alarm_other_project_normal_user(self): - def _test(field): - response = self.get_json('/alarms', - q=[{'field': field, - 'op': 'eq', - 'value': 'other-project'}], - expect_errors=True, - status=401, - headers=self.auth_headers) - faultstring = 'Not Authorized to access project other-project' - self.assertEqual(faultstring, - response.json['error_message']['faultstring']) - - _test('project') - _test('project_id') - - def test_post_alarm_wsme_workaround(self): - jsons = { - 'type': { - 'name': 'missing type', - 'threshold_rule': { - 'meter_name': 'ameter', - 'threshold': 2.0, - } - }, - 'name': { - 'type': 'threshold', - 'threshold_rule': { - 'meter_name': 'ameter', - 'threshold': 2.0, - } - }, - 'threshold_rule/meter_name': { - 'name': 'missing meter_name', - 'type': 'threshold', - 'threshold_rule': { - 'threshold': 2.0, - } - }, - 'threshold_rule/threshold': { - 'name': 'missing threshold', - 'type': 'threshold', - 'threshold_rule': { - 'meter_name': 'ameter', - } - }, - 'combination_rule/alarm_ids': { - 'name': 'missing alarm_ids', - 'type': 'combination', - 'combination_rule': {} - } - } - for field, json in six.iteritems(jsons): - resp = self.post_json('/alarms', params=json, expect_errors=True, - status=400, headers=self.auth_headers) - self.assertEqual("Invalid input for field/attribute %s." - " Value: \'None\'. Mandatory field missing." - % field.split('/', 1)[-1], - resp.json['error_message']['faultstring']) - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(7, len(alarms)) - - def test_post_invalid_alarm_time_constraint_start(self): - json = { - 'name': 'added_alarm_invalid_constraint_duration', - 'type': 'threshold', - 'time_constraints': [ - { - 'name': 'testcons', - 'start': '11:00am', - 'duration': 10 - } - ], - 'threshold_rule': { - 'meter_name': 'ameter', - 'threshold': 300.0 - } - } - self.post_json('/alarms', params=json, expect_errors=True, status=400, - headers=self.auth_headers) - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(7, len(alarms)) - - def test_post_duplicate_time_constraint_name(self): - json = { - 'name': 'added_alarm_duplicate_constraint_name', - 'type': 'threshold', - 'time_constraints': [ - { - 'name': 'testcons', - 'start': '* 11 * * *', - 'duration': 10 - }, - { - 'name': 'testcons', - 'start': '* * * * *', - 'duration': 20 - } - ], - 'threshold_rule': { - 'meter_name': 'ameter', - 'threshold': 300.0 - } - } - resp = self.post_json('/alarms', params=json, expect_errors=True, - status=400, headers=self.auth_headers) - self.assertEqual( - "Time constraint names must be unique for a given alarm.", - resp.json['error_message']['faultstring']) - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(7, len(alarms)) - - def test_post_alarm_null_time_constraint(self): - json = { - 'name': 'added_alarm_invalid_constraint_duration', - 'type': 'threshold', - 'time_constraints': None, - 'threshold_rule': { - 'meter_name': 'ameter', - 'threshold': 300.0 - } - } - self.post_json('/alarms', params=json, status=201, - headers=self.auth_headers) - - def test_post_invalid_alarm_time_constraint_duration(self): - json = { - 'name': 'added_alarm_invalid_constraint_duration', - 'type': 'threshold', - 'time_constraints': [ - { - 'name': 'testcons', - 'start': '* 11 * * *', - 'duration': -1, - } - ], - 'threshold_rule': { - 'meter_name': 'ameter', - 'threshold': 300.0 - } - } - self.post_json('/alarms', params=json, expect_errors=True, status=400, - headers=self.auth_headers) - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(7, len(alarms)) - - def test_post_invalid_alarm_time_constraint_timezone(self): - json = { - 'name': 'added_alarm_invalid_constraint_timezone', - 'type': 'threshold', - 'time_constraints': [ - { - 'name': 'testcons', - 'start': '* 11 * * *', - 'duration': 10, - 'timezone': 'aaaa' - } - ], - 'threshold_rule': { - 'meter_name': 'ameter', - 'threshold': 300.0 - } - } - self.post_json('/alarms', params=json, expect_errors=True, status=400, - headers=self.auth_headers) - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(7, len(alarms)) - - def test_post_invalid_alarm_period(self): - json = { - 'name': 'added_alarm_invalid_period', - 'type': 'threshold', - 'threshold_rule': { - 'meter_name': 'ameter', - 'comparison_operator': 'gt', - 'threshold': 2.0, - 'statistic': 'avg', - 'period': -1, - } - - } - self.post_json('/alarms', params=json, expect_errors=True, status=400, - headers=self.auth_headers) - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(7, len(alarms)) - - def test_post_null_threshold_rule(self): - json = { - 'name': 'added_alarm_invalid_threshold_rule', - 'type': 'threshold', - 'threshold_rule': None, - 'combination_rule': None, - } - resp = self.post_json('/alarms', params=json, expect_errors=True, - status=400, headers=self.auth_headers) - self.assertEqual( - "threshold_rule must be set for threshold type alarm", - resp.json['error_message']['faultstring']) - - def test_post_invalid_alarm_statistic(self): - json = { - 'name': 'added_alarm', - 'type': 'threshold', - 'threshold_rule': { - 'meter_name': 'ameter', - 'comparison_operator': 'gt', - 'threshold': 2.0, - 'statistic': 'magic', - } - } - resp = self.post_json('/alarms', params=json, expect_errors=True, - status=400, headers=self.auth_headers) - expected_err_msg = ("Invalid input for field/attribute" - " statistic. Value: 'magic'.") - self.assertIn(expected_err_msg, - resp.json['error_message']['faultstring']) - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(7, len(alarms)) - - def test_post_invalid_alarm_input_state(self): - json = { - 'name': 'alarm1', - 'state': 'bad_state', - 'type': 'threshold', - 'threshold_rule': { - 'meter_name': 'ameter', - 'comparison_operator': 'gt', - 'threshold': 50.0 - } - } - resp = self.post_json('/alarms', params=json, expect_errors=True, - status=400, headers=self.auth_headers) - expected_err_msg = ("Invalid input for field/attribute state." - " Value: 'bad_state'.") - self.assertIn(expected_err_msg, - resp.json['error_message']['faultstring']) - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(7, len(alarms)) - - def test_post_invalid_alarm_input_severity(self): - json = { - 'name': 'alarm1', - 'state': 'ok', - 'severity': 'bad_value', - 'type': 'threshold', - 'threshold_rule': { - 'meter_name': 'ameter', - 'comparison_operator': 'gt', - 'threshold': 50.0 - } - } - resp = self.post_json('/alarms', params=json, expect_errors=True, - status=400, headers=self.auth_headers) - expected_err_msg = ("Invalid input for field/attribute severity." - " Value: 'bad_value'.") - self.assertIn(expected_err_msg, - resp.json['error_message']['faultstring']) - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(7, len(alarms)) - - def test_post_invalid_alarm_input_comparison_operator(self): - json = { - 'name': 'alarm2', - 'state': 'ok', - 'type': 'threshold', - 'threshold_rule': { - 'meter_name': 'ameter', - 'comparison_operator': 'bad_co', - 'threshold': 50.0 - } - } - resp = self.post_json('/alarms', params=json, expect_errors=True, - status=400, headers=self.auth_headers) - expected_err_msg = ("Invalid input for field/attribute" - " comparison_operator." - " Value: 'bad_co'.") - self.assertIn(expected_err_msg, - resp.json['error_message']['faultstring']) - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(7, len(alarms)) - - def test_post_invalid_alarm_input_type(self): - json = { - 'name': 'alarm3', - 'state': 'ok', - 'type': 'bad_type', - 'threshold_rule': { - 'meter_name': 'ameter', - 'comparison_operator': 'gt', - 'threshold': 50.0 - } - } - resp = self.post_json('/alarms', params=json, expect_errors=True, - status=400, headers=self.auth_headers) - expected_err_msg = ("Invalid input for field/attribute" - " type." - " Value: 'bad_type'.") - self.assertIn(expected_err_msg, - resp.json['error_message']['faultstring']) - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(7, len(alarms)) - - def test_post_invalid_alarm_input_enabled_str(self): - json = { - 'name': 'alarm5', - 'enabled': 'bad_enabled', - 'state': 'ok', - 'type': 'threshold', - 'threshold_rule': { - 'meter_name': 'ameter', - 'comparison_operator': 'gt', - 'threshold': 50.0 - } - } - resp = self.post_json('/alarms', params=json, expect_errors=True, - status=400, headers=self.auth_headers) - expected_err_msg = "Value not an unambiguous boolean: bad_enabled" - self.assertIn(expected_err_msg, - resp.json['error_message']['faultstring']) - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(7, len(alarms)) - - def test_post_invalid_alarm_input_enabled_int(self): - json = { - 'name': 'alarm6', - 'enabled': 0, - 'state': 'ok', - 'type': 'threshold', - 'threshold_rule': { - 'meter_name': 'ameter', - 'comparison_operator': 'gt', - 'threshold': 50.0 - } - } - resp = self.post_json('/alarms', params=json, - headers=self.auth_headers) - self.assertFalse(resp.json['enabled']) - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(8, len(alarms)) - - def test_post_invalid_combination_alarm_input_operator(self): - json = { - 'enabled': False, - 'name': 'alarm6', - 'state': 'ok', - 'type': 'combination', - 'ok_actions': ['http://something/ok'], - 'alarm_actions': ['http://something/alarm'], - 'insufficient_data_actions': ['http://something/no'], - 'repeat_actions': True, - 'combination_rule': { - 'alarm_ids': ['a', - 'b'], - 'operator': 'bad_operator', - } - } - resp = self.post_json('/alarms', params=json, expect_errors=True, - status=400, headers=self.auth_headers) - expected_err_msg = ("Invalid input for field/attribute" - " operator." - " Value: 'bad_operator'.") - self.assertIn(expected_err_msg, - resp.json['error_message']['faultstring']) - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(7, len(alarms)) - - def test_post_invalid_alarm_query(self): - json = { - 'name': 'added_alarm', - 'type': 'threshold', - 'threshold_rule': { - 'meter_name': 'ameter', - 'query': [{'field': 'metadata.invalid', - 'field': 'gt', - 'value': 'value'}], - 'comparison_operator': 'gt', - 'threshold': 2.0, - 'statistic': 'avg', - } - } - self.post_json('/alarms', params=json, expect_errors=True, status=400, - headers=self.auth_headers) - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(7, len(alarms)) - - def test_post_invalid_alarm_query_field_type(self): - json = { - 'name': 'added_alarm', - 'type': 'threshold', - 'threshold_rule': { - 'meter_name': 'ameter', - 'query': [{'field': 'metadata.valid', - 'op': 'eq', - 'value': 'value', - 'type': 'blob'}], - 'comparison_operator': 'gt', - 'threshold': 2.0, - 'statistic': 'avg', - } - } - resp = self.post_json('/alarms', params=json, expect_errors=True, - status=400, headers=self.auth_headers) - expected_error_message = 'The data type blob is not supported.' - resp_string = jsonutils.loads(resp.body) - fault_string = resp_string['error_message']['faultstring'] - self.assertTrue(fault_string.startswith(expected_error_message)) - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(7, len(alarms)) - - def test_post_invalid_alarm_query_non_field(self): - json = { - 'name': 'added_alarm', - 'type': 'threshold', - 'threshold_rule': { - 'meter_name': 'ameter', - 'query': [{'q.field': 'metadata.valid', - 'value': 'value'}], - 'threshold': 2.0, - } - } - resp = self.post_json('/alarms', params=json, expect_errors=True, - status=400, headers=self.auth_headers) - expected_error_message = ("Unknown attribute for argument " - "data.threshold_rule.query: q.field") - fault_string = resp.json['error_message']['faultstring'] - self.assertEqual(expected_error_message, fault_string) - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(7, len(alarms)) - - def test_post_invalid_alarm_query_non_value(self): - json = { - 'name': 'added_alarm', - 'type': 'threshold', - 'threshold_rule': { - 'meter_name': 'ameter', - 'query': [{'field': 'metadata.valid', - 'q.value': 'value'}], - 'threshold': 2.0, - } - } - resp = self.post_json('/alarms', params=json, expect_errors=True, - status=400, headers=self.auth_headers) - expected_error_message = ("Unknown attribute for argument " - "data.threshold_rule.query: q.value") - fault_string = resp.json['error_message']['faultstring'] - self.assertEqual(expected_error_message, fault_string) - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(7, len(alarms)) - - def test_post_invalid_alarm_have_multiple_rules(self): - json = { - 'name': 'added_alarm', - 'type': 'threshold', - 'threshold_rule': { - 'meter_name': 'ameter', - 'query': [{'field': 'meter', - 'value': 'ameter'}], - 'comparison_operator': 'gt', - 'threshold': 2.0, - }, - 'combination_rule': { - 'alarm_ids': ['a', 'b'], - - } - } - resp = self.post_json('/alarms', params=json, expect_errors=True, - status=400, headers=self.auth_headers) - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(7, len(alarms)) - - # threshold_rule and combination_rule order is not - # predictable so it is not possible to do an exact match - # here - error_faultstring = resp.json['error_message']['faultstring'] - for expected_string in ['threshold_rule', 'combination_rule', - 'cannot be set at the same time']: - self.assertIn(expected_string, error_faultstring) - - def test_post_invalid_alarm_timestamp_in_threshold_rule(self): - date_time = datetime.datetime(2012, 7, 2, 10, 41) - isotime = date_time.isoformat() - - json = { - 'name': 'invalid_alarm', - 'type': 'threshold', - 'threshold_rule': { - 'meter_name': 'ameter', - 'query': [{'field': 'timestamp', - 'op': 'gt', - 'value': isotime}], - 'comparison_operator': 'gt', - 'threshold': 2.0, - } - } - resp = self.post_json('/alarms', params=json, expect_errors=True, - status=400, headers=self.auth_headers) - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(7, len(alarms)) - self.assertEqual( - 'Unknown argument: "timestamp": ' - 'not valid for this resource', - resp.json['error_message']['faultstring']) - - def _do_post_alarm_invalid_action(self, ok_actions=None, - alarm_actions=None, - insufficient_data_actions=None, - error_message=None): - - ok_actions = ok_actions or [] - alarm_actions = alarm_actions or [] - insufficient_data_actions = insufficient_data_actions or [] - json = { - 'enabled': False, - 'name': 'added_alarm', - 'state': 'ok', - 'type': 'threshold', - 'ok_actions': ok_actions, - 'alarm_actions': alarm_actions, - 'insufficient_data_actions': insufficient_data_actions, - 'repeat_actions': True, - 'threshold_rule': { - 'meter_name': 'ameter', - 'query': [{'field': 'metadata.field', - 'op': 'eq', - 'value': '5', - 'type': 'string'}], - 'comparison_operator': 'le', - 'statistic': 'count', - 'threshold': 50, - 'evaluation_periods': '3', - 'period': '180', - } - } - resp = self.post_json('/alarms', params=json, status=400, - headers=self.auth_headers) - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(7, len(alarms)) - self.assertEqual(error_message, - resp.json['error_message']['faultstring']) - - def test_post_invalid_alarm_ok_actions(self): - self._do_post_alarm_invalid_action( - ok_actions=['spam://something/ok'], - error_message='Unsupported action spam://something/ok') - - def test_post_invalid_alarm_alarm_actions(self): - self._do_post_alarm_invalid_action( - alarm_actions=['spam://something/alarm'], - error_message='Unsupported action spam://something/alarm') - - def test_post_invalid_alarm_insufficient_data_actions(self): - self._do_post_alarm_invalid_action( - insufficient_data_actions=['spam://something/insufficient'], - error_message='Unsupported action spam://something/insufficient') - - @staticmethod - def _fake_urlsplit(*args, **kwargs): - raise Exception("Evil urlsplit!") - - def test_post_invalid_alarm_actions_format(self): - with mock.patch('oslo_utils.netutils.urlsplit', - self._fake_urlsplit): - self._do_post_alarm_invalid_action( - alarm_actions=['http://[::1'], - error_message='Unable to parse action http://[::1') - - def test_post_alarm_defaults(self): - to_check = { - 'enabled': True, - 'name': 'added_alarm_defaults', - 'state': 'insufficient data', - 'description': ('Alarm when ameter is eq a avg of ' - '300.0 over 60 seconds'), - 'type': 'threshold', - 'ok_actions': [], - 'alarm_actions': [], - 'insufficient_data_actions': [], - 'repeat_actions': False, - 'threshold_rule': { - 'meter_name': 'ameter', - 'query': [{'field': 'project_id', - 'op': 'eq', - 'value': self.auth_headers['X-Project-Id']}], - 'threshold': 300.0, - 'comparison_operator': 'eq', - 'statistic': 'avg', - 'evaluation_periods': 1, - 'period': 60, - } - - } - self._add_default_threshold_rule(to_check) - - json = { - 'name': 'added_alarm_defaults', - 'type': 'threshold', - 'threshold_rule': { - 'meter_name': 'ameter', - 'threshold': 300.0 - } - } - self.post_json('/alarms', params=json, status=201, - headers=self.auth_headers) - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(8, len(alarms)) - for alarm in alarms: - if alarm.name == 'added_alarm_defaults': - for key in to_check: - if key.endswith('_rule'): - storage_key = 'rule' - else: - storage_key = key - self.assertEqual(to_check[key], - getattr(alarm, storage_key)) - break - else: - self.fail("Alarm not found") - - def test_post_conflict(self): - json = { - 'enabled': False, - 'name': 'added_alarm', - 'state': 'ok', - 'type': 'threshold', - 'ok_actions': ['http://something/ok'], - 'alarm_actions': ['http://something/alarm'], - 'insufficient_data_actions': ['http://something/no'], - 'repeat_actions': True, - 'threshold_rule': { - 'meter_name': 'ameter', - 'query': [{'field': 'metadata.field', - 'op': 'eq', - 'value': '5', - 'type': 'string'}], - 'comparison_operator': 'le', - 'statistic': 'count', - 'threshold': 50, - 'evaluation_periods': '3', - 'period': '180', - } - } - - self.post_json('/alarms', params=json, status=201, - headers=self.auth_headers) - self.post_json('/alarms', params=json, status=409, - headers=self.auth_headers) - - def _do_test_post_alarm(self, exclude_outliers=None): - json = { - 'enabled': False, - 'name': 'added_alarm', - 'state': 'ok', - 'type': 'threshold', - 'severity': 'low', - 'ok_actions': ['http://something/ok'], - 'alarm_actions': ['http://something/alarm'], - 'insufficient_data_actions': ['http://something/no'], - 'repeat_actions': True, - 'threshold_rule': { - 'meter_name': 'ameter', - 'query': [{'field': 'metadata.field', - 'op': 'eq', - 'value': '5', - 'type': 'string'}], - 'comparison_operator': 'le', - 'statistic': 'count', - 'threshold': 50, - 'evaluation_periods': '3', - 'period': '180', - } - } - if exclude_outliers is not None: - json['threshold_rule']['exclude_outliers'] = exclude_outliers - - self.post_json('/alarms', params=json, status=201, - headers=self.auth_headers) - alarms = list(self.alarm_conn.get_alarms(enabled=False)) - self.assertEqual(1, len(alarms)) - json['threshold_rule']['query'].append({ - 'field': 'project_id', 'op': 'eq', - 'value': self.auth_headers['X-Project-Id']}) - # to check to IntegerType type conversion - json['threshold_rule']['evaluation_periods'] = 3 - json['threshold_rule']['period'] = 180 - self._verify_alarm(json, alarms[0], 'added_alarm') - - def test_post_alarm_outlier_exclusion_set(self): - self._do_test_post_alarm(True) - - def test_post_alarm_outlier_exclusion_clear(self): - self._do_test_post_alarm(False) - - def test_post_alarm_outlier_exclusion_defaulted(self): - self._do_test_post_alarm() - - def test_post_alarm_noauth(self): - json = { - 'enabled': False, - 'name': 'added_alarm', - 'state': 'ok', - 'type': 'threshold', - 'severity': 'low', - 'ok_actions': ['http://something/ok'], - 'alarm_actions': ['http://something/alarm'], - 'insufficient_data_actions': ['http://something/no'], - 'repeat_actions': True, - 'threshold_rule': { - 'meter_name': 'ameter', - 'query': [{'field': 'metadata.field', - 'op': 'eq', - 'value': '5', - 'type': 'string'}], - 'comparison_operator': 'le', - 'statistic': 'count', - 'threshold': 50, - 'evaluation_periods': '3', - 'exclude_outliers': False, - 'period': '180', - } - } - self.post_json('/alarms', params=json, status=201) - alarms = list(self.alarm_conn.get_alarms(enabled=False)) - self.assertEqual(1, len(alarms)) - # to check to BoundedInt type conversion - json['threshold_rule']['evaluation_periods'] = 3 - json['threshold_rule']['period'] = 180 - if alarms[0].name == 'added_alarm': - for key in json: - if key.endswith('_rule'): - storage_key = 'rule' - else: - storage_key = key - self.assertEqual(getattr(alarms[0], storage_key), - json[key]) - else: - self.fail("Alarm not found") - - def _do_test_post_alarm_as_admin(self, explicit_project_constraint): - """Test the creation of an alarm as admin for another project.""" - json = { - 'enabled': False, - 'name': 'added_alarm', - 'state': 'ok', - 'type': 'threshold', - 'user_id': 'auseridthatisnotmine', - 'project_id': 'aprojectidthatisnotmine', - 'threshold_rule': { - 'meter_name': 'ameter', - 'query': [{'field': 'metadata.field', - 'op': 'eq', - 'value': '5', - 'type': 'string'}], - 'comparison_operator': 'le', - 'statistic': 'count', - 'threshold': 50, - 'evaluation_periods': 3, - 'period': 180, - } - } - if explicit_project_constraint: - project_constraint = {'field': 'project_id', 'op': 'eq', - 'value': 'aprojectidthatisnotmine'} - json['threshold_rule']['query'].append(project_constraint) - headers = {} - headers.update(self.auth_headers) - headers['X-Roles'] = 'admin' - self.post_json('/alarms', params=json, status=201, - headers=headers) - alarms = list(self.alarm_conn.get_alarms(enabled=False)) - self.assertEqual(1, len(alarms)) - self.assertEqual('auseridthatisnotmine', alarms[0].user_id) - self.assertEqual('aprojectidthatisnotmine', alarms[0].project_id) - self._add_default_threshold_rule(json) - if alarms[0].name == 'added_alarm': - for key in json: - if key.endswith('_rule'): - storage_key = 'rule' - if explicit_project_constraint: - self.assertEqual(json[key], - getattr(alarms[0], storage_key)) - else: - query = getattr(alarms[0], storage_key).get('query') - self.assertEqual(2, len(query)) - implicit_constraint = { - u'field': u'project_id', - u'value': u'aprojectidthatisnotmine', - u'op': u'eq' - } - self.assertEqual(implicit_constraint, query[1]) - else: - self.assertEqual(json[key], getattr(alarms[0], key)) - else: - self.fail("Alarm not found") - - def test_post_alarm_as_admin_explicit_project_constraint(self): - """Test the creation of an alarm as admin for another project. - - With an explicit query constraint on the owner's project ID. - """ - self._do_test_post_alarm_as_admin(True) - - def test_post_alarm_as_admin_implicit_project_constraint(self): - """Test the creation of an alarm as admin for another project. - - Test without an explicit query constraint on the owner's project ID. - """ - self._do_test_post_alarm_as_admin(False) - - def test_post_alarm_as_admin_no_user(self): - """Test the creation of an alarm. - - Test the creation of an alarm as admin for another project but - forgetting to set the values. - """ - json = { - 'enabled': False, - 'name': 'added_alarm', - 'state': 'ok', - 'type': 'threshold', - 'project_id': 'aprojectidthatisnotmine', - 'threshold_rule': { - 'meter_name': 'ameter', - 'query': [{'field': 'metadata.field', - 'op': 'eq', - 'value': '5', - 'type': 'string'}, - {'field': 'project_id', 'op': 'eq', - 'value': 'aprojectidthatisnotmine'}], - 'comparison_operator': 'le', - 'statistic': 'count', - 'threshold': 50, - 'evaluation_periods': 3, - 'period': 180, - } - } - headers = {} - headers.update(self.auth_headers) - headers['X-Roles'] = 'admin' - self.post_json('/alarms', params=json, status=201, - headers=headers) - alarms = list(self.alarm_conn.get_alarms(enabled=False)) - self.assertEqual(1, len(alarms)) - self.assertEqual(self.auth_headers['X-User-Id'], alarms[0].user_id) - self.assertEqual('aprojectidthatisnotmine', alarms[0].project_id) - self._verify_alarm(json, alarms[0], 'added_alarm') - - def test_post_alarm_as_admin_no_project(self): - """Test the creation of an alarm. - - Test the creation of an alarm as admin for another project but - forgetting to set the values. - """ - json = { - 'enabled': False, - 'name': 'added_alarm', - 'state': 'ok', - 'type': 'threshold', - 'user_id': 'auseridthatisnotmine', - 'threshold_rule': { - 'meter_name': 'ameter', - 'query': [{'field': 'metadata.field', - 'op': 'eq', - 'value': '5', - 'type': 'string'}, - {'field': 'project_id', 'op': 'eq', - 'value': 'aprojectidthatisnotmine'}], - 'comparison_operator': 'le', - 'statistic': 'count', - 'threshold': 50, - 'evaluation_periods': 3, - 'period': 180, - } - } - headers = {} - headers.update(self.auth_headers) - headers['X-Roles'] = 'admin' - self.post_json('/alarms', params=json, status=201, - headers=headers) - alarms = list(self.alarm_conn.get_alarms(enabled=False)) - self.assertEqual(1, len(alarms)) - self.assertEqual('auseridthatisnotmine', alarms[0].user_id) - self.assertEqual(self.auth_headers['X-Project-Id'], - alarms[0].project_id) - self._verify_alarm(json, alarms[0], 'added_alarm') - - @staticmethod - def _alarm_representation_owned_by(identifiers): - json = { - 'name': 'added_alarm', - 'enabled': False, - 'type': 'threshold', - 'ok_actions': ['http://something/ok'], - 'threshold_rule': { - 'meter_name': 'ameter', - 'query': [{'field': 'metadata.field', - 'op': 'eq', - 'value': '5', - 'type': 'string'}], - 'comparison_operator': 'le', - 'statistic': 'count', - 'threshold': 50, - 'evaluation_periods': 3, - 'period': 180, - } - } - for aspect, id in six.iteritems(identifiers): - json['%s_id' % aspect] = id - return json - - def _do_test_post_alarm_as_nonadmin_on_behalf_of_another(self, - identifiers): - """Test posting an alarm. - - Test that posting an alarm as non-admin on behalf of another - user/project fails with an explicit 401 instead of reverting - to the requestor's identity. - """ - json = self._alarm_representation_owned_by(identifiers) - headers = {} - headers.update(self.auth_headers) - headers['X-Roles'] = 'demo' - resp = self.post_json('/alarms', params=json, status=401, - headers=headers) - aspect = 'user' if 'user' in identifiers else 'project' - params = dict(aspect=aspect, id=identifiers[aspect]) - self.assertEqual("Not Authorized to access %(aspect)s %(id)s" % params, - jsonutils.loads(resp.body)['error_message'] - ['faultstring']) - - def test_post_alarm_as_nonadmin_on_behalf_of_another_user(self): - identifiers = dict(user='auseridthatisnotmine') - self._do_test_post_alarm_as_nonadmin_on_behalf_of_another(identifiers) - - def test_post_alarm_as_nonadmin_on_behalf_of_another_project(self): - identifiers = dict(project='aprojectidthatisnotmine') - self._do_test_post_alarm_as_nonadmin_on_behalf_of_another(identifiers) - - def test_post_alarm_as_nonadmin_on_behalf_of_another_creds(self): - identifiers = dict(user='auseridthatisnotmine', - project='aprojectidthatisnotmine') - self._do_test_post_alarm_as_nonadmin_on_behalf_of_another(identifiers) - - def _do_test_post_alarm_as_nonadmin_on_behalf_of_self(self, identifiers): - """Test posting an alarm. - - Test posting an alarm as non-admin on behalf of own user/project - creates alarm associated with the requestor's identity. - """ - json = self._alarm_representation_owned_by(identifiers) - headers = {} - headers.update(self.auth_headers) - headers['X-Roles'] = 'demo' - self.post_json('/alarms', params=json, status=201, headers=headers) - alarms = list(self.alarm_conn.get_alarms(enabled=False)) - self.assertEqual(1, len(alarms)) - self.assertEqual(alarms[0].user_id, - self.auth_headers['X-User-Id']) - self.assertEqual(alarms[0].project_id, - self.auth_headers['X-Project-Id']) - - def test_post_alarm_as_nonadmin_on_behalf_of_own_user(self): - identifiers = dict(user=self.auth_headers['X-User-Id']) - self._do_test_post_alarm_as_nonadmin_on_behalf_of_self(identifiers) - - def test_post_alarm_as_nonadmin_on_behalf_of_own_project(self): - identifiers = dict(project=self.auth_headers['X-Project-Id']) - self._do_test_post_alarm_as_nonadmin_on_behalf_of_self(identifiers) - - def test_post_alarm_as_nonadmin_on_behalf_of_own_creds(self): - identifiers = dict(user=self.auth_headers['X-User-Id'], - project=self.auth_headers['X-Project-Id']) - self._do_test_post_alarm_as_nonadmin_on_behalf_of_self(identifiers) - - def test_post_alarm_combination(self): - json = { - 'enabled': False, - 'name': 'added_alarm', - 'state': 'ok', - 'type': 'combination', - 'ok_actions': ['http://something/ok'], - 'alarm_actions': ['http://something/alarm'], - 'insufficient_data_actions': ['http://something/no'], - 'repeat_actions': True, - 'combination_rule': { - 'alarm_ids': ['a', - 'b'], - 'operator': 'and', - } - } - self.post_json('/alarms', params=json, status=201, - headers=self.auth_headers) - alarms = list(self.alarm_conn.get_alarms(enabled=False)) - self.assertEqual(1, len(alarms)) - if alarms[0].name == 'added_alarm': - for key in json: - if key.endswith('_rule'): - storage_key = 'rule' - else: - storage_key = key - self.assertEqual(json[key], getattr(alarms[0], storage_key)) - else: - self.fail("Alarm not found") - - def test_post_combination_alarm_as_user_with_unauthorized_alarm(self): - """Test posting a combination alarm. - - Test that post a combination alarm as normal user/project - with an alarm_id unauthorized for this project/user - """ - json = { - 'enabled': False, - 'name': 'added_alarm', - 'state': 'ok', - 'type': 'combination', - 'ok_actions': ['http://something/ok'], - 'alarm_actions': ['http://something/alarm'], - 'insufficient_data_actions': ['http://something/no'], - 'repeat_actions': True, - 'combination_rule': { - 'alarm_ids': ['a', - 'b'], - 'operator': 'and', - } - } - an_other_user_auth = {'X-User-Id': str(uuid.uuid4()), - 'X-Project-Id': str(uuid.uuid4())} - resp = self.post_json('/alarms', params=json, status=404, - headers=an_other_user_auth) - self.assertEqual("Alarm a not found in project " - "%s" % - an_other_user_auth['X-Project-Id'], - jsonutils.loads(resp.body)['error_message'] - ['faultstring']) - - def test_post_combination_alarm_as_admin_on_behalf_of_an_other_user(self): - """Test posting a combination alarm. - - Test that post a combination alarm as admin on behalf of an other - user/project with an alarm_id unauthorized for this project/user - """ - json = { - 'enabled': False, - 'name': 'added_alarm', - 'state': 'ok', - 'user_id': 'auseridthatisnotmine', - 'project_id': 'aprojectidthatisnotmine', - 'type': 'combination', - 'ok_actions': ['http://something/ok'], - 'alarm_actions': ['http://something/alarm'], - 'insufficient_data_actions': ['http://something/no'], - 'repeat_actions': True, - 'combination_rule': { - 'alarm_ids': ['a', - 'b'], - 'operator': 'and', - } - } - - headers = {} - headers.update(self.auth_headers) - headers['X-Roles'] = 'admin' - resp = self.post_json('/alarms', params=json, status=404, - headers=headers) - self.assertEqual("Alarm a not found in project " - "aprojectidthatisnotmine", - jsonutils.loads(resp.body)['error_message'] - ['faultstring']) - - def test_post_combination_alarm_with_reasonable_description(self): - """Test posting a combination alarm. - - Test that post a combination alarm with two blanks around the - operator in alarm description. - """ - json = { - 'enabled': False, - 'name': 'added_alarm', - 'state': 'ok', - 'type': 'combination', - 'ok_actions': ['http://something/ok'], - 'alarm_actions': ['http://something/alarm'], - 'insufficient_data_actions': ['http://something/no'], - 'repeat_actions': True, - 'combination_rule': { - 'alarm_ids': ['a', - 'b'], - 'operator': 'and', - } - } - self.post_json('/alarms', params=json, status=201, - headers=self.auth_headers) - alarms = list(self.alarm_conn.get_alarms(enabled=False)) - self.assertEqual(1, len(alarms)) - self.assertEqual(u'Combined state of alarms a and b', - alarms[0].description) - - def test_post_combination_alarm_as_admin_success_owner_unset(self): - self._do_post_combination_alarm_as_admin_success(False) - - def test_post_combination_alarm_as_admin_success_owner_set(self): - self._do_post_combination_alarm_as_admin_success(True) - - def test_post_combination_alarm_with_threshold_rule(self): - """Test the creation of an combination alarm with threshold rule.""" - json = { - 'enabled': False, - 'name': 'added_alarm', - 'state': 'ok', - 'type': 'combination', - 'ok_actions': ['http://something/ok'], - 'alarm_actions': ['http://something/alarm'], - 'insufficient_data_actions': ['http://something/no'], - 'repeat_actions': True, - 'threshold_rule': { - 'meter_name': 'ameter', - 'query': [{'field': 'metadata.field', - 'op': 'eq', - 'value': '5', - 'type': 'string'}], - 'comparison_operator': 'le', - 'statistic': 'count', - 'threshold': 50, - 'evaluation_periods': '3', - 'period': '180', - } - } - resp = self.post_json('/alarms', params=json, - expect_errors=True, status=400, - headers=self.auth_headers) - self.assertEqual( - "combination_rule must be set for combination type alarm", - resp.json['error_message']['faultstring']) - - def test_post_threshold_alarm_with_combination_rule(self): - """Test the creation of an threshold alarm with combination rule.""" - json = { - 'enabled': False, - 'name': 'added_alarm', - 'state': 'ok', - 'type': 'threshold', - 'ok_actions': ['http://something/ok'], - 'alarm_actions': ['http://something/alarm'], - 'insufficient_data_actions': ['http://something/no'], - 'repeat_actions': True, - 'combination_rule': { - 'alarm_ids': ['a', - 'b'], - 'operator': 'and', - } - } - resp = self.post_json('/alarms', params=json, - expect_errors=True, status=400, - headers=self.auth_headers) - self.assertEqual( - "threshold_rule must be set for threshold type alarm", - resp.json['error_message']['faultstring']) - - def _do_post_combination_alarm_as_admin_success(self, owner_is_set): - """Test posting a combination alarm. - - Test that post a combination alarm as admin on behalf of nobody - with an alarm_id of someone else, with owner set or not - """ - json = { - 'enabled': False, - 'name': 'added_alarm', - 'state': 'ok', - 'type': 'combination', - 'ok_actions': ['http://something/ok'], - 'alarm_actions': ['http://something/alarm'], - 'insufficient_data_actions': ['http://something/no'], - 'repeat_actions': True, - 'combination_rule': { - 'alarm_ids': ['a', - 'b'], - 'operator': 'and', - } - } - an_other_admin_auth = {'X-User-Id': str(uuid.uuid4()), - 'X-Project-Id': str(uuid.uuid4()), - 'X-Roles': 'admin'} - if owner_is_set: - json['project_id'] = an_other_admin_auth['X-Project-Id'] - json['user_id'] = an_other_admin_auth['X-User-Id'] - - self.post_json('/alarms', params=json, status=201, - headers=an_other_admin_auth) - alarms = list(self.alarm_conn.get_alarms(enabled=False)) - if alarms[0].name == 'added_alarm': - for key in json: - if key.endswith('_rule'): - storage_key = 'rule' - else: - storage_key = key - self.assertEqual(json[key], getattr(alarms[0], storage_key)) - else: - self.fail("Alarm not found") - - def test_post_invalid_alarm_combination(self): - """Test that post a combination alarm with a not existing alarm id.""" - json = { - 'enabled': False, - 'name': 'added_alarm', - 'state': 'ok', - 'type': 'combination', - 'ok_actions': ['http://something/ok'], - 'alarm_actions': ['http://something/alarm'], - 'insufficient_data_actions': ['http://something/no'], - 'repeat_actions': True, - 'combination_rule': { - 'alarm_ids': ['not_exists', - 'b'], - 'operator': 'and', - } - } - self.post_json('/alarms', params=json, status=404, - headers=self.auth_headers) - alarms = list(self.alarm_conn.get_alarms(enabled=False)) - self.assertEqual(0, len(alarms)) - - def test_post_alarm_combination_duplicate_alarm_ids(self): - """Test combination alarm doesn't allow duplicate alarm ids.""" - json_body = { - 'name': 'dup_alarm_id', - 'type': 'combination', - 'combination_rule': { - 'alarm_ids': ['a', 'a', 'd', 'a', 'c', 'c', 'b'], - } - } - self.post_json('/alarms', params=json_body, status=201, - headers=self.auth_headers) - alarms = list(self.alarm_conn.get_alarms(name='dup_alarm_id')) - self.assertEqual(1, len(alarms)) - self.assertEqual(['a', 'd', 'c', 'b'], - alarms[0].rule.get('alarm_ids')) - - def _test_post_alarm_combination_rule_less_than_two_alarms(self, - alarm_ids=None): - json_body = { - 'name': 'one_alarm_in_combination_rule', - 'type': 'combination', - 'combination_rule': { - 'alarm_ids': alarm_ids or [] - } - } - - resp = self.post_json('/alarms', params=json_body, - expect_errors=True, status=400, - headers=self.auth_headers) - self.assertEqual( - 'Alarm combination rule should contain at' - ' least two different alarm ids.', - resp.json['error_message']['faultstring']) - - def test_post_alarm_combination_rule_with_no_alarm(self): - self._test_post_alarm_combination_rule_less_than_two_alarms() - - def test_post_alarm_combination_rule_with_one_alarm(self): - self._test_post_alarm_combination_rule_less_than_two_alarms(['a']) - - def test_post_alarm_combination_rule_with_two_same_alarms(self): - self._test_post_alarm_combination_rule_less_than_two_alarms(['a', - 'a']) - - def test_post_alarm_with_duplicate_actions(self): - body = { - 'name': 'dup-alarm-actions', - 'type': 'combination', - 'combination_rule': { - 'alarm_ids': ['a', 'b'], - }, - 'alarm_actions': ['http://no.where', 'http://no.where'] - } - resp = self.post_json('/alarms', params=body, - headers=self.auth_headers) - self.assertEqual(201, resp.status_code) - alarms = list(self.alarm_conn.get_alarms(name='dup-alarm-actions')) - self.assertEqual(1, len(alarms)) - self.assertEqual(['http://no.where'], alarms[0].alarm_actions) - - def test_post_alarm_with_too_many_actions(self): - self.CONF.set_override('alarm_max_actions', 1, group='alarm') - body = { - 'name': 'alarm-with-many-actions', - 'type': 'combination', - 'combination_rule': { - 'alarm_ids': ['a', 'b'], - }, - 'alarm_actions': ['http://no.where', 'http://no.where2'] - } - resp = self.post_json('/alarms', params=body, expect_errors=True, - headers=self.auth_headers) - self.assertEqual(400, resp.status_code) - self.assertEqual("alarm_actions count exceeds maximum value 1", - resp.json['error_message']['faultstring']) - - def test_post_alarm_normal_user_set_log_actions(self): - body = { - 'name': 'log_alarm_actions', - 'type': 'combination', - 'combination_rule': { - 'alarm_ids': ['a', 'b'], - }, - 'alarm_actions': ['log://'] - } - resp = self.post_json('/alarms', params=body, expect_errors=True, - headers=self.auth_headers) - self.assertEqual(401, resp.status_code) - expected_msg = ("You are not authorized to create action: log://") - self.assertEqual(expected_msg, - resp.json['error_message']['faultstring']) - - def test_post_alarm_normal_user_set_test_actions(self): - body = { - 'name': 'test_alarm_actions', - 'type': 'combination', - 'combination_rule': { - 'alarm_ids': ['a', 'b'], - }, - 'alarm_actions': ['test://'] - } - resp = self.post_json('/alarms', params=body, expect_errors=True, - headers=self.auth_headers) - self.assertEqual(401, resp.status_code) - expected_msg = ("You are not authorized to create action: test://") - self.assertEqual(expected_msg, - resp.json['error_message']['faultstring']) - - def test_post_alarm_admin_user_set_log_test_actions(self): - body = { - 'name': 'admin_alarm_actions', - 'type': 'combination', - 'combination_rule': { - 'alarm_ids': ['a', 'b'], - }, - 'alarm_actions': ['test://', 'log://'] - } - headers = self.auth_headers - headers['X-Roles'] = 'admin' - self.post_json('/alarms', params=body, status=201, - headers=headers) - alarms = list(self.alarm_conn.get_alarms(name='admin_alarm_actions')) - self.assertEqual(1, len(alarms)) - self.assertEqual(['test://', 'log://'], - alarms[0].alarm_actions) - - def test_post_alarm_without_actions(self): - body = { - 'name': 'alarm_actions_none', - 'type': 'combination', - 'combination_rule': { - 'alarm_ids': ['a', 'b'], - }, - 'alarm_actions': None - } - headers = self.auth_headers - headers['X-Roles'] = 'admin' - self.post_json('/alarms', params=body, status=201, - headers=headers) - alarms = list(self.alarm_conn.get_alarms(name='alarm_actions_none')) - self.assertEqual(1, len(alarms)) - - # FIXME(sileht): This should really returns [] not None - # but the mongodb and sql just store the json dict as is... - # migration script for sql will be a mess because we have - # to parse all JSON :( - # I guess we assume that wsme convert the None input to [] - # because of the array type, but it won't... - self.assertIsNone(alarms[0].alarm_actions) - - def test_post_alarm_trust(self): - json = { - 'name': 'added_alarm_defaults', - 'type': 'threshold', - 'ok_actions': ['trust+http://my.server:1234/foo'], - 'threshold_rule': { - 'meter_name': 'ameter', - 'threshold': 300.0 - } - } - auth = mock.Mock() - trust_client = mock.Mock() - with mock.patch('ceilometer.keystone_client.get_v3_client') as client: - client.return_value = mock.Mock( - auth_ref=mock.Mock(user_id='my_user')) - with mock.patch('keystoneclient.v3.client.Client') as sub_client: - sub_client.return_value = trust_client - trust_client.trusts.create.return_value = mock.Mock(id='5678') - self.post_json('/alarms', params=json, status=201, - headers=self.auth_headers, - extra_environ={'keystone.token_auth': auth}) - trust_client.trusts.create.assert_called_once_with( - trustor_user=self.auth_headers['X-User-Id'], - trustee_user='my_user', - project=self.auth_headers['X-Project-Id'], - impersonation=True, - role_names=[]) - alarms = list(self.alarm_conn.get_alarms()) - for alarm in alarms: - if alarm.name == 'added_alarm_defaults': - self.assertEqual( - ['trust+http://5678:delete@my.server:1234/foo'], - alarm.ok_actions) - break - else: - self.fail("Alarm not found") - - with mock.patch('ceilometer.keystone_client.get_v3_client') as client: - client.return_value = mock.Mock( - auth_ref=mock.Mock(user_id='my_user')) - with mock.patch('keystoneclient.v3.client.Client') as sub_client: - sub_client.return_value = trust_client - self.delete('/alarms/%s' % alarm.alarm_id, - headers=self.auth_headers, - status=204, - extra_environ={'keystone.token_auth': auth}) - trust_client.trusts.delete.assert_called_once_with('5678') - - def test_put_alarm(self): - json = { - 'enabled': False, - 'name': 'name_put', - 'state': 'ok', - 'type': 'threshold', - 'severity': 'critical', - 'ok_actions': ['http://something/ok'], - 'alarm_actions': ['http://something/alarm'], - 'insufficient_data_actions': ['http://something/no'], - 'repeat_actions': True, - 'threshold_rule': { - 'meter_name': 'ameter', - 'query': [{'field': 'metadata.field', - 'op': 'eq', - 'value': '5', - 'type': 'string'}], - 'comparison_operator': 'le', - 'statistic': 'count', - 'threshold': 50, - 'evaluation_periods': 3, - 'period': 180, - } - } - data = self.get_json('/alarms', - q=[{'field': 'name', - 'value': 'name1', - }]) - self.assertEqual(1, len(data)) - alarm_id = data[0]['alarm_id'] - - self.put_json('/alarms/%s' % alarm_id, - params=json, - headers=self.auth_headers) - alarm = list(self.alarm_conn.get_alarms(alarm_id=alarm_id, - enabled=False))[0] - json['threshold_rule']['query'].append({ - 'field': 'project_id', 'op': 'eq', - 'value': self.auth_headers['X-Project-Id']}) - self._verify_alarm(json, alarm) - - def test_put_alarm_as_admin(self): - json = { - 'user_id': 'myuserid', - 'project_id': 'myprojectid', - 'enabled': False, - 'name': 'name_put', - 'state': 'ok', - 'type': 'threshold', - 'severity': 'critical', - 'ok_actions': ['http://something/ok'], - 'alarm_actions': ['http://something/alarm'], - 'insufficient_data_actions': ['http://something/no'], - 'repeat_actions': True, - 'threshold_rule': { - 'meter_name': 'ameter', - 'query': [{'field': 'metadata.field', - 'op': 'eq', - 'value': '5', - 'type': 'string'}, - {'field': 'project_id', 'op': 'eq', - 'value': 'myprojectid'}], - 'comparison_operator': 'le', - 'statistic': 'count', - 'threshold': 50, - 'evaluation_periods': 3, - 'period': 180, - } - } - headers = {} - headers.update(self.auth_headers) - headers['X-Roles'] = 'admin' - - data = self.get_json('/alarms', - headers=headers, - q=[{'field': 'name', - 'value': 'name1', - }]) - self.assertEqual(1, len(data)) - alarm_id = data[0]['alarm_id'] - - self.put_json('/alarms/%s' % alarm_id, - params=json, - headers=headers) - alarm = list(self.alarm_conn.get_alarms(alarm_id=alarm_id, - enabled=False))[0] - self.assertEqual('myuserid', alarm.user_id) - self.assertEqual('myprojectid', alarm.project_id) - self._verify_alarm(json, alarm) - - def test_put_alarm_wrong_field(self): - json = { - 'this_can_not_be_correct': 'ha', - 'enabled': False, - 'name': 'name1', - 'state': 'ok', - 'type': 'threshold', - 'severity': 'critical', - 'ok_actions': ['http://something/ok'], - 'alarm_actions': ['http://something/alarm'], - 'insufficient_data_actions': ['http://something/no'], - 'repeat_actions': True, - 'threshold_rule': { - 'meter_name': 'ameter', - 'query': [{'field': 'metadata.field', - 'op': 'eq', - 'value': '5', - 'type': 'string'}], - 'comparison_operator': 'le', - 'statistic': 'count', - 'threshold': 50, - 'evaluation_periods': 3, - 'period': 180, - } - } - data = self.get_json('/alarms', - q=[{'field': 'name', - 'value': 'name1', - }]) - self.assertEqual(1, len(data)) - alarm_id = data[0]['alarm_id'] - - resp = self.put_json('/alarms/%s' % alarm_id, - expect_errors=True, - params=json, - headers=self.auth_headers) - self.assertEqual(400, resp.status_code) - - def test_put_alarm_with_existing_name(self): - """Test that update a threshold alarm with an existing name.""" - json = { - 'enabled': False, - 'name': 'name1', - 'state': 'ok', - 'type': 'threshold', - 'severity': 'critical', - 'ok_actions': ['http://something/ok'], - 'alarm_actions': ['http://something/alarm'], - 'insufficient_data_actions': ['http://something/no'], - 'repeat_actions': True, - 'threshold_rule': { - 'meter_name': 'ameter', - 'query': [{'field': 'metadata.field', - 'op': 'eq', - 'value': '5', - 'type': 'string'}], - 'comparison_operator': 'le', - 'statistic': 'count', - 'threshold': 50, - 'evaluation_periods': 3, - 'period': 180, - } - } - data = self.get_json('/alarms', - q=[{'field': 'name', - 'value': 'name2', - }]) - self.assertEqual(1, len(data)) - alarm_id = data[0]['alarm_id'] - - resp = self.put_json('/alarms/%s' % alarm_id, - expect_errors=True, status=409, - params=json, - headers=self.auth_headers) - self.assertEqual( - 'Alarm with name=name1 exists', - resp.json['error_message']['faultstring']) - - def test_put_invalid_alarm_actions(self): - json = { - 'enabled': False, - 'name': 'name1', - 'state': 'ok', - 'type': 'threshold', - 'severity': 'critical', - 'ok_actions': ['spam://something/ok'], - 'alarm_actions': ['http://something/alarm'], - 'insufficient_data_actions': ['http://something/no'], - 'repeat_actions': True, - 'threshold_rule': { - 'meter_name': 'ameter', - 'query': [{'field': 'metadata.field', - 'op': 'eq', - 'value': '5', - 'type': 'string'}], - 'comparison_operator': 'le', - 'statistic': 'count', - 'threshold': 50, - 'evaluation_periods': 3, - 'period': 180, - } - } - data = self.get_json('/alarms', - q=[{'field': 'name', - 'value': 'name2', - }]) - self.assertEqual(1, len(data)) - alarm_id = data[0]['alarm_id'] - - resp = self.put_json('/alarms/%s' % alarm_id, - expect_errors=True, status=400, - params=json, - headers=self.auth_headers) - self.assertEqual( - 'Unsupported action spam://something/ok', - resp.json['error_message']['faultstring']) - - def test_put_alarm_combination_cannot_specify_itself(self): - json = { - 'name': 'name4', - 'type': 'combination', - 'combination_rule': { - 'alarm_ids': ['d', 'a'], - } - } - - data = self.get_json('/alarms', - q=[{'field': 'name', - 'value': 'name4', - }]) - self.assertEqual(1, len(data)) - alarm_id = data[0]['alarm_id'] - - resp = self.put_json('/alarms/%s' % alarm_id, - expect_errors=True, status=400, - params=json, - headers=self.auth_headers) - - msg = 'Cannot specify alarm %s itself in combination rule' % alarm_id - self.assertEqual(msg, resp.json['error_message']['faultstring']) - - def _test_put_alarm_combination_rule_less_than_two_alarms(self, - alarm_ids=None): - json_body = { - 'name': 'name4', - 'type': 'combination', - 'combination_rule': { - 'alarm_ids': alarm_ids or [] - } - } - - data = self.get_json('/alarms', - q=[{'field': 'name', - 'value': 'name4', - }]) - self.assertEqual(1, len(data)) - alarm_id = data[0]['alarm_id'] - - resp = self.put_json('/alarms/%s' % alarm_id, params=json_body, - expect_errors=True, status=400, - headers=self.auth_headers) - self.assertEqual( - 'Alarm combination rule should contain at' - ' least two different alarm ids.', - resp.json['error_message']['faultstring']) - - def test_put_alarm_combination_rule_with_no_alarm(self): - self._test_put_alarm_combination_rule_less_than_two_alarms() - - def test_put_alarm_combination_rule_with_one_alarm(self): - self._test_put_alarm_combination_rule_less_than_two_alarms(['a']) - - def test_put_alarm_combination_rule_with_two_same_alarm_itself(self): - self._test_put_alarm_combination_rule_less_than_two_alarms(['d', - 'd']) - - def test_put_combination_alarm_with_duplicate_ids(self): - """Test combination alarm doesn't allow duplicate alarm ids.""" - alarms = self.get_json('/alarms', - q=[{'field': 'name', - 'value': 'name4', - }]) - self.assertEqual(1, len(alarms)) - alarm_id = alarms[0]['alarm_id'] - - json_body = { - 'name': 'name4', - 'type': 'combination', - 'combination_rule': { - 'alarm_ids': ['c', 'a', 'b', 'a', 'c', 'b'], - } - } - self.put_json('/alarms/%s' % alarm_id, - params=json_body, status=200, - headers=self.auth_headers) - - alarms = list(self.alarm_conn.get_alarms(alarm_id=alarm_id)) - self.assertEqual(1, len(alarms)) - self.assertEqual(['c', 'a', 'b'], alarms[0].rule.get('alarm_ids')) - - def test_put_alarm_trust(self): - data = self._get_alarm('a') - data.update({'ok_actions': ['trust+http://something/ok']}) - trust_client = mock.Mock() - with mock.patch('ceilometer.keystone_client.get_v3_client') as client: - client.return_value = mock.Mock( - auth_ref=mock.Mock(user_id='my_user')) - with mock.patch('keystoneclient.v3.client.Client') as sub_client: - sub_client.return_value = trust_client - trust_client.trusts.create.return_value = mock.Mock(id='5678') - self.put_json('/alarms/%s' % data['alarm_id'], - params=data, - headers=self.auth_headers) - data = self._get_alarm('a') - self.assertEqual( - ['trust+http://5678:delete@something/ok'], data['ok_actions']) - - data.update({'ok_actions': ['http://no-trust-something/ok']}) - - with mock.patch('ceilometer.keystone_client.get_v3_client') as client: - client.return_value = mock.Mock( - auth_ref=mock.Mock(user_id='my_user')) - with mock.patch('keystoneclient.v3.client.Client') as sub_client: - sub_client.return_value = trust_client - self.put_json('/alarms/%s' % data['alarm_id'], - params=data, - headers=self.auth_headers) - trust_client.trusts.delete.assert_called_once_with('5678') - - data = self._get_alarm('a') - self.assertEqual( - ['http://no-trust-something/ok'], data['ok_actions']) - - def test_delete_alarm(self): - data = self.get_json('/alarms') - self.assertEqual(7, len(data)) - - resp = self.delete('/alarms/%s' % data[0]['alarm_id'], - headers=self.auth_headers, - status=204) - self.assertEqual(b'', resp.body) - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(6, len(alarms)) - - def test_get_state_alarm(self): - data = self.get_json('/alarms') - self.assertEqual(7, len(data)) - - resp = self.get_json('/alarms/%s/state' % data[0]['alarm_id'], - headers=self.auth_headers) - self.assertEqual(resp, data[0]['state']) - - def test_set_state_alarm(self): - data = self.get_json('/alarms') - self.assertEqual(7, len(data)) - - resp = self.put_json('/alarms/%s/state' % data[0]['alarm_id'], - headers=self.auth_headers, - params='alarm') - alarms = list(self.alarm_conn.get_alarms(alarm_id=data[0]['alarm_id'])) - self.assertEqual(1, len(alarms)) - self.assertEqual('alarm', alarms[0].state) - self.assertEqual('alarm', resp.json) - - def test_set_invalid_state_alarm(self): - data = self.get_json('/alarms') - self.assertEqual(7, len(data)) - - self.put_json('/alarms/%s/state' % data[0]['alarm_id'], - headers=self.auth_headers, - params='not valid', - status=400) - - def _get_alarm(self, id): - data = self.get_json('/alarms') - match = [a for a in data if a['alarm_id'] == id] - self.assertEqual(1, len(match), 'alarm %s not found' % id) - return match[0] - - def _get_alarm_history(self, alarm, auth_headers=None, query=None, - expect_errors=False, status=200): - url = '/alarms/%s/history' % alarm['alarm_id'] - if query: - url += '?q.op=%(op)s&q.value=%(value)s&q.field=%(field)s' % query - resp = self.get_json(url, - headers=auth_headers or self.auth_headers, - expect_errors=expect_errors) - if expect_errors: - self.assertEqual(status, resp.status_code) - return resp - - def _update_alarm(self, alarm, updated_data, auth_headers=None): - data = self._get_alarm(alarm['alarm_id']) - data.update(updated_data) - self.put_json('/alarms/%s' % alarm['alarm_id'], - params=data, - headers=auth_headers or self.auth_headers) - - def _delete_alarm(self, alarm, auth_headers=None): - self.delete('/alarms/%s' % alarm['alarm_id'], - headers=auth_headers or self.auth_headers, - status=204) - - def _assert_is_subset(self, expected, actual): - for k, v in six.iteritems(expected): - self.assertEqual(v, actual.get(k), 'mismatched field: %s' % k) - self.assertIsNotNone(actual['event_id']) - - def _assert_in_json(self, expected, actual): - actual = jsonutils.dumps(jsonutils.loads(actual), sort_keys=True) - for k, v in six.iteritems(expected): - fragment = jsonutils.dumps({k: v}, sort_keys=True)[1:-1] - self.assertIn(fragment, actual, - '%s not in %s' % (fragment, actual)) - - def test_record_alarm_history_config(self): - self.CONF.set_override('record_history', False, group='alarm') - alarm = self._get_alarm('a') - history = self._get_alarm_history(alarm) - self.assertEqual([], history) - self._update_alarm(alarm, dict(name='renamed')) - history = self._get_alarm_history(alarm) - self.assertEqual([], history) - self.CONF.set_override('record_history', True, group='alarm') - self._update_alarm(alarm, dict(name='foobar')) - history = self._get_alarm_history(alarm) - self.assertEqual(1, len(history)) - - def test_record_alarm_history_severity(self): - alarm = self._get_alarm('a') - history = self._get_alarm_history(alarm) - self.assertEqual([], history) - self.assertEqual('critical', alarm['severity']) - - self._update_alarm(alarm, dict(severity='low')) - new_alarm = self._get_alarm('a') - history = self._get_alarm_history(alarm) - self.assertEqual(1, len(history)) - self.assertEqual(jsonutils.dumps({'severity': 'low'}), - history[0]['detail']) - self.assertEqual('low', new_alarm['severity']) - - def test_redundant_update_alarm_property_no_history_change(self): - alarm = self._get_alarm('a') - history = self._get_alarm_history(alarm) - self.assertEqual([], history) - self.assertEqual('critical', alarm['severity']) - - self._update_alarm(alarm, dict(severity='low')) - new_alarm = self._get_alarm('a') - history = self._get_alarm_history(alarm) - self.assertEqual(1, len(history)) - self.assertEqual(jsonutils.dumps({'severity': 'low'}), - history[0]['detail']) - self.assertEqual('low', new_alarm['severity']) - - self._update_alarm(alarm, dict(severity='low')) - updated_alarm = self._get_alarm('a') - updated_history = self._get_alarm_history(updated_alarm) - self.assertEqual(1, len(updated_history)) - self.assertEqual(jsonutils.dumps({'severity': 'low'}), - updated_history[0]['detail']) - self.assertEqual(history, updated_history) - - def test_record_alarm_history_statistic(self): - alarm = self._get_alarm('a') - history = self._get_alarm_history(alarm) - self.assertEqual([], history) - self.assertEqual('avg', alarm['threshold_rule']['statistic']) - - rule = alarm['threshold_rule'].copy() - rule['statistic'] = 'min' - data = dict(threshold_rule=rule) - self._update_alarm(alarm, data) - new_alarm = self._get_alarm('a') - history = self._get_alarm_history(new_alarm) - self.assertEqual(1, len(history)) - self.assertEqual("min", jsonutils.loads(history[0]['detail']) - ['rule']["statistic"]) - self.assertEqual('min', new_alarm['threshold_rule']['statistic']) - - def test_get_recorded_alarm_history_on_create(self): - new_alarm = { - 'name': 'new_alarm', - 'type': 'threshold', - 'threshold_rule': { - 'meter_name': 'ameter', - 'query': [], - 'comparison_operator': 'le', - 'statistic': 'max', - 'threshold': 42.0, - 'period': 60, - 'evaluation_periods': 1, - } - } - self.post_json('/alarms', params=new_alarm, status=201, - headers=self.auth_headers) - - alarms = self.get_json('/alarms', - q=[{'field': 'name', - 'value': 'new_alarm', - }]) - self.assertEqual(1, len(alarms)) - alarm = alarms[0] - - history = self._get_alarm_history(alarm) - self.assertEqual(1, len(history)) - self._assert_is_subset(dict(alarm_id=alarm['alarm_id'], - on_behalf_of=alarm['project_id'], - project_id=alarm['project_id'], - type='creation', - user_id=alarm['user_id']), - history[0]) - self._add_default_threshold_rule(new_alarm) - new_alarm['rule'] = new_alarm['threshold_rule'] - del new_alarm['threshold_rule'] - new_alarm['rule']['query'].append({ - 'field': 'project_id', 'op': 'eq', - 'value': self.auth_headers['X-Project-Id']}) - self._assert_in_json(new_alarm, history[0]['detail']) - - def _do_test_get_recorded_alarm_history_on_update(self, - data, - type, - detail, - auth=None): - alarm = self._get_alarm('a') - history = self._get_alarm_history(alarm) - self.assertEqual([], history) - self._update_alarm(alarm, data, auth) - history = self._get_alarm_history(alarm) - self.assertEqual(1, len(history)) - project_id = auth['X-Project-Id'] if auth else alarm['project_id'] - user_id = auth['X-User-Id'] if auth else alarm['user_id'] - self._assert_is_subset(dict(alarm_id=alarm['alarm_id'], - detail=detail, - on_behalf_of=alarm['project_id'], - project_id=project_id, - type=type, - user_id=user_id), - history[0]) - - def test_get_recorded_alarm_history_rule_change(self): - data = dict(name='renamed') - detail = '{"name": "renamed"}' - self._do_test_get_recorded_alarm_history_on_update(data, - 'rule change', - detail) - - def test_get_recorded_alarm_history_state_transition_on_behalf_of(self): - # credentials for new non-admin user, on who's behalf the alarm - # is created - member_user = str(uuid.uuid4()) - member_project = str(uuid.uuid4()) - member_auth = {'X-Roles': 'member', - 'X-User-Id': member_user, - 'X-Project-Id': member_project} - new_alarm = { - 'name': 'new_alarm', - 'type': 'threshold', - 'state': 'ok', - 'threshold_rule': { - 'meter_name': 'other_meter', - 'query': [{'field': 'project_id', - 'op': 'eq', - 'value': member_project}], - 'comparison_operator': 'le', - 'statistic': 'max', - 'threshold': 42.0, - 'evaluation_periods': 1, - 'period': 60 - } - } - self.post_json('/alarms', params=new_alarm, status=201, - headers=member_auth) - alarm = self.get_json('/alarms', headers=member_auth)[0] - - # effect a state transition as a new administrative user - admin_user = str(uuid.uuid4()) - admin_project = str(uuid.uuid4()) - admin_auth = {'X-Roles': 'admin', - 'X-User-Id': admin_user, - 'X-Project-Id': admin_project} - data = dict(state='alarm') - self._update_alarm(alarm, data, auth_headers=admin_auth) - - self._add_default_threshold_rule(new_alarm) - new_alarm['rule'] = new_alarm['threshold_rule'] - del new_alarm['threshold_rule'] - - # ensure that both the creation event and state transition - # are visible to the non-admin alarm owner and admin user alike - for auth in [member_auth, admin_auth]: - history = self._get_alarm_history(alarm, auth_headers=auth) - self.assertEqual(2, len(history), 'hist: %s' % history) - self._assert_is_subset(dict(alarm_id=alarm['alarm_id'], - detail='{"state": "alarm"}', - on_behalf_of=alarm['project_id'], - project_id=admin_project, - type='rule change', - user_id=admin_user), - history[0]) - self._assert_is_subset(dict(alarm_id=alarm['alarm_id'], - on_behalf_of=alarm['project_id'], - project_id=member_project, - type='creation', - user_id=member_user), - history[1]) - self._assert_in_json(new_alarm, history[1]['detail']) - - # ensure on_behalf_of cannot be constrained in an API call - query = dict(field='on_behalf_of', - op='eq', - value=alarm['project_id']) - self._get_alarm_history(alarm, auth_headers=auth, query=query, - expect_errors=True, status=400) - - def test_get_recorded_alarm_history_segregation(self): - data = dict(name='renamed') - detail = '{"name": "renamed"}' - self._do_test_get_recorded_alarm_history_on_update(data, - 'rule change', - detail) - auth = {'X-Roles': 'member', - 'X-User-Id': str(uuid.uuid4()), - 'X-Project-Id': str(uuid.uuid4())} - history = self._get_alarm_history(self._get_alarm('a'), auth) - self.assertEqual([], history) - - def test_delete_alarm_history_after_deletion(self): - alarm = self._get_alarm('a') - history = self._get_alarm_history(alarm) - self.assertEqual([], history) - self._update_alarm(alarm, dict(name='renamed')) - history = self._get_alarm_history(alarm) - self.assertEqual(1, len(history)) - alarm = self._get_alarm('a') - self.delete('/alarms/%s' % alarm['alarm_id'], - headers=self.auth_headers, - status=204) - history = self._get_alarm_history(alarm) - self.assertEqual(0, len(history)) - - def test_get_alarm_history_ordered_by_recentness(self): - alarm = self._get_alarm('a') - for i in moves.xrange(10): - self._update_alarm(alarm, dict(name='%s' % i)) - alarm = self._get_alarm('a') - history = self._get_alarm_history(alarm) - self.assertEqual(10, len(history), 'hist: %s' % history) - self._assert_is_subset(dict(alarm_id=alarm['alarm_id'], - type='rule change'), - history[0]) - for i in moves.xrange(1, 11): - detail = '{"name": "%s"}' % (10 - i) - self._assert_is_subset(dict(alarm_id=alarm['alarm_id'], - detail=detail, - type='rule change'), - history[i - 1]) - - def test_get_alarm_history_constrained_by_timestamp(self): - alarm = self._get_alarm('a') - self._update_alarm(alarm, dict(name='renamed')) - after = datetime.datetime.utcnow().isoformat() - query = dict(field='timestamp', op='gt', value=after) - history = self._get_alarm_history(alarm, query=query) - self.assertEqual(0, len(history)) - query['op'] = 'le' - history = self._get_alarm_history(alarm, query=query) - self.assertEqual(1, len(history)) - detail = '{"name": "renamed"}' - self._assert_is_subset(dict(alarm_id=alarm['alarm_id'], - detail=detail, - on_behalf_of=alarm['project_id'], - project_id=alarm['project_id'], - type='rule change', - user_id=alarm['user_id']), - history[0]) - - def test_get_alarm_history_constrained_by_type(self): - alarm = self._get_alarm('a') - self._update_alarm(alarm, dict(name='renamed2')) - query = dict(field='type', op='eq', value='rule change') - history = self._get_alarm_history(alarm, query=query) - self.assertEqual(1, len(history)) - detail = '{"name": "renamed2"}' - self._assert_is_subset(dict(alarm_id=alarm['alarm_id'], - detail=detail, - on_behalf_of=alarm['project_id'], - project_id=alarm['project_id'], - type='rule change', - user_id=alarm['user_id']), - history[0]) - - def test_get_alarm_history_constrained_by_alarm_id_failed(self): - alarm = self._get_alarm('b') - query = dict(field='alarm_id', op='eq', value='b') - resp = self._get_alarm_history(alarm, query=query, - expect_errors=True, status=400) - msg = ('Unknown argument: "alarm_id": unrecognized' - " field in query: [], valid keys: ['project', " - "'search_offset', 'severity', 'timestamp'," - " 'type', 'user']") - msg = msg.format(key=u'alarm_id', value=u'b') - self.assertEqual(msg, - resp.json['error_message']['faultstring']) - - def test_get_alarm_history_constrained_by_not_supported_rule(self): - alarm = self._get_alarm('b') - query = dict(field='abcd', op='eq', value='abcd') - resp = self._get_alarm_history(alarm, query=query, - expect_errors=True, status=400) - msg = ('Unknown argument: "abcd": unrecognized' - " field in query: [], valid keys: ['project', " - "'search_offset', 'severity', 'timestamp'," - " 'type', 'user']") - msg = msg.format(key=u'abcd', value=u'abcd') - self.assertEqual(msg, - resp.json['error_message']['faultstring']) - - def test_get_nonexistent_alarm_history(self): - # the existence of alarm history is independent of the - # continued existence of the alarm itself - history = self._get_alarm_history(dict(alarm_id='foobar')) - self.assertEqual([], history) - - def test_alarms_sends_notification(self): - # Hit the AlarmsController ... - json = { - 'name': 'sent_notification', - 'type': 'threshold', - 'severity': 'low', - 'threshold_rule': { - 'meter_name': 'ameter', - 'comparison_operator': 'gt', - 'threshold': 2.0, - 'statistic': 'avg', - } - - } - endpoint = mock.MagicMock() - target = oslo_messaging.Target(topic="notifications") - listener = messaging.get_notification_listener( - self.transport, [target], [endpoint]) - listener.start() - endpoint.info.side_effect = lambda *args: listener.stop() - self.post_json('/alarms', params=json, headers=self.auth_headers) - listener.wait() - - class PayloadMatcher(object): - def __eq__(self, payload): - return (payload['detail']['name'] == 'sent_notification' and - payload['type'] == 'creation' and - payload['detail']['rule']['meter_name'] == 'ameter' and - set(['alarm_id', 'detail', 'event_id', 'on_behalf_of', - 'project_id', 'timestamp', - 'user_id']).issubset(payload.keys())) - - endpoint.info.assert_called_once_with( - {'resource_uuid': None, - 'domain': None, - 'project_domain': None, - 'auth_token': None, - 'is_admin': False, - 'user': None, - 'tenant': None, - 'read_only': False, - 'show_deleted': False, - 'user_identity': '- - - - -', - 'request_id': mock.ANY, - 'user_domain': None}, - 'ceilometer.api', 'alarm.creation', - PayloadMatcher(), mock.ANY) - - def test_alarm_sends_notification(self): - alarm = self._get_alarm('a') - with mock.patch.object(messaging, 'get_notifier') as get_notifier: - notifier = get_notifier.return_value - self._update_alarm(alarm, dict(name='new_name')) - get_notifier.assert_called_once_with(mock.ANY, - publisher_id='ceilometer.api') - calls = notifier.info.call_args_list - self.assertEqual(1, len(calls)) - args, _ = calls[0] - context, event_type, payload = args - self.assertEqual('alarm.rule_change', event_type) - self.assertEqual('new_name', payload['detail']['name']) - self.assertTrue(set(['alarm_id', 'detail', 'event_id', 'on_behalf_of', - 'project_id', 'timestamp', 'type', - 'user_id']).issubset(payload.keys())) - - @mock.patch('ceilometer.keystone_client.get_client') - def test_post_gnocchi_resources_alarm(self, __): - json = { - 'enabled': False, - 'name': 'name_post', - 'state': 'ok', - 'type': 'gnocchi_resources_threshold', - 'severity': 'critical', - 'ok_actions': ['http://something/ok'], - 'alarm_actions': ['http://something/alarm'], - 'insufficient_data_actions': ['http://something/no'], - 'repeat_actions': True, - 'gnocchi_resources_threshold_rule': { - 'metric': 'ameter', - 'comparison_operator': 'le', - 'aggregation_method': 'count', - 'threshold': 50, - 'evaluation_periods': 3, - 'granularity': 180, - 'resource_type': 'instance', - 'resource_id': '209ef69c-c10c-4efb-90ff-46f4b2d90d2e', - } - } - - with mock.patch('requests.get', - side_effect=requests.ConnectionError()): - resp = self.post_json('/alarms', params=json, - headers=self.auth_headers, - expect_errors=True) - self.assertEqual(503, resp.status_code, resp.body) - - with mock.patch('requests.get', - return_value=mock.Mock(status_code=500, - body="my_custom_error", - text="my_custom_error")): - resp = self.post_json('/alarms', params=json, - headers=self.auth_headers, - expect_errors=True) - self.assertEqual(503, resp.status_code, resp.body) - self.assertIn('my_custom_error', - resp.json['error_message']['faultstring']) - - cap_result = mock.Mock(status_code=201, - text=jsonutils.dumps( - {'aggregation_methods': ['count']})) - resource_result = mock.Mock(status_code=200, text="blob") - with mock.patch('requests.get', side_effect=[cap_result, - resource_result] - ) as gnocchi_get: - self.post_json('/alarms', params=json, headers=self.auth_headers) - - gnocchi_url = self.CONF.alarms.gnocchi_url - capabilities_url = urlparse.urljoin(gnocchi_url, - '/v1/capabilities') - resource_url = urlparse.urljoin( - gnocchi_url, - '/v1/resource/instance/209ef69c-c10c-4efb-90ff-46f4b2d90d2e' - ) - - expected = [mock.call(capabilities_url, - headers=mock.ANY), - mock.call(resource_url, - headers=mock.ANY)] - self.assertEqual(expected, gnocchi_get.mock_calls) - - alarms = list(self.alarm_conn.get_alarms(enabled=False)) - self.assertEqual(1, len(alarms)) - self._verify_alarm(json, alarms[0]) - - @mock.patch('ceilometer.keystone_client.get_client') - def test_post_gnocchi_metrics_alarm(self, __): - json = { - 'enabled': False, - 'name': 'name_post', - 'state': 'ok', - 'type': 'gnocchi_aggregation_by_metrics_threshold', - 'severity': 'critical', - 'ok_actions': ['http://something/ok'], - 'alarm_actions': ['http://something/alarm'], - 'insufficient_data_actions': ['http://something/no'], - 'repeat_actions': True, - 'gnocchi_aggregation_by_metrics_threshold_rule': { - 'metrics': ['b3d9d8ab-05e8-439f-89ad-5e978dd2a5eb', - '009d4faf-c275-46f0-8f2d-670b15bac2b0'], - 'comparison_operator': 'le', - 'aggregation_method': 'count', - 'threshold': 50, - 'evaluation_periods': 3, - 'granularity': 180, - } - } - - cap_result = mock.Mock(status_code=200, - text=jsonutils.dumps( - {'aggregation_methods': ['count']})) - with mock.patch('requests.get', return_value=cap_result): - self.post_json('/alarms', params=json, headers=self.auth_headers) - - alarms = list(self.alarm_conn.get_alarms(enabled=False)) - self.assertEqual(1, len(alarms)) - self._verify_alarm(json, alarms[0]) - - @mock.patch('ceilometer.keystone_client.get_client') - def test_post_gnocchi_aggregation_alarm_project_constraint(self, __): - self.CONF.set_override('gnocchi_url', 'http://localhost:8041', - group='alarms') - json = { - 'enabled': False, - 'name': 'project_constraint', - 'state': 'ok', - 'type': 'gnocchi_aggregation_by_resources_threshold', - 'severity': 'critical', - 'ok_actions': ['http://something/ok'], - 'alarm_actions': ['http://something/alarm'], - 'insufficient_data_actions': ['http://something/no'], - 'repeat_actions': True, - 'gnocchi_aggregation_by_resources_threshold_rule': { - 'metric': 'ameter', - 'comparison_operator': 'le', - 'aggregation_method': 'count', - 'threshold': 50, - 'evaluation_periods': 3, - 'granularity': 180, - 'resource_type': 'instance', - 'query': '{"=": {"server_group": "my_autoscaling_group"}}', - } - } - - cap_result = mock.Mock(status_code=201, - text=jsonutils.dumps( - {'aggregation_methods': ['count']})) - resource_result = mock.Mock(status_code=200, text="blob") - query_check_result = mock.Mock(status_code=200, text="blob") - - expected_query = ('{"and": [{"=": {"created_by_project_id": "%s"}}, ' - '{"=": {"server_group": "my_autoscaling_group"}}]}' % - self.auth_headers['X-Project-Id']) - - with mock.patch('requests.get', - side_effect=[cap_result, resource_result]): - with mock.patch('requests.post', - side_effect=[query_check_result]) as fake_post: - - self.post_json('/alarms', params=json, - headers=self.auth_headers) - - self.assertEqual([mock.call( - url=('http://localhost:8041/v1/aggregation/' - 'resource/instance/metric/ameter'), - headers={'Content-Type': 'application/json', - 'X-Auth-Token': mock.ANY}, - params={'aggregation': 'count'}, - data=expected_query)], - fake_post.mock_calls), - - alarms = list(self.alarm_conn.get_alarms(enabled=False)) - self.assertEqual(1, len(alarms)) - - json['gnocchi_aggregation_by_resources_threshold_rule']['query'] = ( - expected_query) - self._verify_alarm(json, alarms[0]) - - -class TestAlarmsQuotas(v2.FunctionalTest, - tests_db.MixinTestsWithBackendScenarios): - - def setUp(self): - super(TestAlarmsQuotas, self).setUp() - - self.auth_headers = {'X-User-Id': str(uuid.uuid4()), - 'X-Project-Id': str(uuid.uuid4())} - - def _test_alarm_quota(self): - alarm = { - 'name': 'alarm', - 'type': 'threshold', - 'user_id': self.auth_headers['X-User-Id'], - 'project_id': self.auth_headers['X-Project-Id'], - 'threshold_rule': { - 'meter_name': 'testmeter', - 'query': [], - 'comparison_operator': 'le', - 'statistic': 'max', - 'threshold': 42.0, - 'period': 60, - 'evaluation_periods': 1, - } - } - - resp = self.post_json('/alarms', params=alarm, - headers=self.auth_headers) - self.assertEqual(201, resp.status_code) - alarms = self.get_json('/alarms') - self.assertEqual(1, len(alarms)) - - alarm['name'] = 'another_user_alarm' - resp = self.post_json('/alarms', params=alarm, - expect_errors=True, - headers=self.auth_headers) - self.assertEqual(403, resp.status_code) - faultstring = 'Alarm quota exceeded for user' - self.assertIn(faultstring, - resp.json['error_message']['faultstring']) - - alarms = self.get_json('/alarms') - self.assertEqual(1, len(alarms)) - - def test_alarms_quotas(self): - self.CONF.set_override('user_alarm_quota', 1, group='alarm') - self.CONF.set_override('project_alarm_quota', 1, group='alarm') - self._test_alarm_quota() - - def test_project_alarms_quotas(self): - self.CONF.set_override('project_alarm_quota', 1, group='alarm') - self._test_alarm_quota() - - def test_user_alarms_quotas(self): - self.CONF.set_override('user_alarm_quota', 1, group='alarm') - self._test_alarm_quota() - - def test_larger_limit_project_alarms_quotas(self): - self.CONF.set_override('user_alarm_quota', 1, group='alarm') - self.CONF.set_override('project_alarm_quota', 2, group='alarm') - self._test_alarm_quota() - - def test_larger_limit_user_alarms_quotas(self): - self.CONF.set_override('user_alarm_quota', 2, group='alarm') - self.CONF.set_override('project_alarm_quota', 1, group='alarm') - self._test_alarm_quota() - - def test_larger_limit_user_alarm_quotas_multitenant_user(self): - self.CONF.set_override('user_alarm_quota', 2, group='alarm') - self.CONF.set_override('project_alarm_quota', 1, group='alarm') - - def _test(field, value): - query = [{ - 'field': field, - 'op': 'eq', - 'value': value - }] - alarms = self.get_json('/alarms', q=query) - self.assertEqual(1, len(alarms)) - - alarm = { - 'name': 'alarm', - 'type': 'threshold', - 'user_id': self.auth_headers['X-User-Id'], - 'project_id': self.auth_headers['X-Project-Id'], - 'threshold_rule': { - 'meter_name': 'testmeter', - 'query': [], - 'comparison_operator': 'le', - 'statistic': 'max', - 'threshold': 42.0, - 'period': 60, - 'evaluation_periods': 1, - } - } - - resp = self.post_json('/alarms', params=alarm, - headers=self.auth_headers) - - self.assertEqual(201, resp.status_code) - _test('project_id', self.auth_headers['X-Project-Id']) - - self.auth_headers['X-Project-Id'] = str(uuid.uuid4()) - alarm['name'] = 'another_user_alarm' - alarm['project_id'] = self.auth_headers['X-Project-Id'] - resp = self.post_json('/alarms', params=alarm, - headers=self.auth_headers) - - self.assertEqual(201, resp.status_code) - _test('project_id', self.auth_headers['X-Project-Id']) - - alarms = self.get_json('/alarms') - self.assertEqual(2, len(alarms)) diff --git a/ceilometer/tests/functional/api/v2/test_app.py b/ceilometer/tests/functional/api/v2/test_app.py index 032b8908..9aef1612 100644 --- a/ceilometer/tests/functional/api/v2/test_app.py +++ b/ceilometer/tests/functional/api/v2/test_app.py @@ -15,13 +15,6 @@ # under the License. """Test basic ceilometer-api app """ -import json - -import mock -import six -import wsme - -from ceilometer import i18n from ceilometer.tests.functional.api import v2 @@ -85,22 +78,6 @@ class TestApiMiddleware(v2.FunctionalTest): self.assertEqual("application/json", response.content_type) self.assertTrue(response.json['error_message']) - def test_json_parsable_error_middleware_translation_400(self): - # Ensure translated messages get placed properly into json faults - with mock.patch.object(i18n, 'translate', - side_effect=self._fake_translate): - response = self.post_json('/alarms', params={'name': 'foobar', - 'type': 'threshold'}, - expect_errors=True, - headers={"Accept": - "application/json"} - ) - self.assertEqual(400, response.status_int) - self.assertEqual("application/json", response.content_type) - self.assertTrue(response.json['error_message']) - self.assertEqual(self.no_lang_translated_error, - response.json['error_message']['faultstring']) - def test_xml_parsable_error_middleware_404(self): response = self.get_json('/invalid_path', expect_errors=True, @@ -119,64 +96,3 @@ class TestApiMiddleware(v2.FunctionalTest): self.assertEqual(404, response.status_int) self.assertEqual("application/xml", response.content_type) self.assertEqual('error_message', response.xml.tag) - - def test_xml_parsable_error_middleware_translation_400(self): - # Ensure translated messages get placed properly into xml faults - with mock.patch.object(i18n, 'translate', - side_effect=self._fake_translate): - response = self.post_json('/alarms', params={'name': 'foobar', - 'type': 'threshold'}, - expect_errors=True, - headers={"Accept": - "application/xml,*/*"} - ) - self.assertEqual(400, response.status_int) - self.assertEqual("application/xml", response.content_type) - self.assertEqual('error_message', response.xml.tag) - fault = response.xml.findall('./error/faultstring') - for fault_string in fault: - self.assertEqual(self.no_lang_translated_error, fault_string.text) - - def test_best_match_language(self): - # Ensure that we are actually invoking language negotiation - with mock.patch.object(i18n, 'translate', - side_effect=self._fake_translate): - response = self.post_json('/alarms', params={'name': 'foobar', - 'type': 'threshold'}, - expect_errors=True, - headers={"Accept": - "application/xml,*/*", - "Accept-Language": - "en-US"} - ) - - self.assertEqual(400, response.status_int) - self.assertEqual("application/xml", response.content_type) - self.assertEqual('error_message', response.xml.tag) - fault = response.xml.findall('./error/faultstring') - for fault_string in fault: - self.assertEqual(self.en_US_translated_error, fault_string.text) - - def test_translated_then_untranslated_error(self): - resp = self.get_json('/alarms/alarm-id-3', expect_errors=True) - self.assertEqual(404, resp.status_code) - body = resp.body - if six.PY3: - body = body.decode('utf-8') - self.assertEqual("Alarm alarm-id-3 not found", - json.loads(body)['error_message'] - ['faultstring']) - - with mock.patch('ceilometer.api.controllers.' - 'v2.base.AlarmNotFound') as CustomErrorClass: - CustomErrorClass.return_value = wsme.exc.ClientSideError( - "untranslated_error", status_code=404) - resp = self.get_json('/alarms/alarm-id-5', expect_errors=True) - - self.assertEqual(404, resp.status_code) - body = resp.body - if six.PY3: - body = body.decode('utf-8') - self.assertEqual("untranslated_error", - json.loads(body)['error_message'] - ['faultstring']) diff --git a/ceilometer/tests/functional/api/v2/test_complex_query_scenarios.py b/ceilometer/tests/functional/api/v2/test_complex_query_scenarios.py index bc4fb737..dab481fc 100644 --- a/ceilometer/tests/functional/api/v2/test_complex_query_scenarios.py +++ b/ceilometer/tests/functional/api/v2/test_complex_query_scenarios.py @@ -22,7 +22,6 @@ import datetime from oslo_utils import timeutils -from ceilometer.alarm.storage import models from ceilometer.publisher import utils from ceilometer import sample from ceilometer.tests import db as tests_db @@ -316,300 +315,3 @@ class TestQueryMetersController(tests_api.FunctionalTest, self.CONF.set_override('default_api_return_limit', 1, group='api') data = self.post_json(self.url, params={}) self.assertEqual(1, len(data.json)) - - -class TestQueryAlarmsController(tests_api.FunctionalTest, - tests_db.MixinTestsWithBackendScenarios): - - def setUp(self): - super(TestQueryAlarmsController, self).setUp() - self.alarm_url = '/query/alarms' - - for state in ['ok', 'alarm', 'insufficient data']: - for date in [datetime.datetime(2013, 1, 1), - datetime.datetime(2013, 2, 2)]: - for id in [1, 2]: - alarm_id = "-".join([state, date.isoformat(), str(id)]) - project_id = "project-id%d" % id - alarm = models.Alarm(name=alarm_id, - type='threshold', - enabled=True, - alarm_id=alarm_id, - description='a', - state=state, - state_timestamp=date, - timestamp=date, - ok_actions=[], - insufficient_data_actions=[], - alarm_actions=[], - repeat_actions=True, - user_id="user-id%d" % id, - project_id=project_id, - time_constraints=[], - rule=dict(comparison_operator='gt', - threshold=2.0, - statistic='avg', - evaluation_periods=60, - period=1, - meter_name='meter.test', - query=[{'field': - 'project_id', - 'op': 'eq', - 'value': - project_id}]), - severity='critical') - self.alarm_conn.update_alarm(alarm) - - def test_query_all(self): - data = self.post_json(self.alarm_url, - params={}) - - self.assertEqual(12, len(data.json)) - - def test_filter_with_isotime_timestamp(self): - date_time = datetime.datetime(2013, 1, 1) - isotime = date_time.isoformat() - - data = self.post_json(self.alarm_url, - params={"filter": - '{">": {"timestamp": "' - + isotime + '"}}'}) - - self.assertEqual(6, len(data.json)) - for alarm in data.json: - result_time = timeutils.parse_isotime(alarm['timestamp']) - result_time = result_time.replace(tzinfo=None) - self.assertTrue(result_time > date_time) - - def test_filter_with_isotime_state_timestamp(self): - date_time = datetime.datetime(2013, 1, 1) - isotime = date_time.isoformat() - - data = self.post_json(self.alarm_url, - params={"filter": - '{">": {"state_timestamp": "' - + isotime + '"}}'}) - - self.assertEqual(6, len(data.json)) - for alarm in data.json: - result_time = timeutils.parse_isotime(alarm['state_timestamp']) - result_time = result_time.replace(tzinfo=None) - self.assertTrue(result_time > date_time) - - def test_non_admin_tenant_sees_only_its_own_project(self): - data = self.post_json(self.alarm_url, - params={}, - headers=non_admin_header) - for alarm in data.json: - self.assertEqual("project-id1", alarm['project_id']) - - def test_non_admin_tenant_cannot_query_others_project(self): - data = self.post_json(self.alarm_url, - params={"filter": - '{"=": {"project_id": "project-id2"}}'}, - expect_errors=True, - headers=non_admin_header) - - self.assertEqual(401, data.status_int) - self.assertIn(b"Not Authorized to access project project-id2", - data.body) - - def test_non_admin_tenant_can_explicitly_filter_for_own_project(self): - data = self.post_json(self.alarm_url, - params={"filter": - '{"=": {"project_id": "project-id1"}}'}, - headers=non_admin_header) - - for alarm in data.json: - self.assertEqual("project-id1", alarm['project_id']) - - def test_admin_tenant_sees_every_project(self): - data = self.post_json(self.alarm_url, - params={}, - headers=admin_header) - - self.assertEqual(12, len(data.json)) - for alarm in data.json: - self.assertIn(alarm['project_id'], - (["project-id1", "project-id2"])) - - def test_admin_tenant_can_query_any_project(self): - data = self.post_json(self.alarm_url, - params={"filter": - '{"=": {"project_id": "project-id2"}}'}, - headers=admin_header) - - self.assertEqual(6, len(data.json)) - for alarm in data.json: - self.assertIn(alarm['project_id'], set(["project-id2"])) - - def test_query_with_field_project(self): - data = self.post_json(self.alarm_url, - params={"filter": - '{"=": {"project": "project-id2"}}'}) - - self.assertEqual(6, len(data.json)) - for sample_item in data.json: - self.assertIn(sample_item['project_id'], set(["project-id2"])) - - def test_query_with_field_user_in_orderby(self): - data = self.post_json(self.alarm_url, - params={"filter": '{"=": {"state": "alarm"}}', - "orderby": '[{"user": "DESC"}]'}) - - self.assertEqual(4, len(data.json)) - self.assertEqual(["user-id2", "user-id2", "user-id1", "user-id1"], - [s["user_id"] for s in data.json]) - - def test_query_with_filter_orderby_and_limit(self): - orderby = '[{"state_timestamp": "DESC"}]' - data = self.post_json(self.alarm_url, - params={"filter": '{"=": {"state": "alarm"}}', - "orderby": orderby, - "limit": 3}) - - self.assertEqual(3, len(data.json)) - self.assertEqual(["2013-02-02T00:00:00", - "2013-02-02T00:00:00", - "2013-01-01T00:00:00"], - [a["state_timestamp"] for a in data.json]) - for alarm in data.json: - self.assertEqual("alarm", alarm["state"]) - - def test_limit_must_be_positive(self): - data = self.post_json(self.alarm_url, - params={"limit": 0}, - expect_errors=True) - - self.assertEqual(400, data.status_int) - self.assertIn(b"Limit must be positive", data.body) - - def test_default_limit(self): - self.CONF.set_override('default_api_return_limit', 1, group='api') - data = self.post_json(self.alarm_url, params={}) - self.assertEqual(1, len(data.json)) - - -class TestQueryAlarmsHistoryController( - tests_api.FunctionalTest, tests_db.MixinTestsWithBackendScenarios): - - def setUp(self): - super(TestQueryAlarmsHistoryController, self).setUp() - self.url = '/query/alarms/history' - for id in [1, 2]: - for type in ["creation", "state transition"]: - for date in [datetime.datetime(2013, 1, 1), - datetime.datetime(2013, 2, 2)]: - event_id = "-".join([str(id), type, date.isoformat()]) - alarm_change = {"event_id": event_id, - "alarm_id": "alarm-id%d" % id, - "type": type, - "detail": "", - "user_id": "user-id%d" % id, - "project_id": "project-id%d" % id, - "on_behalf_of": "project-id%d" % id, - "timestamp": date} - - self.alarm_conn.record_alarm_change(alarm_change) - - def test_query_all(self): - data = self.post_json(self.url, - params={}) - - self.assertEqual(8, len(data.json)) - - def test_filter_with_isotime(self): - date_time = datetime.datetime(2013, 1, 1) - isotime = date_time.isoformat() - - data = self.post_json(self.url, - params={"filter": - '{">": {"timestamp":"' - + isotime + '"}}'}) - - self.assertEqual(4, len(data.json)) - for history in data.json: - result_time = timeutils.parse_isotime(history['timestamp']) - result_time = result_time.replace(tzinfo=None) - self.assertTrue(result_time > date_time) - - def test_non_admin_tenant_sees_only_its_own_project(self): - data = self.post_json(self.url, - params={}, - headers=non_admin_header) - for history in data.json: - self.assertEqual("project-id1", history['on_behalf_of']) - - def test_non_admin_tenant_cannot_query_others_project(self): - data = self.post_json(self.url, - params={"filter": - '{"=": {"on_behalf_of":' - + ' "project-id2"}}'}, - expect_errors=True, - headers=non_admin_header) - - self.assertEqual(401, data.status_int) - self.assertIn(b"Not Authorized to access project project-id2", - data.body) - - def test_non_admin_tenant_can_explicitly_filter_for_own_project(self): - data = self.post_json(self.url, - params={"filter": - '{"=": {"on_behalf_of":' - + ' "project-id1"}}'}, - headers=non_admin_header) - - for history in data.json: - self.assertEqual("project-id1", history['on_behalf_of']) - - def test_admin_tenant_sees_every_project(self): - data = self.post_json(self.url, - params={}, - headers=admin_header) - - self.assertEqual(8, len(data.json)) - for history in data.json: - self.assertIn(history['on_behalf_of'], - (["project-id1", "project-id2"])) - - def test_query_with_filter_for_project_orderby_with_user(self): - data = self.post_json(self.url, - params={"filter": - '{"=": {"project": "project-id1"}}', - "orderby": '[{"user": "DESC"}]', - "limit": 3}) - - self.assertEqual(3, len(data.json)) - self.assertEqual(["user-id1", - "user-id1", - "user-id1"], - [h["user_id"] for h in data.json]) - for history in data.json: - self.assertEqual("project-id1", history['project_id']) - - def test_query_with_filter_orderby_and_limit(self): - data = self.post_json(self.url, - params={"filter": '{"=": {"type": "creation"}}', - "orderby": '[{"timestamp": "DESC"}]', - "limit": 3}) - - self.assertEqual(3, len(data.json)) - self.assertEqual(["2013-02-02T00:00:00", - "2013-02-02T00:00:00", - "2013-01-01T00:00:00"], - [h["timestamp"] for h in data.json]) - for history in data.json: - self.assertEqual("creation", history['type']) - - def test_limit_must_be_positive(self): - data = self.post_json(self.url, - params={"limit": 0}, - expect_errors=True) - - self.assertEqual(400, data.status_int) - self.assertIn(b"Limit must be positive", data.body) - - def test_default_limit(self): - self.CONF.set_override('default_api_return_limit', 1, group='api') - data = self.post_json(self.url, params={}) - self.assertEqual(1, len(data.json)) diff --git a/ceilometer/tests/functional/gabbi/fixtures.py b/ceilometer/tests/functional/gabbi/fixtures.py index 25dab063..8d63b170 100644 --- a/ceilometer/tests/functional/gabbi/fixtures.py +++ b/ceilometer/tests/functional/gabbi/fixtures.py @@ -75,7 +75,6 @@ class ConfigFixture(fixture.GabbiFixture): conf.set_override('connection', database_name, group='database') conf.set_override('metering_connection', '', group='database') conf.set_override('event_connection', '', group='database') - conf.set_override('alarm_connection', '', group='database') conf.set_override('pecan_debug', True, group='api') conf.set_override('gnocchi_is_enabled', False, group='api') diff --git a/ceilometer/tests/functional/gabbi/gabbits/alarms.yaml b/ceilometer/tests/functional/gabbi/gabbits/alarms.yaml deleted file mode 100644 index 9d0d360c..00000000 --- a/ceilometer/tests/functional/gabbi/gabbits/alarms.yaml +++ /dev/null @@ -1,139 +0,0 @@ -# Requests to cover the basic endpoints for alarms. - -fixtures: - - ConfigFixture - -tests: -- name: list alarms none - desc: Lists alarms, none yet exist - url: /v2/alarms - method: GET - response_strings: - - "[]" - -- name: try to PUT an alarm - desc: what does PUT do - url: /v2/alarms - method: PUT - request_headers: - content-type: application/json - data: - name: added_alarm_defaults2 - type: threshold - threshold_rule: - meter_name: ameter - threshold: 300.0 - status: 405 - response_headers: - allow: GET, POST - -# TODO(chdent): A POST should return a location header. -- name: createAlarm - xfail: true - desc: Creates an alarm. - url: /v2/alarms - method: POST - request_headers: - content-type: application/json - data: - ok_actions: null - name: added_alarm_defaults - type: threshold - threshold_rule: - meter_name: ameter - threshold: 300.0 - status: 201 - response_headers: - location: /$SCHEME://$NETLOC/v2/alarms/ - content-type: application/json; charset=UTF-8 - response_json_paths: - $.severity: low - $.threshold_rule.threshold: 300.0 - $.threshold_rule.comparison_operator: eq - -- name: showAlarm - desc: Shows information for a specified alarm. - url: /v2/alarms/$RESPONSE['$.alarm_id'] - method: GET - response_json_paths: - $.severity: low - $.alarm_id: $RESPONSE['$.alarm_id'] - $.threshold_rule.threshold: 300.0 - $.threshold_rule.comparison_operator: eq - response_headers: - content-type: application/json; charset=UTF-8 - -- name: updateAlarm - desc: Updates a specified alarm. - url: /v2/alarms/$RESPONSE['$.alarm_id'] - method: PUT - request_headers: - content-type: application/json - data: - name: added_alarm_defaults - type: threshold - severity: moderate - threshold_rule: - meter_name: ameter - threshold: 200.0 -# TODO(chdent): why do we have a response, why not status: 204? -# status: 204 - response_json_paths: - $.threshold_rule.threshold: 200.0 - $.severity: moderate - $.state: insufficient data - -- name: showAlarmHistory - desc: Assembles the history for a specified alarm. - url: /v2/alarms/$RESPONSE['$.alarm_id']/history?q.field=type&q.op=eq&q.value=rule%20change - method: GET - response_json_paths: - $[0].type: rule change - -- name: updateAlarmState - desc: Sets the state of a specified alarm. - url: /v2/alarms/$RESPONSE['$[0].alarm_id']/state - request_headers: - content-type: application/json - data: '"alarm"' - method: PUT -# TODO(chdent): really? Of what possible use is this? - response_json_paths: - $: alarm - -# Get a list of alarms so we can extract an id for the next test -- name: list alarms for data - desc: Lists alarms, only one - url: /v2/alarms - method: GET - response_json_paths: - $[0].name: added_alarm_defaults - -- name: showAlarmState - desc: Gets the state of a specified alarm. - url: /v2/alarms/$RESPONSE['$[0].alarm_id']/state - method: GET - response_headers: - content-type: application/json; charset=UTF-8 - response_json_paths: - $: alarm - -- name: list alarms one - desc: Lists alarms, only one - url: /v2/alarms - method: GET - response_json_paths: - $[0].name: added_alarm_defaults - -- name: deleteAlarm - desc: Deletes a specified alarm. - url: /v2/alarms/$RESPONSE['$[0].alarm_id'] - method: DELETE - status: 204 - -- name: list alarms none end - desc: Lists alarms, none now exist - url: /v2/alarms - method: GET - response_strings: - - "[]" diff --git a/ceilometer/tests/functional/gabbi/gabbits/capabilities.yaml b/ceilometer/tests/functional/gabbi/gabbits/capabilities.yaml index 1a78a6c0..5b9c9164 100644 --- a/ceilometer/tests/functional/gabbi/gabbits/capabilities.yaml +++ b/ceilometer/tests/functional/gabbi/gabbits/capabilities.yaml @@ -10,6 +10,5 @@ tests: desc: retrieve capabilities for the mongo store url: /v2/capabilities response_json_paths: - $.alarm_storage.['storage:production_ready']: true $.event_storage.['storage:production_ready']: true $.storage.['storage:production_ready']: true diff --git a/ceilometer/tests/functional/storage/test_impl_db2.py b/ceilometer/tests/functional/storage/test_impl_db2.py index 2cc86faf..3dcc5056 100644 --- a/ceilometer/tests/functional/storage/test_impl_db2.py +++ b/ceilometer/tests/functional/storage/test_impl_db2.py @@ -28,7 +28,6 @@ import mock from oslo_config import cfg from oslo_utils import timeutils -from ceilometer.alarm.storage import impl_db2 as impl_db2_alarm from ceilometer.event.storage import impl_db2 as impl_db2_event from ceilometer.storage import impl_db2 from ceilometer.storage.mongo import utils as pymongo_utils @@ -76,17 +75,6 @@ class CapabilitiesTest(test_base.BaseTestCase): actual_capabilities = impl_db2_event.Connection.get_capabilities() self.assertEqual(expected_capabilities, actual_capabilities) - def test_alarm_capabilities(self): - expected_capabilities = { - 'alarms': {'query': {'simple': True, - 'complex': True}, - 'history': {'query': {'simple': True, - 'complex': True}}}, - } - - actual_capabilities = impl_db2_alarm.Connection.get_capabilities() - self.assertEqual(expected_capabilities, actual_capabilities) - def test_storage_capabilities(self): expected_capabilities = { 'storage': {'production_ready': True}, diff --git a/ceilometer/tests/functional/storage/test_impl_hbase.py b/ceilometer/tests/functional/storage/test_impl_hbase.py index b0b8d419..08fbd762 100644 --- a/ceilometer/tests/functional/storage/test_impl_hbase.py +++ b/ceilometer/tests/functional/storage/test_impl_hbase.py @@ -29,7 +29,6 @@ except ImportError: import testtools.testcase raise testtools.testcase.TestSkipped("happybase is needed") -from ceilometer.alarm.storage import impl_hbase as hbase_alarm from ceilometer.event.storage import impl_hbase as hbase_event from ceilometer.storage import impl_hbase as hbase from ceilometer.tests import base as test_base @@ -92,17 +91,6 @@ class CapabilitiesTest(test_base.BaseTestCase): actual_capabilities = hbase.Connection.get_capabilities() self.assertEqual(expected_capabilities, actual_capabilities) - def test_alarm_capabilities(self): - expected_capabilities = { - 'alarms': {'query': {'simple': True, - 'complex': False}, - 'history': {'query': {'simple': True, - 'complex': False}}}, - } - - actual_capabilities = hbase_alarm.Connection.get_capabilities() - self.assertEqual(expected_capabilities, actual_capabilities) - def test_event_capabilities(self): expected_capabilities = { 'events': {'query': {'simple': True}}, diff --git a/ceilometer/tests/functional/storage/test_impl_mongodb.py b/ceilometer/tests/functional/storage/test_impl_mongodb.py index cdcb6019..05eccf0c 100644 --- a/ceilometer/tests/functional/storage/test_impl_mongodb.py +++ b/ceilometer/tests/functional/storage/test_impl_mongodb.py @@ -21,7 +21,6 @@ """ -from ceilometer.alarm.storage import impl_mongodb as impl_mongodb_alarm from ceilometer.event.storage import impl_mongodb as impl_mongodb_event from ceilometer.storage import impl_mongodb from ceilometer.tests import base as test_base @@ -77,10 +76,6 @@ class IndexTest(tests_db.TestBase, self._test_ttl_index_absent(self.event_conn, 'event', 'event_time_to_live') - def test_alarm_history_ttl_index_absent(self): - self._test_ttl_index_absent(self.alarm_conn, 'alarm_history', - 'alarm_history_time_to_live') - def _test_ttl_index_present(self, conn, coll_name, ttl_opt): coll = getattr(conn.db, coll_name) self.CONF.set_override(ttl_opt, 456789, group='database') @@ -102,10 +97,6 @@ class IndexTest(tests_db.TestBase, self._test_ttl_index_present(self.event_conn, 'event', 'event_time_to_live') - def test_alarm_history_ttl_index_present(self): - self._test_ttl_index_present(self.alarm_conn, 'alarm_history', - 'alarm_history_time_to_live') - class CapabilitiesTest(test_base.BaseTestCase): # Check the returned capabilities list, which is specific to each DB @@ -148,17 +139,6 @@ class CapabilitiesTest(test_base.BaseTestCase): actual_capabilities = impl_mongodb_event.Connection.get_capabilities() self.assertEqual(expected_capabilities, actual_capabilities) - def test_alarm_capabilities(self): - expected_capabilities = { - 'alarms': {'query': {'simple': True, - 'complex': True}, - 'history': {'query': {'simple': True, - 'complex': True}}}, - } - - actual_capabilities = impl_mongodb_alarm.Connection.get_capabilities() - self.assertEqual(expected_capabilities, actual_capabilities) - def test_storage_capabilities(self): expected_capabilities = { 'storage': {'production_ready': True}, diff --git a/ceilometer/tests/functional/storage/test_impl_sqlalchemy.py b/ceilometer/tests/functional/storage/test_impl_sqlalchemy.py index 6ce0cfc7..56fcac40 100644 --- a/ceilometer/tests/functional/storage/test_impl_sqlalchemy.py +++ b/ceilometer/tests/functional/storage/test_impl_sqlalchemy.py @@ -24,7 +24,6 @@ import mock from oslo_utils import timeutils from six.moves import reprlib -from ceilometer.alarm.storage import impl_sqlalchemy as impl_sqla_alarm from ceilometer.event.storage import impl_sqlalchemy as impl_sqla_event from ceilometer.event.storage import models from ceilometer.storage import impl_sqlalchemy @@ -171,17 +170,6 @@ class CapabilitiesTest(test_base.BaseTestCase): actual_capabilities = impl_sqla_event.Connection.get_capabilities() self.assertEqual(expected_capabilities, actual_capabilities) - def test_alarm_capabilities(self): - expected_capabilities = { - 'alarms': {'query': {'simple': True, - 'complex': True}, - 'history': {'query': {'simple': True, - 'complex': True}}}, - } - - actual_capabilities = impl_sqla_alarm.Connection.get_capabilities() - self.assertEqual(expected_capabilities, actual_capabilities) - def test_storage_capabilities(self): expected_capabilities = { 'storage': {'production_ready': True}, diff --git a/ceilometer/tests/functional/storage/test_pymongo_base.py b/ceilometer/tests/functional/storage/test_pymongo_base.py index 4d29b5d7..9c60e25f 100644 --- a/ceilometer/tests/functional/storage/test_pymongo_base.py +++ b/ceilometer/tests/functional/storage/test_pymongo_base.py @@ -19,7 +19,6 @@ import mock from ceilometer.publisher import utils from ceilometer import sample -from ceilometer.tests import constants from ceilometer.tests import db as tests_db from ceilometer.tests.functional.storage import test_storage_scenarios @@ -77,79 +76,6 @@ class CompatibilityTest(test_storage_scenarios.DBTestBase, secret='not-so-secret') self.conn.record_metering_data(self.conn, msg) - # Create the old format alarm with a dict instead of a - # array for matching_metadata - alarm = dict(alarm_id='0ld-4l3rt', - enabled=True, - name='old-alert', - description='old-alert', - timestamp=constants.MIN_DATETIME, - meter_name='cpu', - user_id='me', - project_id='and-da-boys', - comparison_operator='lt', - threshold=36, - statistic='count', - evaluation_periods=1, - period=60, - state="insufficient data", - state_timestamp=constants.MIN_DATETIME, - ok_actions=[], - alarm_actions=['http://nowhere/alarms'], - insufficient_data_actions=[], - repeat_actions=False, - matching_metadata={'key': 'value'}) - - self.alarm_conn.db.alarm.update( - {'alarm_id': alarm['alarm_id']}, - {'$set': alarm}, - upsert=True) - - alarm['alarm_id'] = 'other-kind-of-0ld-4l3rt' - alarm['name'] = 'other-old-alaert' - alarm['matching_metadata'] = [{'key': 'key1', 'value': 'value1'}, - {'key': 'key2', 'value': 'value2'}] - self.alarm_conn.db.alarm.update( - {'alarm_id': alarm['alarm_id']}, - {'$set': alarm}, - upsert=True) - - def test_alarm_get_old_format_matching_metadata_dict(self): - old = list(self.alarm_conn.get_alarms(name='old-alert'))[0] - self.assertEqual('threshold', old.type) - self.assertEqual([{'field': 'key', - 'op': 'eq', - 'value': 'value', - 'type': 'string'}], - old.rule['query']) - self.assertEqual(60, old.rule['period']) - self.assertEqual('cpu', old.rule['meter_name']) - self.assertEqual(1, old.rule['evaluation_periods']) - self.assertEqual('count', old.rule['statistic']) - self.assertEqual('lt', old.rule['comparison_operator']) - self.assertEqual(36, old.rule['threshold']) - - def test_alarm_get_old_format_matching_metadata_array(self): - old = list(self.alarm_conn.get_alarms(name='other-old-alaert'))[0] - self.assertEqual('threshold', old.type) - self.assertEqual(sorted([{'field': 'key1', - 'op': 'eq', - 'value': 'value1', - 'type': 'string'}, - {'field': 'key2', - 'op': 'eq', - 'value': 'value2', - 'type': 'string'}], - key=lambda obj: sorted(obj.items())), - sorted(old.rule['query'], - key=lambda obj: sorted(obj.items()))) - self.assertEqual('cpu', old.rule['meter_name']) - self.assertEqual(60, old.rule['period']) - self.assertEqual(1, old.rule['evaluation_periods']) - self.assertEqual('count', old.rule['statistic']) - self.assertEqual('lt', old.rule['comparison_operator']) - self.assertEqual(36, old.rule['threshold']) - def test_counter_unit(self): meters = list(self.conn.get_meters()) self.assertEqual(1, len(meters)) diff --git a/ceilometer/tests/functional/storage/test_storage_scenarios.py b/ceilometer/tests/functional/storage/test_storage_scenarios.py index ce69f7d9..5812bd87 100644 --- a/ceilometer/tests/functional/storage/test_storage_scenarios.py +++ b/ceilometer/tests/functional/storage/test_storage_scenarios.py @@ -26,12 +26,10 @@ from oslo_utils import timeutils import pymongo import ceilometer -from ceilometer.alarm.storage import models as alarm_models from ceilometer.event.storage import models as event_models from ceilometer.publisher import utils from ceilometer import sample from ceilometer import storage -from ceilometer.tests import constants from ceilometer.tests import db as tests_db @@ -679,46 +677,6 @@ class RawSampleTest(DBTestBase, self.assertIn('DBDeadlock', str(type(err))) self.assertEqual(3, retry_sleep.call_count) - @tests_db.run_with('sqlite', 'mysql', 'pgsql', 'hbase', 'db2') - def test_clear_metering_data_with_alarms(self): - # NOTE(jd) Override this test in MongoDB because our code doesn't clear - # the collections, this is handled by MongoDB TTL feature. - alarm = alarm_models.Alarm(alarm_id='r3d', - enabled=True, - type='threshold', - name='red-alert', - description='my red-alert', - timestamp=constants.MIN_DATETIME, - user_id='user-id', - project_id='project-id', - state="insufficient data", - state_timestamp=constants.MIN_DATETIME, - ok_actions=[], - alarm_actions=['http://nowhere/alarms'], - insufficient_data_actions=[], - repeat_actions=False, - time_constraints=[], - rule=dict(comparison_operator='eq', - threshold=36, - statistic='count', - evaluation_periods=1, - period=60, - meter_name='test.one', - query=[{'field': 'key', - 'op': 'eq', - 'value': 'value', - 'type': 'string'}]), - ) - - self.alarm_conn.create_alarm(alarm) - self.mock_utcnow.return_value = datetime.datetime(2012, 7, 2, 10, 45) - self.conn.clear_expired_metering_data(5) - f = storage.SampleFilter(meter='instance') - results = list(self.conn.get_samples(f)) - self.assertEqual(2, len(results)) - results = list(self.conn.get_resources()) - self.assertEqual(2, len(results)) - class ComplexSampleQueryTest(DBTestBase, tests_db.MixinTestsWithBackendScenarios): @@ -2687,447 +2645,6 @@ class CounterDataTypeTest(DBTestBase, self.assertEqual(1938495037.53697, results[0].counter_volume) -class AlarmTestBase(DBTestBase): - def add_some_alarms(self): - alarms = [alarm_models.Alarm(alarm_id='r3d', - enabled=True, - type='threshold', - name='red-alert', - description='my red-alert', - timestamp=datetime.datetime(2015, 7, - 2, 10, 25), - user_id='me', - project_id='and-da-boys', - state="insufficient data", - state_timestamp=constants.MIN_DATETIME, - ok_actions=[], - alarm_actions=['http://nowhere/alarms'], - insufficient_data_actions=[], - repeat_actions=False, - time_constraints=[dict(name='testcons', - start='0 11 * * *', - duration=300)], - rule=dict(comparison_operator='eq', - threshold=36, - statistic='count', - evaluation_periods=1, - period=60, - meter_name='test.one', - query=[{'field': 'key', - 'op': 'eq', - 'value': 'value', - 'type': 'string'}]), - ), - alarm_models.Alarm(alarm_id='0r4ng3', - enabled=True, - type='threshold', - name='orange-alert', - description='a orange', - timestamp=datetime.datetime(2015, 7, - 2, 10, 40), - user_id='me', - project_id='and-da-boys', - state="insufficient data", - state_timestamp=constants.MIN_DATETIME, - ok_actions=[], - alarm_actions=['http://nowhere/alarms'], - insufficient_data_actions=[], - repeat_actions=False, - time_constraints=[], - rule=dict(comparison_operator='gt', - threshold=75, - statistic='avg', - evaluation_periods=1, - period=60, - meter_name='test.forty', - query=[{'field': 'key2', - 'op': 'eq', - 'value': 'value2', - 'type': 'string'}]), - ), - alarm_models.Alarm(alarm_id='y3ll0w', - enabled=False, - type='threshold', - name='yellow-alert', - description='yellow', - timestamp=datetime.datetime(2015, 7, - 2, 10, 10), - user_id='me', - project_id='and-da-boys', - state="insufficient data", - state_timestamp=constants.MIN_DATETIME, - ok_actions=[], - alarm_actions=['http://nowhere/alarms'], - insufficient_data_actions=[], - repeat_actions=False, - time_constraints=[], - rule=dict(comparison_operator='lt', - threshold=10, - statistic='min', - evaluation_periods=1, - period=60, - meter_name='test.five', - query=[{'field': 'key2', - 'op': 'eq', - 'value': 'value2', - 'type': 'string'}, - {'field': - 'user_metadata.key3', - 'op': 'eq', - 'value': 'value3', - 'type': 'string'}]), - )] - - for a in alarms: - self.alarm_conn.create_alarm(a) - - -class AlarmTest(AlarmTestBase, - tests_db.MixinTestsWithBackendScenarios): - - def test_empty(self): - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual([], alarms) - - def test_list(self): - self.add_some_alarms() - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(3, len(alarms)) - - def test_list_ordered_by_timestamp(self): - self.add_some_alarms() - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(len(alarms), 3) - alarm_l = [a.timestamp for a in alarms] - alarm_l_ordered = [datetime.datetime(2015, 7, 2, 10, 40), - datetime.datetime(2015, 7, 2, 10, 25), - datetime.datetime(2015, 7, 2, 10, 10)] - self.assertEqual(alarm_l_ordered, alarm_l) - - def test_list_enabled(self): - self.add_some_alarms() - alarms = list(self.alarm_conn.get_alarms(enabled=True)) - self.assertEqual(2, len(alarms)) - - def test_list_disabled(self): - self.add_some_alarms() - alarms = list(self.alarm_conn.get_alarms(enabled=False)) - self.assertEqual(1, len(alarms)) - - def test_list_by_type(self): - self.add_some_alarms() - alarms = list(self.alarm_conn.get_alarms(alarm_type='threshold')) - self.assertEqual(3, len(alarms)) - alarms = list(self.alarm_conn.get_alarms(alarm_type='combination')) - self.assertEqual(0, len(alarms)) - - def test_add(self): - self.add_some_alarms() - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(3, len(alarms)) - - meter_names = sorted([a.rule['meter_name'] for a in alarms]) - self.assertEqual(['test.five', 'test.forty', 'test.one'], meter_names) - - def test_update(self): - self.add_some_alarms() - orange = list(self.alarm_conn.get_alarms(name='orange-alert'))[0] - orange.enabled = False - orange.state = alarm_models.Alarm.ALARM_INSUFFICIENT_DATA - query = [{'field': 'metadata.group', - 'op': 'eq', - 'value': 'test.updated', - 'type': 'string'}] - orange.rule['query'] = query - orange.rule['meter_name'] = 'new_meter_name' - updated = self.alarm_conn.update_alarm(orange) - self.assertEqual(False, updated.enabled) - self.assertEqual(alarm_models.Alarm.ALARM_INSUFFICIENT_DATA, - updated.state) - self.assertEqual(query, updated.rule['query']) - self.assertEqual('new_meter_name', updated.rule['meter_name']) - - def test_update_llu(self): - llu = alarm_models.Alarm(alarm_id='llu', - enabled=True, - type='threshold', - name='llu', - description='llu', - timestamp=constants.MIN_DATETIME, - user_id='bla', - project_id='ffo', - state="insufficient data", - state_timestamp=constants.MIN_DATETIME, - ok_actions=[], - alarm_actions=[], - insufficient_data_actions=[], - repeat_actions=False, - time_constraints=[], - rule=dict(comparison_operator='lt', - threshold=34, - statistic='max', - evaluation_periods=1, - period=60, - meter_name='llt', - query=[]) - ) - updated = self.alarm_conn.update_alarm(llu) - updated.state = alarm_models.Alarm.ALARM_OK - updated.description = ':)' - self.alarm_conn.update_alarm(updated) - - all = list(self.alarm_conn.get_alarms()) - self.assertEqual(1, len(all)) - - def test_delete(self): - self.add_some_alarms() - victim = list(self.alarm_conn.get_alarms(name='orange-alert'))[0] - self.alarm_conn.delete_alarm(victim.alarm_id) - survivors = list(self.alarm_conn.get_alarms()) - self.assertEqual(2, len(survivors)) - for s in survivors: - self.assertNotEqual(victim.name, s.name) - - -@tests_db.run_with('sqlite', 'mysql', 'pgsql', 'hbase', 'db2') -class AlarmHistoryTest(AlarmTestBase, - tests_db.MixinTestsWithBackendScenarios): - - def setUp(self): - super(AlarmTestBase, self).setUp() - self.add_some_alarms() - self.prepare_alarm_history() - - def prepare_alarm_history(self): - alarms = list(self.alarm_conn.get_alarms()) - for alarm in alarms: - i = alarms.index(alarm) - alarm_change = { - "event_id": "3e11800c-a3ca-4991-b34b-d97efb6047d%s" % i, - "alarm_id": alarm.alarm_id, - "type": alarm_models.AlarmChange.CREATION, - "detail": "detail %s" % alarm.name, - "user_id": alarm.user_id, - "project_id": alarm.project_id, - "on_behalf_of": alarm.project_id, - "timestamp": datetime.datetime(2014, 4, 7, 7, 30 + i) - } - self.alarm_conn.record_alarm_change(alarm_change=alarm_change) - - def _clear_alarm_history(self, utcnow, ttl, count): - self.mock_utcnow.return_value = utcnow - self.alarm_conn.clear_expired_alarm_history_data(ttl) - history = list(self.alarm_conn.query_alarm_history()) - self.assertEqual(count, len(history)) - - def test_clear_alarm_history_no_data_to_remove(self): - utcnow = datetime.datetime(2013, 4, 7, 7, 30) - self._clear_alarm_history(utcnow, 1, 3) - - def test_clear_some_alarm_history(self): - utcnow = datetime.datetime(2014, 4, 7, 7, 35) - self._clear_alarm_history(utcnow, 3 * 60, 1) - - def test_clear_all_alarm_history(self): - utcnow = datetime.datetime(2014, 4, 7, 7, 45) - self._clear_alarm_history(utcnow, 3 * 60, 0) - - def test_delete_history_when_delete_alarm(self): - alarms = list(self.alarm_conn.get_alarms()) - self.assertEqual(3, len(alarms)) - history = list(self.alarm_conn.query_alarm_history()) - self.assertEqual(3, len(history)) - for alarm in alarms: - self.alarm_conn.delete_alarm(alarm.alarm_id) - self.assertEqual(3, len(alarms)) - history = list(self.alarm_conn.query_alarm_history()) - self.assertEqual(0, len(history)) - - -class ComplexAlarmQueryTest(AlarmTestBase, - tests_db.MixinTestsWithBackendScenarios): - - def test_no_filter(self): - self.add_some_alarms() - result = list(self.alarm_conn.query_alarms()) - self.assertEqual(3, len(result)) - - def test_no_filter_with_limit(self): - self.add_some_alarms() - result = list(self.alarm_conn.query_alarms(limit=2)) - self.assertEqual(2, len(result)) - - def test_filter(self): - self.add_some_alarms() - filter_expr = {"and": - [{"or": - [{"=": {"name": "yellow-alert"}}, - {"=": {"name": "red-alert"}}]}, - {"=": {"enabled": True}}]} - - result = list(self.alarm_conn.query_alarms(filter_expr=filter_expr)) - - self.assertEqual(1, len(result)) - for a in result: - self.assertIn(a.name, set(["yellow-alert", "red-alert"])) - self.assertTrue(a.enabled) - - def test_filter_with_regexp(self): - self.add_some_alarms() - filter_expr = {"and": - [{"or": [{"=": {"name": "yellow-alert"}}, - {"=": {"name": "red-alert"}}]}, - {"=~": {"description": "yel.*"}}]} - - result = list(self.alarm_conn.query_alarms(filter_expr=filter_expr)) - - self.assertEqual(1, len(result)) - for a in result: - self.assertEqual("yellow", a.description) - - def test_filter_for_alarm_id(self): - self.add_some_alarms() - filter_expr = {"=": {"alarm_id": "0r4ng3"}} - - result = list(self.alarm_conn.query_alarms(filter_expr=filter_expr)) - - self.assertEqual(1, len(result)) - for a in result: - self.assertEqual("0r4ng3", a.alarm_id) - - def test_filter_and_orderby(self): - self.add_some_alarms() - result = list(self.alarm_conn.query_alarms(filter_expr=( - {"=": {"enabled": True}}), - orderby=[{"name": "asc"}])) - self.assertEqual(2, len(result)) - self.assertEqual(["orange-alert", "red-alert"], - [a.name for a in result]) - for a in result: - self.assertTrue(a.enabled) - - -class ComplexAlarmHistoryQueryTest(AlarmTestBase, - tests_db.MixinTestsWithBackendScenarios): - def setUp(self): - super(DBTestBase, self).setUp() - self.filter_expr = {"and": - [{"or": - [{"=": {"type": "rule change"}}, - {"=": {"type": "state transition"}}]}, - {"=": {"alarm_id": "0r4ng3"}}]} - self.add_some_alarms() - self.prepare_alarm_history() - - def prepare_alarm_history(self): - alarms = list(self.alarm_conn.get_alarms()) - name_index = { - 'red-alert': 0, - 'orange-alert': 1, - 'yellow-alert': 2 - } - - for alarm in alarms: - i = name_index[alarm.name] - alarm_change = dict(event_id=( - "16fd2706-8baf-433b-82eb-8c7fada847c%s" % i), - alarm_id=alarm.alarm_id, - type=alarm_models.AlarmChange.CREATION, - detail="detail %s" % alarm.name, - user_id=alarm.user_id, - project_id=alarm.project_id, - on_behalf_of=alarm.project_id, - timestamp=datetime.datetime(2012, 9, 24, - 7 + i, - 30 + i)) - self.alarm_conn.record_alarm_change(alarm_change=alarm_change) - - alarm_change2 = dict(event_id=( - "16fd2706-8baf-433b-82eb-8c7fada847d%s" % i), - alarm_id=alarm.alarm_id, - type=alarm_models.AlarmChange.RULE_CHANGE, - detail="detail %s" % i, - user_id=alarm.user_id, - project_id=alarm.project_id, - on_behalf_of=alarm.project_id, - timestamp=datetime.datetime(2012, 9, 25, - 10 + i, - 30 + i)) - self.alarm_conn.record_alarm_change(alarm_change=alarm_change2) - - alarm_change3 = dict( - event_id="16fd2706-8baf-433b-82eb-8c7fada847e%s" % i, - alarm_id=alarm.alarm_id, - type=alarm_models.AlarmChange.STATE_TRANSITION, - detail="detail %s" % (i + 1), - user_id=alarm.user_id, - project_id=alarm.project_id, - on_behalf_of=alarm.project_id, - timestamp=datetime.datetime(2012, 9, 26, 10 + i, 30 + i) - ) - - if alarm.name == "red-alert": - alarm_change3['on_behalf_of'] = 'and-da-girls' - - self.alarm_conn.record_alarm_change(alarm_change=alarm_change3) - - def test_alarm_history_with_no_filter(self): - history = list(self.alarm_conn.query_alarm_history()) - self.assertEqual(9, len(history)) - - def test_alarm_history_with_no_filter_and_limit(self): - history = list(self.alarm_conn.query_alarm_history(limit=3)) - self.assertEqual(3, len(history)) - - def test_alarm_history_with_filter(self): - history = list( - self.alarm_conn.query_alarm_history(filter_expr=self.filter_expr)) - self.assertEqual(2, len(history)) - - def test_alarm_history_with_regexp(self): - filter_expr = {"and": - [{"=~": {"type": "(rule)|(state)"}}, - {"=": {"alarm_id": "0r4ng3"}}]} - history = list( - self.alarm_conn.query_alarm_history(filter_expr=filter_expr)) - self.assertEqual(2, len(history)) - - def test_alarm_history_with_filter_and_orderby(self): - history = list( - self.alarm_conn.query_alarm_history(filter_expr=self.filter_expr, - orderby=[{"timestamp": - "asc"}])) - self.assertEqual([alarm_models.AlarmChange.RULE_CHANGE, - alarm_models.AlarmChange.STATE_TRANSITION], - [h.type for h in history]) - - def test_alarm_history_with_filter_and_orderby_and_limit(self): - history = list( - self.alarm_conn.query_alarm_history(filter_expr=self.filter_expr, - orderby=[{"timestamp": - "asc"}], - limit=1)) - self.assertEqual(alarm_models.AlarmChange.RULE_CHANGE, history[0].type) - - def test_alarm_history_with_on_behalf_of_filter(self): - filter_expr = {"=": {"on_behalf_of": "and-da-girls"}} - history = list(self.alarm_conn.query_alarm_history( - filter_expr=filter_expr)) - self.assertEqual(1, len(history)) - self.assertEqual("16fd2706-8baf-433b-82eb-8c7fada847e0", - history[0].event_id) - - def test_alarm_history_with_alarm_id_as_filter(self): - filter_expr = {"=": {"alarm_id": "r3d"}} - history = list(self.alarm_conn.query_alarm_history( - filter_expr=filter_expr, orderby=[{"timestamp": "asc"}])) - self.assertEqual(3, len(history)) - self.assertEqual([alarm_models.AlarmChange.CREATION, - alarm_models.AlarmChange.RULE_CHANGE, - alarm_models.AlarmChange.STATE_TRANSITION], - [h.type for h in history]) - - class EventTestBase(tests_db.TestBase, tests_db.MixinTestsWithBackendScenarios): """Separate test base class. diff --git a/ceilometer/tests/functional/test_bin.py b/ceilometer/tests/functional/test_bin.py index 817c0e52..cbd5c1af 100644 --- a/ceilometer/tests/functional/test_bin.py +++ b/ceilometer/tests/functional/test_bin.py @@ -61,8 +61,6 @@ class BinTestCase(base.BaseTestCase): b"time to live is disabled", err) self.assertIn(b"Nothing to clean, database event " b"time to live is disabled", err) - self.assertIn(b"Nothing to clean, database alarm history " - b"time to live is disabled", err) def _test_run_expirer_ttl_enabled(self, ttl_name, data_name): content = ("[DEFAULT]\n" @@ -91,8 +89,6 @@ class BinTestCase(base.BaseTestCase): 'metering') self._test_run_expirer_ttl_enabled('time_to_live', 'metering') self._test_run_expirer_ttl_enabled('event_time_to_live', 'event') - self._test_run_expirer_ttl_enabled('alarm_history_time_to_live', - 'alarm history') class BinSendSampleTestCase(base.BaseTestCase): @@ -206,37 +202,6 @@ class BinApiTestCase(base.BaseTestCase): content = content.decode('utf-8') self.assertEqual([], json.loads(content)) - def test_v2_with_bad_storage_conn(self): - - content = ("[DEFAULT]\n" - "rpc_backend=fake\n" - "auth_strategy=noauth\n" - "debug=true\n" - "pipeline_cfg_file={0}\n" - "policy_file={1}\n" - "api_paste_config={2}\n" - "[api]\n" - "port={3}\n" - "[database]\n" - "max_retries=1\n" - "alarm_connection=log://localhost\n" - "connection=dummy://localhost\n". - format(self.pipeline_cfg_file, - self.policy_file, - self.paste, - self.api_port)) - - self.subp = self.run_api(content, err_pipe=True) - - response, content = self.get_response('v2/alarms') - self.assertEqual(200, response.status) - if six.PY3: - content = content.decode('utf-8') - self.assertEqual([], json.loads(content)) - - response, content = self.get_response('v2/meters') - self.assertEqual(500, response.status) - def test_v2_with_all_bad_conns(self): content = ("[DEFAULT]\n" @@ -250,7 +215,6 @@ class BinApiTestCase(base.BaseTestCase): "port={3}\n" "[database]\n" "max_retries=1\n" - "alarm_connection=dummy://localhost\n" "connection=dummy://localhost\n" "event_connection=dummy://localhost\n". format(self.pipeline_cfg_file, @@ -263,7 +227,7 @@ class BinApiTestCase(base.BaseTestCase): __, err = self.subp.communicate() self.assertIn(b"Api failed to start. Failed to connect to" - b" databases, purpose: metering, event, alarm", err) + b" databases, purpose: metering, event", err) class BinCeilometerPollingServiceTestCase(base.BaseTestCase): diff --git a/ceilometer/tests/unit/alarm/__init__.py b/ceilometer/tests/unit/alarm/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/ceilometer/tests/unit/alarm/evaluator/__init__.py b/ceilometer/tests/unit/alarm/evaluator/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/ceilometer/tests/unit/alarm/evaluator/base.py b/ceilometer/tests/unit/alarm/evaluator/base.py deleted file mode 100644 index 6ba36727..00000000 --- a/ceilometer/tests/unit/alarm/evaluator/base.py +++ /dev/null @@ -1,43 +0,0 @@ -# -# Copyright 2013 eNovance -# -# 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. -"""Base class for tests in ceilometer/alarm/evaluator/ -""" -import mock -from oslotest import base - - -class TestEvaluatorBase(base.BaseTestCase): - def setUp(self): - super(TestEvaluatorBase, self).setUp() - self.api_client = mock.Mock() - self.notifier = mock.MagicMock() - self.evaluator = self.EVALUATOR(self.notifier) - self.prepare_alarms() - - @staticmethod - def prepare_alarms(self): - self.alarms = [] - - def _evaluate_all_alarms(self): - for alarm in self.alarms: - self.evaluator.evaluate(alarm) - - def _set_all_alarms(self, state): - for alarm in self.alarms: - alarm.state = state - - def _assert_all_alarms(self, state): - for alarm in self.alarms: - self.assertEqual(state, alarm.state) diff --git a/ceilometer/tests/unit/alarm/evaluator/test_base.py b/ceilometer/tests/unit/alarm/evaluator/test_base.py deleted file mode 100644 index fd725876..00000000 --- a/ceilometer/tests/unit/alarm/evaluator/test_base.py +++ /dev/null @@ -1,156 +0,0 @@ -# -# Copyright 2013 IBM Corp -# -# 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. -"""class for tests in ceilometer/alarm/evaluator/__init__.py -""" -import datetime - -import mock -from oslo_utils import timeutils -from oslotest import base - -from ceilometer.alarm import evaluator - - -class TestEvaluatorBaseClass(base.BaseTestCase): - def setUp(self): - super(TestEvaluatorBaseClass, self).setUp() - self.called = False - - def _notify(self, alarm, previous, reason, details): - self.called = True - raise Exception('Boom!') - - def test_base_refresh(self): - notifier = mock.MagicMock() - notifier.notify = self._notify - - class EvaluatorSub(evaluator.Evaluator): - def evaluate(self, alarm): - pass - - ev = EvaluatorSub(notifier) - ev.api_client = mock.MagicMock() - ev._refresh(mock.MagicMock(), mock.MagicMock(), - mock.MagicMock(), mock.MagicMock()) - self.assertTrue(self.called) - - @mock.patch.object(timeutils, 'utcnow') - def test_base_time_constraints(self, mock_utcnow): - alarm = mock.MagicMock() - alarm.time_constraints = [ - {'name': 'test', - 'description': 'test', - 'start': '0 11 * * *', # daily at 11:00 - 'duration': 10800, # 3 hours - 'timezone': ''}, - {'name': 'test2', - 'description': 'test', - 'start': '0 23 * * *', # daily at 23:00 - 'duration': 10800, # 3 hours - 'timezone': ''}, - ] - cls = evaluator.Evaluator - mock_utcnow.return_value = datetime.datetime(2014, 1, 1, 12, 0, 0) - self.assertTrue(cls.within_time_constraint(alarm)) - - mock_utcnow.return_value = datetime.datetime(2014, 1, 2, 1, 0, 0) - self.assertTrue(cls.within_time_constraint(alarm)) - - mock_utcnow.return_value = datetime.datetime(2014, 1, 2, 5, 0, 0) - self.assertFalse(cls.within_time_constraint(alarm)) - - @mock.patch.object(timeutils, 'utcnow') - def test_base_time_constraints_by_month(self, mock_utcnow): - alarm = mock.MagicMock() - alarm.time_constraints = [ - {'name': 'test', - 'description': 'test', - 'start': '0 11 31 1,3,5,7,8,10,12 *', # every 31st at 11:00 - 'duration': 10800, # 3 hours - 'timezone': ''}, - ] - cls = evaluator.Evaluator - mock_utcnow.return_value = datetime.datetime(2015, 3, 31, 11, 30, 0) - self.assertTrue(cls.within_time_constraint(alarm)) - - @mock.patch.object(timeutils, 'utcnow') - def test_base_time_constraints_complex(self, mock_utcnow): - alarm = mock.MagicMock() - alarm.time_constraints = [ - {'name': 'test', - 'description': 'test', - # Every consecutive 2 minutes (from the 3rd to the 57th) past - # every consecutive 2 hours (between 3:00 and 12:59) on every day. - 'start': '3-57/2 3-12/2 * * *', - 'duration': 30, - 'timezone': ''} - ] - cls = evaluator.Evaluator - - # test minutes inside - mock_utcnow.return_value = datetime.datetime(2014, 1, 5, 3, 3, 0) - self.assertTrue(cls.within_time_constraint(alarm)) - mock_utcnow.return_value = datetime.datetime(2014, 1, 5, 3, 31, 0) - self.assertTrue(cls.within_time_constraint(alarm)) - mock_utcnow.return_value = datetime.datetime(2014, 1, 5, 3, 57, 0) - self.assertTrue(cls.within_time_constraint(alarm)) - - # test minutes outside - mock_utcnow.return_value = datetime.datetime(2014, 1, 5, 3, 2, 0) - self.assertFalse(cls.within_time_constraint(alarm)) - mock_utcnow.return_value = datetime.datetime(2014, 1, 5, 3, 4, 0) - self.assertFalse(cls.within_time_constraint(alarm)) - mock_utcnow.return_value = datetime.datetime(2014, 1, 5, 3, 58, 0) - self.assertFalse(cls.within_time_constraint(alarm)) - - # test hours inside - mock_utcnow.return_value = datetime.datetime(2014, 1, 5, 3, 31, 0) - self.assertTrue(cls.within_time_constraint(alarm)) - mock_utcnow.return_value = datetime.datetime(2014, 1, 5, 5, 31, 0) - self.assertTrue(cls.within_time_constraint(alarm)) - mock_utcnow.return_value = datetime.datetime(2014, 1, 5, 11, 31, 0) - self.assertTrue(cls.within_time_constraint(alarm)) - - # test hours outside - mock_utcnow.return_value = datetime.datetime(2014, 1, 5, 1, 31, 0) - self.assertFalse(cls.within_time_constraint(alarm)) - mock_utcnow.return_value = datetime.datetime(2014, 1, 5, 4, 31, 0) - self.assertFalse(cls.within_time_constraint(alarm)) - mock_utcnow.return_value = datetime.datetime(2014, 1, 5, 12, 31, 0) - self.assertFalse(cls.within_time_constraint(alarm)) - - @mock.patch.object(timeutils, 'utcnow') - def test_base_time_constraints_timezone(self, mock_utcnow): - alarm = mock.MagicMock() - cls = evaluator.Evaluator - mock_utcnow.return_value = datetime.datetime(2014, 1, 1, 11, 0, 0) - - alarm.time_constraints = [ - {'name': 'test', - 'description': 'test', - 'start': '0 11 * * *', # daily at 11:00 - 'duration': 10800, # 3 hours - 'timezone': 'Europe/Ljubljana'} - ] - self.assertTrue(cls.within_time_constraint(alarm)) - - alarm.time_constraints = [ - {'name': 'test2', - 'description': 'test2', - 'start': '0 11 * * *', # daily at 11:00 - 'duration': 10800, # 3 hours - 'timezone': 'US/Eastern'} - ] - self.assertFalse(cls.within_time_constraint(alarm)) diff --git a/ceilometer/tests/unit/alarm/evaluator/test_combination.py b/ceilometer/tests/unit/alarm/evaluator/test_combination.py deleted file mode 100644 index b7cdd6ee..00000000 --- a/ceilometer/tests/unit/alarm/evaluator/test_combination.py +++ /dev/null @@ -1,408 +0,0 @@ -# -# Copyright 2013 eNovance -# -# Authors: Mehdi Abaakouk -# -# 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. - -"""Tests for ceilometer/alarm/evaluator/combination.py -""" - -import datetime -import uuid - -from ceilometerclient import exc -from ceilometerclient.v2 import alarms -import mock -from oslo_utils import timeutils -import pytz - -from ceilometer.alarm.evaluator import combination -from ceilometer.alarm.storage import models -from ceilometer.tests import constants -from ceilometer.tests.unit.alarm.evaluator import base - - -class TestEvaluate(base.TestEvaluatorBase): - EVALUATOR = combination.CombinationEvaluator - - def prepare_alarms(self): - self.alarms = [ - models.Alarm(name='or-alarm', - description='the or alarm', - type='combination', - enabled=True, - user_id='foobar', - project_id='snafu', - alarm_id=str(uuid.uuid4()), - state='insufficient data', - state_timestamp=constants.MIN_DATETIME, - timestamp=constants.MIN_DATETIME, - insufficient_data_actions=[], - ok_actions=[], - alarm_actions=[], - repeat_actions=False, - time_constraints=[], - rule=dict( - alarm_ids=[ - '9cfc3e51-2ff1-4b1d-ac01-c1bd4c6d0d1e', - '1d441595-d069-4e05-95ab-8693ba6a8302'], - operator='or', - ), - severity='critical'), - models.Alarm(name='and-alarm', - description='the and alarm', - type='combination', - enabled=True, - user_id='foobar', - project_id='snafu', - alarm_id=str(uuid.uuid4()), - state='insufficient data', - state_timestamp=constants.MIN_DATETIME, - timestamp=constants.MIN_DATETIME, - insufficient_data_actions=[], - ok_actions=[], - alarm_actions=[], - repeat_actions=False, - time_constraints=[], - rule=dict( - alarm_ids=[ - 'b82734f4-9d06-48f3-8a86-fa59a0c99dc8', - '15a700e5-2fe8-4b3d-8c55-9e92831f6a2b'], - operator='and', - ), - severity='critical') - ] - - @staticmethod - def _get_alarm(state): - return alarms.Alarm(None, {'state': state}) - - @staticmethod - def _reason_data(alarm_ids): - return {'type': 'combination', 'alarm_ids': alarm_ids} - - def _combination_transition_reason(self, state, alarm_ids1, alarm_ids2): - return ([('Transition to %(state)s due to alarms %(alarm_ids)s' - ' in state %(state)s') - % {'state': state, 'alarm_ids': ",".join(alarm_ids1)}, - ('Transition to %(state)s due to alarms %(alarm_ids)s' - ' in state %(state)s') - % {'state': state, 'alarm_ids': ",".join(alarm_ids2)}], - [self._reason_data(alarm_ids1), self._reason_data(alarm_ids2)]) - - def _combination_remaining_reason(self, state, alarm_ids1, alarm_ids2): - return ([('Remaining as %(state)s due to alarms %(alarm_ids)s' - ' in state %(state)s') - % {'state': state, 'alarm_ids': ",".join(alarm_ids1)}, - ('Remaining as %(state)s due to alarms %(alarm_ids)s' - ' in state %(state)s') - % {'state': state, 'alarm_ids': ",".join(alarm_ids2)}], - [self._reason_data(alarm_ids1), self._reason_data(alarm_ids2)]) - - def test_retry_transient_api_failure(self): - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - broken = exc.CommunicationError(message='broken') - self.api_client.alarms.get.side_effect = [ - broken, - broken, - broken, - broken, - self._get_alarm('ok'), - self._get_alarm('ok'), - self._get_alarm('ok'), - self._get_alarm('ok'), - ] - self._evaluate_all_alarms() - self._assert_all_alarms('insufficient data') - self._evaluate_all_alarms() - self._assert_all_alarms('ok') - - def test_simple_insufficient(self): - self._set_all_alarms('ok') - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - broken = exc.CommunicationError(message='broken') - self.api_client.alarms.get.side_effect = broken - self._evaluate_all_alarms() - self._assert_all_alarms('insufficient data') - expected = [mock.call(alarm.alarm_id, state='insufficient data') - for alarm in self.alarms] - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual(expected, update_calls) - expected = [mock.call( - alarm, - 'ok', - ('Alarms %s are in unknown state' % - (",".join(alarm.rule['alarm_ids']))), - self._reason_data(alarm.rule['alarm_ids'])) - for alarm in self.alarms] - self.assertEqual(expected, self.notifier.notify.call_args_list) - - def test_to_ok_with_all_ok(self): - self._set_all_alarms('insufficient data') - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - self.api_client.alarms.get.side_effect = [ - self._get_alarm('ok'), - self._get_alarm('ok'), - self._get_alarm('ok'), - self._get_alarm('ok'), - ] - self._evaluate_all_alarms() - expected = [mock.call(alarm.alarm_id, state='ok') - for alarm in self.alarms] - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual(expected, update_calls) - reasons, reason_datas = self._combination_transition_reason( - 'ok', - self.alarms[0].rule['alarm_ids'], - self.alarms[1].rule['alarm_ids']) - expected = [mock.call(alarm, 'insufficient data', - reason, reason_data) - for alarm, reason, reason_data - in zip(self.alarms, reasons, reason_datas)] - self.assertEqual(expected, self.notifier.notify.call_args_list) - - def test_to_ok_with_one_alarm(self): - self._set_all_alarms('alarm') - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - self.api_client.alarms.get.side_effect = [ - self._get_alarm('ok'), - self._get_alarm('ok'), - self._get_alarm('alarm'), - self._get_alarm('ok'), - ] - self._evaluate_all_alarms() - expected = [mock.call(alarm.alarm_id, state='ok') - for alarm in self.alarms] - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual(expected, update_calls) - reasons, reason_datas = self._combination_transition_reason( - 'ok', - self.alarms[0].rule['alarm_ids'], - [self.alarms[1].rule['alarm_ids'][1]]) - expected = [mock.call(alarm, 'alarm', reason, reason_data) - for alarm, reason, reason_data - in zip(self.alarms, reasons, reason_datas)] - self.assertEqual(expected, self.notifier.notify.call_args_list) - - def test_to_alarm_with_all_alarm(self): - self._set_all_alarms('ok') - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - self.api_client.alarms.get.side_effect = [ - self._get_alarm('alarm'), - self._get_alarm('alarm'), - self._get_alarm('alarm'), - self._get_alarm('alarm'), - ] - self._evaluate_all_alarms() - expected = [mock.call(alarm.alarm_id, state='alarm') - for alarm in self.alarms] - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual(expected, update_calls) - reasons, reason_datas = self._combination_transition_reason( - 'alarm', - self.alarms[0].rule['alarm_ids'], - self.alarms[1].rule['alarm_ids']) - expected = [mock.call(alarm, 'ok', reason, reason_data) - for alarm, reason, reason_data - in zip(self.alarms, reasons, reason_datas)] - self.assertEqual(expected, self.notifier.notify.call_args_list) - - def test_to_alarm_with_one_insufficient_data(self): - self._set_all_alarms('ok') - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - self.api_client.alarms.get.side_effect = [ - self._get_alarm('insufficient data'), - self._get_alarm('alarm'), - self._get_alarm('alarm'), - self._get_alarm('alarm'), - ] - self._evaluate_all_alarms() - expected = [mock.call(alarm.alarm_id, state='alarm') - for alarm in self.alarms] - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual(expected, update_calls) - reasons, reason_datas = self._combination_transition_reason( - 'alarm', - [self.alarms[0].rule['alarm_ids'][1]], - self.alarms[1].rule['alarm_ids']) - expected = [mock.call(alarm, 'ok', reason, reason_data) - for alarm, reason, reason_data - in zip(self.alarms, reasons, reason_datas)] - self.assertEqual(expected, self.notifier.notify.call_args_list) - - def test_to_alarm_with_one_ok(self): - self._set_all_alarms('ok') - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - self.api_client.alarms.get.side_effect = [ - self._get_alarm('ok'), - self._get_alarm('alarm'), - self._get_alarm('alarm'), - self._get_alarm('alarm'), - ] - self._evaluate_all_alarms() - expected = [mock.call(alarm.alarm_id, state='alarm') - for alarm in self.alarms] - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual(expected, update_calls) - reasons, reason_datas = self._combination_transition_reason( - 'alarm', - [self.alarms[0].rule['alarm_ids'][1]], - self.alarms[1].rule['alarm_ids']) - expected = [mock.call(alarm, 'ok', reason, reason_data) - for alarm, reason, reason_data - in zip(self.alarms, reasons, reason_datas)] - self.assertEqual(expected, self.notifier.notify.call_args_list) - - def test_to_unknown(self): - self._set_all_alarms('ok') - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - broken = exc.CommunicationError(message='broken') - self.api_client.alarms.get.side_effect = [ - broken, - self._get_alarm('ok'), - self._get_alarm('insufficient data'), - self._get_alarm('ok'), - ] - self._evaluate_all_alarms() - expected = [mock.call(alarm.alarm_id, state='insufficient data') - for alarm in self.alarms] - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual(expected, update_calls) - reasons = ['Alarms %s are in unknown state' - % self.alarms[0].rule['alarm_ids'][0], - 'Alarms %s are in unknown state' - % self.alarms[1].rule['alarm_ids'][0]] - reason_datas = [ - self._reason_data([self.alarms[0].rule['alarm_ids'][0]]), - self._reason_data([self.alarms[1].rule['alarm_ids'][0]])] - expected = [mock.call(alarm, 'ok', reason, reason_data) - for alarm, reason, reason_data - in zip(self.alarms, reasons, reason_datas)] - self.assertEqual(expected, self.notifier.notify.call_args_list) - - def test_no_state_change(self): - self._set_all_alarms('ok') - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - self.api_client.alarms.get.side_effect = [ - self._get_alarm('ok'), - self._get_alarm('ok'), - self._get_alarm('ok'), - self._get_alarm('ok'), - ] - self._evaluate_all_alarms() - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual([], update_calls) - self.assertEqual([], self.notifier.notify.call_args_list) - - def test_no_state_change_and_repeat_actions(self): - self.alarms[0].repeat_actions = True - self.alarms[1].repeat_actions = True - self._set_all_alarms('ok') - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - self.api_client.alarms.get.side_effect = [ - self._get_alarm('ok'), - self._get_alarm('ok'), - self._get_alarm('ok'), - self._get_alarm('ok'), - ] - self._evaluate_all_alarms() - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual([], update_calls) - reasons, reason_datas = self._combination_remaining_reason( - 'ok', - self.alarms[0].rule['alarm_ids'], - self.alarms[1].rule['alarm_ids']) - expected = [mock.call(alarm, 'ok', reason, reason_data) - for alarm, reason, reason_data - in zip(self.alarms, reasons, reason_datas)] - - self.assertEqual(expected, self.notifier.notify.call_args_list) - - @mock.patch.object(timeutils, 'utcnow') - def test_state_change_inside_time_constraint(self, mock_utcnow): - self._set_all_alarms('insufficient data') - self.alarms[0].time_constraints = [ - {'name': 'test', - 'description': 'test', - 'start': '0 11 * * *', # daily at 11:00 - 'duration': 10800, # 3 hours - 'timezone': 'Europe/Ljubljana'} - ] - self.alarms[1].time_constraints = self.alarms[0].time_constraints - dt = datetime.datetime(2014, 1, 1, 12, 0, 0, - tzinfo=pytz.timezone('Europe/Ljubljana')) - mock_utcnow.return_value = dt.astimezone(pytz.UTC) - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - self.api_client.alarms.get.side_effect = [ - self._get_alarm('ok'), - self._get_alarm('ok'), - self._get_alarm('ok'), - self._get_alarm('ok'), - ] - self._evaluate_all_alarms() - expected = [mock.call(alarm.alarm_id, state='ok') - for alarm in self.alarms] - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual(expected, update_calls, - "Alarm should change state if the current " - "time is inside its time constraint.") - reasons, reason_datas = self._combination_transition_reason( - 'ok', - self.alarms[0].rule['alarm_ids'], - self.alarms[1].rule['alarm_ids']) - expected = [mock.call(alarm, 'insufficient data', - reason, reason_data) - for alarm, reason, reason_data - in zip(self.alarms, reasons, reason_datas)] - self.assertEqual(expected, self.notifier.notify.call_args_list) - - @mock.patch.object(timeutils, 'utcnow') - def test_no_state_change_outside_time_constraint(self, mock_utcnow): - self._set_all_alarms('insufficient data') - self.alarms[0].time_constraints = [ - {'name': 'test', - 'description': 'test', - 'start': '0 11 * * *', # daily at 11:00 - 'duration': 10800, # 3 hours - 'timezone': 'Europe/Ljubljana'} - ] - self.alarms[1].time_constraints = self.alarms[0].time_constraints - dt = datetime.datetime(2014, 1, 1, 15, 0, 0, - tzinfo=pytz.timezone('Europe/Ljubljana')) - mock_utcnow.return_value = dt.astimezone(pytz.UTC) - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - self.api_client.alarms.get.side_effect = [ - self._get_alarm('ok'), - self._get_alarm('ok'), - self._get_alarm('ok'), - self._get_alarm('ok'), - ] - self._evaluate_all_alarms() - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual([], update_calls, - "Alarm should not change state if the current " - " time is outside its time constraint.") - self.assertEqual([], self.notifier.notify.call_args_list) diff --git a/ceilometer/tests/unit/alarm/evaluator/test_gnocchi.py b/ceilometer/tests/unit/alarm/evaluator/test_gnocchi.py deleted file mode 100644 index ea60b6a9..00000000 --- a/ceilometer/tests/unit/alarm/evaluator/test_gnocchi.py +++ /dev/null @@ -1,438 +0,0 @@ -# -# Copyright 2015 eNovance -# -# 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 datetime -import unittest -import uuid - -import mock -from oslo_serialization import jsonutils -from oslo_utils import timeutils -from oslotest import mockpatch -import pytz -import six -from six import moves - -from ceilometer.alarm.evaluator import gnocchi -from ceilometer.alarm.storage import models -from ceilometer.tests import constants -from ceilometer.tests.unit.alarm.evaluator import base - - -class FakeResponse(object): - def __init__(self, code, data): - if code == 200: - self.values = [d[2] for d in data] - else: - self.values = [] - self.text = jsonutils.dumps(data) - self.status_code = code - - -class TestGnocchiThresholdEvaluate(base.TestEvaluatorBase): - EVALUATOR = gnocchi.GnocchiThresholdEvaluator - - def setUp(self): - ks_client = mock.Mock(auth_token='fake_token') - ks_client.users.find.return_value = 'gnocchi' - self.useFixture(mockpatch.Patch( - 'keystoneclient.v2_0.client.Client', - return_value=ks_client)) - - super(TestGnocchiThresholdEvaluate, self).setUp() - - self.useFixture(mockpatch.Patch('ceilometerclient.client.get_client', - return_value=self.api_client)) - self.requests = self.useFixture(mockpatch.Patch( - 'ceilometer.alarm.evaluator.gnocchi.requests')).mock - - def prepare_alarms(self): - self.alarms = [ - models.Alarm(name='instance_running_hot', - description='instance_running_hot', - type='gnocchi_resources_threshold', - enabled=True, - user_id='foobar', - project_id='snafu', - alarm_id=str(uuid.uuid4()), - state='insufficient data', - state_timestamp=constants.MIN_DATETIME, - timestamp=constants.MIN_DATETIME, - insufficient_data_actions=[], - ok_actions=[], - alarm_actions=[], - repeat_actions=False, - time_constraints=[], - rule=dict( - comparison_operator='gt', - threshold=80.0, - evaluation_periods=5, - aggregation_method='mean', - granularity=60, - metric='cpu_util', - resource_type='instance', - resource_id='my_instance') - ), - models.Alarm(name='group_running_idle', - description='group_running_idle', - type='gnocchi_aggregation_by_metrics_threshold', - enabled=True, - user_id='foobar', - project_id='snafu', - state='insufficient data', - state_timestamp=constants.MIN_DATETIME, - timestamp=constants.MIN_DATETIME, - insufficient_data_actions=[], - ok_actions=[], - alarm_actions=[], - repeat_actions=False, - alarm_id=str(uuid.uuid4()), - time_constraints=[], - rule=dict( - comparison_operator='le', - threshold=10.0, - evaluation_periods=4, - aggregation_method='max', - granularity=300, - metrics=['0bb1604d-1193-4c0a-b4b8-74b170e35e83', - '9ddc209f-42f8-41e1-b8f1-8804f59c4053']), - ), - models.Alarm(name='instance_not_running', - description='instance_running_hot', - type='gnocchi_aggregation_by_resources_threshold', - enabled=True, - user_id='foobar', - project_id='snafu', - alarm_id=str(uuid.uuid4()), - state='insufficient data', - state_timestamp=constants.MIN_DATETIME, - timestamp=constants.MIN_DATETIME, - insufficient_data_actions=[], - ok_actions=[], - alarm_actions=[], - repeat_actions=False, - time_constraints=[], - rule=dict( - comparison_operator='gt', - threshold=80.0, - evaluation_periods=6, - aggregation_method='mean', - granularity=50, - metric='cpu_util', - resource_type='instance', - query='{"=": {"server_group": ' - '"my_autoscaling_group"}}') - ), - - ] - - @staticmethod - def _get_stats(granularity, values): - now = timeutils.utcnow_ts() - return FakeResponse( - 200, [[six.text_type(now - len(values) * granularity), - granularity, value] for value in values]) - - @staticmethod - def _reason_data(disposition, count, most_recent): - return {'type': 'threshold', 'disposition': disposition, - 'count': count, 'most_recent': most_recent} - - def _set_all_rules(self, field, value): - for alarm in self.alarms: - alarm.rule[field] = value - - def test_retry_transient_api_failure(self): - means = self._get_stats(60, [self.alarms[0].rule['threshold'] - v - for v in moves.xrange(5)]) - maxs = self._get_stats(300, [self.alarms[1].rule['threshold'] + v - for v in moves.xrange(1, 4)]) - avgs2 = self._get_stats(50, [self.alarms[2].rule['threshold'] - v - for v in moves.xrange(6)]) - self.requests.get.side_effect = [Exception('boom'), - FakeResponse(500, "error"), - means, - maxs] - self.requests.post.side_effect = [FakeResponse(500, "error"), avgs2] - self._evaluate_all_alarms() - self._assert_all_alarms('insufficient data') - self._evaluate_all_alarms() - self._assert_all_alarms('ok') - - def test_simple_insufficient(self): - self._set_all_alarms('ok') - self.requests.get.return_value = FakeResponse(200, []) - self.requests.post.return_value = FakeResponse(200, []) - self._evaluate_all_alarms() - self._assert_all_alarms('insufficient data') - expected = [mock.call(alarm.alarm_id, state='insufficient data') - for alarm in self.alarms] - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual(expected, update_calls) - expected = [mock.call( - alarm, - 'ok', - ('%d datapoints are unknown' - % alarm.rule['evaluation_periods']), - self._reason_data('unknown', - alarm.rule['evaluation_periods'], - None)) - for alarm in self.alarms] - self.assertEqual(expected, self.notifier.notify.call_args_list) - - @mock.patch.object(timeutils, 'utcnow') - def test_simple_alarm_trip(self, utcnow): - utcnow.return_value = datetime.datetime(2015, 1, 26, 12, 57, 0, 0) - self._set_all_alarms('ok') - avgs = self._get_stats(60, [self.alarms[0].rule['threshold'] + v - for v in moves.xrange(1, 6)]) - maxs = self._get_stats(300, [self.alarms[1].rule['threshold'] - v - for v in moves.xrange(4)]) - avgs2 = self._get_stats(50, [self.alarms[2].rule['threshold'] + v - for v in moves.xrange(1, 7)]) - - self.requests.get.side_effect = [avgs, maxs] - self.requests.post.side_effect = [avgs2] - self._evaluate_all_alarms() - - expected_headers = {'X-Auth-Token': 'fake_token', - 'Content-Type': 'application/json'} - - start_alarm1 = "2015-01-26T12:51:00" - start_alarm2 = "2015-01-26T12:32:00" - start_alarm3 = "2015-01-26T12:51:10" - end = "2015-01-26T12:57:00" - - self.assertEqual([ - mock.call(url='http://localhost:8041/v1/resource/instance/' - 'my_instance/metric/cpu_util/measures', - params={'aggregation': 'mean', - 'start': start_alarm1, 'end': end}, - headers=expected_headers), - mock.call(url='http://localhost:8041/v1/aggregation/metric', - params={'aggregation': 'max', - 'start': start_alarm2, 'end': end, - 'metric[]': [ - '0bb1604d-1193-4c0a-b4b8-74b170e35e83', - '9ddc209f-42f8-41e1-b8f1-8804f59c4053']}, - headers=expected_headers)], - - self.requests.get.mock_calls) - self.assertEqual([ - mock.call(url='http://localhost:8041/v1/aggregation/resource/' - 'instance/metric/cpu_util', - params={'aggregation': 'mean', - 'start': start_alarm3, 'end': end}, - data='{"=": {"server_group": "my_autoscaling_group"}}', - headers=expected_headers), - ], - self.requests.post.mock_calls) - - self._assert_all_alarms('alarm') - expected = [mock.call(alarm.alarm_id, state='alarm') - for alarm in self.alarms] - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual(expected, update_calls) - reasons = ['Transition to alarm due to 5 samples outside' - ' threshold, most recent: %s' % avgs.values[-1], - 'Transition to alarm due to 4 samples outside' - ' threshold, most recent: %s' % maxs.values[-1], - 'Transition to alarm due to 6 samples outside' - ' threshold, most recent: %s' % avgs2.values[-1], - ] - reason_datas = [self._reason_data('outside', 5, avgs.values[-1]), - self._reason_data('outside', 4, maxs.values[-1]), - self._reason_data('outside', 6, avgs2.values[-1])] - expected = [mock.call(alarm, 'ok', reason, reason_data) - for alarm, reason, reason_data - in zip(self.alarms, reasons, reason_datas)] - self.assertEqual(expected, self.notifier.notify.call_args_list) - - def test_simple_alarm_clear(self): - self._set_all_alarms('alarm') - avgs = self._get_stats(60, [self.alarms[0].rule['threshold'] - v - for v in moves.xrange(5)]) - maxs = self._get_stats(300, [self.alarms[1].rule['threshold'] + v - for v in moves.xrange(1, 5)]) - avgs2 = self._get_stats(50, [self.alarms[2].rule['threshold'] - v - for v in moves.xrange(6)]) - self.requests.post.side_effect = [avgs2] - self.requests.get.side_effect = [avgs, maxs] - self._evaluate_all_alarms() - self._assert_all_alarms('ok') - expected = [mock.call(alarm.alarm_id, state='ok') - for alarm in self.alarms] - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual(expected, update_calls) - reasons = ['Transition to ok due to 5 samples inside' - ' threshold, most recent: %s' % avgs.values[-1], - 'Transition to ok due to 4 samples inside' - ' threshold, most recent: %s' % maxs.values[-1], - 'Transition to ok due to 6 samples inside' - ' threshold, most recent: %s' % avgs2.values[-1]] - reason_datas = [self._reason_data('inside', 5, avgs.values[-1]), - self._reason_data('inside', 4, maxs.values[-1]), - self._reason_data('inside', 6, avgs2.values[-1])] - expected = [mock.call(alarm, 'alarm', reason, reason_data) - for alarm, reason, reason_data - in zip(self.alarms, reasons, reason_datas)] - self.assertEqual(expected, self.notifier.notify.call_args_list) - - def test_equivocal_from_known_state(self): - self._set_all_alarms('ok') - avgs = self._get_stats(60, [self.alarms[0].rule['threshold'] + v - for v in moves.xrange(5)]) - maxs = self._get_stats(300, [self.alarms[1].rule['threshold'] - v - for v in moves.xrange(-1, 3)]) - avgs2 = self._get_stats(50, [self.alarms[2].rule['threshold'] + v - for v in moves.xrange(6)]) - self.requests.post.side_effect = [avgs2] - self.requests.get.side_effect = [avgs, maxs] - self._evaluate_all_alarms() - self._assert_all_alarms('ok') - self.assertEqual( - [], - self.api_client.alarms.set_state.call_args_list) - self.assertEqual([], self.notifier.notify.call_args_list) - - def test_equivocal_from_known_state_and_repeat_actions(self): - self._set_all_alarms('ok') - self.alarms[1].repeat_actions = True - avgs = self._get_stats(60, [self.alarms[0].rule['threshold'] + v - for v in moves.xrange(5)]) - maxs = self._get_stats(300, [self.alarms[1].rule['threshold'] - v - for v in moves.xrange(-1, 3)]) - avgs2 = self._get_stats(50, [self.alarms[2].rule['threshold'] + v - for v in moves.xrange(6)]) - self.requests.post.side_effect = [avgs2] - self.requests.get.side_effect = [avgs, maxs] - self._evaluate_all_alarms() - self._assert_all_alarms('ok') - self.assertEqual([], self.api_client.alarms.set_state.call_args_list) - reason = ('Remaining as ok due to 4 samples inside' - ' threshold, most recent: 8.0') - reason_datas = self._reason_data('inside', 4, 8.0) - expected = [mock.call(self.alarms[1], 'ok', reason, reason_datas)] - self.assertEqual(expected, self.notifier.notify.call_args_list) - - def test_unequivocal_from_known_state_and_repeat_actions(self): - self._set_all_alarms('alarm') - self.alarms[1].repeat_actions = True - avgs = self._get_stats(60, [self.alarms[0].rule['threshold'] + v - for v in moves.xrange(1, 6)]) - maxs = self._get_stats(300, [self.alarms[1].rule['threshold'] - v - for v in moves.xrange(4)]) - avgs2 = self._get_stats(50, [self.alarms[2].rule['threshold'] + v - for v in moves.xrange(6)]) - self.requests.post.side_effect = [avgs2] - self.requests.get.side_effect = [avgs, maxs] - self._evaluate_all_alarms() - self._assert_all_alarms('alarm') - self.assertEqual([], self.api_client.alarms.set_state.call_args_list) - reason = ('Remaining as alarm due to 4 samples outside' - ' threshold, most recent: 7.0') - reason_datas = self._reason_data('outside', 4, 7.0) - expected = [mock.call(self.alarms[1], 'alarm', - reason, reason_datas)] - self.assertEqual(expected, self.notifier.notify.call_args_list) - - def test_state_change_and_repeat_actions(self): - self._set_all_alarms('ok') - self.alarms[0].repeat_actions = True - self.alarms[1].repeat_actions = True - avgs = self._get_stats(60, [self.alarms[0].rule['threshold'] + v - for v in moves.xrange(1, 6)]) - maxs = self._get_stats(300, [self.alarms[1].rule['threshold'] - v - for v in moves.xrange(4)]) - avgs2 = self._get_stats(50, [self.alarms[2].rule['threshold'] + v - for v in moves.xrange(1, 7)]) - self.requests.post.side_effect = [avgs2] - self.requests.get.side_effect = [avgs, maxs] - self._evaluate_all_alarms() - self._assert_all_alarms('alarm') - expected = [mock.call(alarm.alarm_id, state='alarm') - for alarm in self.alarms] - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual(expected, update_calls) - reasons = ['Transition to alarm due to 5 samples outside' - ' threshold, most recent: %s' % avgs.values[-1], - 'Transition to alarm due to 4 samples outside' - ' threshold, most recent: %s' % maxs.values[-1], - 'Transition to alarm due to 6 samples outside' - ' threshold, most recent: %s' % avgs2.values[-1]] - reason_datas = [self._reason_data('outside', 5, avgs.values[-1]), - self._reason_data('outside', 4, maxs.values[-1]), - self._reason_data('outside', 6, avgs2.values[-1])] - expected = [mock.call(alarm, 'ok', reason, reason_data) - for alarm, reason, reason_data - in zip(self.alarms, reasons, reason_datas)] - self.assertEqual(expected, self.notifier.notify.call_args_list) - - def test_equivocal_from_unknown(self): - self._set_all_alarms('insufficient data') - avgs = self._get_stats(60, [self.alarms[0].rule['threshold'] + v - for v in moves.xrange(1, 6)]) - maxs = self._get_stats(300, [self.alarms[1].rule['threshold'] - v - for v in moves.xrange(4)]) - avgs2 = self._get_stats(50, [self.alarms[2].rule['threshold'] + v - for v in moves.xrange(1, 7)]) - self.requests.post.side_effect = [avgs2] - self.requests.get.side_effect = [avgs, maxs] - self._evaluate_all_alarms() - self._assert_all_alarms('alarm') - expected = [mock.call(alarm.alarm_id, state='alarm') - for alarm in self.alarms] - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual(expected, update_calls) - reasons = ['Transition to alarm due to 5 samples outside' - ' threshold, most recent: %s' % avgs.values[-1], - 'Transition to alarm due to 4 samples outside' - ' threshold, most recent: %s' % maxs.values[-1], - 'Transition to alarm due to 6 samples outside' - ' threshold, most recent: %s' % avgs2.values[-1]] - reason_datas = [self._reason_data('outside', 5, avgs.values[-1]), - self._reason_data('outside', 4, maxs.values[-1]), - self._reason_data('outside', 6, avgs2.values[-1])] - expected = [mock.call(alarm, 'insufficient data', - reason, reason_data) - for alarm, reason, reason_data - in zip(self.alarms, reasons, reason_datas)] - self.assertEqual(expected, self.notifier.notify.call_args_list) - - @unittest.skipIf(six.PY3, - "the ceilometer base class is not python 3 ready") - @mock.patch.object(timeutils, 'utcnow') - def test_no_state_change_outside_time_constraint(self, mock_utcnow): - self._set_all_alarms('ok') - self.alarms[0].time_constraints = [ - {'name': 'test', - 'description': 'test', - 'start': '0 11 * * *', # daily at 11:00 - 'duration': 10800, # 3 hours - 'timezone': 'Europe/Ljubljana'} - ] - self.alarms[1].time_constraints = self.alarms[0].time_constraints - self.alarms[2].time_constraints = self.alarms[0].time_constraints - dt = datetime.datetime(2014, 1, 1, 15, 0, 0, - tzinfo=pytz.timezone('Europe/Ljubljana')) - mock_utcnow.return_value = dt.astimezone(pytz.UTC) - self.requests.get.return_value = [] - self._evaluate_all_alarms() - self._assert_all_alarms('ok') - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual([], update_calls, - "Alarm should not change state if the current " - " time is outside its time constraint.") - self.assertEqual([], self.notifier.notify.call_args_list) diff --git a/ceilometer/tests/unit/alarm/evaluator/test_threshold.py b/ceilometer/tests/unit/alarm/evaluator/test_threshold.py deleted file mode 100644 index 9b59008b..00000000 --- a/ceilometer/tests/unit/alarm/evaluator/test_threshold.py +++ /dev/null @@ -1,540 +0,0 @@ -# -# Copyright 2013 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. -"""Tests for ceilometer/alarm/evaluator/threshold.py -""" -import datetime -import uuid - -from ceilometerclient import exc -from ceilometerclient.v2 import statistics -import mock -from oslo_config import cfg -from oslo_utils import timeutils -import pytz -from six import moves - -from ceilometer.alarm.evaluator import threshold -from ceilometer.alarm.storage import models -from ceilometer.tests import constants -from ceilometer.tests.unit.alarm.evaluator import base - - -class TestEvaluate(base.TestEvaluatorBase): - EVALUATOR = threshold.ThresholdEvaluator - - def prepare_alarms(self): - self.alarms = [ - models.Alarm(name='instance_running_hot', - description='instance_running_hot', - type='threshold', - enabled=True, - user_id='foobar', - project_id='snafu', - alarm_id=str(uuid.uuid4()), - state='insufficient data', - state_timestamp=constants.MIN_DATETIME, - timestamp=constants.MIN_DATETIME, - insufficient_data_actions=[], - ok_actions=[], - alarm_actions=[], - repeat_actions=False, - time_constraints=[], - rule=dict( - comparison_operator='gt', - threshold=80.0, - evaluation_periods=5, - statistic='avg', - period=60, - meter_name='cpu_util', - query=[{'field': 'meter', - 'op': 'eq', - 'value': 'cpu_util'}, - {'field': 'resource_id', - 'op': 'eq', - 'value': 'my_instance'}]), - severity='critical' - ), - models.Alarm(name='group_running_idle', - description='group_running_idle', - type='threshold', - enabled=True, - user_id='foobar', - project_id='snafu', - state='insufficient data', - state_timestamp=constants.MIN_DATETIME, - timestamp=constants.MIN_DATETIME, - insufficient_data_actions=[], - ok_actions=[], - alarm_actions=[], - repeat_actions=False, - alarm_id=str(uuid.uuid4()), - time_constraints=[], - rule=dict( - comparison_operator='le', - threshold=10.0, - evaluation_periods=4, - statistic='max', - period=300, - meter_name='cpu_util', - query=[{'field': 'meter', - 'op': 'eq', - 'value': 'cpu_util'}, - {'field': 'metadata.user_metadata.AS', - 'op': 'eq', - 'value': 'my_group'}]), - severity='critical' - ), - ] - - @staticmethod - def _get_stat(attr, value, count=1): - return statistics.Statistics(None, {attr: value, 'count': count}) - - @staticmethod - def _reason_data(disposition, count, most_recent): - return {'type': 'threshold', 'disposition': disposition, - 'count': count, 'most_recent': most_recent} - - def _set_all_rules(self, field, value): - for alarm in self.alarms: - alarm.rule[field] = value - - def test_retry_transient_api_failure(self): - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - broken = exc.CommunicationError(message='broken') - avgs = [self._get_stat('avg', self.alarms[0].rule['threshold'] - v) - for v in moves.xrange(5)] - maxs = [self._get_stat('max', self.alarms[1].rule['threshold'] + v) - for v in moves.xrange(1, 5)] - self.api_client.statistics.list.side_effect = [broken, - broken, - avgs, - maxs] - self._evaluate_all_alarms() - self._assert_all_alarms('insufficient data') - self._evaluate_all_alarms() - self._assert_all_alarms('ok') - - def test_simple_insufficient(self): - self._set_all_alarms('ok') - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - self.api_client.statistics.list.return_value = [] - self._evaluate_all_alarms() - self._assert_all_alarms('insufficient data') - expected = [mock.call(alarm.alarm_id, state='insufficient data') - for alarm in self.alarms] - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual(expected, update_calls) - expected = [mock.call( - alarm, - 'ok', - ('%d datapoints are unknown' - % alarm.rule['evaluation_periods']), - self._reason_data('unknown', - alarm.rule['evaluation_periods'], - None)) - for alarm in self.alarms] - self.assertEqual(expected, self.notifier.notify.call_args_list) - - def test_less_insufficient_data(self): - self._set_all_alarms('ok') - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - avgs = [self._get_stat('avg', self.alarms[0].rule['threshold'] - v) - for v in moves.xrange(4)] - maxs = [self._get_stat('max', self.alarms[1].rule['threshold'] - v) - for v in moves.xrange(1, 4)] - self.api_client.statistics.list.side_effect = [avgs, maxs] - self._evaluate_all_alarms() - self._assert_all_alarms('insufficient data') - expected = [mock.call(alarm.alarm_id, state='insufficient data') - for alarm in self.alarms] - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual(update_calls, expected) - expected = [mock.call( - alarm, - 'ok', - ('%d datapoints are unknown' - % alarm.rule['evaluation_periods']), - self._reason_data('unknown', - alarm.rule['evaluation_periods'], - alarm.rule['threshold'] - 3)) - for alarm in self.alarms] - self.assertEqual(expected, self.notifier.notify.call_args_list) - - def test_simple_alarm_trip(self): - self._set_all_alarms('ok') - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - avgs = [self._get_stat('avg', self.alarms[0].rule['threshold'] + v) - for v in moves.xrange(1, 6)] - maxs = [self._get_stat('max', self.alarms[1].rule['threshold'] - v) - for v in moves.xrange(4)] - self.api_client.statistics.list.side_effect = [avgs, maxs] - self._evaluate_all_alarms() - self._assert_all_alarms('alarm') - expected = [mock.call(alarm.alarm_id, state='alarm') - for alarm in self.alarms] - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual(expected, update_calls) - reasons = ['Transition to alarm due to 5 samples outside' - ' threshold, most recent: %s' % avgs[-1].avg, - 'Transition to alarm due to 4 samples outside' - ' threshold, most recent: %s' % maxs[-1].max] - reason_datas = [self._reason_data('outside', 5, avgs[-1].avg), - self._reason_data('outside', 4, maxs[-1].max)] - expected = [mock.call(alarm, 'ok', reason, reason_data) - for alarm, reason, reason_data - in zip(self.alarms, reasons, reason_datas)] - self.assertEqual(expected, self.notifier.notify.call_args_list) - - def test_simple_alarm_clear(self): - self._set_all_alarms('alarm') - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - avgs = [self._get_stat('avg', self.alarms[0].rule['threshold'] - v) - for v in moves.xrange(5)] - maxs = [self._get_stat('max', self.alarms[1].rule['threshold'] + v) - for v in moves.xrange(1, 5)] - self.api_client.statistics.list.side_effect = [avgs, maxs] - self._evaluate_all_alarms() - self._assert_all_alarms('ok') - expected = [mock.call(alarm.alarm_id, state='ok') - for alarm in self.alarms] - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual(expected, update_calls) - reasons = ['Transition to ok due to 5 samples inside' - ' threshold, most recent: %s' % avgs[-1].avg, - 'Transition to ok due to 4 samples inside' - ' threshold, most recent: %s' % maxs[-1].max] - reason_datas = [self._reason_data('inside', 5, avgs[-1].avg), - self._reason_data('inside', 4, maxs[-1].max)] - expected = [mock.call(alarm, 'alarm', reason, reason_data) - for alarm, reason, reason_data - in zip(self.alarms, reasons, reason_datas)] - self.assertEqual(expected, self.notifier.notify.call_args_list) - - def test_equivocal_from_known_state(self): - self._set_all_alarms('ok') - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - avgs = [self._get_stat('avg', self.alarms[0].rule['threshold'] + v) - for v in moves.xrange(5)] - maxs = [self._get_stat('max', self.alarms[1].rule['threshold'] - v) - for v in moves.xrange(-1, 3)] - self.api_client.statistics.list.side_effect = [avgs, maxs] - self._evaluate_all_alarms() - self._assert_all_alarms('ok') - self.assertEqual( - [], - self.api_client.alarms.set_state.call_args_list) - self.assertEqual([], self.notifier.notify.call_args_list) - - def test_equivocal_from_known_state_and_repeat_actions(self): - self._set_all_alarms('ok') - self.alarms[1].repeat_actions = True - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - avgs = [self._get_stat('avg', - self.alarms[0].rule['threshold'] + v) - for v in moves.xrange(5)] - maxs = [self._get_stat('max', - self.alarms[1].rule['threshold'] - v) - for v in moves.xrange(-1, 3)] - self.api_client.statistics.list.side_effect = [avgs, maxs] - self._evaluate_all_alarms() - self._assert_all_alarms('ok') - self.assertEqual([], - self.api_client.alarms.set_state.call_args_list) - reason = ('Remaining as ok due to 4 samples inside' - ' threshold, most recent: 8.0') - reason_datas = self._reason_data('inside', 4, 8.0) - expected = [mock.call(self.alarms[1], 'ok', reason, reason_datas)] - self.assertEqual(expected, self.notifier.notify.call_args_list) - - def test_unequivocal_from_known_state_and_repeat_actions(self): - self._set_all_alarms('alarm') - self.alarms[1].repeat_actions = True - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - avgs = [self._get_stat('avg', - self.alarms[0].rule['threshold'] + v) - for v in moves.xrange(1, 6)] - maxs = [self._get_stat('max', - self.alarms[1].rule['threshold'] - v) - for v in moves.xrange(4)] - self.api_client.statistics.list.side_effect = [avgs, maxs] - self._evaluate_all_alarms() - self._assert_all_alarms('alarm') - self.assertEqual([], - self.api_client.alarms.set_state.call_args_list) - reason = ('Remaining as alarm due to 4 samples outside' - ' threshold, most recent: 7.0') - reason_datas = self._reason_data('outside', 4, 7.0) - expected = [mock.call(self.alarms[1], 'alarm', - reason, reason_datas)] - self.assertEqual(expected, self.notifier.notify.call_args_list) - - def test_state_change_and_repeat_actions(self): - self._set_all_alarms('ok') - self.alarms[0].repeat_actions = True - self.alarms[1].repeat_actions = True - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - avgs = [self._get_stat('avg', self.alarms[0].rule['threshold'] + v) - for v in moves.xrange(1, 6)] - maxs = [self._get_stat('max', self.alarms[1].rule['threshold'] - v) - for v in moves.xrange(4)] - self.api_client.statistics.list.side_effect = [avgs, maxs] - self._evaluate_all_alarms() - self._assert_all_alarms('alarm') - expected = [mock.call(alarm.alarm_id, state='alarm') - for alarm in self.alarms] - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual(expected, update_calls) - reasons = ['Transition to alarm due to 5 samples outside' - ' threshold, most recent: %s' % avgs[-1].avg, - 'Transition to alarm due to 4 samples outside' - ' threshold, most recent: %s' % maxs[-1].max] - reason_datas = [self._reason_data('outside', 5, avgs[-1].avg), - self._reason_data('outside', 4, maxs[-1].max)] - expected = [mock.call(alarm, 'ok', reason, reason_data) - for alarm, reason, reason_data - in zip(self.alarms, reasons, reason_datas)] - self.assertEqual(expected, self.notifier.notify.call_args_list) - - def test_equivocal_from_unknown(self): - self._set_all_alarms('insufficient data') - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - avgs = [self._get_stat('avg', self.alarms[0].rule['threshold'] + v) - for v in moves.xrange(1, 6)] - maxs = [self._get_stat('max', self.alarms[1].rule['threshold'] - v) - for v in moves.xrange(4)] - self.api_client.statistics.list.side_effect = [avgs, maxs] - self._evaluate_all_alarms() - self._assert_all_alarms('alarm') - expected = [mock.call(alarm.alarm_id, state='alarm') - for alarm in self.alarms] - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual(expected, update_calls) - reasons = ['Transition to alarm due to 5 samples outside' - ' threshold, most recent: %s' % avgs[-1].avg, - 'Transition to alarm due to 4 samples outside' - ' threshold, most recent: %s' % maxs[-1].max] - reason_datas = [self._reason_data('outside', 5, avgs[-1].avg), - self._reason_data('outside', 4, maxs[-1].max)] - expected = [mock.call(alarm, 'insufficient data', - reason, reason_data) - for alarm, reason, reason_data - in zip(self.alarms, reasons, reason_datas)] - self.assertEqual(expected, self.notifier.notify.call_args_list) - - def _do_test_bound_duration(self, start, exclude_outliers=None): - alarm = self.alarms[0] - if exclude_outliers is not None: - alarm.rule['exclude_outliers'] = exclude_outliers - with mock.patch.object(timeutils, 'utcnow') as mock_utcnow: - mock_utcnow.return_value = datetime.datetime(2012, 7, 2, 10, 45) - constraint = self.evaluator._bound_duration(alarm, []) - self.assertEqual([ - {'field': 'timestamp', - 'op': 'le', - 'value': timeutils.utcnow().isoformat()}, - {'field': 'timestamp', - 'op': 'ge', - 'value': start}, - ], constraint) - - def test_bound_duration_outlier_exclusion_defaulted(self): - self._do_test_bound_duration('2012-07-02T10:39:00') - - def test_bound_duration_outlier_exclusion_clear(self): - self._do_test_bound_duration('2012-07-02T10:39:00', False) - - def test_bound_duration_outlier_exclusion_set(self): - self._do_test_bound_duration('2012-07-02T10:35:00', True) - - def test_threshold_endpoint_types(self): - endpoint_types = ["internalURL", "publicURL"] - for endpoint_type in endpoint_types: - cfg.CONF.set_override('os_endpoint_type', - endpoint_type, - group='service_credentials') - with mock.patch('ceilometerclient.client.get_client') as client: - self.evaluator.api_client = None - self._evaluate_all_alarms() - conf = cfg.CONF.service_credentials - expected = [mock.call(2, - os_auth_url=conf.os_auth_url, - os_region_name=conf.os_region_name, - os_tenant_name=conf.os_tenant_name, - os_password=conf.os_password, - os_username=conf.os_username, - os_cacert=conf.os_cacert, - os_endpoint_type=conf.os_endpoint_type, - timeout=cfg.CONF.http_timeout, - insecure=conf.insecure)] - actual = client.call_args_list - self.assertEqual(expected, actual) - - def _do_test_simple_alarm_trip_outlier_exclusion(self, exclude_outliers): - self._set_all_rules('exclude_outliers', exclude_outliers) - self._set_all_alarms('ok') - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - # most recent datapoints inside threshold but with - # anomalously low sample count - threshold = self.alarms[0].rule['threshold'] - avgs = [self._get_stat('avg', - threshold + (v if v < 10 else -v), - count=20 if v < 10 else 1) - for v in moves.xrange(1, 11)] - threshold = self.alarms[1].rule['threshold'] - maxs = [self._get_stat('max', - threshold - (v if v < 7 else -v), - count=20 if v < 7 else 1) - for v in moves.xrange(8)] - self.api_client.statistics.list.side_effect = [avgs, maxs] - self._evaluate_all_alarms() - self._assert_all_alarms('alarm' if exclude_outliers else 'ok') - if exclude_outliers: - expected = [mock.call(alarm.alarm_id, state='alarm') - for alarm in self.alarms] - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual(expected, update_calls) - reasons = ['Transition to alarm due to 5 samples outside' - ' threshold, most recent: %s' % avgs[-2].avg, - 'Transition to alarm due to 4 samples outside' - ' threshold, most recent: %s' % maxs[-2].max] - reason_datas = [self._reason_data('outside', 5, avgs[-2].avg), - self._reason_data('outside', 4, maxs[-2].max)] - expected = [mock.call(alarm, 'ok', reason, reason_data) - for alarm, reason, reason_data - in zip(self.alarms, reasons, reason_datas)] - self.assertEqual(expected, self.notifier.notify.call_args_list) - - def test_simple_alarm_trip_with_outlier_exclusion(self): - self. _do_test_simple_alarm_trip_outlier_exclusion(True) - - def test_simple_alarm_no_trip_without_outlier_exclusion(self): - self. _do_test_simple_alarm_trip_outlier_exclusion(False) - - def _do_test_simple_alarm_clear_outlier_exclusion(self, exclude_outliers): - self._set_all_rules('exclude_outliers', exclude_outliers) - self._set_all_alarms('alarm') - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - # most recent datapoints outside threshold but with - # anomalously low sample count - threshold = self.alarms[0].rule['threshold'] - avgs = [self._get_stat('avg', - threshold - (v if v < 9 else -v), - count=20 if v < 9 else 1) - for v in moves.xrange(10)] - threshold = self.alarms[1].rule['threshold'] - maxs = [self._get_stat('max', - threshold + (v if v < 8 else -v), - count=20 if v < 8 else 1) - for v in moves.xrange(1, 9)] - self.api_client.statistics.list.side_effect = [avgs, maxs] - self._evaluate_all_alarms() - self._assert_all_alarms('ok' if exclude_outliers else 'alarm') - if exclude_outliers: - expected = [mock.call(alarm.alarm_id, state='ok') - for alarm in self.alarms] - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual(expected, update_calls) - reasons = ['Transition to ok due to 5 samples inside' - ' threshold, most recent: %s' % avgs[-2].avg, - 'Transition to ok due to 4 samples inside' - ' threshold, most recent: %s' % maxs[-2].max] - reason_datas = [self._reason_data('inside', 5, avgs[-2].avg), - self._reason_data('inside', 4, maxs[-2].max)] - expected = [mock.call(alarm, 'alarm', reason, reason_data) - for alarm, reason, reason_data - in zip(self.alarms, reasons, reason_datas)] - self.assertEqual(expected, self.notifier.notify.call_args_list) - - def test_simple_alarm_clear_with_outlier_exclusion(self): - self. _do_test_simple_alarm_clear_outlier_exclusion(True) - - def test_simple_alarm_no_clear_without_outlier_exclusion(self): - self. _do_test_simple_alarm_clear_outlier_exclusion(False) - - @mock.patch.object(timeutils, 'utcnow') - def test_state_change_inside_time_constraint(self, mock_utcnow): - self._set_all_alarms('ok') - self.alarms[0].time_constraints = [ - {'name': 'test', - 'description': 'test', - 'start': '0 11 * * *', # daily at 11:00 - 'duration': 10800, # 3 hours - 'timezone': 'Europe/Ljubljana'} - ] - self.alarms[1].time_constraints = self.alarms[0].time_constraints - dt = datetime.datetime(2014, 1, 1, 12, 0, 0, - tzinfo=pytz.timezone('Europe/Ljubljana')) - mock_utcnow.return_value = dt.astimezone(pytz.UTC) - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - # the following part based on test_simple_insufficient - self.api_client.statistics.list.return_value = [] - self._evaluate_all_alarms() - self._assert_all_alarms('insufficient data') - expected = [mock.call(alarm.alarm_id, - state='insufficient data') - for alarm in self.alarms] - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual(expected, update_calls, - "Alarm should change state if the current " - "time is inside its time constraint.") - expected = [mock.call( - alarm, - 'ok', - ('%d datapoints are unknown' - % alarm.rule['evaluation_periods']), - self._reason_data('unknown', - alarm.rule['evaluation_periods'], - None)) - for alarm in self.alarms] - self.assertEqual(expected, self.notifier.notify.call_args_list) - - @mock.patch.object(timeutils, 'utcnow') - def test_no_state_change_outside_time_constraint(self, mock_utcnow): - self._set_all_alarms('ok') - self.alarms[0].time_constraints = [ - {'name': 'test', - 'description': 'test', - 'start': '0 11 * * *', # daily at 11:00 - 'duration': 10800, # 3 hours - 'timezone': 'Europe/Ljubljana'} - ] - self.alarms[1].time_constraints = self.alarms[0].time_constraints - dt = datetime.datetime(2014, 1, 1, 15, 0, 0, - tzinfo=pytz.timezone('Europe/Ljubljana')) - mock_utcnow.return_value = dt.astimezone(pytz.UTC) - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - self.api_client.statistics.list.return_value = [] - self._evaluate_all_alarms() - self._assert_all_alarms('ok') - update_calls = self.api_client.alarms.set_state.call_args_list - self.assertEqual([], update_calls, - "Alarm should not change state if the current " - " time is outside its time constraint.") - self.assertEqual([], self.notifier.notify.call_args_list) diff --git a/ceilometer/tests/unit/alarm/test_alarm_svc.py b/ceilometer/tests/unit/alarm/test_alarm_svc.py deleted file mode 100644 index 256ab89d..00000000 --- a/ceilometer/tests/unit/alarm/test_alarm_svc.py +++ /dev/null @@ -1,158 +0,0 @@ -# -# Copyright 2013 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. -"""Tests for ceilometer.alarm.service.SingletonAlarmService. -""" -import mock -from oslo_config import fixture as fixture_config -from stevedore import extension - -from ceilometer.alarm import service -from ceilometer.tests import base as tests_base - - -class TestAlarmEvaluationService(tests_base.BaseTestCase): - def setUp(self): - super(TestAlarmEvaluationService, self).setUp() - self.CONF = self.useFixture(fixture_config.Config()).conf - self.setup_messaging(self.CONF) - - self.threshold_eval = mock.Mock() - self.evaluators = extension.ExtensionManager.make_test_instance( - [ - extension.Extension( - 'threshold', - None, - None, - self.threshold_eval), - ] - ) - self.api_client = mock.MagicMock() - self.svc = service.AlarmEvaluationService() - self.svc.tg = mock.Mock() - self.svc.partition_coordinator = mock.MagicMock() - p_coord = self.svc.partition_coordinator - p_coord.extract_my_subset.side_effect = lambda _, x: x - self.svc.evaluators = self.evaluators - self.svc.supported_evaluators = ['threshold'] - - def _do_test_start(self, test_interval=120, - coordination_heartbeat=1.0, - coordination_active=False): - self.CONF.set_override('evaluation_interval', - test_interval, - group='alarm') - self.CONF.set_override('heartbeat', - coordination_heartbeat, - group='coordination') - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - p_coord_mock = self.svc.partition_coordinator - p_coord_mock.is_active.return_value = coordination_active - - self.svc.start() - self.svc.partition_coordinator.start.assert_called_once_with() - self.svc.partition_coordinator.join_group.assert_called_once_with( - self.svc.PARTITIONING_GROUP_NAME) - - initial_delay = test_interval if coordination_active else None - expected = [ - mock.call(test_interval, - self.svc._evaluate_assigned_alarms, - initial_delay=initial_delay), - mock.call(604800, mock.ANY), - ] - if coordination_active: - hb_interval = min(coordination_heartbeat, test_interval / 4) - hb_call = mock.call(hb_interval, - self.svc.partition_coordinator.heartbeat) - expected.insert(1, hb_call) - actual = self.svc.tg.add_timer.call_args_list - self.assertEqual(expected, actual) - - def test_start_singleton(self): - self._do_test_start(coordination_active=False) - - def test_start_coordinated(self): - self._do_test_start(coordination_active=True) - - def test_start_coordinated_high_hb_interval(self): - self._do_test_start(coordination_active=True, test_interval=10, - coordination_heartbeat=5) - - def test_evaluation_cycle(self): - alarm = mock.Mock(type='threshold') - self.api_client.alarms.list.return_value = [alarm] - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - p_coord_mock = self.svc.partition_coordinator - p_coord_mock.extract_my_subset.return_value = [alarm] - - self.svc._evaluate_assigned_alarms() - - p_coord_mock.extract_my_subset.assert_called_once_with( - self.svc.PARTITIONING_GROUP_NAME, [alarm]) - self.threshold_eval.evaluate.assert_called_once_with(alarm) - - def test_evaluation_cycle_with_bad_alarm(self): - alarms = [ - mock.Mock(type='threshold', name='bad'), - mock.Mock(type='threshold', name='good'), - ] - self.api_client.alarms.list.return_value = alarms - self.threshold_eval.evaluate.side_effect = [Exception('Boom!'), None] - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - p_coord_mock = self.svc.partition_coordinator - p_coord_mock.extract_my_subset.return_value = alarms - - self.svc._evaluate_assigned_alarms() - self.assertEqual([mock.call(alarms[0]), mock.call(alarms[1])], - self.threshold_eval.evaluate.call_args_list) - - def test_unknown_extension_skipped(self): - alarms = [ - mock.Mock(type='not_existing_type'), - mock.Mock(type='threshold') - ] - - self.api_client.alarms.list.return_value = alarms - with mock.patch('ceilometerclient.client.get_client', - return_value=self.api_client): - self.svc.start() - self.svc._evaluate_assigned_alarms() - self.threshold_eval.evaluate.assert_called_once_with(alarms[1]) - - def test_singleton_endpoint_types(self): - endpoint_types = ["internalURL", "publicURL"] - for endpoint_type in endpoint_types: - self.CONF.set_override('os_endpoint_type', - endpoint_type, - group='service_credentials') - with mock.patch('ceilometerclient.client.get_client') as client: - self.svc.api_client = None - self.svc._evaluate_assigned_alarms() - conf = self.CONF.service_credentials - expected = [mock.call(2, - os_auth_url=conf.os_auth_url, - os_region_name=conf.os_region_name, - os_tenant_name=conf.os_tenant_name, - os_password=conf.os_password, - os_username=conf.os_username, - os_cacert=conf.os_cacert, - os_endpoint_type=conf.os_endpoint_type, - timeout=self.CONF.http_timeout, - insecure=conf.insecure)] - actual = client.call_args_list - self.assertEqual(expected, actual) diff --git a/ceilometer/tests/unit/alarm/test_notifier.py b/ceilometer/tests/unit/alarm/test_notifier.py deleted file mode 100644 index fb82a9ef..00000000 --- a/ceilometer/tests/unit/alarm/test_notifier.py +++ /dev/null @@ -1,266 +0,0 @@ -# -# Copyright 2013-2014 eNovance -# -# 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 mock -from oslo_config import fixture as fixture_config -from oslo_context import context -from oslo_serialization import jsonutils -from oslotest import mockpatch -import requests -import six.moves.urllib.parse as urlparse - -from ceilometer import alarm as ceilometer_alarm -from ceilometer.alarm import service -from ceilometer.tests import base as tests_base - - -DATA_JSON = jsonutils.loads( - '{"current": "ALARM", "alarm_id": "foobar", "alarm_name": "testalarm",' - ' "severity": "critical", "reason": "what ?",' - ' "reason_data": {"test": "test"}, "previous": "OK"}' -) -NOTIFICATION = dict(alarm_id='foobar', - alarm_name='testalarm', - severity='critical', - condition=dict(threshold=42), - reason='what ?', - reason_data={'test': 'test'}, - previous='OK', - current='ALARM') - - -class TestAlarmNotifier(tests_base.BaseTestCase): - - def setUp(self): - super(TestAlarmNotifier, self).setUp() - self.CONF = self.useFixture(fixture_config.Config()).conf - self.setup_messaging(self.CONF) - self.service = service.AlarmNotifierService() - self.useFixture(mockpatch.Patch( - 'oslo_context.context.generate_request_id', - self._fake_generate_request_id)) - - @mock.patch('ceilometer.pipeline.setup_pipeline', mock.MagicMock()) - def test_init_host(self): - # If we try to create a real RPC connection, init_host() never - # returns. Mock it out so we can establish the service - # configuration. - with mock.patch.object(self.service.rpc_server, 'start'): - self.service.start() - - def test_notify_alarm(self): - data = { - 'actions': ['test://'], - 'alarm_id': 'foobar', - 'alarm_name': 'testalarm', - 'severity': 'critical', - 'previous': 'OK', - 'current': 'ALARM', - 'reason': 'Everything is on fire', - 'reason_data': {'fire': 'everywhere'} - } - self.service.notify_alarm(context.get_admin_context(), data) - notifications = ceilometer_alarm.NOTIFIERS['test'].obj.notifications - self.assertEqual(1, len(notifications)) - self.assertEqual((urlparse.urlsplit(data['actions'][0]), - data['alarm_id'], - data['alarm_name'], - data['severity'], - data['previous'], - data['current'], - data['reason'], - data['reason_data']), - notifications[0]) - - def test_notify_alarm_no_action(self): - self.service.notify_alarm(context.get_admin_context(), {}) - - def test_notify_alarm_log_action(self): - self.service.notify_alarm(context.get_admin_context(), - { - 'actions': ['log://'], - 'alarm_id': 'foobar', - 'condition': {'threshold': 42}}) - - @staticmethod - def _fake_spawn_n(func, *args, **kwargs): - func(*args, **kwargs) - - @staticmethod - def _notification(action): - notification = {} - notification.update(NOTIFICATION) - notification['actions'] = [action] - return notification - - HTTP_HEADERS = {'x-openstack-request-id': 'fake_request_id', - 'content-type': 'application/json'} - - def _fake_generate_request_id(self): - return self.HTTP_HEADERS['x-openstack-request-id'] - - def test_notify_alarm_rest_action_ok(self): - action = 'http://host/action' - - with mock.patch('eventlet.spawn_n', self._fake_spawn_n): - with mock.patch.object(requests.Session, 'post') as poster: - self.service.notify_alarm(context.get_admin_context(), - self._notification(action)) - poster.assert_called_with(action, data=mock.ANY, - headers=mock.ANY) - args, kwargs = poster.call_args - self.assertEqual(self.HTTP_HEADERS, kwargs['headers']) - self.assertEqual(DATA_JSON, jsonutils.loads(kwargs['data'])) - - def test_notify_alarm_rest_action_with_ssl_client_cert(self): - action = 'https://host/action' - certificate = "/etc/ssl/cert/whatever.pem" - - self.CONF.set_override("rest_notifier_certificate_file", certificate, - group='alarm') - - with mock.patch('eventlet.spawn_n', self._fake_spawn_n): - with mock.patch.object(requests.Session, 'post') as poster: - self.service.notify_alarm(context.get_admin_context(), - self._notification(action)) - poster.assert_called_with(action, data=mock.ANY, - headers=mock.ANY, - cert=certificate, verify=True) - args, kwargs = poster.call_args - self.assertEqual(self.HTTP_HEADERS, kwargs['headers']) - self.assertEqual(DATA_JSON, jsonutils.loads(kwargs['data'])) - - def test_notify_alarm_rest_action_with_ssl_client_cert_and_key(self): - action = 'https://host/action' - certificate = "/etc/ssl/cert/whatever.pem" - key = "/etc/ssl/cert/whatever.key" - - self.CONF.set_override("rest_notifier_certificate_file", certificate, - group='alarm') - self.CONF.set_override("rest_notifier_certificate_key", key, - group='alarm') - - with mock.patch('eventlet.spawn_n', self._fake_spawn_n): - with mock.patch.object(requests.Session, 'post') as poster: - self.service.notify_alarm(context.get_admin_context(), - self._notification(action)) - poster.assert_called_with(action, data=mock.ANY, - headers=mock.ANY, - cert=(certificate, key), verify=True) - args, kwargs = poster.call_args - self.assertEqual(self.HTTP_HEADERS, kwargs['headers']) - self.assertEqual(DATA_JSON, jsonutils.loads(kwargs['data'])) - - def test_notify_alarm_rest_action_with_ssl_verify_disable_by_cfg(self): - action = 'https://host/action' - - self.CONF.set_override("rest_notifier_ssl_verify", False, - group='alarm') - - with mock.patch('eventlet.spawn_n', self._fake_spawn_n): - with mock.patch.object(requests.Session, 'post') as poster: - self.service.notify_alarm(context.get_admin_context(), - self._notification(action)) - poster.assert_called_with(action, data=mock.ANY, - headers=mock.ANY, - verify=False) - args, kwargs = poster.call_args - self.assertEqual(self.HTTP_HEADERS, kwargs['headers']) - self.assertEqual(DATA_JSON, jsonutils.loads(kwargs['data'])) - - def test_notify_alarm_rest_action_with_ssl_verify_disable(self): - action = 'https://host/action?ceilometer-alarm-ssl-verify=0' - - with mock.patch('eventlet.spawn_n', self._fake_spawn_n): - with mock.patch.object(requests.Session, 'post') as poster: - self.service.notify_alarm(context.get_admin_context(), - self._notification(action)) - poster.assert_called_with(action, data=mock.ANY, - headers=mock.ANY, - verify=False) - args, kwargs = poster.call_args - self.assertEqual(self.HTTP_HEADERS, kwargs['headers']) - self.assertEqual(DATA_JSON, jsonutils.loads(kwargs['data'])) - - def test_notify_alarm_rest_action_with_ssl_verify_enable_by_user(self): - action = 'https://host/action?ceilometer-alarm-ssl-verify=1' - - self.CONF.set_override("rest_notifier_ssl_verify", False, - group='alarm') - - with mock.patch('eventlet.spawn_n', self._fake_spawn_n): - with mock.patch.object(requests.Session, 'post') as poster: - self.service.notify_alarm(context.get_admin_context(), - self._notification(action)) - poster.assert_called_with(action, data=mock.ANY, - headers=mock.ANY, - verify=True) - args, kwargs = poster.call_args - self.assertEqual(self.HTTP_HEADERS, kwargs['headers']) - self.assertEqual(DATA_JSON, jsonutils.loads(kwargs['data'])) - - @staticmethod - def _fake_urlsplit(*args, **kwargs): - raise Exception("Evil urlsplit!") - - def test_notify_alarm_invalid_url(self): - with mock.patch('oslo_utils.netutils.urlsplit', - self._fake_urlsplit): - LOG = mock.MagicMock() - with mock.patch('ceilometer.alarm.service.LOG', LOG): - self.service.notify_alarm( - context.get_admin_context(), - { - 'actions': ['no-such-action-i-am-sure'], - 'alarm_id': 'foobar', - 'condition': {'threshold': 42}, - }) - self.assertTrue(LOG.error.called) - - def test_notify_alarm_invalid_action(self): - LOG = mock.MagicMock() - with mock.patch('ceilometer.alarm.service.LOG', LOG): - self.service.notify_alarm( - context.get_admin_context(), - { - 'actions': ['no-such-action-i-am-sure://'], - 'alarm_id': 'foobar', - 'condition': {'threshold': 42}, - }) - self.assertTrue(LOG.error.called) - - def test_notify_alarm_trust_action(self): - action = 'trust+http://trust-1234@host/action' - url = 'http://host/action' - - client = mock.MagicMock() - client.auth_token = 'token_1234' - headers = {'X-Auth-Token': 'token_1234'} - headers.update(self.HTTP_HEADERS) - - self.useFixture(mockpatch.Patch('keystoneclient.v3.client.Client', - lambda **kwargs: client)) - - with mock.patch('eventlet.spawn_n', self._fake_spawn_n): - with mock.patch.object(requests.Session, 'post') as poster: - self.service.notify_alarm(context.get_admin_context(), - self._notification(action)) - headers = {'X-Auth-Token': 'token_1234'} - headers.update(self.HTTP_HEADERS) - poster.assert_called_with( - url, data=mock.ANY, headers=mock.ANY) - args, kwargs = poster.call_args - self.assertEqual(headers, kwargs['headers']) - self.assertEqual(DATA_JSON, jsonutils.loads(kwargs['data'])) diff --git a/ceilometer/tests/unit/alarm/test_rpc.py b/ceilometer/tests/unit/alarm/test_rpc.py deleted file mode 100644 index f5027211..00000000 --- a/ceilometer/tests/unit/alarm/test_rpc.py +++ /dev/null @@ -1,170 +0,0 @@ -# -# Copyright 2013-2014 eNovance -# -# Authors: Mehdi Abaakouk -# -# 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 uuid - -from ceilometerclient.v2 import alarms -from oslo_config import fixture as fixture_config -import six - -from ceilometer.alarm import rpc as rpc_alarm -from ceilometer.alarm.storage import models -from ceilometer import messaging -from ceilometer.tests import base as tests_base - - -class FakeNotifier(object): - def __init__(self, transport): - self.rpc = messaging.get_rpc_server( - transport, "alarm_notifier", self) - self.notified = [] - - def start(self, expected_length): - self.expected_length = expected_length - self.rpc.start() - - def notify_alarm(self, context, data): - self.notified.append(data) - if len(self.notified) == self.expected_length: - self.rpc.stop() - - -class TestRPCAlarmNotifier(tests_base.BaseTestCase): - def setUp(self): - super(TestRPCAlarmNotifier, self).setUp() - self.CONF = self.useFixture(fixture_config.Config()).conf - self.setup_messaging(self.CONF) - - self.notifier_server = FakeNotifier(self.transport) - self.notifier = rpc_alarm.RPCAlarmNotifier() - self.alarms = [ - alarms.Alarm(None, info={ - 'name': 'instance_running_hot', - 'meter_name': 'cpu_util', - 'comparison_operator': 'gt', - 'threshold': 80.0, - 'evaluation_periods': 5, - 'statistic': 'avg', - 'state': 'ok', - 'ok_actions': ['http://host:8080/path'], - 'user_id': 'foobar', - 'project_id': 'snafu', - 'period': 60, - 'alarm_id': str(uuid.uuid4()), - 'severity': 'critical', - 'matching_metadata':{'resource_id': - 'my_instance'} - }), - alarms.Alarm(None, info={ - 'name': 'group_running_idle', - 'meter_name': 'cpu_util', - 'comparison_operator': 'le', - 'threshold': 10.0, - 'statistic': 'max', - 'evaluation_periods': 4, - 'state': 'insufficient data', - 'insufficient_data_actions': ['http://other_host/path'], - 'user_id': 'foobar', - 'project_id': 'snafu', - 'period': 300, - 'alarm_id': str(uuid.uuid4()), - 'severity': 'critical', - 'matching_metadata':{'metadata.user_metadata.AS': - 'my_group'} - }), - ] - - def test_rpc_target(self): - topic = self.notifier.client.target.topic - self.assertEqual('alarm_notifier', topic) - - def test_notify_alarm(self): - self.notifier_server.start(2) - - previous = ['alarm', 'ok'] - for i, a in enumerate(self.alarms): - self.notifier.notify(a, previous[i], "what? %d" % i, - {'fire': '%d' % i}) - - self.notifier_server.rpc.wait() - - self.assertEqual(2, len(self.notifier_server.notified)) - for i, a in enumerate(self.alarms): - actions = getattr(a, models.Alarm.ALARM_ACTIONS_MAP[a.state]) - self.assertEqual(self.alarms[i].alarm_id, - self.notifier_server.notified[i]["alarm_id"]) - self.assertEqual(self.alarms[i].name, - self.notifier_server.notified[i]["alarm_name"]) - self.assertEqual(self.alarms[i].severity, - self.notifier_server.notified[i]["severity"]) - self.assertEqual(actions, - self.notifier_server.notified[i]["actions"]) - self.assertEqual(previous[i], - self.notifier_server.notified[i]["previous"]) - self.assertEqual(self.alarms[i].state, - self.notifier_server.notified[i]["current"]) - self.assertEqual("what? %d" % i, - self.notifier_server.notified[i]["reason"]) - self.assertEqual({'fire': '%d' % i}, - self.notifier_server.notified[i]["reason_data"]) - - def test_notify_non_string_reason(self): - self.notifier_server.start(1) - self.notifier.notify(self.alarms[0], 'ok', 42, {}) - self.notifier_server.rpc.wait() - reason = self.notifier_server.notified[0]['reason'] - self.assertIsInstance(reason, six.string_types) - - def test_notify_no_actions(self): - alarm = alarms.Alarm(None, info={ - 'name': 'instance_running_hot', - 'meter_name': 'cpu_util', - 'comparison_operator': 'gt', - 'threshold': 80.0, - 'evaluation_periods': 5, - 'statistic': 'avg', - 'state': 'ok', - 'user_id': 'foobar', - 'project_id': 'snafu', - 'period': 60, - 'ok_actions': [], - 'alarm_id': str(uuid.uuid4()), - 'matching_metadata': {'resource_id': - 'my_instance'} - }) - self.notifier.notify(alarm, 'alarm', "what?", {}) - self.assertEqual(0, len(self.notifier_server.notified)) - - -class FakeCoordinator(object): - def __init__(self, transport): - self.rpc = messaging.get_rpc_server( - transport, "alarm_partition_coordination", self) - self.notified = [] - - def presence(self, context, data): - self._record('presence', data) - - def allocate(self, context, data): - self._record('allocate', data) - - def assign(self, context, data): - self._record('assign', data) - - def _record(self, method, data): - self.notified.append((method, data)) - self.rpc.stop() diff --git a/ceilometer/tests/unit/api/v2/test_complex_query.py b/ceilometer/tests/unit/api/v2/test_complex_query.py index 4e112526..363e2112 100644 --- a/ceilometer/tests/unit/api/v2/test_complex_query.py +++ b/ceilometer/tests/unit/api/v2/test_complex_query.py @@ -24,7 +24,6 @@ import mock from oslotest import base import wsme -from ceilometer.alarm.storage import models as alarm_models from ceilometer.api.controllers.v2 import query from ceilometer.storage import models @@ -54,9 +53,6 @@ class TestComplexQuery(base.BaseTestCase): self.query = FakeComplexQuery(models.Sample, sample_name_mapping, True) - self.query_alarm = FakeComplexQuery(alarm_models.Alarm) - self.query_alarmchange = FakeComplexQuery( - alarm_models.AlarmChange) def test_replace_isotime_utc(self): filter_expr = {"=": {"timestamp": "2013-12-05T19:38:29Z"}} @@ -117,18 +113,6 @@ class TestComplexQuery(base.BaseTestCase): self.query._validate_filter, filter) - def test_invalid_filter_misstyped_field_name_alarms(self): - filter = {"=": {"enabbled": True}} - self.assertRaises(jsonschema.ValidationError, - self.query_alarm._validate_filter, - filter) - - def test_invalid_filter_misstyped_field_name_alarmchange(self): - filter = {"=": {"tpe": "rule change"}} - self.assertRaises(jsonschema.ValidationError, - self.query_alarmchange._validate_filter, - filter) - def test_invalid_complex_filter_wrong_field_names(self): filter = {"and": [{"=": {"non_existing_field": 42}}, @@ -137,20 +121,6 @@ class TestComplexQuery(base.BaseTestCase): self.query._validate_filter, filter) - filter = {"and": - [{"=": {"project_id": 42}}, - {"=": {"non_existing_field": 42}}]} - self.assertRaises(jsonschema.ValidationError, - self.query_alarm._validate_filter, - filter) - - filter = {"and": - [{"=": {"project_id11": 42}}, - {"=": {"project_id": 42}}]} - self.assertRaises(jsonschema.ValidationError, - self.query_alarmchange._validate_filter, - filter) - filter = {"or": [{"=": {"non_existing_field": 42}}, {"and": @@ -160,15 +130,6 @@ class TestComplexQuery(base.BaseTestCase): self.query._validate_filter, filter) - filter = {"or": - [{"=": {"project_id": 43}}, - {"and": - [{"=": {"project_id": 44}}, - {"=": {"non_existing_field": 42}}]}]} - self.assertRaises(jsonschema.ValidationError, - self.query_alarm._validate_filter, - filter) - def test_convert_orderby(self): orderby = [] self.query._convert_orderby_to_lower_case(orderby) diff --git a/ceilometer/tests/unit/api/v2/test_query.py b/ceilometer/tests/unit/api/v2/test_query.py index d88ff05d..7d6fdd68 100644 --- a/ceilometer/tests/unit/api/v2/test_query.py +++ b/ceilometer/tests/unit/api/v2/test_query.py @@ -23,7 +23,6 @@ from oslotest import base from oslotest import mockpatch import wsme -from ceilometer.alarm.storage import base as alarm_storage_base from ceilometer.api.controllers.v2 import base as v2_base from ceilometer.api.controllers.v2 import events from ceilometer.api.controllers.v2 import meters @@ -359,21 +358,6 @@ class TestQueryToKwArgs(tests_base.BaseTestCase): 'invalid timestamp format') self.assertEqual(str(expected_exc), str(exc)) - def test_get_alarm_changes_filter_valid_fields(self): - q = [v2_base.Query(field='abc', - op='eq', - value='abc')] - exc = self.assertRaises( - wsme.exc.UnknownArgument, - utils.query_to_kwargs, q, - alarm_storage_base.Connection.get_alarm_changes) - valid_keys = ['alarm_id', 'on_behalf_of', 'project', 'search_offset', - 'severity', 'timestamp', 'type', 'user'] - msg = ("unrecognized field in query: %s, " - "valid keys: %s") % (q, valid_keys) - expected_exc = wsme.exc.UnknownArgument('abc', msg) - self.assertEqual(str(expected_exc), str(exc)) - def test_sample_filter_valid_fields(self): q = [v2_base.Query(field='abc', op='eq', @@ -416,18 +400,3 @@ class TestQueryToKwArgs(tests_base.BaseTestCase): "valid keys: %s") % (q, valid_keys) expected_exc = wsme.exc.UnknownArgument('abc', msg) self.assertEqual(str(expected_exc), str(exc)) - - def test_get_alarms_filter_valid_fields(self): - q = [v2_base.Query(field='abc', - op='eq', - value='abc')] - exc = self.assertRaises( - wsme.exc.UnknownArgument, - utils.query_to_kwargs, q, - alarm_storage_base.Connection.get_alarms) - valid_keys = ['alarm_id', 'enabled', 'meter', 'name', - 'project', 'severity', 'state', 'type', 'user'] - msg = ("unrecognized field in query: %s, " - "valid keys: %s") % (q, valid_keys) - expected_exc = wsme.exc.UnknownArgument('abc', msg) - self.assertEqual(str(expected_exc), str(exc)) diff --git a/ceilometer/tests/unit/storage/test_base.py b/ceilometer/tests/unit/storage/test_base.py index f86393b5..f6b3e989 100644 --- a/ceilometer/tests/unit/storage/test_base.py +++ b/ceilometer/tests/unit/storage/test_base.py @@ -46,9 +46,6 @@ class BaseTest(testbase.BaseTestCase): times[21]) def test_handle_sort_key(self): - sort_keys_alarm = base._handle_sort_key('alarm') - self.assertEqual(['name', 'user_id', 'project_id'], sort_keys_alarm) - sort_keys_meter = base._handle_sort_key('meter', 'foo') self.assertEqual(['foo', 'user_id', 'project_id'], sort_keys_meter) diff --git a/ceilometer/tests/unit/storage/test_get_connection.py b/ceilometer/tests/unit/storage/test_get_connection.py index 6a7785a7..4eb094e9 100644 --- a/ceilometer/tests/unit/storage/test_get_connection.py +++ b/ceilometer/tests/unit/storage/test_get_connection.py @@ -21,8 +21,6 @@ from oslo_config import fixture as fixture_config from oslotest import base import retrying -from ceilometer.alarm.storage import impl_log as impl_log_alarm -from ceilometer.alarm.storage import impl_sqlalchemy as impl_sqlalchemy_alarm try: from ceilometer.event.storage import impl_hbase as impl_hbase_event except ImportError: @@ -78,33 +76,23 @@ class ConnectionConfigTest(base.BaseTestCase): self.assertIsInstance(conn, impl_log.Connection) conn = storage.get_connection_from_config(self.CONF, 'metering') self.assertIsInstance(conn, impl_log.Connection) - conn = storage.get_connection_from_config(self.CONF, 'alarm') - self.assertIsInstance(conn, impl_log_alarm.Connection) def test_two_urls(self): self.CONF.set_override("connection", "log://", group="database") - self.CONF.set_override("alarm_connection", "sqlite://", - group="database") conn = storage.get_connection_from_config(self.CONF) self.assertIsInstance(conn, impl_log.Connection) conn = storage.get_connection_from_config(self.CONF, 'metering') self.assertIsInstance(conn, impl_log.Connection) - conn = storage.get_connection_from_config(self.CONF, 'alarm') - self.assertIsInstance(conn, impl_sqlalchemy_alarm.Connection) @unittest.skipUnless(impl_hbase_event, 'need hbase implementation') def test_three_urls(self): self.CONF.set_override("connection", "log://", group="database") - self.CONF.set_override("alarm_connection", "sqlite://", - group="database") self.CONF.set_override("event_connection", "hbase://__test__", group="database") conn = storage.get_connection_from_config(self.CONF) self.assertIsInstance(conn, impl_log.Connection) conn = storage.get_connection_from_config(self.CONF, 'metering') self.assertIsInstance(conn, impl_log.Connection) - conn = storage.get_connection_from_config(self.CONF, 'alarm') - self.assertIsInstance(conn, impl_sqlalchemy_alarm.Connection) conn = storage.get_connection_from_config(self.CONF, 'event') self.assertIsInstance(conn, impl_hbase_event.Connection) @@ -113,14 +101,10 @@ class ConnectionConfigTest(base.BaseTestCase): self.CONF.set_override("connection", None, group="database") self.CONF.set_override("metering_connection", "log://", group="database") - self.CONF.set_override("alarm_connection", "sqlite://", - group="database") self.CONF.set_override("event_connection", "hbase://__test__", group="database") conn = storage.get_connection_from_config(self.CONF) self.assertIsInstance(conn, impl_log.Connection) - conn = storage.get_connection_from_config(self.CONF, 'alarm') - self.assertIsInstance(conn, impl_sqlalchemy_alarm.Connection) conn = storage.get_connection_from_config(self.CONF, 'event') self.assertIsInstance(conn, impl_hbase_event.Connection) @@ -131,5 +115,3 @@ class ConnectionConfigTest(base.BaseTestCase): self.assertIsInstance(conn, impl_sqlalchemy.Connection) conn = storage.get_connection_from_config(self.CONF, 'metering') self.assertIsInstance(conn, impl_sqlalchemy.Connection) - conn = storage.get_connection_from_config(self.CONF, 'alarm') - self.assertIsInstance(conn, impl_sqlalchemy_alarm.Connection) diff --git a/ceilometer/tests/unit/storage/test_models.py b/ceilometer/tests/unit/storage/test_models.py index f6572f81..9790d241 100644 --- a/ceilometer/tests/unit/storage/test_models.py +++ b/ceilometer/tests/unit/storage/test_models.py @@ -18,7 +18,6 @@ import datetime from oslotest import base as testbase import six -from ceilometer.alarm.storage import models as alarm_models from ceilometer.event.storage import models as event_models from ceilometer.storage import base from ceilometer.storage import models @@ -71,24 +70,6 @@ class ModelTest(testbase.BaseTestCase): self.assertEqual(set(sample_fields), set(models.Sample.get_field_names())) - def test_get_field_names_of_alarm(self): - alarm_fields = ["alarm_id", "type", "enabled", "name", "description", - "timestamp", "user_id", "project_id", "state", - "state_timestamp", "ok_actions", "alarm_actions", - "insufficient_data_actions", "repeat_actions", "rule", - "severity", "time_constraints"] - - self.assertEqual(set(alarm_fields), - set(alarm_models.Alarm.get_field_names())) - - def test_get_field_names_of_alarmchange(self): - alarmchange_fields = ["event_id", "alarm_id", "type", "detail", - "user_id", "project_id", "severity", - "on_behalf_of", "timestamp"] - - self.assertEqual(set(alarmchange_fields), - set(alarm_models.AlarmChange.get_field_names())) - class TestTraitModel(testbase.BaseTestCase): @@ -111,47 +92,3 @@ class TestTraitModel(testbase.BaseTestCase): event_models.Trait.TEXT_TYPE, 10) self.assertEqual("10", v) self.assertIsInstance(v, six.text_type) - - -class TestClassModel(testbase.BaseTestCase): - - ALARM = { - 'alarm_id': '503490ea-ee9e-40d6-9cad-93a71583f4b2', - 'enabled': True, - 'type': 'threshold', - 'name': 'alarm-test', - 'description': 'alarm-test-description', - 'timestamp': None, - 'user_id': '5c76351f5fef4f6490d1048355094ca3', - 'project_id': 'd83ed14a457141fc8661b4dcb3fd883d', - 'state': "insufficient data", - 'state_timestamp': None, - 'ok_actions': [], - 'alarm_actions': [], - 'insufficient_data_actions': [], - 'repeat_actions': False, - 'time_constraints': [], - 'rule': { - 'comparison_operator': 'lt', - 'threshold': 34, - 'statistic': 'max', - 'evaluation_periods': 1, - 'period': 60, - 'meter_name': 'cpu_util', - 'query': [] - } - } - - def test_timestamp_cannot_be_none(self): - self.ALARM['timestamp'] = None - self.ALARM['state_timestamp'] = datetime.datetime.utcnow() - self.assertRaises(TypeError, - alarm_models.Alarm.__init__, - **self.ALARM) - - def test_state_timestamp_cannot_be_none(self): - self.ALARM['timestamp'] = datetime.datetime.utcnow() - self.ALARM['state_timestamp'] = None - self.assertRaises(TypeError, - alarm_models.Alarm.__init__, - **self.ALARM) diff --git a/devstack/plugin.sh b/devstack/plugin.sh index edd7b5ba..5c51d281 100644 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -8,9 +8,7 @@ # # By default all ceilometer services are started (see # devstack/settings). To disable a specific service use the -# disable_service function. For example to turn off alarming: -# -# disable_service ceilometer-alarm-notifier ceilometer-alarm-evaluator +# disable_service function. # # NOTE: Currently, there are two ways to get the IPMI based meters in # OpenStack. One way is to configure Ironic conductor to report those meters @@ -213,17 +211,14 @@ function cleanup_ceilometer { # Set configuration for storage backend. function _ceilometer_configure_storage_backend { if [ "$CEILOMETER_BACKEND" = 'mysql' ] || [ "$CEILOMETER_BACKEND" = 'postgresql' ] ; then - iniset $CEILOMETER_CONF database alarm_connection $(database_connection_url ceilometer) iniset $CEILOMETER_CONF database event_connection $(database_connection_url ceilometer) iniset $CEILOMETER_CONF database metering_connection $(database_connection_url ceilometer) elif [ "$CEILOMETER_BACKEND" = 'es' ] ; then - # es is only supported for events. we will use sql for alarming/metering. - iniset $CEILOMETER_CONF database alarm_connection $(database_connection_url ceilometer) + # es is only supported for events. we will use sql for metering. iniset $CEILOMETER_CONF database event_connection es://localhost:9200 iniset $CEILOMETER_CONF database metering_connection $(database_connection_url ceilometer) ${TOP_DIR}/pkg/elasticsearch.sh start elif [ "$CEILOMETER_BACKEND" = 'mongodb' ] ; then - iniset $CEILOMETER_CONF database alarm_connection mongodb://localhost:27017/ceilometer iniset $CEILOMETER_CONF database event_connection mongodb://localhost:27017/ceilometer iniset $CEILOMETER_CONF database metering_connection mongodb://localhost:27017/ceilometer else @@ -272,7 +267,6 @@ function configure_ceilometer { # The compute and central agents need these credentials in order to # call out to other services' public APIs. - # The alarm evaluator needs these options to call ceilometer APIs iniset $CEILOMETER_CONF service_credentials os_username ceilometer iniset $CEILOMETER_CONF service_credentials os_password $SERVICE_PASSWORD iniset $CEILOMETER_CONF service_credentials os_tenant_name $SERVICE_TENANT_NAME @@ -397,9 +391,6 @@ function start_ceilometer { die $LINENO "ceilometer-api did not start" fi fi - - run_process ceilometer-alarm-notifier "$CEILOMETER_BIN_DIR/ceilometer-alarm-notifier --config-file $CEILOMETER_CONF" - run_process ceilometer-alarm-evaluator "$CEILOMETER_BIN_DIR/ceilometer-alarm-evaluator --config-file $CEILOMETER_CONF" } # stop_ceilometer() - Stop running processes @@ -414,7 +405,7 @@ function stop_ceilometer { fi # Kill the ceilometer screen windows - for serv in ceilometer-acompute ceilometer-acentral ceilometer-aipmi ceilometer-anotification ceilometer-collector ceilometer-alarm-notifier ceilometer-alarm-evaluator; do + for serv in ceilometer-acompute ceilometer-acentral ceilometer-aipmi ceilometer-anotification ceilometer-collector; do stop_process $serv done } diff --git a/devstack/settings b/devstack/settings index 3b80acb6..4afcd50d 100644 --- a/devstack/settings +++ b/devstack/settings @@ -7,8 +7,6 @@ enable_service ceilometer-anotification enable_service ceilometer-collector # API service enable_service ceilometer-api -# Alarming -enable_service ceilometer-alarm-notifier ceilometer-alarm-evaluator # Default directories CEILOMETER_DIR=$DEST/ceilometer diff --git a/devstack/upgrade/settings b/devstack/upgrade/settings index 4a02b503..6a2dad94 100644 --- a/devstack/upgrade/settings +++ b/devstack/upgrade/settings @@ -1,7 +1,7 @@ register_project_for_upgrade ceilometer devstack_localrc base enable_plugin ceilometer git://git.openstack.org/openstack/ceilometer -devstack_localrc base enable_service ceilometer-acompute ceilometer-acentral ceilometer-aipmi ceilometer-anotification ceilometer-collector ceilometer-api ceilometer-alarm-notifier ceilometer-alarm-evaluator tempest +devstack_localrc base enable_service ceilometer-acompute ceilometer-acentral ceilometer-aipmi ceilometer-anotification ceilometer-collector ceilometer-api tempest devstack_localrc target enable_plugin ceilometer git://git.openstack.org/openstack/ceilometer -devstack_localrc target enable_service ceilometer-acompute ceilometer-acentral ceilometer-aipmi ceilometer-anotification ceilometer-collector ceilometer-api ceilometer-alarm-notifier ceilometer-alarm-evaluator tempest +devstack_localrc target enable_service ceilometer-acompute ceilometer-acentral ceilometer-aipmi ceilometer-anotification ceilometer-collector ceilometer-api tempest diff --git a/devstack/upgrade/shutdown.sh b/devstack/upgrade/shutdown.sh index 391e1389..ec0e692b 100755 --- a/devstack/upgrade/shutdown.sh +++ b/devstack/upgrade/shutdown.sh @@ -22,6 +22,6 @@ stop_ceilometer # ensure everything is stopped -SERVICES_DOWN="ceilometer-acompute ceilometer-acentral ceilometer-aipmi ceilometer-anotification ceilometer-collector ceilometer-api ceilometer-alarm-notifier ceilometer-alarm-evaluator" +SERVICES_DOWN="ceilometer-acompute ceilometer-acentral ceilometer-aipmi ceilometer-anotification ceilometer-collector ceilometer-api" ensure_services_stopped $SERVICES_DOWN diff --git a/devstack/upgrade/upgrade.sh b/devstack/upgrade/upgrade.sh index 78f17031..1d7b56dc 100755 --- a/devstack/upgrade/upgrade.sh +++ b/devstack/upgrade/upgrade.sh @@ -78,8 +78,6 @@ ensure_services_started "ceilometer-polling --polling-namespaces compute" \ "ceilometer-polling --polling-namespaces central" \ "ceilometer-polling --polling-namespaces ipmi" \ ceilometer-agent-notification \ - ceilometer-alarm-evaluator \ - ceilometer-alarm-notifier \ ceilometer-api \ ceilometer-collector diff --git a/doc/source/architecture.rst b/doc/source/architecture.rst index 2beb8143..d31ea327 100644 --- a/doc/source/architecture.rst +++ b/doc/source/architecture.rst @@ -25,10 +25,9 @@ High-Level Architecture An overall summary of Ceilometer's logical architecture. Each of Ceilometer's services are designed to scale horizontally. Additional -workers and nodes can be added depending on the expected load. Ceilometer offers -five core services, the data agents designed to work independently from -collection and alarming, but also designed to work together as a -complete solution: +workers and nodes can be added depending on the expected load. Ceilometer +offers five core services, the data agents designed to work independently from +collection, but also designed to work together as a complete solution: 1. polling agent - daemon designed to poll OpenStack services and build Meters. 2. notification agent - daemon designed to listen to notifications on message queue, @@ -36,7 +35,6 @@ complete solution: 3. collector - daemon designed to gather and record event and metering data created by notification and polling agents. 4. api - service to query and view data recorded by collector service. -5. alarming - daemons to evaluate and notify based on defined alarming rules. As Ceilometer has grown to capture more data, it became apparent that data storage would need to be optimised. To address this, Gnocchi_ (resource metering @@ -301,49 +299,6 @@ layer, and use the same tools for metering your entire cloud. Moreover, end users can also :ref:`send their own application specific data ` into the -database through the REST API for a various set of use cases (see the section -"Alarming" later in this article). +database through the REST API for a various set of use cases. .. _send their own application centric data: ./webapi/v2.html#user-defined-data - - -Evaluating the data -=================== - -Alarming Service ----------------- - -.. note:: - - This functionality has been moved to Aodh_ project. Existing functionality - is deprecated and will be removed post-Liberty. - -The alarming component of Ceilometer, first delivered in the Havana -version, allows you to set alarms based on threshold evaluation for a -collection of samples. An alarm can be set on a single meter, or on a -combination. For example, you may want to trigger an alarm when the memory -consumption reaches 70% on a given instance if the instance has been up for -more than 10 min. To setup an alarm, you will call -:ref:`Ceilometer's API server ` specifying the alarm conditions and -an action to take. - -Of course, if you are not administrator of the cloud itself, you can only set -alarms on meters for your own components. You can also -:ref:`send your own meters ` from within your instances, -meaning that you can trigger alarms based on application centric data. - -There can be multiple form of actions, but two have been implemented so far: - -1. :term:`HTTP callback`: you provide a URL to be called whenever the alarm has - been set off. The payload of the request contains all the details of why the - alarm was triggered. -2. :term:`log`: mostly useful for debugging, stores alarms in a log file. - -For more details on this, we recommend that you read the blog post by -Mehdi Abaakouk `Autoscaling with Heat and Ceilometer`_. Particular attention -should be given to the section "Some notes about deploying alarming" as the -database setup (using a separate database from the one used for metering) -will be critical in all cases of production deployment. - -.. _Aodh: https://github.com/openstack/Aodh -.. _Autoscaling with Heat and Ceilometer: http://techs.enovance.com/5991/autoscaling-with-heat-and-ceilometer diff --git a/doc/source/glossary.rst b/doc/source/glossary.rst index ab69d660..2787d5d4 100644 --- a/doc/source/glossary.rst +++ b/doc/source/glossary.rst @@ -24,9 +24,6 @@ Software service running on the OpenStack infrastructure measuring usage and sending the results to the :term:`collector`. - alarm - An action triggered whenever a meter reaches a certain threshold. - API server HTTP REST API service for ceilometer. @@ -64,15 +61,6 @@ data store Storage system for recording data collected by ceilometer. - http callback - HTTP callback is used for calling a predefined URL, whenever an - alarm has been set off. The payload of the request contains - all the details of why the alarm was triggered. - - log - Logging is one of the alarm actions that is useful mostly for debugging, - it stores the alarms in a log file. - meter The measurements tracked for a resource. For example, an instance has a number of meters, such as duration of instance, CPU time used, diff --git a/doc/source/install/manual.rst b/doc/source/install/manual.rst index e82bb068..30d58f24 100644 --- a/doc/source/install/manual.rst +++ b/doc/source/install/manual.rst @@ -91,8 +91,7 @@ HBase .. In case of HBase, the needed database tables (`project`, `user`, `resource`, - `meter`, `alarm`, `alarm_h`) should be created manually with `f` column - family for each one. + `meter`) should be created manually with `f` column family for each one. To use HBase as the storage backend, change the 'database' section in ceilometer.conf as follows:: diff --git a/doc/source/overview.rst b/doc/source/overview.rst index 971d16b6..602522ea 100644 --- a/doc/source/overview.rst +++ b/doc/source/overview.rst @@ -21,13 +21,6 @@ metering backend. We labelled this effort as "multi-publisher". .. _increasing number of meters: http://docs.openstack.org/developer/ceilometer/measurements.html -Most recently, as the Heat project started to come to -life, it soon became clear that the OpenStack project needed a tool to watch -for variations in key values in order to trigger various reactions. -As Ceilometer already had the tooling to collect vast quantities of data, it -seemed logical to add this as an extension of the Ceilometer project, which we -tagged as "alarming". - Metering ======== diff --git a/doc/source/webapi/v2.rst b/doc/source/webapi/v2.rst index c8f6e09a..6c2f6a9b 100644 --- a/doc/source/webapi/v2.rst +++ b/doc/source/webapi/v2.rst @@ -80,45 +80,6 @@ available in the backend. .. autotype:: ceilometer.api.controllers.v2.capabilities.Capabilities :members: -.. _alarms-api: - -Alarms -====== - -The Alarms API is deprecated in favor of the new -`Telemetry Alarming service API `_. -This API endpoint will be removed post-Liberty. - -.. rest-controller:: ceilometer.api.controllers.v2.alarms:AlarmsController - :webprefix: /v2/alarms - -.. rest-controller:: ceilometer.api.controllers.v2.alarms:AlarmController - :webprefix: /v2/alarms - -.. autotype:: ceilometer.api.controllers.v2.alarms.Alarm - :members: - -.. autotype:: ceilometer.api.controllers.v2.alarm_rules.threshold.AlarmThresholdRule - :members: - -.. autotype:: ceilometer.api.controllers.v2.alarm_rules.combination.AlarmCombinationRule - :members: - -.. autotype:: ceilometer.api.controllers.v2.alarm_rules.gnocchi.MetricOfResourceRule - :members: - -.. autotype:: ceilometer.api.controllers.v2.alarm_rules.gnocchi.AggregationMetricByResourcesLookupRule - :members: - -.. autotype:: ceilometer.api.controllers.v2.alarm_rules.gnocchi.AggregationMetricsByIdLookupRule - :members: - -.. autotype:: ceilometer.api.controllers.v2.alarms.AlarmTimeConstraint - :members: - -.. autotype:: ceilometer.api.controllers.v2.alarms.AlarmChange - :members: - Events and Traits ================= @@ -190,7 +151,7 @@ Complex Query +++++++++++++ The filter expressions of the Complex Query feature operate on the fields -of *Sample*, *Alarm* and *AlarmChange*. The following comparison operators are +of *Sample*. The following comparison operators are supported: *=*, *!=*, *<*, *<=*, *>*, *>=* and *in*; and the following logical operators can be used: *and* *or* and *not*. The field names are validated against the database models. See :ref:`api-queries` for how to query the API. @@ -215,12 +176,6 @@ The *filter*, *orderby* and *limit* are all optional fields in a query. .. rest-controller:: ceilometer.api.controllers.v2.query:QuerySamplesController :webprefix: /v2/query/samples -.. rest-controller:: ceilometer.api.controllers.v2.query:QueryAlarmsController - :webprefix: /v2/query/alarms - -.. rest-controller:: ceilometer.api.controllers.v2.query:QueryAlarmHistoryController - :webprefix: /v2/query/alarms/history - .. autotype:: ceilometer.api.controllers.v2.query.ComplexQuery :members: diff --git a/etc/ceilometer/policy.json.sample b/etc/ceilometer/policy.json.sample index 7e5dc86b..685f80f4 100644 --- a/etc/ceilometer/policy.json.sample +++ b/etc/ceilometer/policy.json.sample @@ -16,19 +16,3 @@ "telemetry:get_resource": "rule:context_is_admin", "telemetry:get_resources": "rule:context_is_admin", - - "telemetry:get_alarm": "rule:context_is_admin", - "telemetry:query_alarm": "rule:context_is_admin", - "telemetry:get_alarm_state": "rule:context_is_admin", - "telemetry:get_alarms": "rule:context_is_admin", - "telemetry:create_alarm": "rule:context_is_admin", - "telemetry:set_alarm": "rule:context_is_admin", - "telemetry:delete_alarm": "rule:context_is_admin", - - "telemetry:alarm_history": "rule:context_is_admin", - "telemetry:change_alarm_state": "rule:context_is_admin", - "telemetry:query_alarm_history": "rule:context_is_admin", - - "telemetry:events:index": "rule:context_is_admin", - "telemetry:events:show": "rule:context_is_admin" -} diff --git a/rally-jobs/ceilometer.yaml b/rally-jobs/ceilometer.yaml index 059a8a69..32c1022f 100644 --- a/rally-jobs/ceilometer.yaml +++ b/rally-jobs/ceilometer.yaml @@ -1,102 +1,5 @@ --- - CeilometerAlarms.create_alarm: - - - args: - meter_name: "ram_util" - threshold: 10.0 - type: "threshold" - statistic: "avg" - alarm_actions: ["http://localhost:8776/alarm"] - ok_actions: ["http://localhost:8776/ok"] - insufficient_data_actions: ["http://localhost:8776/notok"] - runner: - type: "constant" - times: 10 - concurrency: 10 - context: - users: - tenants: 1 - users_per_tenant: 1 - sla: - max_failure_percent: 0 - - CeilometerAlarms.create_and_delete_alarm: - - - args: - meter_name: "ram_util" - threshold: 10.0 - type: "threshold" - statistic: "avg" - alarm_actions: ["http://localhost:8776/alarm"] - ok_actions: ["http://localhost:8776/ok"] - insufficient_data_actions: ["http://localhost:8776/notok"] - runner: - type: "constant" - times: 10 - concurrency: 10 - context: - users: - tenants: 1 - users_per_tenant: 1 - sla: - max_failure_percent: 0 - - CeilometerAlarms.create_and_list_alarm: - - - args: - meter_name: "ram_util" - threshold: 10.0 - type: "threshold" - statistic: "avg" - alarm_actions: ["http://localhost:8776/alarm"] - ok_actions: ["http://localhost:8776/ok"] - insufficient_data_actions: ["http://localhost:8776/notok"] - runner: - type: "constant" - times: 10 - concurrency: 10 - context: - users: - tenants: 1 - users_per_tenant: 1 - sla: - max_failure_percent: 0 - - CeilometerAlarms.create_and_update_alarm: - - - args: - meter_name: "ram_util" - threshold: 10.0 - type: "threshold" - statistic: "avg" - alarm_actions: ["http://localhost:8776/alarm"] - ok_actions: ["http://localhost:8776/ok"] - insufficient_data_actions: ["http://localhost:8776/notok"] - runner: - type: "constant" - times: 10 - concurrency: 10 - context: - users: - tenants: 1 - users_per_tenant: 1 - sla: - max_failure_percent: 0 - - CeilometerAlarms.list_alarms: - - - runner: - type: "constant" - times: 10 - concurrency: 10 - context: - users: - tenants: 1 - users_per_tenant: 1 - sla: - max_failure_percent: 0 - CeilometerMeters.list_meters: - runner: @@ -142,53 +45,6 @@ sla: max_failure_percent: 0 - CeilometerQueries.create_and_query_alarms: - - - args: - filter: {"and": [{"!=": {"state": "dummy_state"}},{"=": {"type": "threshold"}}]} - orderby: !!null - limit: 10 - meter_name: "ram_util" - threshold: 10.0 - type: "threshold" - statistic: "avg" - alarm_actions: ["http://localhost:8776/alarm"] - ok_actions: ["http://localhost:8776/ok"] - insufficient_data_actions: ["http://localhost:8776/notok"] - runner: - type: "constant" - times: 20 - concurrency: 10 - context: - users: - tenants: 1 - users_per_tenant: 1 - sla: - max_failure_percent: 0 - - CeilometerQueries.create_and_query_alarm_history: - - - args: - orderby: !!null - limit: !!null - meter_name: "ram_util" - threshold: 10.0 - type: "threshold" - statistic: "avg" - alarm_actions: ["http://localhost:8776/alarm"] - ok_actions: ["http://localhost:8776/ok"] - insufficient_data_actions: ["http://localhost:8776/notok"] - runner: - type: "constant" - times: 20 - concurrency: 10 - context: - users: - tenants: 1 - users_per_tenant: 1 - sla: - max_failure_percent: 0 - CeilometerQueries.create_and_query_samples: - args: diff --git a/requirements.txt b/requirements.txt index 68818294..d0c83bd1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,6 @@ # process, which may cause wedges in the gate later. retrying!=1.3.0,>=1.2.3 # Apache-2.0 -croniter>=0.3.4 # MIT License eventlet>=0.17.4 jsonpath-rw-ext>=0.1.9 jsonschema!=2.5.0,<3.0.0,>=2.0.0 @@ -46,3 +45,6 @@ tooz>=1.19.0 # Apache-2.0 Werkzeug>=0.7 # BSD License WebOb>=1.2.3 WSME>=0.8 +# NOTE(jd) We do not import it directly, but WSME datetime string parsing +# behaviour changes when this library is installed +python-dateutil>=2.4.2 diff --git a/setup.cfg b/setup.cfg index 68ef0683..95553013 100644 --- a/setup.cfg +++ b/setup.cfg @@ -181,15 +181,6 @@ ceilometer.poll.central = ceilometer.builder.poll.central = hardware.snmp = ceilometer.hardware.pollsters.generic:GenericHardwareDeclarativePollster -ceilometer.alarm.storage = - log = ceilometer.alarm.storage.impl_log:Connection - mongodb = ceilometer.alarm.storage.impl_mongodb:Connection - mysql = ceilometer.alarm.storage.impl_sqlalchemy:Connection - postgresql = ceilometer.alarm.storage.impl_sqlalchemy:Connection - sqlite = ceilometer.alarm.storage.impl_sqlalchemy:Connection - hbase = ceilometer.alarm.storage.impl_hbase:Connection - db2 = ceilometer.alarm.storage.impl_db2:Connection - ceilometer.event.storage = es = ceilometer.event.storage.impl_elasticsearch:Connection log = ceilometer.event.storage.impl_log:Connection @@ -243,28 +234,6 @@ ceilometer.event.publisher = notifier = ceilometer.publisher.messaging:EventNotifierPublisher kafka = ceilometer.publisher.kafka_broker:KafkaBrokerPublisher -ceilometer.alarm.rule = - threshold = ceilometer.api.controllers.v2.alarm_rules.threshold:AlarmThresholdRule - combination = ceilometer.api.controllers.v2.alarm_rules.combination:AlarmCombinationRule - gnocchi_resources_threshold = ceilometer.api.controllers.v2.alarm_rules.gnocchi:MetricOfResourceRule - gnocchi_aggregation_by_metrics_threshold = ceilometer.api.controllers.v2.alarm_rules.gnocchi:AggregationMetricsByIdLookupRule - gnocchi_aggregation_by_resources_threshold = ceilometer.api.controllers.v2.alarm_rules.gnocchi:AggregationMetricByResourcesLookupRule - -ceilometer.alarm.evaluator = - threshold = ceilometer.alarm.evaluator.threshold:ThresholdEvaluator - combination = ceilometer.alarm.evaluator.combination:CombinationEvaluator - gnocchi_resources_threshold = ceilometer.alarm.evaluator.gnocchi:GnocchiThresholdEvaluator - gnocchi_aggregation_by_metrics_threshold = ceilometer.alarm.evaluator.gnocchi:GnocchiThresholdEvaluator - gnocchi_aggregation_by_resources_threshold = ceilometer.alarm.evaluator.gnocchi:GnocchiThresholdEvaluator - -ceilometer.alarm.notifier = - log = ceilometer.alarm.notifier.log:LogAlarmNotifier - test = ceilometer.alarm.notifier.test:TestAlarmNotifier - http = ceilometer.alarm.notifier.rest:RestAlarmNotifier - https = ceilometer.alarm.notifier.rest:RestAlarmNotifier - trust+http = ceilometer.alarm.notifier.trust:TrustRestAlarmNotifier - trust+https = ceilometer.alarm.notifier.trust:TrustRestAlarmNotifier - ceilometer.event.trait_plugin = split = ceilometer.event.trait_plugins:SplitterTraitPlugin bitfield = ceilometer.event.trait_plugins:BitfieldTraitPlugin @@ -278,8 +247,6 @@ console_scripts = ceilometer-expirer = ceilometer.cmd.eventlet.storage:expirer ceilometer-rootwrap = oslo_rootwrap.cmd:main ceilometer-collector = ceilometer.cmd.eventlet.collector:main - ceilometer-alarm-evaluator = ceilometer.cmd.eventlet.alarm:evaluator - ceilometer-alarm-notifier = ceilometer.cmd.eventlet.alarm:notifier ceilometer.dispatcher.meter = database = ceilometer.dispatcher.database:DatabaseDispatcher diff --git a/tools/test_hbase_table_utils.py b/tools/test_hbase_table_utils.py index d6dbcbb0..ba90e4ba 100755 --- a/tools/test_hbase_table_utils.py +++ b/tools/test_hbase_table_utils.py @@ -27,16 +27,13 @@ def main(argv): (os.getenv("CEILOMETER_TEST_HBASE_URL"), os.getenv("CEILOMETER_TEST_HBASE_TABLE_PREFIX", "test"))) conn = storage.get_connection(url, 'ceilometer.metering.storage') - alarm_conn = storage.get_connection(url, 'ceilometer.alarm.storage') event_conn = storage.get_connection(url, 'ceilometer.event.storage') for arg in argv: if arg == "--upgrade": conn.upgrade() - alarm_conn.upgrade() event_conn.upgrade() if arg == "--clear": conn.clear() - alarm_conn.clear() event_conn.clear()