Alarm API update

This updates the alarm API to match the latest discussion:

https://wiki.openstack.org/wiki/Ceilometer/blueprints/alarm-api

It allows creation of different kinds of alarm.
The current kind of alarm has been named 'threshold'.

It move the defaults values from the storage models to the API
with all tools provided by wsme to ensure mandatory field and default.

A behavior change, it is now mandatory to PUT a full alarm description
in a PUT call.
In the future a new endpoint can be added to allow to modify only one
field (example for state: /v2/alarms/<id>/state)

Implements blueprint alarming-logical-combination

Change-Id: Ib85636728d427cdb70ef530ff9ff20d2b75c5ed1
This commit is contained in:
Mehdi Abaakouk 2013-08-30 10:00:52 +02:00
parent 0f17a245f0
commit 48c85f740a
14 changed files with 1084 additions and 328 deletions

View File

@ -80,20 +80,12 @@ class Evaluator(object):
self.api_client = ceiloclient.get_client(2, **creds)
return self.api_client
@staticmethod
def _constraints(alarm):
"""Assert the constraints on the statistics query."""
constraints = []
for (field, value) in alarm.matching_metadata.iteritems():
constraints.append(dict(field=field, op='eq', value=value))
return constraints
@classmethod
def _bound_duration(cls, alarm, constraints):
"""Bound the duration of the statistics query."""
now = timeutils.utcnow()
window = (alarm.period *
(alarm.evaluation_periods + cls.look_back))
window = (alarm.rule['period'] *
(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})
@ -111,7 +103,7 @@ class Evaluator(object):
LOG.debug(_('sanitize stats %s') % statistics)
# in practice statistics are always sorted by period start, not
# strictly required by the API though
statistics = statistics[:alarm.evaluation_periods]
statistics = statistics[:alarm.rule['evaluation_periods']]
LOG.debug(_('pruned statistics to %d') % len(statistics))
return statistics
@ -119,9 +111,9 @@ class Evaluator(object):
"""Retrieve statistics over the current window."""
LOG.debug(_('stats query %s') % query)
try:
return self._client.statistics.list(alarm.meter_name,
q=query,
period=alarm.period)
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 []
@ -151,7 +143,8 @@ class Evaluator(object):
"""
sufficient = len(statistics) >= self.quorum
if not sufficient and alarm.state != UNKNOWN:
reason = _('%d datapoints are unknown') % alarm.evaluation_periods
reason = _('%d datapoints are unknown') % alarm.rule[
'evaluation_periods']
self._refresh(alarm, UNKNOWN, reason)
return sufficient
@ -160,7 +153,7 @@ class Evaluator(object):
"""Fabricate reason string."""
count = len(statistics)
disposition = 'inside' if state == OK else 'outside'
last = getattr(statistics[-1], alarm.statistic)
last = getattr(statistics[-1], alarm.rule['statistic'])
transition = alarm.state != state
if transition:
return (_('Transition to %(state)s due to %(count)d samples'
@ -216,7 +209,7 @@ class Evaluator(object):
query = self._bound_duration(
alarm,
self._constraints(alarm)
alarm.rule['query']
)
statistics = self._sanitize(
@ -227,9 +220,9 @@ class Evaluator(object):
if self._sufficient(alarm, statistics):
def _compare(stat):
op = COMPARATORS[alarm.comparison_operator]
value = getattr(stat, alarm.statistic)
limit = alarm.threshold
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})

View File

@ -38,6 +38,7 @@ import inspect
import json
import uuid
import pecan
import six
from pecan import rest
from oslo.config import cfg
@ -73,6 +74,51 @@ cfg.CONF.register_opts(ALARM_API_OPTS, group='alarm')
operation_kind = wtypes.Enum(str, 'lt', 'le', 'eq', 'ne', 'ge', 'gt')
class BoundedInt(wtypes.UserType):
basetype = int
name = 'bounded int'
def __init__(self, min=None, max=None):
self.min = min
self.max = max
def validate(self, value):
if self.min is not None and value < self.min:
error = _('Value %(value)s is invalid (should be greater or equal '
'to %(min)s)') % dict(value=value, min=self.min)
pecan.response.translatable_error = error
raise wsme.exc.ClientSideError(unicode(error))
if self.max is not None and value > self.max:
error = _('Value %(value)s is invalid (should be lower or equal '
'to %(max)s)') % dict(value=value, max=self.max)
pecan.response.translatable_error = error
raise wsme.exc.ClientSideError(unicode(error))
return value
class AdvEnum(wtypes.wsproperty):
"""Handle default and mandatory for wtypes.Enum
"""
def __init__(self, name, *args, **kwargs):
self._name = '_advenum_%s' % name
self._default = kwargs.pop('default', None)
mandatory = kwargs.pop('mandatory', False)
enum = wtypes.Enum(*args, **kwargs)
super(AdvEnum, self).__init__(datatype=enum, fget=self._get,
fset=self._set, mandatory=mandatory)
def _get(self, parent):
if hasattr(parent, self._name):
value = getattr(parent, self._name)
return value or self._default
return self._default
def _set(self, parent, value):
if self.datatype.validate(value):
setattr(parent, self._name, value)
class _Base(wtypes.Base):
@classmethod
@ -87,9 +133,11 @@ class _Base(wtypes.Base):
valid_keys = inspect.getargspec(db_model.__init__)[0]
if 'self' in valid_keys:
valid_keys.remove('self')
return self.as_dict_from_keys(valid_keys)
def as_dict_from_keys(self, keys):
return dict((k, getattr(self, k))
for k in valid_keys
for k in keys
if hasattr(self, k) and
getattr(self, k) != wsme.Unset)
@ -154,6 +202,9 @@ class Query(_Base):
type='string'
)
def as_dict(self):
return self.as_dict_from_keys(['field', 'op', 'type', 'value'])
def _get_value_as_type(self):
"""Convert metadata value to the specified data type.
@ -852,6 +903,83 @@ class ResourcesController(rest.RestController):
return resources
class AlarmThresholdRule(_Base):
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([Query], default=[])
"""The query to find the data for computing statistics.
Ownership settings are automatically included based on the Alarm owner.
"""
period = wsme.wsattr(BoundedInt(min=1), default=60)
"The time range in seconds over which query"
comparison_operator = 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 = AdvEnum('statistic', str, 'max', 'min', 'avg', 'sum',
'count', default='avg')
"The statistic to compare to the threshold"
evaluation_periods = wsme.wsattr(BoundedInt(min=1), default=1)
"The number of historical periods to evaluate the threshold"
def __init__(self, query=None, **kwargs):
if query:
query = [Query(**q) for q in query]
super(AlarmThresholdRule, self).__init__(query=query, **kwargs)
@staticmethod
def validate(threshold_rule):
if not threshold_rule.query:
threshold_rule.query = []
#note(sileht): _query_to_kwargs implicitly call _sanitize_query
#that add project_id in query
_query_to_kwargs(threshold_rule.query, storage.SampleFilter.__init__,
internal_keys=['timestamp', 'start', 'start_timestamp'
'end', 'end_timestamp'])
return threshold_rule
@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'])
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'}])
class Alarm(_Base):
"""Representation of an alarm.
"""
@ -859,79 +987,81 @@ class Alarm(_Base):
alarm_id = wtypes.text
"The UUID of the alarm"
name = wtypes.text
name = wsme.wsattr(wtypes.text, mandatory=True)
"The name for the alarm"
description = wtypes.text
_description = None # provide a default
def get_description(self):
rule = getattr(self, '%s_rule' % self.type, None)
if not self._description and rule:
return six.text_type(rule.default_description)
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"
meter_name = wtypes.text
"The name of meter"
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 = AdvEnum('type', str, 'threshold', mandatory=True)
"Explicit type specifier to select which rule to follow below."
threshold_rule = AlarmThresholdRule
"Describe when to trigger the alarm based on computed statistics"
# 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"
comparison_operator = wtypes.Enum(str, 'lt', 'le', 'eq', 'ne', 'ge', 'gt')
"The comparison against the alarm threshold"
threshold = float
"The threshold of the alarm"
statistic = wtypes.Enum(str, 'max', 'min', 'avg', 'sum', 'count')
"The statistic to compare to the threshold"
enabled = bool
"This alarm is enabled?"
evaluation_periods = int
"The number of periods to evaluate the threshold"
period = int
"The time range in seconds over which to evaluate the threshold"
timestamp = datetime.datetime
"The date of the last alarm definition update"
state = wtypes.Enum(str, 'ok', 'alarm', 'insufficient data')
#TODO(sileht): Add an explicit "set_state" operation instead of
#forcing the caller to PUT the entire definition of the alarm to test it.
#(example: POST/PUT? alarms/<alarm_id>/state)
state = AdvEnum('state', str, 'ok', 'alarm', 'insufficient data',
default='insufficient data')
"The state offset the alarm"
state_timestamp = datetime.datetime
"The date of the last alarm state changed"
ok_actions = [wtypes.text]
"The actions to do when alarm state change to ok"
alarm_actions = [wtypes.text]
"The actions to do when alarm state change to alarm"
insufficient_data_actions = [wtypes.text]
"The actions to do when alarm state change to insufficient data"
repeat_actions = bool
"The actions should be re-triggered on each evaluation cycle"
matching_metadata = {wtypes.text: wtypes.text}
"The matching_metadata of the alarm"
def __init__(self, **kwargs):
def __init__(self, rule=None, **kwargs):
super(Alarm, self).__init__(**kwargs)
if rule and self.type == 'threshold':
self.threshold_rule = AlarmThresholdRule(**rule)
@classmethod
def sample(cls):
return cls(alarm_id=None,
name="SwiftObjectAlarm",
description="An alarm",
meter_name="storage.objects",
comparison_operator="gt",
threshold=200,
statistic="avg",
type='threshold',
threshold_rule=None,
user_id="c96c887c216949acbdfbd8b494863567",
project_id="c96c887c216949acbdfbd8b494863567",
evaluation_periods=2,
period=240,
enabled=True,
timestamp=datetime.datetime.utcnow(),
state="ok",
@ -939,11 +1069,17 @@ class Alarm(_Base):
ok_actions=["http://site:8000/ok"],
alarm_actions=["http://site:8000/alarm"],
insufficient_data_actions=["http://site:8000/nodata"],
matching_metadata={"key_name":
"key_value"},
repeat_actions=False,
)
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()
return d
class AlarmChange(_Base):
"""Representation of an event in an alarm's history
@ -1017,9 +1153,7 @@ class AlarmController(rest.RestController):
def _record_change(self, data, now, on_behalf_of=None, type=None):
if not cfg.CONF.alarm.record_history:
return
type = type or (storage.models.AlarmChange.STATE_TRANSITION
if data.get('state')
else storage.models.AlarmChange.RULE_CHANGE)
type = type or storage.models.AlarmChange.RULE_CHANGE
detail = json.dumps(utils.stringify_timestamps(data))
user_id = pecan.request.headers.get('X-User-Id')
project_id = pecan.request.headers.get('X-Project-Id')
@ -1045,20 +1179,35 @@ class AlarmController(rest.RestController):
@wsme_pecan.wsexpose(Alarm, wtypes.text, body=Alarm)
def put(self, data):
"""Modify this alarm."""
# merge the new values from kwargs into the current
# alarm "alarm_in".
# Ensure alarm exists
alarm_in = self._alarm()
now = timeutils.utcnow()
change = data.as_dict(storage.models.Alarm)
data.state_timestamp = wsme.Unset
data.alarm_id = self._id
kwargs = data.as_dict(storage.models.Alarm)
for k, v in kwargs.iteritems():
setattr(alarm_in, k, v)
if k == 'state':
alarm_in.state_timestamp = now
data.user_id = alarm_in.user_id
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
old_alarm = Alarm.from_db_model(alarm_in).as_dict(storage.models.Alarm)
updated_alarm = data.as_dict(storage.models.Alarm)
try:
alarm_in = storage.models.Alarm(**updated_alarm)
except Exception:
LOG.exception("Error while putting alarm: %s" % updated_alarm)
error = _("Alarm incorrect")
pecan.response.translatable_error = error
raise wsme.exc.ClientSideError(unicode(error))
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)
@ -1126,14 +1275,15 @@ class AlarmsController(rest.RestController):
def post(self, data):
"""Create a new alarm."""
conn = pecan.request.storage_conn
now = timeutils.utcnow()
data.alarm_id = str(uuid.uuid4())
data.user_id = pecan.request.headers.get('X-User-Id')
data.project_id = pecan.request.headers.get('X-Project-Id')
data.state_timestamp = wsme.Unset
change = data.as_dict(storage.models.Alarm)
data.timestamp = now
data.state_timestamp = now
change = data.as_dict(storage.models.Alarm)
# make sure alarms are unique by name per project.
alarms = list(conn.get_alarms(name=data.name,
@ -1144,10 +1294,9 @@ class AlarmsController(rest.RestController):
raise wsme.exc.ClientSideError(unicode(error))
try:
kwargs = data.as_dict(storage.models.Alarm)
alarm_in = storage.models.Alarm(**kwargs)
except Exception as ex:
LOG.exception(ex)
alarm_in = storage.models.Alarm(**change)
except Exception:
LOG.exception("Error while posting alarm: %s" % change)
error = _("Alarm incorrect")
pecan.response.translatable_error = error
raise wsme.exc.ClientSideError(unicode(error))

View File

@ -615,14 +615,55 @@ class Connection(base.Connection):
new_matching_metadata[elem['key']] = elem['value']
return new_matching_metadata
@staticmethod
def _encode_matching_metadata(matching_metadata):
if matching_metadata:
new_matching_metadata = []
for k, v in matching_metadata.iteritems():
new_matching_metadata.append({'key': k, 'value': v})
return new_matching_metadata
return matching_metadata
@classmethod
def _ensure_encapsulated_rule_format(cls, alarm):
"""This 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_period', '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]})
del alarm['matching_metadata']
alarm['rule']['query'] = query
def get_alarms(self, name=None, user=None,
project=None, enabled=True, alarm_id=None, pagination=None):
@ -655,17 +696,13 @@ class Connection(base.Connection):
a = {}
a.update(alarm)
del a['_id']
a['matching_metadata'] = \
self._decode_matching_metadata(a['matching_metadata'])
self._ensure_encapsulated_rule_format(a)
yield models.Alarm(**a)
def update_alarm(self, alarm):
"""update alarm
"""
data = alarm.as_dict()
data['matching_metadata'] = \
self._encode_matching_metadata(data['matching_metadata'])
self.db.alarm.update(
{'alarm_id': alarm.alarm_id},
{'$set': data},
@ -673,8 +710,7 @@ class Connection(base.Connection):
stored_alarm = self.db.alarm.find({'alarm_id': alarm.alarm_id})[0]
del stored_alarm['_id']
stored_alarm['matching_metadata'] = \
self._decode_matching_metadata(stored_alarm['matching_metadata'])
self._ensure_encapsulated_rule_format(stored_alarm)
return models.Alarm(**stored_alarm)
create_alarm = update_alarm

View File

@ -789,7 +789,8 @@ class Connection(base.Connection):
@staticmethod
def _decode_matching_metadata(matching_metadata):
if isinstance(matching_metadata, dict):
#note(sileht): keep compatibility with old db format
#note(sileht): keep compatibility with alarm
#with matching_metadata as a dict
return matching_metadata
else:
new_matching_metadata = {}
@ -797,14 +798,56 @@ class Connection(base.Connection):
new_matching_metadata[elem['key']] = elem['value']
return new_matching_metadata
@staticmethod
def _encode_matching_metadata(matching_metadata):
if matching_metadata:
new_matching_metadata = []
for k, v in matching_metadata.iteritems():
new_matching_metadata.append({'key': k, 'value': v})
return new_matching_metadata
return matching_metadata
@classmethod
def _ensure_encapsulated_rule_format(cls, alarm):
"""This 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
def get_alarms(self, name=None, user=None,
project=None, enabled=True, alarm_id=None, pagination=None):
@ -835,16 +878,13 @@ class Connection(base.Connection):
a = {}
a.update(alarm)
del a['_id']
a['matching_metadata'] = \
self._decode_matching_metadata(a['matching_metadata'])
self._ensure_encapsulated_rule_format(a)
yield models.Alarm(**a)
def update_alarm(self, alarm):
"""update alarm
"""
data = alarm.as_dict()
data['matching_metadata'] = \
self._encode_matching_metadata(data['matching_metadata'])
self.db.alarm.update(
{'alarm_id': alarm.alarm_id},
@ -853,8 +893,7 @@ class Connection(base.Connection):
stored_alarm = self.db.alarm.find({'alarm_id': alarm.alarm_id})[0]
del stored_alarm['_id']
stored_alarm['matching_metadata'] = \
self._decode_matching_metadata(stored_alarm['matching_metadata'])
self._ensure_encapsulated_rule_format(stored_alarm)
return models.Alarm(**stored_alarm)
create_alarm = update_alarm

View File

@ -581,24 +581,19 @@ class Connection(base.Connection):
def _row_to_alarm_model(row):
return api_models.Alarm(alarm_id=row.id,
enabled=row.enabled,
type=row.type,
name=row.name,
description=row.description,
timestamp=row.timestamp,
meter_name=row.meter_name,
user_id=row.user_id,
project_id=row.project_id,
comparison_operator=row.comparison_operator,
threshold=row.threshold,
statistic=row.statistic,
evaluation_periods=row.evaluation_periods,
period=row.period,
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,
matching_metadata=row.matching_metadata,
rule=row.rule,
repeat_actions=row.repeat_actions)
def get_alarms(self, name=None, user=None,

View File

@ -265,14 +265,12 @@ class Alarm(Model):
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 (alarm/nodata/ok)
:param meter_name: The counter that the alarm is based on
:param comparison_operator: How to compare the samples and the threshold
:param threshold: the value to compare to the samples
:param statistic: the function from Statistic (min/max/avg/count)
: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
@ -284,47 +282,23 @@ class Alarm(Model):
alarm state
:param insufficient_data_actions: the list of webhooks to call when
entering the insufficient data state
:param matching_metadata: the key/values of metadata to match on.
:param repeat_actions: Is the actions should be triggered on each
alarm evaluation.
"""
def __init__(self, alarm_id, name, meter_name,
comparison_operator, threshold, statistic,
user_id, project_id,
evaluation_periods=1,
period=60,
enabled=True,
description='',
timestamp=None,
state=ALARM_INSUFFICIENT_DATA,
state_timestamp=None,
ok_actions=[],
alarm_actions=[],
insufficient_data_actions=[],
matching_metadata={},
repeat_actions=False
):
if not description:
# make a nice user friendly description by default
description = 'Alarm when %s is %s a %s of %s over %s seconds' % (
meter_name, comparison_operator,
statistic, threshold, period)
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):
Model.__init__(
self,
alarm_id=alarm_id,
type=type,
enabled=enabled,
name=name,
description=description,
timestamp=timestamp,
meter_name=meter_name,
user_id=user_id,
project_id=project_id,
comparison_operator=comparison_operator,
threshold=threshold,
statistic=statistic,
evaluation_periods=evaluation_periods,
period=period,
state=state,
state_timestamp=state_timestamp,
ok_actions=ok_actions,
@ -332,7 +306,7 @@ class Alarm(Model):
insufficient_data_actions=
insufficient_data_actions,
repeat_actions=repeat_actions,
matching_metadata=matching_metadata)
rule=rule)
class AlarmChange(Model):

