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:
parent
0f17a245f0
commit
48c85f740a
@ -80,20 +80,12 @@ class Evaluator(object):
|
|||||||
self.api_client = ceiloclient.get_client(2, **creds)
|
self.api_client = ceiloclient.get_client(2, **creds)
|
||||||
return self.api_client
|
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
|
@classmethod
|
||||||
def _bound_duration(cls, alarm, constraints):
|
def _bound_duration(cls, alarm, constraints):
|
||||||
"""Bound the duration of the statistics query."""
|
"""Bound the duration of the statistics query."""
|
||||||
now = timeutils.utcnow()
|
now = timeutils.utcnow()
|
||||||
window = (alarm.period *
|
window = (alarm.rule['period'] *
|
||||||
(alarm.evaluation_periods + cls.look_back))
|
(alarm.rule['evaluation_periods'] + cls.look_back))
|
||||||
start = now - datetime.timedelta(seconds=window)
|
start = now - datetime.timedelta(seconds=window)
|
||||||
LOG.debug(_('query stats from %(start)s to '
|
LOG.debug(_('query stats from %(start)s to '
|
||||||
'%(now)s') % {'start': start, 'now': now})
|
'%(now)s') % {'start': start, 'now': now})
|
||||||
@ -111,7 +103,7 @@ class Evaluator(object):
|
|||||||
LOG.debug(_('sanitize stats %s') % statistics)
|
LOG.debug(_('sanitize stats %s') % statistics)
|
||||||
# in practice statistics are always sorted by period start, not
|
# in practice statistics are always sorted by period start, not
|
||||||
# strictly required by the API though
|
# 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))
|
LOG.debug(_('pruned statistics to %d') % len(statistics))
|
||||||
return statistics
|
return statistics
|
||||||
|
|
||||||
@ -119,9 +111,9 @@ class Evaluator(object):
|
|||||||
"""Retrieve statistics over the current window."""
|
"""Retrieve statistics over the current window."""
|
||||||
LOG.debug(_('stats query %s') % query)
|
LOG.debug(_('stats query %s') % query)
|
||||||
try:
|
try:
|
||||||
return self._client.statistics.list(alarm.meter_name,
|
return self._client.statistics.list(
|
||||||
q=query,
|
meter_name=alarm.rule['meter_name'], q=query,
|
||||||
period=alarm.period)
|
period=alarm.rule['period'])
|
||||||
except Exception:
|
except Exception:
|
||||||
LOG.exception(_('alarm stats retrieval failed'))
|
LOG.exception(_('alarm stats retrieval failed'))
|
||||||
return []
|
return []
|
||||||
@ -151,7 +143,8 @@ class Evaluator(object):
|
|||||||
"""
|
"""
|
||||||
sufficient = len(statistics) >= self.quorum
|
sufficient = len(statistics) >= self.quorum
|
||||||
if not sufficient and alarm.state != UNKNOWN:
|
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)
|
self._refresh(alarm, UNKNOWN, reason)
|
||||||
return sufficient
|
return sufficient
|
||||||
|
|
||||||
@ -160,7 +153,7 @@ class Evaluator(object):
|
|||||||
"""Fabricate reason string."""
|
"""Fabricate reason string."""
|
||||||
count = len(statistics)
|
count = len(statistics)
|
||||||
disposition = 'inside' if state == OK else 'outside'
|
disposition = 'inside' if state == OK else 'outside'
|
||||||
last = getattr(statistics[-1], alarm.statistic)
|
last = getattr(statistics[-1], alarm.rule['statistic'])
|
||||||
transition = alarm.state != state
|
transition = alarm.state != state
|
||||||
if transition:
|
if transition:
|
||||||
return (_('Transition to %(state)s due to %(count)d samples'
|
return (_('Transition to %(state)s due to %(count)d samples'
|
||||||
@ -216,7 +209,7 @@ class Evaluator(object):
|
|||||||
|
|
||||||
query = self._bound_duration(
|
query = self._bound_duration(
|
||||||
alarm,
|
alarm,
|
||||||
self._constraints(alarm)
|
alarm.rule['query']
|
||||||
)
|
)
|
||||||
|
|
||||||
statistics = self._sanitize(
|
statistics = self._sanitize(
|
||||||
@ -227,9 +220,9 @@ class Evaluator(object):
|
|||||||
if self._sufficient(alarm, statistics):
|
if self._sufficient(alarm, statistics):
|
||||||
|
|
||||||
def _compare(stat):
|
def _compare(stat):
|
||||||
op = COMPARATORS[alarm.comparison_operator]
|
op = COMPARATORS[alarm.rule['comparison_operator']]
|
||||||
value = getattr(stat, alarm.statistic)
|
value = getattr(stat, alarm.rule['statistic'])
|
||||||
limit = alarm.threshold
|
limit = alarm.rule['threshold']
|
||||||
LOG.debug(_('comparing value %(value)s against threshold'
|
LOG.debug(_('comparing value %(value)s against threshold'
|
||||||
' %(limit)s') %
|
' %(limit)s') %
|
||||||
{'value': value, 'limit': limit})
|
{'value': value, 'limit': limit})
|
||||||
|
@ -38,6 +38,7 @@ import inspect
|
|||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
import pecan
|
import pecan
|
||||||
|
import six
|
||||||
from pecan import rest
|
from pecan import rest
|
||||||
|
|
||||||
from oslo.config import cfg
|
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')
|
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):
|
class _Base(wtypes.Base):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -87,9 +133,11 @@ class _Base(wtypes.Base):
|
|||||||
valid_keys = inspect.getargspec(db_model.__init__)[0]
|
valid_keys = inspect.getargspec(db_model.__init__)[0]
|
||||||
if 'self' in valid_keys:
|
if 'self' in valid_keys:
|
||||||
valid_keys.remove('self')
|
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))
|
return dict((k, getattr(self, k))
|
||||||
for k in valid_keys
|
for k in keys
|
||||||
if hasattr(self, k) and
|
if hasattr(self, k) and
|
||||||
getattr(self, k) != wsme.Unset)
|
getattr(self, k) != wsme.Unset)
|
||||||
|
|
||||||
@ -154,6 +202,9 @@ class Query(_Base):
|
|||||||
type='string'
|
type='string'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
return self.as_dict_from_keys(['field', 'op', 'type', 'value'])
|
||||||
|
|
||||||
def _get_value_as_type(self):
|
def _get_value_as_type(self):
|
||||||
"""Convert metadata value to the specified data type.
|
"""Convert metadata value to the specified data type.
|
||||||
|
|
||||||
@ -852,6 +903,83 @@ class ResourcesController(rest.RestController):
|
|||||||
return resources
|
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):
|
class Alarm(_Base):
|
||||||
"""Representation of an alarm.
|
"""Representation of an alarm.
|
||||||
"""
|
"""
|
||||||
@ -859,79 +987,81 @@ class Alarm(_Base):
|
|||||||
alarm_id = wtypes.text
|
alarm_id = wtypes.text
|
||||||
"The UUID of the alarm"
|
"The UUID of the alarm"
|
||||||
|
|
||||||
name = wtypes.text
|
name = wsme.wsattr(wtypes.text, mandatory=True)
|
||||||
"The name for the alarm"
|
"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"
|
"The description of the alarm"
|
||||||
|
|
||||||
meter_name = wtypes.text
|
enabled = wsme.wsattr(bool, default=True)
|
||||||
"The name of meter"
|
"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
|
project_id = wtypes.text
|
||||||
"The ID of the project or tenant that owns the alarm"
|
"The ID of the project or tenant that owns the alarm"
|
||||||
|
|
||||||
user_id = wtypes.text
|
user_id = wtypes.text
|
||||||
"The ID of the user who created the alarm"
|
"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
|
timestamp = datetime.datetime
|
||||||
"The date of the last alarm definition update"
|
"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"
|
"The state offset the alarm"
|
||||||
|
|
||||||
state_timestamp = datetime.datetime
|
state_timestamp = datetime.datetime
|
||||||
"The date of the last alarm state changed"
|
"The date of the last alarm state changed"
|
||||||
|
|
||||||
ok_actions = [wtypes.text]
|
def __init__(self, rule=None, **kwargs):
|
||||||
"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):
|
|
||||||
super(Alarm, self).__init__(**kwargs)
|
super(Alarm, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
if rule and self.type == 'threshold':
|
||||||
|
self.threshold_rule = AlarmThresholdRule(**rule)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def sample(cls):
|
def sample(cls):
|
||||||
return cls(alarm_id=None,
|
return cls(alarm_id=None,
|
||||||
name="SwiftObjectAlarm",
|
name="SwiftObjectAlarm",
|
||||||
description="An alarm",
|
description="An alarm",
|
||||||
meter_name="storage.objects",
|
type='threshold',
|
||||||
comparison_operator="gt",
|
threshold_rule=None,
|
||||||
threshold=200,
|
|
||||||
statistic="avg",
|
|
||||||
user_id="c96c887c216949acbdfbd8b494863567",
|
user_id="c96c887c216949acbdfbd8b494863567",
|
||||||
project_id="c96c887c216949acbdfbd8b494863567",
|
project_id="c96c887c216949acbdfbd8b494863567",
|
||||||
evaluation_periods=2,
|
|
||||||
period=240,
|
|
||||||
enabled=True,
|
enabled=True,
|
||||||
timestamp=datetime.datetime.utcnow(),
|
timestamp=datetime.datetime.utcnow(),
|
||||||
state="ok",
|
state="ok",
|
||||||
@ -939,11 +1069,17 @@ class Alarm(_Base):
|
|||||||
ok_actions=["http://site:8000/ok"],
|
ok_actions=["http://site:8000/ok"],
|
||||||
alarm_actions=["http://site:8000/alarm"],
|
alarm_actions=["http://site:8000/alarm"],
|
||||||
insufficient_data_actions=["http://site:8000/nodata"],
|
insufficient_data_actions=["http://site:8000/nodata"],
|
||||||
matching_metadata={"key_name":
|
|
||||||
"key_value"},
|
|
||||||
repeat_actions=False,
|
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):
|
class AlarmChange(_Base):
|
||||||
"""Representation of an event in an alarm's history
|
"""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):
|
def _record_change(self, data, now, on_behalf_of=None, type=None):
|
||||||
if not cfg.CONF.alarm.record_history:
|
if not cfg.CONF.alarm.record_history:
|
||||||
return
|
return
|
||||||
type = type or (storage.models.AlarmChange.STATE_TRANSITION
|
type = type or storage.models.AlarmChange.RULE_CHANGE
|
||||||
if data.get('state')
|
|
||||||
else storage.models.AlarmChange.RULE_CHANGE)
|
|
||||||
detail = json.dumps(utils.stringify_timestamps(data))
|
detail = json.dumps(utils.stringify_timestamps(data))
|
||||||
user_id = pecan.request.headers.get('X-User-Id')
|
user_id = pecan.request.headers.get('X-User-Id')
|
||||||
project_id = pecan.request.headers.get('X-Project-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)
|
@wsme_pecan.wsexpose(Alarm, wtypes.text, body=Alarm)
|
||||||
def put(self, data):
|
def put(self, data):
|
||||||
"""Modify this alarm."""
|
"""Modify this alarm."""
|
||||||
# merge the new values from kwargs into the current
|
# Ensure alarm exists
|
||||||
# alarm "alarm_in".
|
|
||||||
alarm_in = self._alarm()
|
alarm_in = self._alarm()
|
||||||
|
|
||||||
now = timeutils.utcnow()
|
now = timeutils.utcnow()
|
||||||
change = data.as_dict(storage.models.Alarm)
|
|
||||||
data.state_timestamp = wsme.Unset
|
|
||||||
data.alarm_id = self._id
|
data.alarm_id = self._id
|
||||||
kwargs = data.as_dict(storage.models.Alarm)
|
data.user_id = alarm_in.user_id
|
||||||
for k, v in kwargs.iteritems():
|
data.project_id = alarm_in.project_id
|
||||||
setattr(alarm_in, k, v)
|
data.timestamp = now
|
||||||
if k == 'state':
|
if alarm_in.state != data.state:
|
||||||
alarm_in.state_timestamp = now
|
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)
|
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)
|
self._record_change(change, now, on_behalf_of=alarm.project_id)
|
||||||
return Alarm.from_db_model(alarm)
|
return Alarm.from_db_model(alarm)
|
||||||
|
|
||||||
@ -1126,14 +1275,15 @@ class AlarmsController(rest.RestController):
|
|||||||
def post(self, data):
|
def post(self, data):
|
||||||
"""Create a new alarm."""
|
"""Create a new alarm."""
|
||||||
conn = pecan.request.storage_conn
|
conn = pecan.request.storage_conn
|
||||||
|
|
||||||
now = timeutils.utcnow()
|
now = timeutils.utcnow()
|
||||||
|
|
||||||
data.alarm_id = str(uuid.uuid4())
|
data.alarm_id = str(uuid.uuid4())
|
||||||
data.user_id = pecan.request.headers.get('X-User-Id')
|
data.user_id = pecan.request.headers.get('X-User-Id')
|
||||||
data.project_id = pecan.request.headers.get('X-Project-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.timestamp = now
|
||||||
|
data.state_timestamp = now
|
||||||
|
|
||||||
|
change = data.as_dict(storage.models.Alarm)
|
||||||
|
|
||||||
# make sure alarms are unique by name per project.
|
# make sure alarms are unique by name per project.
|
||||||
alarms = list(conn.get_alarms(name=data.name,
|
alarms = list(conn.get_alarms(name=data.name,
|
||||||
@ -1144,10 +1294,9 @@ class AlarmsController(rest.RestController):
|
|||||||
raise wsme.exc.ClientSideError(unicode(error))
|
raise wsme.exc.ClientSideError(unicode(error))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
kwargs = data.as_dict(storage.models.Alarm)
|
alarm_in = storage.models.Alarm(**change)
|
||||||
alarm_in = storage.models.Alarm(**kwargs)
|
except Exception:
|
||||||
except Exception as ex:
|
LOG.exception("Error while posting alarm: %s" % change)
|
||||||
LOG.exception(ex)
|
|
||||||
error = _("Alarm incorrect")
|
error = _("Alarm incorrect")
|
||||||
pecan.response.translatable_error = error
|
pecan.response.translatable_error = error
|
||||||
raise wsme.exc.ClientSideError(unicode(error))
|
raise wsme.exc.ClientSideError(unicode(error))
|
||||||
|
@ -615,14 +615,55 @@ class Connection(base.Connection):
|
|||||||
new_matching_metadata[elem['key']] = elem['value']
|
new_matching_metadata[elem['key']] = elem['value']
|
||||||
return new_matching_metadata
|
return new_matching_metadata
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def _encode_matching_metadata(matching_metadata):
|
def _ensure_encapsulated_rule_format(cls, alarm):
|
||||||
if matching_metadata:
|
"""This ensure the alarm returned by the storage have the correct
|
||||||
new_matching_metadata = []
|
format. The previous format looks like:
|
||||||
for k, v in matching_metadata.iteritems():
|
{
|
||||||
new_matching_metadata.append({'key': k, 'value': v})
|
'alarm_id': '0ld-4l3rt',
|
||||||
return new_matching_metadata
|
'enabled': True,
|
||||||
return matching_metadata
|
'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,
|
def get_alarms(self, name=None, user=None,
|
||||||
project=None, enabled=True, alarm_id=None, pagination=None):
|
project=None, enabled=True, alarm_id=None, pagination=None):
|
||||||
@ -655,17 +696,13 @@ class Connection(base.Connection):
|
|||||||
a = {}
|
a = {}
|
||||||
a.update(alarm)
|
a.update(alarm)
|
||||||
del a['_id']
|
del a['_id']
|
||||||
a['matching_metadata'] = \
|
self._ensure_encapsulated_rule_format(a)
|
||||||
self._decode_matching_metadata(a['matching_metadata'])
|
|
||||||
yield models.Alarm(**a)
|
yield models.Alarm(**a)
|
||||||
|
|
||||||
def update_alarm(self, alarm):
|
def update_alarm(self, alarm):
|
||||||
"""update alarm
|
"""update alarm
|
||||||
"""
|
"""
|
||||||
data = alarm.as_dict()
|
data = alarm.as_dict()
|
||||||
data['matching_metadata'] = \
|
|
||||||
self._encode_matching_metadata(data['matching_metadata'])
|
|
||||||
|
|
||||||
self.db.alarm.update(
|
self.db.alarm.update(
|
||||||
{'alarm_id': alarm.alarm_id},
|
{'alarm_id': alarm.alarm_id},
|
||||||
{'$set': data},
|
{'$set': data},
|
||||||
@ -673,8 +710,7 @@ class Connection(base.Connection):
|
|||||||
|
|
||||||
stored_alarm = self.db.alarm.find({'alarm_id': alarm.alarm_id})[0]
|
stored_alarm = self.db.alarm.find({'alarm_id': alarm.alarm_id})[0]
|
||||||
del stored_alarm['_id']
|
del stored_alarm['_id']
|
||||||
stored_alarm['matching_metadata'] = \
|
self._ensure_encapsulated_rule_format(stored_alarm)
|
||||||
self._decode_matching_metadata(stored_alarm['matching_metadata'])
|
|
||||||
return models.Alarm(**stored_alarm)
|
return models.Alarm(**stored_alarm)
|
||||||
|
|
||||||
create_alarm = update_alarm
|
create_alarm = update_alarm
|
||||||
|
@ -789,7 +789,8 @@ class Connection(base.Connection):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _decode_matching_metadata(matching_metadata):
|
def _decode_matching_metadata(matching_metadata):
|
||||||
if isinstance(matching_metadata, dict):
|
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
|
return matching_metadata
|
||||||
else:
|
else:
|
||||||
new_matching_metadata = {}
|
new_matching_metadata = {}
|
||||||
@ -797,14 +798,56 @@ class Connection(base.Connection):
|
|||||||
new_matching_metadata[elem['key']] = elem['value']
|
new_matching_metadata[elem['key']] = elem['value']
|
||||||
return new_matching_metadata
|
return new_matching_metadata
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def _encode_matching_metadata(matching_metadata):
|
def _ensure_encapsulated_rule_format(cls, alarm):
|
||||||
if matching_metadata:
|
"""This ensure the alarm returned by the storage have the correct
|
||||||
new_matching_metadata = []
|
format. The previous format looks like:
|
||||||
for k, v in matching_metadata.iteritems():
|
{
|
||||||
new_matching_metadata.append({'key': k, 'value': v})
|
'alarm_id': '0ld-4l3rt',
|
||||||
return new_matching_metadata
|
'enabled': True,
|
||||||
return matching_metadata
|
'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,
|
def get_alarms(self, name=None, user=None,
|
||||||
project=None, enabled=True, alarm_id=None, pagination=None):
|
project=None, enabled=True, alarm_id=None, pagination=None):
|
||||||
@ -835,16 +878,13 @@ class Connection(base.Connection):
|
|||||||
a = {}
|
a = {}
|
||||||
a.update(alarm)
|
a.update(alarm)
|
||||||
del a['_id']
|
del a['_id']
|
||||||
a['matching_metadata'] = \
|
self._ensure_encapsulated_rule_format(a)
|
||||||
self._decode_matching_metadata(a['matching_metadata'])
|
|
||||||
yield models.Alarm(**a)
|
yield models.Alarm(**a)
|
||||||
|
|
||||||
def update_alarm(self, alarm):
|
def update_alarm(self, alarm):
|
||||||
"""update alarm
|
"""update alarm
|
||||||
"""
|
"""
|
||||||
data = alarm.as_dict()
|
data = alarm.as_dict()
|
||||||
data['matching_metadata'] = \
|
|
||||||
self._encode_matching_metadata(data['matching_metadata'])
|
|
||||||
|
|
||||||
self.db.alarm.update(
|
self.db.alarm.update(
|
||||||
{'alarm_id': alarm.alarm_id},
|
{'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]
|
stored_alarm = self.db.alarm.find({'alarm_id': alarm.alarm_id})[0]
|
||||||
del stored_alarm['_id']
|
del stored_alarm['_id']
|
||||||
stored_alarm['matching_metadata'] = \
|
self._ensure_encapsulated_rule_format(stored_alarm)
|
||||||
self._decode_matching_metadata(stored_alarm['matching_metadata'])
|
|
||||||
return models.Alarm(**stored_alarm)
|
return models.Alarm(**stored_alarm)
|
||||||
|
|
||||||
create_alarm = update_alarm
|
create_alarm = update_alarm
|
||||||
|
@ -581,24 +581,19 @@ class Connection(base.Connection):
|
|||||||
def _row_to_alarm_model(row):
|
def _row_to_alarm_model(row):
|
||||||
return api_models.Alarm(alarm_id=row.id,
|
return api_models.Alarm(alarm_id=row.id,
|
||||||
enabled=row.enabled,
|
enabled=row.enabled,
|
||||||
|
type=row.type,
|
||||||
name=row.name,
|
name=row.name,
|
||||||
description=row.description,
|
description=row.description,
|
||||||
timestamp=row.timestamp,
|
timestamp=row.timestamp,
|
||||||
meter_name=row.meter_name,
|
|
||||||
user_id=row.user_id,
|
user_id=row.user_id,
|
||||||
project_id=row.project_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=row.state,
|
||||||
state_timestamp=row.state_timestamp,
|
state_timestamp=row.state_timestamp,
|
||||||
ok_actions=row.ok_actions,
|
ok_actions=row.ok_actions,
|
||||||
alarm_actions=row.alarm_actions,
|
alarm_actions=row.alarm_actions,
|
||||||
insufficient_data_actions=
|
insufficient_data_actions=
|
||||||
row.insufficient_data_actions,
|
row.insufficient_data_actions,
|
||||||
matching_metadata=row.matching_metadata,
|
rule=row.rule,
|
||||||
repeat_actions=row.repeat_actions)
|
repeat_actions=row.repeat_actions)
|
||||||
|
|
||||||
def get_alarms(self, name=None, user=None,
|
def get_alarms(self, name=None, user=None,
|
||||||
|
@ -265,14 +265,12 @@ class Alarm(Model):
|
|||||||
An alarm to monitor.
|
An alarm to monitor.
|
||||||
|
|
||||||
:param alarm_id: UUID of the alarm
|
:param alarm_id: UUID of the alarm
|
||||||
|
:param type: type of the alarm
|
||||||
:param name: The Alarm name
|
:param name: The Alarm name
|
||||||
:param description: User friendly description of the alarm
|
:param description: User friendly description of the alarm
|
||||||
:param enabled: Is the alarm enabled
|
:param enabled: Is the alarm enabled
|
||||||
:param state: Alarm state (alarm/nodata/ok)
|
:param state: Alarm state (ok/alarm/insufficient data)
|
||||||
:param meter_name: The counter that the alarm is based on
|
:param rule: A rule that defines when the alarm fires
|
||||||
: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 user_id: the owner/creator of the alarm
|
:param user_id: the owner/creator of the alarm
|
||||||
:param project_id: the project_id of the creator
|
:param project_id: the project_id of the creator
|
||||||
:param evaluation_periods: the number of periods
|
:param evaluation_periods: the number of periods
|
||||||
@ -284,47 +282,23 @@ class Alarm(Model):
|
|||||||
alarm state
|
alarm state
|
||||||
:param insufficient_data_actions: the list of webhooks to call when
|
:param insufficient_data_actions: the list of webhooks to call when
|
||||||
entering the insufficient data state
|
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
|
:param repeat_actions: Is the actions should be triggered on each
|
||||||
alarm evaluation.
|
alarm evaluation.
|
||||||
"""
|
"""
|
||||||
def __init__(self, alarm_id, name, meter_name,
|
def __init__(self, alarm_id, type, enabled, name, description,
|
||||||
comparison_operator, threshold, statistic,
|
timestamp, user_id, project_id, state, state_timestamp,
|
||||||
user_id, project_id,
|
ok_actions, alarm_actions, insufficient_data_actions,
|
||||||
evaluation_periods=1,
|
repeat_actions, rule):
|
||||||
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)
|
|
||||||
|
|
||||||
Model.__init__(
|
Model.__init__(
|
||||||
self,
|
self,
|
||||||
alarm_id=alarm_id,
|
alarm_id=alarm_id,
|
||||||
|
type=type,
|
||||||
enabled=enabled,
|
enabled=enabled,
|
||||||
name=name,
|
name=name,
|
||||||
description=description,
|
description=description,
|
||||||
timestamp=timestamp,
|
timestamp=timestamp,
|
||||||
meter_name=meter_name,
|
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
comparison_operator=comparison_operator,
|
|
||||||
threshold=threshold,
|
|
||||||
statistic=statistic,
|
|
||||||
evaluation_periods=evaluation_periods,
|
|
||||||
period=period,
|
|
||||||
state=state,
|
state=state,
|
||||||
state_timestamp=state_timestamp,
|
state_timestamp=state_timestamp,
|
||||||
ok_actions=ok_actions,
|
ok_actions=ok_actions,
|
||||||
@ -332,7 +306,7 @@ class Alarm(Model):
|
|||||||
insufficient_data_actions=
|
insufficient_data_actions=
|
||||||
insufficient_data_actions,
|
insufficient_data_actions,
|
||||||
repeat_actions=repeat_actions,
|
repeat_actions=repeat_actions,
|
||||||
matching_metadata=matching_metadata)
|
rule=rule)
|
||||||
|
|
||||||
|
|
||||||
class AlarmChange(Model):
|
class AlarmChange(Model):
|
||||||
|
@ -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()
|
@ -175,24 +175,17 @@ class Alarm(Base):
|
|||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
Index('ix_alarm_user_id', 'user_id'),
|
Index('ix_alarm_user_id', 'user_id'),
|
||||||
Index('ix_alarm_project_id', 'project_id'),
|
Index('ix_alarm_project_id', 'project_id'),
|
||||||
Index('ix_alarm_meter_name', 'meter_name'),
|
|
||||||
)
|
)
|
||||||
id = Column(String(255), primary_key=True)
|
id = Column(String(255), primary_key=True)
|
||||||
enabled = Column(Boolean)
|
enabled = Column(Boolean)
|
||||||
name = Column(Text)
|
name = Column(Text)
|
||||||
|
type = Column(String(50))
|
||||||
description = Column(Text)
|
description = Column(Text)
|
||||||
timestamp = Column(DateTime, default=timeutils.utcnow)
|
timestamp = Column(DateTime, default=timeutils.utcnow)
|
||||||
meter_name = Column(String(255))
|
|
||||||
|
|
||||||
user_id = Column(String(255), ForeignKey('user.id'))
|
user_id = Column(String(255), ForeignKey('user.id'))
|
||||||
project_id = Column(String(255), ForeignKey('project.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 = Column(String(255))
|
||||||
state_timestamp = Column(DateTime, default=timeutils.utcnow)
|
state_timestamp = Column(DateTime, default=timeutils.utcnow)
|
||||||
|
|
||||||
@ -201,7 +194,7 @@ class Alarm(Base):
|
|||||||
insufficient_data_actions = Column(JSONEncodedDict)
|
insufficient_data_actions = Column(JSONEncodedDict)
|
||||||
repeat_actions = Column(Boolean)
|
repeat_actions = Column(Boolean)
|
||||||
|
|
||||||
matching_metadata = Column(JSONEncodedDict)
|
rule = Column(JSONEncodedDict)
|
||||||
|
|
||||||
|
|
||||||
class AlarmChange(Base):
|
class AlarmChange(Base):
|
||||||
|
@ -63,15 +63,27 @@ class TestSingletonAlarmService(base.TestCase):
|
|||||||
def test_evaluation_cycle(self):
|
def test_evaluation_cycle(self):
|
||||||
alarms = [
|
alarms = [
|
||||||
models.Alarm(name='instance_running_hot',
|
models.Alarm(name='instance_running_hot',
|
||||||
meter_name='cpu_util',
|
type='threshold',
|
||||||
comparison_operator='gt',
|
|
||||||
threshold=80.0,
|
|
||||||
evaluation_periods=5,
|
|
||||||
statistic='avg',
|
|
||||||
user_id='foobar',
|
user_id='foobar',
|
||||||
project_id='snafu',
|
project_id='snafu',
|
||||||
period=60,
|
enabled=True,
|
||||||
alarm_id=str(uuid.uuid4())),
|
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
|
self.api_client.alarms.list.return_value = alarms
|
||||||
with mock.patch('ceilometerclient.client.get_client',
|
with mock.patch('ceilometerclient.client.get_client',
|
||||||
|
@ -36,29 +36,61 @@ class TestEvaluate(base.TestCase):
|
|||||||
self.notifier = mock.MagicMock()
|
self.notifier = mock.MagicMock()
|
||||||
self.alarms = [
|
self.alarms = [
|
||||||
models.Alarm(name='instance_running_hot',
|
models.Alarm(name='instance_running_hot',
|
||||||
meter_name='cpu_util',
|
description='instance_running_hot',
|
||||||
comparison_operator='gt',
|
type='threshold',
|
||||||
threshold=80.0,
|
enabled=True,
|
||||||
evaluation_periods=5,
|
|
||||||
statistic='avg',
|
|
||||||
user_id='foobar',
|
user_id='foobar',
|
||||||
project_id='snafu',
|
project_id='snafu',
|
||||||
period=60,
|
|
||||||
alarm_id=str(uuid.uuid4()),
|
alarm_id=str(uuid.uuid4()),
|
||||||
matching_metadata={'resource_id':
|
state='insufficient data',
|
||||||
'my_instance'}),
|
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',
|
models.Alarm(name='group_running_idle',
|
||||||
meter_name='cpu_util',
|
description='group_running_idle',
|
||||||
comparison_operator='le',
|
type='threshold',
|
||||||
threshold=10.0,
|
enabled=True,
|
||||||
statistic='max',
|
|
||||||
evaluation_periods=4,
|
|
||||||
user_id='foobar',
|
user_id='foobar',
|
||||||
project_id='snafu',
|
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()),
|
alarm_id=str(uuid.uuid4()),
|
||||||
matching_metadata={'metadata.user_metadata.AS':
|
rule=dict(
|
||||||
'my_group'}),
|
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 = threshold_evaluation.Evaluator(self.notifier)
|
||||||
self.evaluator.assign_alarms(self.alarms)
|
self.evaluator.assign_alarms(self.alarms)
|
||||||
@ -83,9 +115,9 @@ class TestEvaluate(base.TestCase):
|
|||||||
with mock.patch('ceilometerclient.client.get_client',
|
with mock.patch('ceilometerclient.client.get_client',
|
||||||
return_value=self.api_client):
|
return_value=self.api_client):
|
||||||
broken = exc.CommunicationError(message='broken')
|
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)]
|
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)]
|
for v in xrange(1, 4)]
|
||||||
self.api_client.statistics.list.side_effect = [broken,
|
self.api_client.statistics.list.side_effect = [broken,
|
||||||
broken,
|
broken,
|
||||||
@ -110,7 +142,7 @@ class TestEvaluate(base.TestCase):
|
|||||||
expected = [mock.call(alarm,
|
expected = [mock.call(alarm,
|
||||||
'ok',
|
'ok',
|
||||||
('%d datapoints are unknown' %
|
('%d datapoints are unknown' %
|
||||||
alarm.evaluation_periods))
|
alarm.rule['evaluation_periods']))
|
||||||
for alarm in self.alarms]
|
for alarm in self.alarms]
|
||||||
self.assertEqual(self.notifier.notify.call_args_list, expected)
|
self.assertEqual(self.notifier.notify.call_args_list, expected)
|
||||||
|
|
||||||
@ -137,9 +169,9 @@ class TestEvaluate(base.TestCase):
|
|||||||
self._set_all_alarms('ok')
|
self._set_all_alarms('ok')
|
||||||
with mock.patch('ceilometerclient.client.get_client',
|
with mock.patch('ceilometerclient.client.get_client',
|
||||||
return_value=self.api_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)]
|
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)]
|
for v in xrange(4)]
|
||||||
self.api_client.statistics.list.side_effect = [avgs, maxs]
|
self.api_client.statistics.list.side_effect = [avgs, maxs]
|
||||||
self.evaluator.evaluate()
|
self.evaluator.evaluate()
|
||||||
@ -160,9 +192,9 @@ class TestEvaluate(base.TestCase):
|
|||||||
self._set_all_alarms('alarm')
|
self._set_all_alarms('alarm')
|
||||||
with mock.patch('ceilometerclient.client.get_client',
|
with mock.patch('ceilometerclient.client.get_client',
|
||||||
return_value=self.api_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)]
|
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)]
|
for v in xrange(1, 5)]
|
||||||
self.api_client.statistics.list.side_effect = [avgs, maxs]
|
self.api_client.statistics.list.side_effect = [avgs, maxs]
|
||||||
self.evaluator.evaluate()
|
self.evaluator.evaluate()
|
||||||
@ -183,9 +215,9 @@ class TestEvaluate(base.TestCase):
|
|||||||
self._set_all_alarms('ok')
|
self._set_all_alarms('ok')
|
||||||
with mock.patch('ceilometerclient.client.get_client',
|
with mock.patch('ceilometerclient.client.get_client',
|
||||||
return_value=self.api_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)]
|
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)]
|
for v in xrange(-1, 3)]
|
||||||
self.api_client.statistics.list.side_effect = [avgs, maxs]
|
self.api_client.statistics.list.side_effect = [avgs, maxs]
|
||||||
self.evaluator.evaluate()
|
self.evaluator.evaluate()
|
||||||
@ -199,9 +231,9 @@ class TestEvaluate(base.TestCase):
|
|||||||
self.alarms[1].repeat_actions = True
|
self.alarms[1].repeat_actions = True
|
||||||
with mock.patch('ceilometerclient.client.get_client',
|
with mock.patch('ceilometerclient.client.get_client',
|
||||||
return_value=self.api_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)]
|
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)]
|
for v in xrange(-1, 3)]
|
||||||
self.api_client.statistics.list.side_effect = [avgs, maxs]
|
self.api_client.statistics.list.side_effect = [avgs, maxs]
|
||||||
self.evaluator.evaluate()
|
self.evaluator.evaluate()
|
||||||
@ -218,9 +250,9 @@ class TestEvaluate(base.TestCase):
|
|||||||
self.alarms[1].repeat_actions = True
|
self.alarms[1].repeat_actions = True
|
||||||
with mock.patch('ceilometerclient.client.get_client',
|
with mock.patch('ceilometerclient.client.get_client',
|
||||||
return_value=self.api_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)]
|
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)]
|
for v in xrange(4)]
|
||||||
self.api_client.statistics.list.side_effect = [avgs, maxs]
|
self.api_client.statistics.list.side_effect = [avgs, maxs]
|
||||||
self.evaluator.evaluate()
|
self.evaluator.evaluate()
|
||||||
@ -238,9 +270,9 @@ class TestEvaluate(base.TestCase):
|
|||||||
self.alarms[1].repeat_actions = True
|
self.alarms[1].repeat_actions = True
|
||||||
with mock.patch('ceilometerclient.client.get_client',
|
with mock.patch('ceilometerclient.client.get_client',
|
||||||
return_value=self.api_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)]
|
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)]
|
for v in xrange(4)]
|
||||||
self.api_client.statistics.list.side_effect = [avgs, maxs]
|
self.api_client.statistics.list.side_effect = [avgs, maxs]
|
||||||
self.evaluator.evaluate()
|
self.evaluator.evaluate()
|
||||||
@ -261,9 +293,9 @@ class TestEvaluate(base.TestCase):
|
|||||||
self._set_all_alarms('insufficient data')
|
self._set_all_alarms('insufficient data')
|
||||||
with mock.patch('ceilometerclient.client.get_client',
|
with mock.patch('ceilometerclient.client.get_client',
|
||||||
return_value=self.api_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)]
|
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)]
|
for v in xrange(4)]
|
||||||
self.api_client.statistics.list.side_effect = [avgs, maxs]
|
self.api_client.statistics.list.side_effect = [avgs, maxs]
|
||||||
self.evaluator.evaluate()
|
self.evaluator.evaluate()
|
||||||
|
@ -33,6 +33,7 @@ from .base import FunctionalTest
|
|||||||
from ceilometer.storage.models import Alarm
|
from ceilometer.storage.models import Alarm
|
||||||
from ceilometer.tests import db as tests_db
|
from ceilometer.tests import db as tests_db
|
||||||
|
|
||||||
|
|
||||||
load_tests = testscenarios.load_tests_apply_scenarios
|
load_tests = testscenarios.load_tests_apply_scenarios
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
@ -55,27 +56,83 @@ class TestAlarms(FunctionalTest,
|
|||||||
self.auth_headers = {'X-User-Id': str(uuid.uuid4()),
|
self.auth_headers = {'X-User-Id': str(uuid.uuid4()),
|
||||||
'X-Project-Id': str(uuid.uuid4())}
|
'X-Project-Id': str(uuid.uuid4())}
|
||||||
for alarm in [Alarm(name='name1',
|
for alarm in [Alarm(name='name1',
|
||||||
|
type='threshold',
|
||||||
|
enabled=True,
|
||||||
alarm_id='a',
|
alarm_id='a',
|
||||||
meter_name='meter.test',
|
description='a',
|
||||||
comparison_operator='gt', threshold=2.0,
|
state='insufficient data',
|
||||||
statistic='avg',
|
state_timestamp=None,
|
||||||
|
timestamp=None,
|
||||||
|
ok_actions=[],
|
||||||
|
insufficient_data_actions=[],
|
||||||
|
alarm_actions=[],
|
||||||
repeat_actions=True,
|
repeat_actions=True,
|
||||||
user_id=self.auth_headers['X-User-Id'],
|
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',
|
Alarm(name='name2',
|
||||||
|
type='threshold',
|
||||||
|
enabled=True,
|
||||||
alarm_id='b',
|
alarm_id='b',
|
||||||
meter_name='meter.mine',
|
description='b',
|
||||||
comparison_operator='gt', threshold=2.0,
|
state='insufficient data',
|
||||||
statistic='avg',
|
state_timestamp=None,
|
||||||
|
timestamp=None,
|
||||||
|
ok_actions=[],
|
||||||
|
insufficient_data_actions=[],
|
||||||
|
alarm_actions=[],
|
||||||
|
repeat_actions=False,
|
||||||
user_id=self.auth_headers['X-User-Id'],
|
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',
|
Alarm(name='name3',
|
||||||
|
type='threshold',
|
||||||
|
enabled=True,
|
||||||
alarm_id='c',
|
alarm_id='c',
|
||||||
meter_name='meter.test',
|
description='c',
|
||||||
comparison_operator='gt', threshold=2.0,
|
state='insufficient data',
|
||||||
statistic='avg',
|
state_timestamp=None,
|
||||||
|
timestamp=None,
|
||||||
|
ok_actions=[],
|
||||||
|
insufficient_data_actions=[],
|
||||||
|
alarm_actions=[],
|
||||||
|
repeat_actions=False,
|
||||||
user_id=self.auth_headers['X-User-Id'],
|
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)
|
self.conn.update_alarm(alarm)
|
||||||
|
|
||||||
def test_list_alarms(self):
|
def test_list_alarms(self):
|
||||||
@ -83,7 +140,8 @@ class TestAlarms(FunctionalTest,
|
|||||||
self.assertEqual(3, len(data))
|
self.assertEqual(3, len(data))
|
||||||
self.assertEqual(set(r['name'] for r in data),
|
self.assertEqual(set(r['name'] for r in data),
|
||||||
set(['name1', 'name2', 'name3']))
|
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']))
|
set(['meter.test', 'meter.mine']))
|
||||||
|
|
||||||
def test_get_alarm(self):
|
def test_get_alarm(self):
|
||||||
@ -94,51 +152,183 @@ class TestAlarms(FunctionalTest,
|
|||||||
for a in alarms:
|
for a in alarms:
|
||||||
print('%s: %s' % (a['name'], a['alarm_id']))
|
print('%s: %s' % (a['name'], a['alarm_id']))
|
||||||
self.assertEqual(alarms[0]['name'], 'name1')
|
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'])
|
one = self.get_json('/alarms/%s' % alarms[0]['alarm_id'])
|
||||||
self.assertEqual(one['name'], 'name1')
|
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['alarm_id'], alarms[0]['alarm_id'])
|
||||||
self.assertEqual(one['repeat_actions'], alarms[0]['repeat_actions'])
|
self.assertEqual(one['repeat_actions'], alarms[0]['repeat_actions'])
|
||||||
|
|
||||||
def test_post_invalid_alarm(self):
|
def test_post_invalid_alarm_period(self):
|
||||||
json = {
|
json = {
|
||||||
'name': 'added_alarm',
|
'name': 'added_alarm_invalid_period',
|
||||||
'meter_name': 'ameter',
|
'type': 'threshold',
|
||||||
'comparison_operator': 'gt',
|
'threshold_rule': {
|
||||||
'threshold': 2.0,
|
'meter_name': 'ameter',
|
||||||
'statistic': 'magic',
|
'comparison_operator': 'gt',
|
||||||
|
'threshold': 2.0,
|
||||||
|
'statistic': 'avg',
|
||||||
|
'period': -1,
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
self.post_json('/alarms', params=json, expect_errors=True, status=400,
|
self.post_json('/alarms', params=json, expect_errors=True, status=400,
|
||||||
headers=self.auth_headers)
|
headers=self.auth_headers)
|
||||||
alarms = list(self.conn.get_alarms())
|
alarms = list(self.conn.get_alarms())
|
||||||
self.assertEqual(3, len(alarms))
|
self.assertEqual(3, len(alarms))
|
||||||
|
|
||||||
def test_post_alarm(self):
|
def test_post_invalid_alarm_statistic(self):
|
||||||
json = {
|
json = {
|
||||||
'name': 'added_alarm',
|
'name': 'added_alarm',
|
||||||
'meter_name': 'ameter',
|
'type': 'threshold',
|
||||||
'comparison_operator': 'gt',
|
'threshold_rule': {
|
||||||
'threshold': 2.0,
|
'meter_name': 'ameter',
|
||||||
'statistic': 'avg',
|
'comparison_operator': 'gt',
|
||||||
'repeat_actions': True,
|
'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,
|
self.post_json('/alarms', params=json, status=201,
|
||||||
headers=self.auth_headers)
|
headers=self.auth_headers)
|
||||||
alarms = list(self.conn.get_alarms())
|
alarms = list(self.conn.get_alarms())
|
||||||
self.assertEqual(4, len(alarms))
|
self.assertEqual(4, len(alarms))
|
||||||
for alarm in alarms:
|
for alarm in alarms:
|
||||||
if alarm.name == 'added_alarm':
|
if alarm.name == 'added_alarm_defaults':
|
||||||
self.assertEqual(alarm.repeat_actions, True)
|
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
|
break
|
||||||
else:
|
else:
|
||||||
self.fail("Alarm not found")
|
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):
|
def test_put_alarm(self):
|
||||||
json = {
|
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,
|
'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',
|
data = self.get_json('/alarms',
|
||||||
q=[{'field': 'name',
|
q=[{'field': 'name',
|
||||||
@ -150,27 +340,53 @@ class TestAlarms(FunctionalTest,
|
|||||||
self.put_json('/alarms/%s' % alarm_id,
|
self.put_json('/alarms/%s' % alarm_id,
|
||||||
params=json,
|
params=json,
|
||||||
headers=self.auth_headers)
|
headers=self.auth_headers)
|
||||||
alarm = list(self.conn.get_alarms(alarm_id=alarm_id))[0]
|
alarm = list(self.conn.get_alarms(alarm_id=alarm_id, enabled=False))[0]
|
||||||
self.assertEqual(alarm.name, json['name'])
|
json['threshold_rule']['query'].append({
|
||||||
self.assertEqual(alarm.repeat_actions, json['repeat_actions'])
|
'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):
|
def test_put_alarm_wrong_field(self):
|
||||||
# Note: wsme will ignore unknown fields so will just not appear in
|
# Note: wsme will ignore unknown fields so will just not appear in
|
||||||
# the Alarm.
|
# the Alarm.
|
||||||
json = {
|
json = {
|
||||||
'name': 'renamed_alarm',
|
|
||||||
'this_can_not_be_correct': 'ha',
|
'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',
|
data = self.get_json('/alarms',
|
||||||
q=[{'field': 'name',
|
q=[{'field': 'name',
|
||||||
'value': 'name1',
|
'value': 'name1',
|
||||||
}],
|
}])
|
||||||
headers=self.auth_headers)
|
|
||||||
self.assertEqual(1, len(data))
|
self.assertEqual(1, len(data))
|
||||||
|
alarm_id = data[0]['alarm_id']
|
||||||
|
|
||||||
resp = self.put_json('/alarms/%s' % data[0]['alarm_id'],
|
resp = self.put_json('/alarms/%s' % alarm_id,
|
||||||
params=json,
|
|
||||||
expect_errors=True,
|
expect_errors=True,
|
||||||
|
params=json,
|
||||||
headers=self.auth_headers)
|
headers=self.auth_headers)
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
@ -202,7 +418,9 @@ class TestAlarms(FunctionalTest,
|
|||||||
self.assertEqual(resp.status_code, status)
|
self.assertEqual(resp.status_code, status)
|
||||||
return resp
|
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'],
|
self.put_json('/alarms/%s' % alarm['alarm_id'],
|
||||||
params=data,
|
params=data,
|
||||||
headers=auth_headers or self.auth_headers)
|
headers=auth_headers or self.auth_headers)
|
||||||
@ -237,11 +455,19 @@ class TestAlarms(FunctionalTest,
|
|||||||
self.assertEqual(1, len(history))
|
self.assertEqual(1, len(history))
|
||||||
|
|
||||||
def test_get_recorded_alarm_history_on_create(self):
|
def test_get_recorded_alarm_history_on_create(self):
|
||||||
new_alarm = dict(name='new_alarm',
|
new_alarm = {
|
||||||
meter_name='other_meter',
|
'name': 'new_alarm',
|
||||||
comparison_operator='le',
|
'type': 'threshold',
|
||||||
threshold=42.0,
|
'threshold_rule': {
|
||||||
statistic='max')
|
'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,
|
self.post_json('/alarms', params=new_alarm, status=201,
|
||||||
headers=self.auth_headers)
|
headers=self.auth_headers)
|
||||||
alarm = self.get_json('/alarms')[3]
|
alarm = self.get_json('/alarms')[3]
|
||||||
@ -253,6 +479,11 @@ class TestAlarms(FunctionalTest,
|
|||||||
type='creation',
|
type='creation',
|
||||||
user_id=alarm['user_id']),
|
user_id=alarm['user_id']),
|
||||||
history[0])
|
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'])
|
self._assert_in_json(new_alarm, history[0]['detail'])
|
||||||
|
|
||||||
def _do_test_get_recorded_alarm_history_on_update(self,
|
def _do_test_get_recorded_alarm_history_on_update(self,
|
||||||
@ -277,20 +508,12 @@ class TestAlarms(FunctionalTest,
|
|||||||
history[0])
|
history[0])
|
||||||
|
|
||||||
def test_get_recorded_alarm_history_rule_change(self):
|
def test_get_recorded_alarm_history_rule_change(self):
|
||||||
now = datetime.datetime.utcnow().isoformat()
|
data = dict(name='renamed')
|
||||||
data = dict(name='renamed', timestamp=now)
|
detail = '{"name": "renamed"}'
|
||||||
detail = '{"timestamp": "%s", "name": "renamed"}' % now
|
|
||||||
self._do_test_get_recorded_alarm_history_on_update(data,
|
self._do_test_get_recorded_alarm_history_on_update(data,
|
||||||
'rule change',
|
'rule change',
|
||||||
detail)
|
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):
|
def test_get_recorded_alarm_history_state_transition_on_behalf_of(self):
|
||||||
# credentials for new non-admin user, on who's behalf the alarm
|
# credentials for new non-admin user, on who's behalf the alarm
|
||||||
# is created
|
# is created
|
||||||
@ -299,11 +522,22 @@ class TestAlarms(FunctionalTest,
|
|||||||
member_auth = {'X-Roles': 'member',
|
member_auth = {'X-Roles': 'member',
|
||||||
'X-User-Id': member_user,
|
'X-User-Id': member_user,
|
||||||
'X-Project-Id': member_project}
|
'X-Project-Id': member_project}
|
||||||
new_alarm = dict(name='new_alarm',
|
new_alarm = {
|
||||||
meter_name='other_meter',
|
'name': 'new_alarm',
|
||||||
comparison_operator='le',
|
'type': 'threshold',
|
||||||
threshold=42.0,
|
'state': 'ok',
|
||||||
statistic='max')
|
'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,
|
self.post_json('/alarms', params=new_alarm, status=201,
|
||||||
headers=member_auth)
|
headers=member_auth)
|
||||||
alarm = self.get_json('/alarms', headers=member_auth)[0]
|
alarm = self.get_json('/alarms', headers=member_auth)[0]
|
||||||
@ -317,16 +551,19 @@ class TestAlarms(FunctionalTest,
|
|||||||
data = dict(state='alarm')
|
data = dict(state='alarm')
|
||||||
self._update_alarm(alarm, data, auth_headers=admin_auth)
|
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
|
# ensure that both the creation event and state transition
|
||||||
# are visible to the non-admin alarm owner and admin user alike
|
# are visible to the non-admin alarm owner and admin user alike
|
||||||
for auth in [member_auth, admin_auth]:
|
for auth in [member_auth, admin_auth]:
|
||||||
history = self._get_alarm_history(alarm, auth_headers=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'],
|
self._assert_is_subset(dict(alarm_id=alarm['alarm_id'],
|
||||||
detail='{"state": "alarm"}',
|
detail='{"state": "alarm"}',
|
||||||
on_behalf_of=alarm['project_id'],
|
on_behalf_of=alarm['project_id'],
|
||||||
project_id=admin_project,
|
project_id=admin_project,
|
||||||
type='state transition',
|
type='rule change',
|
||||||
user_id=admin_user),
|
user_id=admin_user),
|
||||||
history[0])
|
history[0])
|
||||||
self._assert_is_subset(dict(alarm_id=alarm['alarm_id'],
|
self._assert_is_subset(dict(alarm_id=alarm['alarm_id'],
|
||||||
@ -375,9 +612,12 @@ class TestAlarms(FunctionalTest,
|
|||||||
type='deletion',
|
type='deletion',
|
||||||
user_id=alarm['user_id']),
|
user_id=alarm['user_id']),
|
||||||
history[0])
|
history[0])
|
||||||
|
alarm['rule'] = alarm['threshold_rule']
|
||||||
|
del alarm['threshold_rule']
|
||||||
self._assert_in_json(alarm, history[0]['detail'])
|
self._assert_in_json(alarm, history[0]['detail'])
|
||||||
|
detail = '{"name": "renamed"}'
|
||||||
self._assert_is_subset(dict(alarm_id=alarm['alarm_id'],
|
self._assert_is_subset(dict(alarm_id=alarm['alarm_id'],
|
||||||
detail='{"name": "renamed"}',
|
detail=detail,
|
||||||
on_behalf_of=alarm['project_id'],
|
on_behalf_of=alarm['project_id'],
|
||||||
project_id=alarm['project_id'],
|
project_id=alarm['project_id'],
|
||||||
type='rule change',
|
type='rule change',
|
||||||
@ -395,6 +635,8 @@ class TestAlarms(FunctionalTest,
|
|||||||
self._assert_is_subset(dict(alarm_id=alarm['alarm_id'],
|
self._assert_is_subset(dict(alarm_id=alarm['alarm_id'],
|
||||||
type='deletion'),
|
type='deletion'),
|
||||||
history[0])
|
history[0])
|
||||||
|
alarm['rule'] = alarm['threshold_rule']
|
||||||
|
del alarm['threshold_rule']
|
||||||
self._assert_in_json(alarm, history[0]['detail'])
|
self._assert_in_json(alarm, history[0]['detail'])
|
||||||
for i in xrange(1, 10):
|
for i in xrange(1, 10):
|
||||||
detail = '{"name": "%s"}' % (10 - i)
|
detail = '{"name": "%s"}' % (10 - i)
|
||||||
@ -434,6 +676,8 @@ class TestAlarms(FunctionalTest,
|
|||||||
type='deletion',
|
type='deletion',
|
||||||
user_id=alarm['user_id']),
|
user_id=alarm['user_id']),
|
||||||
history[0])
|
history[0])
|
||||||
|
alarm['rule'] = alarm['threshold_rule']
|
||||||
|
del alarm['threshold_rule']
|
||||||
self._assert_in_json(alarm, history[0]['detail'])
|
self._assert_in_json(alarm, history[0]['detail'])
|
||||||
|
|
||||||
def test_get_nonexistent_alarm_history(self):
|
def test_get_nonexistent_alarm_history(self):
|
||||||
|
58
tests/api/v2/test_wsme_custom_type.py
Normal file
58
tests/api/v2/test_wsme_custom_type.py
Normal 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")
|
@ -26,14 +26,12 @@
|
|||||||
|
|
||||||
import copy
|
import copy
|
||||||
import datetime
|
import datetime
|
||||||
import uuid
|
|
||||||
|
|
||||||
from oslo.config import cfg
|
from oslo.config import cfg
|
||||||
|
|
||||||
from ceilometer.publisher import rpc
|
from ceilometer.publisher import rpc
|
||||||
from ceilometer import sample
|
from ceilometer import sample
|
||||||
from ceilometer.storage import impl_mongodb
|
from ceilometer.storage import impl_mongodb
|
||||||
from ceilometer.storage import models
|
|
||||||
from ceilometer.storage.base import NoResultFound
|
from ceilometer.storage.base import NoResultFound
|
||||||
from ceilometer.storage.base import MultipleResultsFound
|
from ceilometer.storage.base import MultipleResultsFound
|
||||||
from ceilometer.tests import db as tests_db
|
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
|
# Create the old format alarm with a dict instead of a
|
||||||
# array for matching_metadata
|
# array for matching_metadata
|
||||||
alarm = models.Alarm('0ld-4l3rt', 'old-alert',
|
alarm = dict(alarm_id='0ld-4l3rt',
|
||||||
'test.one', 'eq', 36, 'count',
|
enabled=True,
|
||||||
'me', 'and-da-boys',
|
name='old-alert',
|
||||||
evaluation_periods=1,
|
description='old-alert',
|
||||||
period=60,
|
timestamp=None,
|
||||||
alarm_actions=['http://nowhere/alarms'],
|
meter_name='cpu',
|
||||||
matching_metadata={'key': 'value'})
|
user_id='me',
|
||||||
alarm.alarm_id = str(uuid.uuid1())
|
project_id='and-da-boys',
|
||||||
data = alarm.as_dict()
|
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(
|
self.conn.db.alarm.update(
|
||||||
{'alarm_id': alarm.alarm_id},
|
{'alarm_id': alarm['alarm_id']},
|
||||||
{'$set': data},
|
{'$set': alarm},
|
||||||
upsert=True)
|
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]
|
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):
|
def test_counter_unit(self):
|
||||||
meters = list(self.conn.get_meters())
|
meters = list(self.conn.get_meters())
|
||||||
@ -224,7 +273,7 @@ class AlarmTestPagination(test_storage_scenarios.AlarmTestBase,
|
|||||||
marker_pairs = {'name': 'red-alert'}
|
marker_pairs = {'name': 'red-alert'}
|
||||||
ret = impl_mongodb.Connection._get_marker(self.conn.db.alarm,
|
ret = impl_mongodb.Connection._get_marker(self.conn.db.alarm,
|
||||||
marker_pairs=marker_pairs)
|
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):
|
def test_alarm_get_marker_None(self):
|
||||||
self.add_some_alarms()
|
self.add_some_alarms()
|
||||||
@ -232,7 +281,8 @@ class AlarmTestPagination(test_storage_scenarios.AlarmTestBase,
|
|||||||
marker_pairs = {'name': 'user-id-foo'}
|
marker_pairs = {'name': 'user-id-foo'}
|
||||||
ret = impl_mongodb.Connection._get_marker(self.conn.db.alarm,
|
ret = impl_mongodb.Connection._get_marker(self.conn.db.alarm,
|
||||||
marker_pairs)
|
marker_pairs)
|
||||||
self.assertEqual(ret['meter_name'], 'meter_name-foo')
|
self.assertEqual(ret['rule']['meter_name'],
|
||||||
|
'meter_name-foo')
|
||||||
except NoResultFound:
|
except NoResultFound:
|
||||||
self.assertTrue(True)
|
self.assertTrue(True)
|
||||||
|
|
||||||
@ -242,6 +292,7 @@ class AlarmTestPagination(test_storage_scenarios.AlarmTestBase,
|
|||||||
marker_pairs = {'user_id': 'me'}
|
marker_pairs = {'user_id': 'me'}
|
||||||
ret = impl_mongodb.Connection._get_marker(self.conn.db.alarm,
|
ret = impl_mongodb.Connection._get_marker(self.conn.db.alarm,
|
||||||
marker_pairs)
|
marker_pairs)
|
||||||
self.assertEqual(ret['meter_name'], 'counter-name-foo')
|
self.assertEqual(ret['rule']['meter_name'],
|
||||||
|
'counter-name-foo')
|
||||||
except MultipleResultsFound:
|
except MultipleResultsFound:
|
||||||
self.assertTrue(True)
|
self.assertTrue(True)
|
||||||
|
@ -1852,26 +1852,87 @@ class CounterDataTypeTest(DBTestBase,
|
|||||||
|
|
||||||
class AlarmTestBase(DBTestBase):
|
class AlarmTestBase(DBTestBase):
|
||||||
def add_some_alarms(self):
|
def add_some_alarms(self):
|
||||||
alarms = [models.Alarm('r3d', 'red-alert',
|
alarms = [models.Alarm(alarm_id='r3d',
|
||||||
'test.one', 'eq', 36, 'count',
|
enabled=True,
|
||||||
'me', 'and-da-boys',
|
type='threshold',
|
||||||
evaluation_periods=1,
|
name='red-alert',
|
||||||
period=60,
|
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'],
|
alarm_actions=['http://nowhere/alarms'],
|
||||||
matching_metadata={'key': 'value'}),
|
insufficient_data_actions=[],
|
||||||
models.Alarm('0r4ng3', 'orange-alert',
|
repeat_actions=False,
|
||||||
'test.fourty', 'gt', 75, 'avg',
|
rule=dict(comparison_operator='eq',
|
||||||
'me', 'and-da-boys',
|
threshold=36,
|
||||||
period=60,
|
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'],
|
alarm_actions=['http://nowhere/alarms'],
|
||||||
matching_metadata={'key2': 'value2'}),
|
insufficient_data_actions=[],
|
||||||
models.Alarm('y3ll0w', 'yellow-alert',
|
repeat_actions=False,
|
||||||
'test.five', 'lt', 10, 'min',
|
rule=dict(comparison_operator='gt',
|
||||||
'me', 'and-da-boys',
|
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'],
|
alarm_actions=['http://nowhere/alarms'],
|
||||||
matching_metadata=
|
insufficient_data_actions=[],
|
||||||
{'key2': 'value2',
|
repeat_actions=False,
|
||||||
'user_metadata.key3': 'value3'})]
|
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:
|
for a in alarms:
|
||||||
self.conn.create_alarm(a)
|
self.conn.create_alarm(a)
|
||||||
|
|
||||||
@ -1887,40 +1948,50 @@ class AlarmTest(AlarmTestBase,
|
|||||||
self.add_some_alarms()
|
self.add_some_alarms()
|
||||||
alarms = list(self.conn.get_alarms())
|
alarms = list(self.conn.get_alarms())
|
||||||
self.assertEqual(len(alarms), 3)
|
self.assertEqual(len(alarms), 3)
|
||||||
|
self.assertEqual(alarms[0].rule['meter_name'], 'test.one')
|
||||||
def test_defaults(self):
|
self.assertEqual(alarms[1].rule['meter_name'], 'test.fourty')
|
||||||
self.add_some_alarms()
|
self.assertEqual(alarms[2].rule['meter_name'], 'test.five')
|
||||||
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'})
|
|
||||||
|
|
||||||
def test_update(self):
|
def test_update(self):
|
||||||
self.add_some_alarms()
|
self.add_some_alarms()
|
||||||
orange = list(self.conn.get_alarms(name='orange-alert'))[0]
|
orange = list(self.conn.get_alarms(name='orange-alert'))[0]
|
||||||
orange.enabled = False
|
orange.enabled = False
|
||||||
orange.state = models.Alarm.ALARM_INSUFFICIENT_DATA
|
orange.state = models.Alarm.ALARM_INSUFFICIENT_DATA
|
||||||
orange.matching_metadata = {'new': 'value',
|
query = [{'field': 'metadata.group',
|
||||||
'user_metadata.new2': 'value4'}
|
'op': 'eq',
|
||||||
|
'value': 'test.updated',
|
||||||
|
'type': 'string'}]
|
||||||
|
orange.rule['query'] = query
|
||||||
|
orange.rule['meter_name'] = 'new_meter_name'
|
||||||
updated = self.conn.update_alarm(orange)
|
updated = self.conn.update_alarm(orange)
|
||||||
self.assertEqual(updated.enabled, False)
|
self.assertEqual(updated.enabled, False)
|
||||||
self.assertEqual(updated.state, models.Alarm.ALARM_INSUFFICIENT_DATA)
|
self.assertEqual(updated.state, models.Alarm.ALARM_INSUFFICIENT_DATA)
|
||||||
self.assertEqual(updated.matching_metadata,
|
self.assertEqual(updated.rule['query'], query)
|
||||||
{'new': 'value', 'user_metadata.new2': 'value4'})
|
self.assertEqual(updated.rule['meter_name'], 'new_meter_name')
|
||||||
|
|
||||||
def test_update_llu(self):
|
def test_update_llu(self):
|
||||||
llu = models.Alarm('llu', 'llu',
|
llu = models.Alarm(alarm_id='llu',
|
||||||
'meter_name', 'lt', 34, 'max',
|
enabled=True,
|
||||||
'bla', 'ffo')
|
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 = self.conn.update_alarm(llu)
|
||||||
updated.state = models.Alarm.ALARM_OK
|
updated.state = models.Alarm.ALARM_OK
|
||||||
updated.description = ':)'
|
updated.description = ':)'
|
||||||
|
Loading…
Reference in New Issue
Block a user