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
This commit is contained in:
parent
3549f782d1
commit
6bc86f75ea
@ -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) <doug.hellmann@dreamhost.com>
|
||||
|
@ -1,26 +0,0 @@
|
||||
#
|
||||
# Copyright 2015 Red Hat, Inc
|
||||
#
|
||||
# Authors: Eoghan Glynn <eglynn@redhat.com>
|
||||
#
|
||||
# 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)
|
@ -1,134 +0,0 @@
|
||||
#
|
||||
# Copyright 2013 eNovance <licensing@enovance.com>
|
||||
#
|
||||
# Authors: Mehdi Abaakouk <mehdi.abaakouk@enovance.com>
|
||||
#
|
||||
# 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
|
||||
"""
|
@ -1,114 +0,0 @@
|
||||
#
|
||||
# Copyright 2013 eNovance <licensing@enovance.com>
|
||||
#
|
||||
# Authors: Mehdi Abaakouk <mehdi.abaakouk@enovance.com>
|
||||
#
|
||||
# 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)
|
@ -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)))
|
@ -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])
|
@ -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
|
@ -1,38 +0,0 @@
|
||||
#
|
||||
# Copyright 2013 eNovance <licensing@enovance.com>
|
||||
#
|
||||
# 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.
|
||||
"""
|
@ -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}))
|
@ -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)
|
@ -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))
|
@ -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)
|
@ -1,65 +0,0 @@
|
||||
#
|
||||
# Copyright 2013 eNovance <licensing@enovance.com>
|
||||
#
|
||||
# Authors: Mehdi Abaakouk <mehdi.abaakouk@enovance.com>
|
||||
#
|
||||
# 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})
|
@ -1,231 +0,0 @@
|
||||
#
|
||||
# Copyright 2013 Red Hat, Inc
|
||||
# Copyright 2013 eNovance <licensing@enovance.com>
|
||||
#
|
||||
# Authors: Eoghan Glynn <eglynn@redhat.com>
|
||||
# Julien Danjou <julien@danjou.info>
|
||||
#
|
||||
# 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'))
|
@ -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')
|
@ -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()
|
@ -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)
|
@ -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)
|
@ -1,87 +0,0 @@
|
||||
#
|
||||
# Copyright 2012 New Dream Network, LLC (DreamHost)
|
||||
# Copyright 2013 eNovance
|
||||
# Copyright 2014 Red Hat, Inc
|
||||
#
|
||||
# Authors: Doug Hellmann <doug.hellmann@dreamhost.com>
|
||||
# Julien Danjou <julien@danjou.info>
|
||||
# Eoghan Glynn <eglynn@redhat.com>
|
||||
#
|
||||
# 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.")
|
@ -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)
|
@ -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)
|
@ -1,307 +0,0 @@
|
||||
#
|
||||
# Copyright Ericsson AB 2013. All rights reserved
|
||||
#
|
||||
# Authors: Ildiko Vancsa <ildiko.vancsa@ericsson.com>
|
||||
# Balazs Gibizer <balazs.gibizer@ericsson.com>
|
||||
#
|
||||
# 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'] = []
|
@ -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'])
|
@ -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
|
@ -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'}])
|
@ -1,793 +0,0 @@
|
||||
#
|
||||
# Copyright 2012 New Dream Network, LLC (DreamHost)
|
||||
# Copyright 2013 IBM Corp.
|
||||
# Copyright 2013 eNovance <licensing@enovance.com>
|
||||
# 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)]
|
@ -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."""
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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()
|
@ -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"))
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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'],
|
||||
}
|
||||
|
||||
|
@ -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}
|
||||
|
||||
|
@ -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'
|
||||
|
@ -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)
|
@ -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
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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'])
|
||||
|
@ -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))
|
||||
|
@ -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')
|
||||
|
@ -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:
|
||||
- "[]"
|
@ -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
|
||||
|
@ -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},
|
||||
|
@ -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}},
|
||||
|
@ -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},
|
||||
|
@ -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},
|
||||
|
@ -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))
|
||||
|
@ -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.
|
||||
|
@ -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):
|
||||
|
@ -1,43 +0,0 @@
|
||||
#
|
||||
# Copyright 2013 eNovance <licensing@enovance.com>
|
||||
#
|
||||
# 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)
|
@ -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))
|
@ -1,408 +0,0 @@
|
||||
#
|
||||
# Copyright 2013 eNovance <licensing@enovance.com>
|
||||
#
|
||||
# Authors: Mehdi Abaakouk <mehdi.abaakouk@enovance.com>
|
||||
#
|
||||
# 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)
|
@ -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)
|
@ -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)
|
@ -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)
|
@ -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']))
|
@ -1,170 +0,0 @@
|
||||
#
|
||||
# Copyright 2013-2014 eNovance <licensing@enovance.com>
|
||||
#
|
||||
# Authors: Mehdi Abaakouk <mehdi.abaakouk@enovance.com>
|
||||
#
|
||||
# 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()
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 <user-defined-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 <alarms-api>` 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 <user-defined-data>` 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
|
||||
|
@ -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,
|
||||
|
@ -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::
|
||||
|
@ -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
|
||||
========
|
||||
|
||||
|
@ -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 <http://docs.openstack.org/developer/aodh/webapi/v2.html#alarms>`_.
|
||||
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:
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
33
setup.cfg
33
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
|
||||
|
@ -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()
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user