From f8a44f490459fb8ad2700bbbb68dcd2d7deeb488 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20M=C3=A1gr?= Date: Tue, 29 Aug 2023 15:05:29 +0200 Subject: [PATCH] Add OS::Aodh::PrometheusAlarm resource This patch adds support for Aodh alarms of type Prometheus. Depends-On: I72e124cca4398b78f7ed12e1db3f66bdbfcb196e Change-Id: I5bb7c4d9086715fc22c0f7abc36d9bbfc88a60c9 --- heat/engine/resources/openstack/aodh/alarm.py | 117 ++++++++++++------ heat/tests/openstack/aodh/test_alarm.py | 117 ++++++++++++++++++ ...aodh-prometheusalarm-a090815d2f9f8e77.yaml | 5 + 3 files changed, 199 insertions(+), 40 deletions(-) create mode 100644 releasenotes/notes/aodh-prometheusalarm-a090815d2f9f8e77.yaml diff --git a/heat/engine/resources/openstack/aodh/alarm.py b/heat/engine/resources/openstack/aodh/alarm.py index f646867efa..25d3a6ca21 100644 --- a/heat/engine/resources/openstack/aodh/alarm.py +++ b/heat/engine/resources/openstack/aodh/alarm.py @@ -20,7 +20,22 @@ from heat.engine import support from heat.engine import translation -class AodhAlarm(alarm_base.BaseAlarm): +class AodhBaseActionsMixin: + def handle_create(self): + props = self.get_alarm_props(self.properties) + props['name'] = self.physical_resource_name() + alarm = self.client().alarm.create(props) + self.resource_id_set(alarm['alarm_id']) + + def handle_update(self, json_snippet, tmpl_diff, prop_diff): + if prop_diff: + new_props = json_snippet.properties(self.properties_schema, + self.context) + self.client().alarm.update(self.resource_id, + self.get_alarm_props(new_props)) + + +class AodhAlarm(AodhBaseActionsMixin, alarm_base.BaseAlarm): """A resource that implements alarming service of Aodh. A resource that allows for the setting alarms based on threshold evaluation @@ -176,19 +191,6 @@ class AodhAlarm(alarm_base.BaseAlarm): kwargs['threshold_rule'] = rule return kwargs - def handle_create(self): - props = self.get_alarm_props(self.properties) - props['name'] = self.physical_resource_name() - alarm = self.client().alarm.create(props) - self.resource_id_set(alarm['alarm_id']) - - def handle_update(self, json_snippet, tmpl_diff, prop_diff): - if prop_diff: - new_props = json_snippet.properties(self.properties_schema, - self.context) - self.client().alarm.update(self.resource_id, - self.get_alarm_props(new_props)) - def parse_live_resource_data(self, resource_properties, resource_data): record_reality = {} threshold_data = resource_data.get('threshold_rule').copy() @@ -232,7 +234,7 @@ class CombinationAlarm(none_resource.NoneResource): ) -class EventAlarm(alarm_base.BaseAlarm): +class EventAlarm(AodhBaseActionsMixin, alarm_base.BaseAlarm): """A resource that implements event alarms. Allows users to define alarms which can be evaluated based on events @@ -313,21 +315,8 @@ class EventAlarm(alarm_base.BaseAlarm): kwargs['event_rule'] = rule return kwargs - def handle_create(self): - props = self.get_alarm_props(self.properties) - props['name'] = self.physical_resource_name() - alarm = self.client().alarm.create(props) - self.resource_id_set(alarm['alarm_id']) - def handle_update(self, json_snippet, tmpl_diff, prop_diff): - if prop_diff: - new_props = json_snippet.properties(self.properties_schema, - self.context) - self.client().alarm.update(self.resource_id, - self.get_alarm_props(new_props)) - - -class LBMemberHealthAlarm(alarm_base.BaseAlarm): +class LBMemberHealthAlarm(AodhBaseActionsMixin, alarm_base.BaseAlarm): """A resource that implements a Loadbalancer Member Health Alarm. Allows setting alarms based on the health of load balancer pool members, @@ -411,18 +400,65 @@ class LBMemberHealthAlarm(alarm_base.BaseAlarm): ] return translation_rules - def handle_create(self): - props = self.get_alarm_props(self.properties) - props['name'] = self.physical_resource_name() - alarm = self.client().alarm.create(props) - self.resource_id_set(alarm['alarm_id']) - def handle_update(self, json_snippet, tmpl_diff, prop_diff): - if prop_diff: - new_props = json_snippet.properties(self.properties_schema, - self.context) - self.client().alarm.update(self.resource_id, - self.get_alarm_props(new_props)) +class PrometheusAlarm(AodhBaseActionsMixin, + alarm_base.BaseAlarm): + """A resource that implements Aodh alarm of type prometheus. + + An alarm that evaluates threshold based on metric data fetched from + Prometheus. + """ + + support_status = support.SupportStatus(version='22.0.0') + + PROPERTIES = ( + COMPARISON_OPERATOR, QUERY, THRESHOLD, + ) = ( + 'comparison_operator', 'query', 'threshold', + ) + + properties_schema = { + COMPARISON_OPERATOR: properties.Schema( + properties.Schema.STRING, + _('Operator used to compare specified statistic with threshold.'), + constraints=[alarm_base.BaseAlarm.QF_OP_VALS], + update_allowed=True + ), + QUERY: properties.Schema( + properties.Schema.STRING, + _('The PromQL query string to fetch metrics data ' + 'from Prometheus.'), + required=True, + update_allowed=True + ), + THRESHOLD: properties.Schema( + properties.Schema.NUMBER, + _('Threshold to evaluate against.'), + required=True, + update_allowed=True + ), + } + properties_schema.update(alarm_base.common_properties_schema) + + alarm_type = 'prometheus' + + def get_alarm_props(self, props): + kwargs = self.actions_to_urls(props) + kwargs['type'] = self.alarm_type + return self._reformat_properties(kwargs) + + def parse_live_resource_data(self, resource_properties, + resource_data): + record_reality = {} + rule = self.alarm_type + '_rule' + data = resource_data.get(rule).copy() + data.update(resource_data) + for key in self.properties_schema.keys(): + if key in alarm_base.INTERNAL_PROPERTIES: + continue + if self.properties_schema[key].update_allowed: + record_reality.update({key: data.get(key)}) + return record_reality def resource_mapping(): @@ -431,4 +467,5 @@ def resource_mapping(): 'OS::Aodh::CombinationAlarm': CombinationAlarm, 'OS::Aodh::EventAlarm': EventAlarm, 'OS::Aodh::LBMemberHealthAlarm': LBMemberHealthAlarm, + 'OS::Aodh::PrometheusAlarm': PrometheusAlarm, } diff --git a/heat/tests/openstack/aodh/test_alarm.py b/heat/tests/openstack/aodh/test_alarm.py index a75f2382c4..48b756fddb 100644 --- a/heat/tests/openstack/aodh/test_alarm.py +++ b/heat/tests/openstack/aodh/test_alarm.py @@ -166,6 +166,28 @@ lbmemberhealth_alarm_template = ''' } ''' +prometheus_alarm_template = ''' +{ + "heat_template_version" : "wallaby", + "description" : "Prometheus alarm test", + "parameters" : {}, + "resources" : { + "test_prometheus_alarm": { + "type": "OS::Aodh::PrometheusAlarm", + "properties": { + "alarm_actions": [], + "threshold": "10", + "comparison_operator": "gt", + "query": "some_metric{some_label='some_value'}" + } + }, + "signal_handler" : { + "type" : "SignalResourceType" + } + } +} +''' + FakeAodhAlarm = {'other_attrs': 'val', 'alarm_id': 'foo'} @@ -830,3 +852,98 @@ class LBMemberHealthAlarmTest(common.HeatTestCase): self.assertEqual((rsrc.UPDATE, rsrc.COMPLETE), rsrc.state) self.assertEqual(1, update_mock.call_count) + + +class PrometheusAlarmTest(common.HeatTestCase): + + def setUp(self): + super(PrometheusAlarmTest, self).setUp() + self.fa = mock.Mock() + + def create_stack(self, template=None): + if template is None: + template = prometheus_alarm_template + temp = template_format.parse(template) + template = tmpl.Template(temp) + ctx = utils.dummy_context() + ctx.tenant = 'test_tenant' + stack = parser.Stack(ctx, utils.random_name(), template, + disable_rollback=True) + stack.store() + + self.patchobject(aodh.AodhClientPlugin, + '_create').return_value = self.fa + + self.patchobject(self.fa.alarm, 'create').return_value = FakeAodhAlarm + + return stack + + def _prepare_resource(self, for_check=True): + snippet = template_format.parse(prometheus_alarm_template) + self.stack = utils.parse_stack(snippet) + res = self.stack['test_prometheus_alarm'] + if for_check: + res.state_set(res.CREATE, res.COMPLETE) + res.client = mock.Mock() + mock_alarm = mock.Mock(enabled=True, state='ok') + res.client().alarm.get.return_value = mock_alarm + return res + + def test_delete(self): + test_stack = self.create_stack() + rsrc = test_stack['test_prometheus_alarm'] + rsrc.resource_id = '12345' + + self.patchobject(aodh.AodhClientPlugin, 'client', + return_value=self.fa) + self.patchobject(self.fa.alarm, 'delete') + + self.assertEqual('12345', rsrc.handle_delete()) + self.assertEqual(1, self.fa.alarm.delete.call_count) + + def test_check(self): + res = self._prepare_resource() + scheduler.TaskRunner(res.check)() + self.assertEqual((res.CHECK, res.COMPLETE), res.state) + + def test_check_alarm_failure(self): + res = self._prepare_resource() + res.client().alarm.get.side_effect = Exception('Boom') + + self.assertRaises(exception.ResourceFailure, + scheduler.TaskRunner(res.check)) + self.assertEqual((res.CHECK, res.FAILED), res.state) + self.assertIn('Boom', res.status_reason) + + def test_show_resource(self): + res = self._prepare_resource(for_check=False) + res.client().alarm.create.return_value = FakeAodhAlarm + res.client().alarm.get.return_value = FakeAodhAlarm + scheduler.TaskRunner(res.create)() + self.assertEqual(FakeAodhAlarm, res.FnGetAtt('show')) + + def test_update(self): + test_stack = self.create_stack() + update_mock = self.patchobject(self.fa.alarm, 'update') + test_stack.create() + rsrc = test_stack['test_prometheus_alarm'] + + update_props = copy.deepcopy(rsrc.properties.data) + update_props.update({ + 'comparison_operator': 'lt', + 'enabled': True, + 'threshold': '9', + 'insufficient_data_actions': [], + 'alarm_actions': [], + 'ok_actions': ['signal_handler'], + 'query': "some_other_metric{some_other_label='value'}" + }) + + snippet = rsrc_defn.ResourceDefinition(rsrc.name, + rsrc.type(), + update_props) + + scheduler.TaskRunner(rsrc.update, snippet)() + + self.assertEqual((rsrc.UPDATE, rsrc.COMPLETE), rsrc.state) + self.assertEqual(1, update_mock.call_count) diff --git a/releasenotes/notes/aodh-prometheusalarm-a090815d2f9f8e77.yaml b/releasenotes/notes/aodh-prometheusalarm-a090815d2f9f8e77.yaml new file mode 100644 index 0000000000..85a4f4f6c3 --- /dev/null +++ b/releasenotes/notes/aodh-prometheusalarm-a090815d2f9f8e77.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add OS::Aodh::PrometheusAlarm resource to enable autoscaling + with Prometheus instead of Gnocchi.