View File

@ -0,0 +1,109 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2013 eNovance <licensing@enovance.com>
#
# Author: 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 json
from sqlalchemy import MetaData, Table, Column, Index
from sqlalchemy import String, Float, Integer, Text
meta = MetaData()
def upgrade(migrate_engine):
meta.bind = migrate_engine
table = Table('alarm', meta, autoload=True)
type = Column('type', String(50), default='threshold')
type.create(table, populate_default=True)
rule = Column('rule', Text())
rule.create(table)
for row in table.select().execute().fetchall():
query = []
if row.matching_metadata is not None:
matching_metadata = json.loads(row.matching_metadata)
for key in matching_metadata:
query.append({'field': key,
'op': 'eq',
'value': matching_metadata[key]})
rule = {
'meter_name': row.meter_name,
'comparison_operator': row.comparison_operator,
'threshold': row.threshold,
'statistic': row.statistic,
'evaluation_periods': row.evaluation_periods,
'period': row.period,
'query': query
}
table.update().where(table.c.id == row.id).values(rule=rule).execute()
index = Index('ix_alarm_counter_name', table.c.meter_name)
index.drop(bind=migrate_engine)
table.c.meter_name.drop()
table.c.comparison_operator.drop()
table.c.threshold.drop()
table.c.statistic.drop()
table.c.evaluation_periods.drop()
table.c.period.drop()
table.c.matching_metadata.drop()
def downgrade(migrate_engine):
meta.bind = migrate_engine
table = Table('alarm', meta, autoload=True)
columns = [
Column('meter_name', String(255)),
Column('comparison_operator', String(2)),
Column('threshold', Float),
Column('statistic', String(255)),
Column('evaluation_periods', Integer),
Column('period', Integer),
Column('matching_metadata', Text())
]
for c in columns:
c.create(table)
for row in table.select().execute().fetchall():
if row.type != 'threshold':
#note: type insupported in previous version
table.delete().where(table.c.id == row.id).execute()
else:
rule = json.loads(row.rule)
values = {'comparison_operator': rule['comparison_operator'],
'threshold': float(rule['threshold']),
'statistic': rule['statistic'],
'evaluation_periods': int(rule['evaluation_periods']),
'period': int(rule['period']),
'meter_name': int(rule['mater_name']),
'matching_metadata': {}}
#note: op are ignored because previous format don't support it
for q in rule['query']:
values['matching_metadata'][q['field']] = q['value']
values['matching_metadata'] = json.dumps(
values['matching_metadata'])
table.update().where(table.c.id == row.id
).values(**values).execute()
index = Index('ix_alarm_counter_name', table.c.meter_name)
index.create(bind=migrate_engine)
table.c.type.drop()
table.c.rule.drop()

