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:
Julien Danjou 2015-06-30 18:05:19 +02:00
parent 3549f782d1
commit 6bc86f75ea
86 changed files with 26 additions and 10904 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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'] = []

View File

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

View File

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

View File

@ -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'}])

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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'],
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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:
- "[]"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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