Merge "Add composite rule alarm API support"
This commit is contained in:
commit
f8a263029d
119
aodh/api/controllers/v2/alarm_rules/composite.py
Normal file
119
aodh/api/controllers/v2/alarm_rules/composite.py
Normal file
@ -0,0 +1,119 @@
|
||||
#
|
||||
# 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 stevedore import named
|
||||
from wsme.rest import json as wjson
|
||||
from wsme import types as wtypes
|
||||
|
||||
from aodh.api.controllers.v2 import base
|
||||
from aodh.i18n import _
|
||||
|
||||
|
||||
class InvalidCompositeRule(base.ClientSideError):
|
||||
def __init__(self, error):
|
||||
err = _('Invalid input composite rule: %s, it should '
|
||||
'be a dict with an "and" or "or" as key, and the '
|
||||
'value of dict should be a list of basic threshold '
|
||||
'rules or sub composite rules, can be nested.') % error
|
||||
super(InvalidCompositeRule, self).__init__(err)
|
||||
|
||||
|
||||
class CompositeRule(wtypes.UserType):
|
||||
"""Composite alarm rule.
|
||||
|
||||
A simple dict type to preset composite rule.
|
||||
"""
|
||||
|
||||
basetype = wtypes.text
|
||||
name = 'composite_rule'
|
||||
|
||||
threshold_plugins = None
|
||||
|
||||
def __init__(self):
|
||||
threshold_rules = ('threshold',
|
||||
'gnocchi_resources_threshold',
|
||||
'gnocchi_aggregation_by_metrics_threshold',
|
||||
'gnocchi_aggregation_by_resources_threshold')
|
||||
CompositeRule.threshold_plugins = named.NamedExtensionManager(
|
||||
"aodh.alarm.rule", threshold_rules)
|
||||
super(CompositeRule, self).__init__()
|
||||
|
||||
@staticmethod
|
||||
def valid_composite_rule(rules):
|
||||
if isinstance(rules, dict) and len(rules) == 1:
|
||||
and_or_key = rules.keys()[0]
|
||||
if and_or_key not in ('and', 'or'):
|
||||
raise base.ClientSideError(
|
||||
_('Threshold rules should be combined with "and" or "or"'))
|
||||
if isinstance(rules[and_or_key], list):
|
||||
for sub_rule in rules[and_or_key]:
|
||||
CompositeRule.valid_composite_rule(sub_rule)
|
||||
else:
|
||||
raise InvalidCompositeRule(rules)
|
||||
elif isinstance(rules, dict):
|
||||
rule_type = rules.pop('type', None)
|
||||
if not rule_type:
|
||||
raise base.ClientSideError(_('type must be set in every rule'))
|
||||
|
||||
if rule_type not in CompositeRule.threshold_plugins:
|
||||
plugins = sorted(CompositeRule.threshold_plugins.names())
|
||||
err = _('Unsupported sub-rule type :%(rule)s in composite '
|
||||
'rule, should be one of: %(plugins)s') % {
|
||||
'rule': rule_type,
|
||||
'plugins': plugins}
|
||||
raise base.ClientSideError(err)
|
||||
plugin = CompositeRule.threshold_plugins[rule_type].plugin
|
||||
wjson.fromjson(plugin, rules)
|
||||
rule_dict = plugin(**rules).as_dict()
|
||||
rules.update(rule_dict)
|
||||
rules.update(type=rule_type)
|
||||
else:
|
||||
raise InvalidCompositeRule(rules)
|
||||
|
||||
@staticmethod
|
||||
def validate(value):
|
||||
try:
|
||||
json.dumps(value)
|
||||
except TypeError:
|
||||
raise base.ClientSideError(_('%s is not JSON serializable')
|
||||
% value)
|
||||
else:
|
||||
CompositeRule.valid_composite_rule(value)
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def frombasetype(value):
|
||||
return CompositeRule.validate(value)
|
||||
|
||||
@staticmethod
|
||||
def create_hook(alarm):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def validate_alarm(alarm):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def update_hook(alarm):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def as_dict():
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def __call__(**rule):
|
||||
return rule
|
||||
|
||||
composite_rule = CompositeRule()
|
@ -58,8 +58,7 @@ class AlarmThresholdRule(base.AlarmRule):
|
||||
"Whether datapoints with anomalously low sample counts are excluded"
|
||||
|
||||
def __init__(self, query=None, **kwargs):
|
||||
if query:
|
||||
query = [base.Query(**q) for q in query]
|
||||
query = [base.Query(**q) for q in query] if query else []
|
||||
super(AlarmThresholdRule, self).__init__(query=query, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
|
@ -371,7 +371,8 @@ class Alarm(base.Base):
|
||||
for k in d:
|
||||
if k.endswith('_rule'):
|
||||
del d[k]
|
||||
d['rule'] = getattr(self, "%s_rule" % self.type).as_dict()
|
||||
rule = getattr(self, "%s_rule" % self.type)
|
||||
d['rule'] = rule if isinstance(rule, dict) else rule.as_dict()
|
||||
if self.time_constraints:
|
||||
d['time_constraints'] = [tc.as_dict()
|
||||
for tc in self.time_constraints]
|
||||
|
@ -3085,3 +3085,158 @@ class TestAlarmsEvent(TestAlarmsBase):
|
||||
break
|
||||
else:
|
||||
self.fail("Alarm not found")
|
||||
|
||||
|
||||
class TestAlarmsCompositeRule(TestAlarmsBase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestAlarmsCompositeRule, self).setUp()
|
||||
self.sub_rule1 = {
|
||||
"type": "threshold",
|
||||
"meter_name": "cpu_util",
|
||||
"evaluation_periods": 5,
|
||||
"threshold": 0.8,
|
||||
"query": [{
|
||||
"field": "metadata.metering.stack_id",
|
||||
"value": "36b20eb3-d749-4964-a7d2-a71147cd8147",
|
||||
"op": "eq"
|
||||
}],
|
||||
"statistic": "avg",
|
||||
"period": 60,
|
||||
"exclude_outliers": False,
|
||||
"comparison_operator": "gt"
|
||||
}
|
||||
self.sub_rule2 = {
|
||||
"type": "threshold",
|
||||
"meter_name": "disk.iops",
|
||||
"evaluation_periods": 4,
|
||||
"threshold": 200,
|
||||
"query": [{
|
||||
"field": "metadata.metering.stack_id",
|
||||
"value": "36b20eb3-d749-4964-a7d2-a71147cd8147",
|
||||
"op": "eq"
|
||||
}],
|
||||
"statistic": "max",
|
||||
"period": 60,
|
||||
"exclude_outliers": False,
|
||||
"comparison_operator": "gt"
|
||||
}
|
||||
self.sub_rule3 = {
|
||||
"type": "threshold",
|
||||
"meter_name": "network.incoming.packets.rate",
|
||||
"evaluation_periods": 3,
|
||||
"threshold": 1000,
|
||||
"query": [{
|
||||
"field": "metadata.metering.stack_id",
|
||||
"value":
|
||||
"36b20eb3-d749-4964-a7d2-a71147cd8147",
|
||||
"op": "eq"
|
||||
}],
|
||||
"statistic": "avg",
|
||||
"period": 60,
|
||||
"exclude_outliers": False,
|
||||
"comparison_operator": "gt"
|
||||
}
|
||||
|
||||
self.rule = {
|
||||
"or": [self.sub_rule1,
|
||||
{
|
||||
"and": [self.sub_rule2, self.sub_rule3]
|
||||
}]}
|
||||
|
||||
def test_list_alarms(self):
|
||||
alarm = models.Alarm(name='composite_alarm',
|
||||
type='composite',
|
||||
enabled=True,
|
||||
alarm_id='composite',
|
||||
description='composite',
|
||||
state='insufficient data',
|
||||
severity='moderate',
|
||||
state_timestamp=constants.MIN_DATETIME,
|
||||
timestamp=constants.MIN_DATETIME,
|
||||
ok_actions=[],
|
||||
insufficient_data_actions=[],
|
||||
alarm_actions=[],
|
||||
repeat_actions=False,
|
||||
user_id=self.auth_headers['X-User-Id'],
|
||||
project_id=self.auth_headers['X-Project-Id'],
|
||||
time_constraints=[],
|
||||
rule=self.rule,
|
||||
)
|
||||
self.alarm_conn.update_alarm(alarm)
|
||||
|
||||
data = self.get_json('/alarms', headers=self.auth_headers)
|
||||
self.assertEqual(1, len(data))
|
||||
self.assertEqual(set(['composite_alarm']),
|
||||
set(r['name'] for r in data))
|
||||
self.assertEqual(self.rule, data[0]['composite_rule'])
|
||||
|
||||
def test_post_with_composite_rule(self):
|
||||
json = {
|
||||
"type": "composite",
|
||||
"name": "composite_alarm",
|
||||
"composite_rule": self.rule,
|
||||
"repeat_actions": False
|
||||
}
|
||||
self.post_json('/alarms', params=json, status=201,
|
||||
headers=self.auth_headers)
|
||||
alarms = list(self.alarm_conn.get_alarms())
|
||||
self.assertEqual(1, len(alarms))
|
||||
self.assertEqual(self.rule, alarms[0].rule)
|
||||
|
||||
def test_post_with_sub_rule_with_wrong_type(self):
|
||||
self.sub_rule1['type'] = 'non-type'
|
||||
json = {
|
||||
"type": "composite",
|
||||
"name": "composite_alarm",
|
||||
"composite_rule": self.rule,
|
||||
"repeat_actions": False
|
||||
}
|
||||
response = self.post_json('/alarms', params=json, status=400,
|
||||
expect_errors=True,
|
||||
headers=self.auth_headers)
|
||||
|
||||
err = ("Unsupported sub-rule type :non-type in composite "
|
||||
"rule, should be one of: "
|
||||
"['gnocchi_aggregation_by_metrics_threshold', "
|
||||
"'gnocchi_aggregation_by_resources_threshold', "
|
||||
"'gnocchi_resources_threshold', 'threshold']")
|
||||
faultstring = response.json['error_message']['faultstring']
|
||||
self.assertEqual(err, faultstring)
|
||||
|
||||
def test_post_with_sub_rule_with_only_required_params(self):
|
||||
sub_rulea = {
|
||||
"meter_name": "cpu_util",
|
||||
"threshold": 0.8,
|
||||
"type": "threshold"}
|
||||
sub_ruleb = {
|
||||
"meter_name": "disk.iops",
|
||||
"threshold": 200,
|
||||
"type": "threshold"}
|
||||
json = {
|
||||
"type": "composite",
|
||||
"name": "composite_alarm",
|
||||
"composite_rule": {"and": [sub_rulea, sub_ruleb]},
|
||||
"repeat_actions": False
|
||||
}
|
||||
self.post_json('/alarms', params=json, status=201,
|
||||
headers=self.auth_headers)
|
||||
alarms = list(self.alarm_conn.get_alarms())
|
||||
self.assertEqual(1, len(alarms))
|
||||
|
||||
def test_post_with_sub_rule_with_invalid_params(self):
|
||||
self.sub_rule1['threshold'] = False
|
||||
json = {
|
||||
"type": "composite",
|
||||
"name": "composite_alarm",
|
||||
"composite_rule": self.rule,
|
||||
"repeat_actions": False
|
||||
}
|
||||
response = self.post_json('/alarms', params=json, status=400,
|
||||
expect_errors=True,
|
||||
headers=self.auth_headers)
|
||||
faultstring = ("Invalid input for field/attribute threshold. "
|
||||
"Value: 'False'. Wrong type. Expected '<type '"
|
||||
"float'>', got '<type 'bool'>'")
|
||||
self.assertEqual(faultstring,
|
||||
response.json['error_message']['faultstring'])
|
||||
|
@ -39,7 +39,7 @@ class TestEvaluate(base.TestEvaluatorBase):
|
||||
"threshold": 0.8,
|
||||
"query": [{
|
||||
"field": "metadata.metering.stack_id",
|
||||
"value": "36b20eb3-d749-4964-a7d2-a71147cd8147",
|
||||
"value": "36b20eb3-d749-4964-a7d2-a71147cd8145",
|
||||
"op": "eq"
|
||||
}],
|
||||
"statistic": "avg",
|
||||
@ -55,7 +55,7 @@ class TestEvaluate(base.TestEvaluatorBase):
|
||||
"threshold": 200,
|
||||
"query": [{
|
||||
"field": "metadata.metering.stack_id",
|
||||
"value": "36b20eb3-d749-4964-a7d2-a71147cd8147",
|
||||
"value": "36b20eb3-d749-4964-a7d2-a71147cd8145",
|
||||
"op": "eq"
|
||||
}],
|
||||
"statistic": "max",
|
||||
@ -71,7 +71,7 @@ class TestEvaluate(base.TestEvaluatorBase):
|
||||
"threshold": 1000,
|
||||
"query": [{
|
||||
"field": "metadata.metering.stack_id",
|
||||
"value": "36b20eb3-d749-4964-a7d2-a71147cd8147",
|
||||
"value": "36b20eb3-d749-4964-a7d2-a71147cd8145",
|
||||
"op": "eq"
|
||||
}],
|
||||
"statistic": "avg",
|
||||
|
@ -83,6 +83,7 @@ aodh.alarm.rule =
|
||||
gnocchi_aggregation_by_metrics_threshold = aodh.api.controllers.v2.alarm_rules.gnocchi:AggregationMetricsByIdLookupRule
|
||||
gnocchi_aggregation_by_resources_threshold = aodh.api.controllers.v2.alarm_rules.gnocchi:AggregationMetricByResourcesLookupRule
|
||||
event = aodh.api.controllers.v2.alarm_rules.event:AlarmEventRule
|
||||
composite = aodh.api.controllers.v2.alarm_rules.composite:composite_rule
|
||||
|
||||
aodh.evaluator =
|
||||
threshold = aodh.evaluator.threshold:ThresholdEvaluator
|
||||
|
Loading…
x
Reference in New Issue
Block a user