View File

@ -175,24 +175,17 @@ class Alarm(Base):
__table_args__ = (
Index('ix_alarm_user_id', 'user_id'),
Index('ix_alarm_project_id', 'project_id'),
Index('ix_alarm_meter_name', 'meter_name'),
)
id = Column(String(255), primary_key=True)
enabled = Column(Boolean)
name = Column(Text)
type = Column(String(50))
description = Column(Text)
timestamp = Column(DateTime, default=timeutils.utcnow)
meter_name = Column(String(255))
user_id = Column(String(255), ForeignKey('user.id'))
project_id = Column(String(255), ForeignKey('project.id'))
comparison_operator = Column(String(2))
threshold = Column(Float)
statistic = Column(String(255))
evaluation_periods = Column(Integer)
period = Column(Integer)
state = Column(String(255))
state_timestamp = Column(DateTime, default=timeutils.utcnow)
@ -201,7 +194,7 @@ class Alarm(Base):
insufficient_data_actions = Column(JSONEncodedDict)
repeat_actions = Column(Boolean)
matching_metadata = Column(JSONEncodedDict)
rule = Column(JSONEncodedDict)
class AlarmChange(Base):

View File

@ -63,15 +63,27 @@ class TestSingletonAlarmService(base.TestCase):
def test_evaluation_cycle(self):
alarms = [
models.Alarm(name='instance_running_hot',
meter_name='cpu_util',
comparison_operator='gt',
threshold=80.0,
evaluation_periods=5,
statistic='avg',
type='threshold',
user_id='foobar',
project_id='snafu',
period=60,
alarm_id=str(uuid.uuid4())),
enabled=True,
description='',
repeat_actions=False,
state='insufficient data',
state_timestamp=None,
timestamp=None,
ok_actions=[],
alarm_actions=[],
insufficient_data_actions=[],
alarm_id=str(uuid.uuid4()),
rule=dict(
statistic='avg',
comparison_operator='gt',
threshold=80.0,
evaluation_periods=5,
period=60,
query=[],
)),
]
self.api_client.alarms.list.return_value = alarms
with mock.patch('ceilometerclient.client.get_client',

View File

@ -36,29 +36,61 @@ class TestEvaluate(base.TestCase):
self.notifier = mock.MagicMock()
self.alarms = [
models.Alarm(name='instance_running_hot',
meter_name='cpu_util',
comparison_operator='gt',
threshold=80.0,
evaluation_periods=5,
statistic='avg',
description='instance_running_hot',
type='threshold',
enabled=True,
user_id='foobar',
project_id='snafu',
period=60,
alarm_id=str(uuid.uuid4()),
matching_metadata={'resource_id':
'my_instance'}),
state='insufficient data',
state_timestamp=None,
timestamp=None,
insufficient_data_actions=[],
ok_actions=[],
alarm_actions=[],
repeat_actions=False,
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'}])
),
models.Alarm(name='group_running_idle',
meter_name='cpu_util',
comparison_operator='le',
threshold=10.0,
statistic='max',
evaluation_periods=4,
description='group_running_idle',
type='threshold',
enabled=True,
user_id='foobar',
project_id='snafu',
period=300,
state='insufficient data',
state_timestamp=None,
timestamp=None,
insufficient_data_actions=[],
ok_actions=[],
alarm_actions=[],
repeat_actions=False,
alarm_id=str(uuid.uuid4()),
matching_metadata={'metadata.user_metadata.AS':
'my_group'}),
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'}])
),
]
self.evaluator = threshold_evaluation.Evaluator(self.notifier)
self.evaluator.assign_alarms(self.alarms)
@ -83,9 +115,9 @@ class TestEvaluate(base.TestCase):
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].threshold - v)
avgs = [self._get_stat('avg', self.alarms[0].rule['threshold'] - v)
for v in xrange(5)]
maxs = [self._get_stat('max', self.alarms[1].threshold + v)
maxs = [self._get_stat('max', self.alarms[1].rule['threshold'] + v)
for v in xrange(1, 4)]
self.api_client.statistics.list.side_effect = [broken,
broken,
@ -110,7 +142,7 @@ class TestEvaluate(base.TestCase):
expected = [mock.call(alarm,
'ok',
('%d datapoints are unknown' %
alarm.evaluation_periods))
alarm.rule['evaluation_periods']))
for alarm in self.alarms]
self.assertEqual(self.notifier.notify.call_args_list, expected)
@ -137,9 +169,9 @@ class TestEvaluate(base.TestCase):
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].threshold + v)
avgs = [self._get_stat('avg', self.alarms[0].rule['threshold'] + v)
for v in xrange(1, 6)]
maxs = [self._get_stat('max', self.alarms[1].threshold - v)
maxs = [self._get_stat('max', self.alarms[1].rule['threshold'] - v)
for v in xrange(4)]
self.api_client.statistics.list.side_effect = [avgs, maxs]
self.evaluator.evaluate()
@ -160,9 +192,9 @@ class TestEvaluate(base.TestCase):
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].threshold - v)
avgs = [self._get_stat('avg', self.alarms[0].rule['threshold'] - v)
for v in xrange(5)]
maxs = [self._get_stat('max', self.alarms[1].threshold + v)
maxs = [self._get_stat('max', self.alarms[1].rule['threshold'] + v)
for v in xrange(1, 5)]
self.api_client.statistics.list.side_effect = [avgs, maxs]
self.evaluator.evaluate()
@ -183,9 +215,9 @@ class TestEvaluate(base.TestCase):
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].threshold + v)
avgs = [self._get_stat('avg', self.alarms[0].rule['threshold'] + v)
for v in xrange(5)]
maxs = [self._get_stat('max', self.alarms[1].threshold - v)
maxs = [self._get_stat('max', self.alarms[1].rule['threshold'] - v)
for v in xrange(-1, 3)]
self.api_client.statistics.list.side_effect = [avgs, maxs]
self.evaluator.evaluate()
@ -199,9 +231,9 @@ class TestEvaluate(base.TestCase):
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].threshold + v)
avgs = [self._get_stat('avg', self.alarms[0].rule['threshold'] + v)
for v in xrange(5)]
maxs = [self._get_stat('max', self.alarms[1].threshold - v)
maxs = [self._get_stat('max', self.alarms[1].rule['threshold'] - v)
for v in xrange(-1, 3)]
self.api_client.statistics.list.side_effect = [avgs, maxs]
self.evaluator.evaluate()
@ -218,9 +250,9 @@ class TestEvaluate(base.TestCase):
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].threshold + v)
avgs = [self._get_stat('avg', self.alarms[0].rule['threshold'] + v)
for v in xrange(1, 6)]
maxs = [self._get_stat('max', self.alarms[1].threshold - v)
maxs = [self._get_stat('max', self.alarms[1].rule['threshold'] - v)
for v in xrange(4)]
self.api_client.statistics.list.side_effect = [avgs, maxs]
self.evaluator.evaluate()
@ -238,9 +270,9 @@ class TestEvaluate(base.TestCase):
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].threshold + v)
avgs = [self._get_stat('avg', self.alarms[0].rule['threshold'] + v)
for v in xrange(1, 6)]
maxs = [self._get_stat('max', self.alarms[1].threshold - v)
maxs = [self._get_stat('max', self.alarms[1].rule['threshold'] - v)
for v in xrange(4)]
self.api_client.statistics.list.side_effect = [avgs, maxs]
self.evaluator.evaluate()
@ -261,9 +293,9 @@ class TestEvaluate(base.TestCase):
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].threshold + v)
avgs = [self._get_stat('avg', self.alarms[0].rule['threshold'] + v)
for v in xrange(1, 6)]
maxs = [self._get_stat('max', self.alarms[1].threshold - v)
maxs = [self._get_stat('max', self.alarms[1].rule['threshold'] - v)
for v in xrange(4)]
self.api_client.statistics.list.side_effect = [avgs, maxs]
self.evaluator.evaluate()

View File

@ -33,6 +33,7 @@ from .base import FunctionalTest
from ceilometer.storage.models import Alarm
from ceilometer.tests import db as tests_db
load_tests = testscenarios.load_tests_apply_scenarios
LOG = logging.getLogger(__name__)
@ -55,27 +56,83 @@ class TestAlarms(FunctionalTest,
self.auth_headers = {'X-User-Id': str(uuid.uuid4()),
'X-Project-Id': str(uuid.uuid4())}
for alarm in [Alarm(name='name1',
type='threshold',
enabled=True,
alarm_id='a',
meter_name='meter.test',
comparison_operator='gt', threshold=2.0,
statistic='avg',
description='a',
state='insufficient data',
state_timestamp=None,
timestamp=None,
ok_actions=[],
insufficient_data_actions=[],
alarm_actions=[],
repeat_actions=True,
user_id=self.auth_headers['X-User-Id'],
project_id=self.auth_headers['X-Project-Id']),
project_id=self.auth_headers['X-Project-Id'],
rule=dict(comparison_operator='gt',
threshold=2.0,
statistic='avg',
evaluation_periods=60,
period=1,
meter_name='meter.test',
query=[
{'field': 'project_id',
'op': 'eq', 'value':
self.auth_headers['X-Project-Id']}
])
),
Alarm(name='name2',
type='threshold',
enabled=True,
alarm_id='b',
meter_name='meter.mine',
comparison_operator='gt', threshold=2.0,
statistic='avg',
description='b',
state='insufficient data',
state_timestamp=None,
timestamp=None,
ok_actions=[],
insufficient_data_actions=[],
alarm_actions=[],
repeat_actions=False,
user_id=self.auth_headers['X-User-Id'],
project_id=self.auth_headers['X-Project-Id']),
project_id=self.auth_headers['X-Project-Id'],
rule=dict(comparison_operator='gt',
threshold=4.0,
statistic='avg',
evaluation_periods=60,
period=1,
meter_name='meter.test',
query=[
{'field': 'project_id',
'op': 'eq', 'value':
self.auth_headers['X-Project-Id']}
])
),
Alarm(name='name3',
type='threshold',
enabled=True,
alarm_id='c',
meter_name='meter.test',
comparison_operator='gt', threshold=2.0,
statistic='avg',
description='c',
state='insufficient data',
state_timestamp=None,
timestamp=None,
ok_actions=[],
insufficient_data_actions=[],
alarm_actions=[],
repeat_actions=False,
user_id=self.auth_headers['X-User-Id'],
project_id=self.auth_headers['X-Project-Id'])]:
project_id=self.auth_headers['X-Project-Id'],
rule=dict(comparison_operator='gt',
threshold=3.0,
statistic='avg',
evaluation_periods=60,
period=1,
meter_name='meter.mine',
query=[
{'field': 'project_id',
'op': 'eq', 'value':
self.auth_headers['X-Project-Id']}
])
)]:
self.conn.update_alarm(alarm)
def test_list_alarms(self):
@ -83,7 +140,8 @@ class TestAlarms(FunctionalTest,
self.assertEqual(3, len(data))
self.assertEqual(set(r['name'] for r in data),
set(['name1', 'name2', 'name3']))
self.assertEqual(set(r['meter_name'] for r in data),
self.assertEqual(set(r['threshold_rule']['meter_name']
for r in data),
set(['meter.test', 'meter.mine']))
def test_get_alarm(self):
@ -94,51 +152,183 @@ class TestAlarms(FunctionalTest,
for a in alarms:
print('%s: %s' % (a['name'], a['alarm_id']))
self.assertEqual(alarms[0]['name'], 'name1')
self.assertEqual(alarms[0]['meter_name'], 'meter.test')
self.assertEqual(alarms[0]['threshold_rule']['meter_name'],
'meter.test')
one = self.get_json('/alarms/%s' % alarms[0]['alarm_id'])
self.assertEqual(one['name'], 'name1')
self.assertEqual(one['meter_name'], 'meter.test')
self.assertEqual(one['threshold_rule']['meter_name'],
'meter.test')
self.assertEqual(one['alarm_id'], alarms[0]['alarm_id'])
self.assertEqual(one['repeat_actions'], alarms[0]['repeat_actions'])
def test_post_invalid_alarm(self):
def test_post_invalid_alarm_period(self):
json = {
'name': 'added_alarm',
'meter_name': 'ameter',
'comparison_operator': 'gt',
'threshold': 2.0,
'statistic': 'magic',
'name': 'added_alarm_invalid_period',
'type': 'threshold',
'threshold_rule': {
'meter_name': 'ameter',
'comparison_operator': 'gt',
'threshold': 2.0,
'statistic': 'avg',
'period': -1,
}
}
self.post_json('/alarms', params=json, expect_errors=True, status=400,
headers=self.auth_headers)
alarms = list(self.conn.get_alarms())
self.assertEqual(3, len(alarms))
def test_post_alarm(self):
def test_post_invalid_alarm_statistic(self):
json = {
'name': 'added_alarm',
'meter_name': 'ameter',
'comparison_operator': 'gt',
'threshold': 2.0,
'statistic': 'avg',
'repeat_actions': True,
'type': 'threshold',
'threshold_rule': {
'meter_name': 'ameter',
'comparison_operator': 'gt',
'threshold': 2.0,
'statistic': 'magic',
}
}
self.post_json('/alarms', params=json, expect_errors=True, status=400,
headers=self.auth_headers)
alarms = list(self.conn.get_alarms())
self.assertEqual(3, len(alarms))
def test_post_invalid_alarm_query(self):
json = {
'name': 'added_alarm',
'type': 'threshold',
'threshold_rule': {
'meter_name': 'ameter',
'query': [{'field': 'metadata.invalid',
'field': 'gt',
'value': 'value'}],
'comparison_operator': 'gt',
'threshold': 2.0,
'statistic': 'avg',
}
}
self.post_json('/alarms', params=json, expect_errors=True, status=400,
headers=self.auth_headers)
alarms = list(self.conn.get_alarms())
self.assertEqual(3, len(alarms))
def test_post_alarm_defaults(self):
to_check = {
'enabled': True,
'name': 'added_alarm_defaults',
'state': 'insufficient data',
'description': ('Alarm when ameter is eq a avg of '
'300.0 over 60 seconds'),
'type': 'threshold',
'ok_actions': [],
'alarm_actions': [],
'insufficient_data_actions': [],
'repeat_actions': False,
'threshold_rule': {
'meter_name': 'ameter',
'query': [{'field': 'project_id',
'op': 'eq',
'value': self.auth_headers['X-Project-Id']}],
'threshold': 300.0,
'comparison_operator': 'eq',
'statistic': 'avg',
'evaluation_periods': 1,
'period': 60,
}
}
json = {
'name': 'added_alarm_defaults',
'type': 'threshold',
'threshold_rule': {
'meter_name': 'ameter',
'threshold': 300.0
}
}
self.post_json('/alarms', params=json, status=201,
headers=self.auth_headers)
alarms = list(self.conn.get_alarms())
self.assertEqual(4, len(alarms))
for alarm in alarms:
if alarm.name == 'added_alarm':
self.assertEqual(alarm.repeat_actions, True)
if alarm.name == 'added_alarm_defaults':
for key in to_check:
if key.endswith('_rule'):
storage_key = 'rule'
else:
storage_key = key
self.assertEqual(getattr(alarm, storage_key),
to_check[key])
break
else:
self.fail("Alarm not found")
def test_post_alarm(self):
json = {
'enabled': False,
'name': 'added_alarm',
'state': 'ok',
'type': 'threshold',
'ok_actions': ['http://something/ok'],
'alarm_actions': ['http://something/alarm'],
'insufficient_data_actions': ['http://something/no'],
'repeat_actions': True,
'threshold_rule': {
'meter_name': 'ameter',
'query': [{'field': 'metadata.field',
'op': 'eq',
'value': '5',
'type': 'string'}],
'comparison_operator': 'le',
'statistic': 'count',
'threshold': 50,
'evaluation_periods': 3,
'period': 180,
}
}
self.post_json('/alarms', params=json, status=201,
headers=self.auth_headers)
alarms = list(self.conn.get_alarms(enabled=False))
self.assertEqual(1, len(alarms))
json['threshold_rule']['query'].append({
'field': 'project_id', 'op': 'eq',
'value': self.auth_headers['X-Project-Id']})
if alarms[0].name == 'added_alarm':
for key in json:
if key.endswith('_rule'):
storage_key = 'rule'
else:
storage_key = key
self.assertEqual(getattr(alarms[0], storage_key),
json[key])
else:
self.fail("Alarm not found")
def test_put_alarm(self):
json = {
'name': 'renamed_alarm',
'enabled': False,
'name': 'name_put',
'state': 'ok',
'type': 'threshold',
'ok_actions': ['http://something/ok'],
'alarm_actions': ['http://something/alarm'],
'insufficient_data_actions': ['http://something/no'],
'repeat_actions': True,
'threshold_rule': {
'meter_name': 'ameter',
'query': [{'field': 'metadata.field',
'op': 'eq',
'value': '5',
'type': 'string'}],
'comparison_operator': 'le',
'statistic': 'count',
'threshold': 50,
'evaluation_periods': 3,
'period': 180,
}
}
data = self.get_json('/alarms',
q=[{'field': 'name',
@ -150,27 +340,53 @@ class TestAlarms(FunctionalTest,
self.put_json('/alarms/%s' % alarm_id,
params=json,
headers=self.auth_headers)
alarm = list(self.conn.get_alarms(alarm_id=alarm_id))[0]
self.assertEqual(alarm.name, json['name'])
self.assertEqual(alarm.repeat_actions, json['repeat_actions'])
alarm = list(self.conn.get_alarms(alarm_id=alarm_id, enabled=False))[0]
json['threshold_rule']['query'].append({
'field': 'project_id', 'op': 'eq',
'value': self.auth_headers['X-Project-Id']})
for key in json:
if key.endswith('_rule'):
storage_key = 'rule'
else:
storage_key = key
self.assertEqual(getattr(alarm, storage_key), json[key])
def test_put_alarm_wrong_field(self):
# Note: wsme will ignore unknown fields so will just not appear in
# the Alarm.
json = {
'name': 'renamed_alarm',
'this_can_not_be_correct': 'ha',
'enabled': False,
'name': 'name1',
'state': 'ok',
'type': 'threshold',
'ok_actions': ['http://something/ok'],
'alarm_actions': ['http://something/alarm'],
'insufficient_data_actions': ['http://something/no'],
'repeat_actions': True,
'threshold_rule': {
'meter_name': 'ameter',
'query': [{'field': 'metadata.field',
'op': 'eq',
'value': '5',
'type': 'string'}],
'comparison_operator': 'le',
'statistic': 'count',
'threshold': 50,
'evaluation_periods': 3,
'period': 180,
}
}
data = self.get_json('/alarms',
q=[{'field': 'name',
'value': 'name1',
}],
headers=self.auth_headers)
}])
self.assertEqual(1, len(data))
alarm_id = data[0]['alarm_id']
resp = self.put_json('/alarms/%s' % data[0]['alarm_id'],
params=json,
resp = self.put_json('/alarms/%s' % alarm_id,
expect_errors=True,
params=json,
headers=self.auth_headers)
self.assertEqual(resp.status_code, 200)
@ -202,7 +418,9 @@ class TestAlarms(FunctionalTest,
self.assertEqual(resp.status_code, status)
return resp
def _update_alarm(self, alarm, data, auth_headers=None):
def _update_alarm(self, alarm, updated_data, auth_headers=None):
data = self._get_alarm(alarm['alarm_id'])
data.update(updated_data)
self.put_json('/alarms/%s' % alarm['alarm_id'],
params=data,
headers=auth_headers or self.auth_headers)
@ -237,11 +455,19 @@ class TestAlarms(FunctionalTest,
self.assertEqual(1, len(history))
def test_get_recorded_alarm_history_on_create(self):
new_alarm = dict(name='new_alarm',
meter_name='other_meter',
comparison_operator='le',
threshold=42.0,
statistic='max')
new_alarm = {
'name': 'new_alarm',
'type': 'threshold',
'threshold_rule': {
'meter_name': 'ameter',
'query': [],
'comparison_operator': 'le',
'statistic': 'max',
'threshold': 42.0,
'period': 60,
'evaluation_periods': 1,
}
}
self.post_json('/alarms', params=new_alarm, status=201,
headers=self.auth_headers)
alarm = self.get_json('/alarms')[3]
@ -253,6 +479,11 @@ class TestAlarms(FunctionalTest,
type='creation',
user_id=alarm['user_id']),
history[0])
new_alarm['rule'] = new_alarm['threshold_rule']
del new_alarm['threshold_rule']
new_alarm['rule']['query'].append({
'field': 'project_id', 'op': 'eq',
'value': self.auth_headers['X-Project-Id']})
self._assert_in_json(new_alarm, history[0]['detail'])
def _do_test_get_recorded_alarm_history_on_update(self,
@ -277,20 +508,12 @@ class TestAlarms(FunctionalTest,
history[0])
def test_get_recorded_alarm_history_rule_change(self):
now = datetime.datetime.utcnow().isoformat()
data = dict(name='renamed', timestamp=now)
detail = '{"timestamp": "%s", "name": "renamed"}' % now
data = dict(name='renamed')
detail = '{"name": "renamed"}'
self._do_test_get_recorded_alarm_history_on_update(data,
'rule change',
detail)
def test_get_recorded_alarm_history_state_transition(self):
data = dict(state='alarm')
detail = '{"state": "alarm"}'
self._do_test_get_recorded_alarm_history_on_update(data,
'state transition',
detail)
def test_get_recorded_alarm_history_state_transition_on_behalf_of(self):
# credentials for new non-admin user, on who's behalf the alarm
# is created
@ -299,11 +522,22 @@ class TestAlarms(FunctionalTest,
member_auth = {'X-Roles': 'member',
'X-User-Id': member_user,
'X-Project-Id': member_project}
new_alarm = dict(name='new_alarm',
meter_name='other_meter',
comparison_operator='le',
threshold=42.0,
statistic='max')
new_alarm = {
'name': 'new_alarm',
'type': 'threshold',
'state': 'ok',
'threshold_rule': {
'meter_name': 'other_meter',
'query': [{'field': 'project_id',
'op': 'eq',
'value': member_project}],
'comparison_operator': 'le',
'statistic': 'max',
'threshold': 42.0,
'evaluation_periods': 1,
'period': 60
}
}
self.post_json('/alarms', params=new_alarm, status=201,
headers=member_auth)
alarm = self.get_json('/alarms', headers=member_auth)[0]
@ -317,16 +551,19 @@ class TestAlarms(FunctionalTest,
data = dict(state='alarm')
self._update_alarm(alarm, data, auth_headers=admin_auth)
new_alarm['rule'] = new_alarm['threshold_rule']
del new_alarm['threshold_rule']
# ensure that both the creation event and state transition
# are visible to the non-admin alarm owner and admin user alike
for auth in [member_auth, admin_auth]:
history = self._get_alarm_history(alarm, auth_headers=auth)
self.assertEqual(2, len(history))
self.assertEqual(2, len(history), 'hist: %s' % history)
self._assert_is_subset(dict(alarm_id=alarm['alarm_id'],
detail='{"state": "alarm"}',
on_behalf_of=alarm['project_id'],
project_id=admin_project,
type='state transition',
type='rule change',
user_id=admin_user),
history[0])
self._assert_is_subset(dict(alarm_id=alarm['alarm_id'],
@ -375,9 +612,12 @@ class TestAlarms(FunctionalTest,
type='deletion',
user_id=alarm['user_id']),
history[0])
alarm['rule'] = alarm['threshold_rule']
del alarm['threshold_rule']
self._assert_in_json(alarm, history[0]['detail'])
detail = '{"name": "renamed"}'
self._assert_is_subset(dict(alarm_id=alarm['alarm_id'],
detail='{"name": "renamed"}',
detail=detail,
on_behalf_of=alarm['project_id'],
project_id=alarm['project_id'],
type='rule change',
@ -395,6 +635,8 @@ class TestAlarms(FunctionalTest,
self._assert_is_subset(dict(alarm_id=alarm['alarm_id'],
type='deletion'),
history[0])
alarm['rule'] = alarm['threshold_rule']
del alarm['threshold_rule']
self._assert_in_json(alarm, history[0]['detail'])
for i in xrange(1, 10):
detail = '{"name": "%s"}' % (10 - i)
@ -434,6 +676,8 @@ class TestAlarms(FunctionalTest,
type='deletion',
user_id=alarm['user_id']),
history[0])
alarm['rule'] = alarm['threshold_rule']
del alarm['threshold_rule']
self._assert_in_json(alarm, history[0]['detail'])
def test_get_nonexistent_alarm_history(self):

View File

@ -0,0 +1,58 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2013 eNovance <licensing@enovance.com>
#
# Author: 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 mock
import pecan
import wsme
from ceilometer.api.controllers import v2
from ceilometer.tests import base
class TestWsmeCustomType(base.TestCase):
def setUp(self):
super(TestWsmeCustomType, self).setUp()
pecan.response = mock.MagicMock()
def test_bounded_int_maxmin(self):
bi = v2.BoundedInt(1, 5)
self.assertRaises(wsme.exc.ClientSideError, bi.validate, -1)
self.assertRaises(wsme.exc.ClientSideError, bi.validate, 7)
self.assertEqual(bi.validate(2), 2)
def test_bounded_int_max(self):
bi = v2.BoundedInt(max=5)
self.assertEqual(bi.validate(-1), -1)
self.assertRaises(wsme.exc.ClientSideError, bi.validate, 7)
def test_bounded_int_min(self):
bi = v2.BoundedInt(min=5)
self.assertEqual(bi.validate(7), 7)
self.assertRaises(wsme.exc.ClientSideError, bi.validate, -1)
def test_advenum_default(self):
class dummybase(wsme.types.Base):
ae = v2.AdvEnum("name", str, "one", "other", default="other")
obj = dummybase()
self.assertEqual(obj.ae, "other")
obj = dummybase(ae="one")
self.assertEqual(obj.ae, "one")
self.assertRaises(ValueError, dummybase, ae="not exists")

View File

@ -26,14 +26,12 @@
import copy
import datetime
import uuid
from oslo.config import cfg
from ceilometer.publisher import rpc
from ceilometer import sample
from ceilometer.storage import impl_mongodb
from ceilometer.storage import models
from ceilometer.storage.base import NoResultFound
from ceilometer.storage.base import MultipleResultsFound
from ceilometer.tests import db as tests_db
@ -194,23 +192,74 @@ class CompatibilityTest(test_storage_scenarios.DBTestBase,
# Create the old format alarm with a dict instead of a
# array for matching_metadata
alarm = models.Alarm('0ld-4l3rt', 'old-alert',
'test.one', 'eq', 36, 'count',
'me', 'and-da-boys',
evaluation_periods=1,
period=60,
alarm_actions=['http://nowhere/alarms'],
matching_metadata={'key': 'value'})
alarm.alarm_id = str(uuid.uuid1())
data = alarm.as_dict()
alarm = dict(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'})
self.conn.db.alarm.update(
{'alarm_id': alarm.alarm_id},
{'$set': data},
{'alarm_id': alarm['alarm_id']},
{'$set': alarm},
upsert=True)
def test_alarm_get_old_matching_metadata_format(self):
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.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.conn.get_alarms(name='old-alert'))[0]
self.assertEqual(old.matching_metadata, {'key': 'value'})
self.assertEqual(old.type, 'threshold')
self.assertEqual(old.rule['query'],
[{'field': 'key',
'op': 'eq',
'value': 'value',
'type': 'string'}])
self.assertEqual(old.rule['period'], 60)
self.assertEqual(old.rule['meter_name'], 'cpu')
self.assertEqual(old.rule['evaluation_periods'], 1)
self.assertEqual(old.rule['statistic'], 'count')
self.assertEqual(old.rule['comparison_operator'], 'lt')
self.assertEqual(old.rule['threshold'], 36)
def test_alarm_get_old_format_matching_metadata_array(self):
old = list(self.conn.get_alarms(name='other-old-alaert'))[0]
self.assertEqual(old.type, 'threshold')
self.assertEqual(sorted(old.rule['query']),
sorted([{'field': 'key1',
'op': 'eq',
'value': 'value1',
'type': 'string'},
{'field': 'key2',
'op': 'eq',
'value': 'value2',
'type': 'string'}]))
self.assertEqual(old.rule['meter_name'], 'cpu')
self.assertEqual(old.rule['period'], 60)
self.assertEqual(old.rule['evaluation_periods'], 1)
self.assertEqual(old.rule['statistic'], 'count')
self.assertEqual(old.rule['comparison_operator'], 'lt')
self.assertEqual(old.rule['threshold'], 36)
def test_counter_unit(self):
meters = list(self.conn.get_meters())
@ -224,7 +273,7 @@ class AlarmTestPagination(test_storage_scenarios.AlarmTestBase,
marker_pairs = {'name': 'red-alert'}
ret = impl_mongodb.Connection._get_marker(self.conn.db.alarm,
marker_pairs=marker_pairs)
self.assertEqual(ret['meter_name'], 'test.one')
self.assertEqual(ret['rule']['meter_name'], 'test.one')
def test_alarm_get_marker_None(self):
self.add_some_alarms()
@ -232,7 +281,8 @@ class AlarmTestPagination(test_storage_scenarios.AlarmTestBase,
marker_pairs = {'name': 'user-id-foo'}
ret = impl_mongodb.Connection._get_marker(self.conn.db.alarm,
marker_pairs)
self.assertEqual(ret['meter_name'], 'meter_name-foo')
self.assertEqual(ret['rule']['meter_name'],
'meter_name-foo')
except NoResultFound:
self.assertTrue(True)
@ -242,6 +292,7 @@ class AlarmTestPagination(test_storage_scenarios.AlarmTestBase,
marker_pairs = {'user_id': 'me'}
ret = impl_mongodb.Connection._get_marker(self.conn.db.alarm,
marker_pairs)
self.assertEqual(ret['meter_name'], 'counter-name-foo')
self.assertEqual(ret['rule']['meter_name'],
'counter-name-foo')
except MultipleResultsFound:
self.assertTrue(True)

View File

@ -1852,26 +1852,87 @@ class CounterDataTypeTest(DBTestBase,
class AlarmTestBase(DBTestBase):
def add_some_alarms(self):
alarms = [models.Alarm('r3d', 'red-alert',
'test.one', 'eq', 36, 'count',
'me', 'and-da-boys',
evaluation_periods=1,
period=60,
alarms = [models.Alarm(alarm_id='r3d',
enabled=True,
type='threshold',
name='red-alert',
description='my red-alert',
timestamp=None,
user_id='me',
project_id='and-da-boys',
state="insufficient data",
state_timestamp=None,
ok_actions=[],
alarm_actions=['http://nowhere/alarms'],
matching_metadata={'key': 'value'}),
models.Alarm('0r4ng3', 'orange-alert',
'test.fourty', 'gt', 75, 'avg',
'me', 'and-da-boys',
period=60,
insufficient_data_actions=[],
repeat_actions=False,
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'}]),
),
models.Alarm(alarm_id='0r4ng3',
enabled=True,
type='threshold',
name='orange-alert',
description='a orange',
timestamp=None,
user_id='me',
project_id='and-da-boys',
state="insufficient data",
state_timestamp=None,
ok_actions=[],
alarm_actions=['http://nowhere/alarms'],
matching_metadata={'key2': 'value2'}),
models.Alarm('y3ll0w', 'yellow-alert',
'test.five', 'lt', 10, 'min',
'me', 'and-da-boys',
insufficient_data_actions=[],
repeat_actions=False,
rule=dict(comparison_operator='gt',
threshold=75,
statistic='avg',
evaluation_periods=1,
period=60,
meter_name='test.fourty',
query=[{'field': 'key2',
'op': 'eq',
'value': 'value2',
'type': 'string'}]),
),
models.Alarm(alarm_id='y3ll0w',
enabled=True,
type='threshold',
name='yellow-alert',
description='yellow',
timestamp=None,
user_id='me',
project_id='and-da-boys',
state="insufficient data",
state_timestamp=None,
ok_actions=[],
alarm_actions=['http://nowhere/alarms'],
matching_metadata=
{'key2': 'value2',
'user_metadata.key3': 'value3'})]
insufficient_data_actions=[],
repeat_actions=False,
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.conn.create_alarm(a)
@ -1887,40 +1948,50 @@ class AlarmTest(AlarmTestBase,
self.add_some_alarms()
alarms = list(self.conn.get_alarms())
self.assertEqual(len(alarms), 3)
def test_defaults(self):
self.add_some_alarms()
yellow = list(self.conn.get_alarms(name='yellow-alert'))[0]
self.assertEqual(yellow.evaluation_periods, 1)
self.assertEqual(yellow.period, 60)
self.assertEqual(yellow.enabled, True)
self.assertEqual(yellow.description,
'Alarm when test.five is lt '
'a min of 10 over 60 seconds')
self.assertEqual(yellow.state, models.Alarm.ALARM_INSUFFICIENT_DATA)
self.assertEqual(yellow.ok_actions, [])
self.assertEqual(yellow.insufficient_data_actions, [])
self.assertEqual(yellow.matching_metadata,
{'key2': 'value2', 'user_metadata.key3': 'value3'})
self.assertEqual(alarms[0].rule['meter_name'], 'test.one')
self.assertEqual(alarms[1].rule['meter_name'], 'test.fourty')
self.assertEqual(alarms[2].rule['meter_name'], 'test.five')
def test_update(self):
self.add_some_alarms()
orange = list(self.conn.get_alarms(name='orange-alert'))[0]
orange.enabled = False
orange.state = models.Alarm.ALARM_INSUFFICIENT_DATA
orange.matching_metadata = {'new': 'value',
'user_metadata.new2': 'value4'}
query = [{'field': 'metadata.group',
'op': 'eq',
'value': 'test.updated',
'type': 'string'}]
orange.rule['query'] = query
orange.rule['meter_name'] = 'new_meter_name'
updated = self.conn.update_alarm(orange)
self.assertEqual(updated.enabled, False)
self.assertEqual(updated.state, models.Alarm.ALARM_INSUFFICIENT_DATA)
self.assertEqual(updated.matching_metadata,
{'new': 'value', 'user_metadata.new2': 'value4'})
self.assertEqual(updated.rule['query'], query)
self.assertEqual(updated.rule['meter_name'], 'new_meter_name')
def test_update_llu(self):
llu = models.Alarm('llu', 'llu',
'meter_name', 'lt', 34, 'max',
'bla', 'ffo')
llu = models.Alarm(alarm_id='llu',
enabled=True,
type='threshold',
name='llu',
description='llu',
timestamp=None,
user_id='bla',
project_id='ffo',
state="insufficient data",
state_timestamp=None,
ok_actions=[],
alarm_actions=[],
insufficient_data_actions=[],
repeat_actions=False,
rule=dict(comparison_operator='lt',
threshold=34,
statistic='max',
evaluation_periods=1,
period=60,
meter_name='llt',
query=[])
)
updated = self.conn.update_alarm(llu)
updated.state = models.Alarm.ALARM_OK
updated.description = ':)'