diff --git a/heat/engine/resources/openstack/aodh/alarm.py b/heat/engine/resources/openstack/aodh/alarm.py index d9c6eada88..34685b2733 100644 --- a/heat/engine/resources/openstack/aodh/alarm.py +++ b/heat/engine/resources/openstack/aodh/alarm.py @@ -19,6 +19,7 @@ from heat.engine import properties from heat.engine.resources import alarm_base from heat.engine.resources.openstack.heat import none_resource from heat.engine import support +from heat.engine import translation class AodhAlarm(alarm_base.BaseAlarm): @@ -328,9 +329,108 @@ class EventAlarm(alarm_base.BaseAlarm): self.get_alarm_props(new_props)) +class LBMemberHealthAlarm(alarm_base.BaseAlarm): + """A resource that implements a Loadbalancer Member Health Alarm. + + Allows setting alarms based on the health of load balancer pool members, + where the health of a member is determined by the member reporting an + operating_status of ERROR beyond an initial grace period after creation + (120 seconds by default). + """ + + alarm_type = "loadbalancer_member_health" + + support_status = support.SupportStatus(version='13.0.0') + + PROPERTIES = ( + POOL, STACK, AUTOSCALING_GROUP_ID + ) = ( + "pool", "stack", "autoscaling_group_id" + ) + + RULE_PROPERTIES = ( + POOL_ID, STACK_ID + ) = ( + "pool_id", "stack_id" + ) + + properties_schema = { + POOL: properties.Schema( + properties.Schema.STRING, + _("Name or ID of the loadbalancer pool for which the health of " + "each member will be evaluated."), + update_allowed=True, + required=True, + ), + STACK: properties.Schema( + properties.Schema.STRING, + _("Name or ID of the root / top level Heat stack containing the " + "loadbalancer pool and members. An update will be triggered " + "on the root Stack if an unhealthy member is detected in the " + "loadbalancer pool."), + update_allowed=False, + required=True, + ), + AUTOSCALING_GROUP_ID: properties.Schema( + properties.Schema.STRING, + _("ID of the Heat autoscaling group that contains the " + "loadbalancer members. Unhealthy members will be marked " + "as such before an update is triggered on the root stack."), + update_allowed=True, + required=True, + ), + } + + properties_schema.update(alarm_base.common_properties_schema) + + def get_alarm_props(self, props): + """Apply all relevant compatibility xforms.""" + kwargs = self.actions_to_urls(props) + kwargs['type'] = self.alarm_type + + for prop in (self.POOL, self.STACK, self.AUTOSCALING_GROUP_ID): + if prop in kwargs: + del kwargs[prop] + + rule = { + self.POOL_ID: props[self.POOL], + self.STACK_ID: props[self.STACK], + self.AUTOSCALING_GROUP_ID: props[self.AUTOSCALING_GROUP_ID] + } + + kwargs["loadbalancer_member_health_rule"] = rule + return kwargs + + def translation_rules(self, properties): + translation_rules = [ + translation.TranslationRule( + properties, + translation.TranslationRule.RESOLVE, + [self.POOL], + client_plugin=self.client_plugin('octavia'), + finder='get_pool' + ), + ] + 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)) + + def resource_mapping(): return { 'OS::Aodh::Alarm': AodhAlarm, 'OS::Aodh::CombinationAlarm': CombinationAlarm, 'OS::Aodh::EventAlarm': EventAlarm, + 'OS::Aodh::LBMemberHealthAlarm': LBMemberHealthAlarm, } diff --git a/heat/tests/openstack/aodh/test_alarm.py b/heat/tests/openstack/aodh/test_alarm.py index 23ab45d966..61f19d2d85 100644 --- a/heat/tests/openstack/aodh/test_alarm.py +++ b/heat/tests/openstack/aodh/test_alarm.py @@ -1,3 +1,4 @@ + # # 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 @@ -20,6 +21,7 @@ import six from heat.common import exception from heat.common import template_format from heat.engine.clients.os import aodh +from heat.engine.clients.os import octavia from heat.engine import resource from heat.engine.resources.openstack.aodh import alarm from heat.engine import rsrc_defn @@ -142,6 +144,29 @@ event_alarm_template = ''' } ''' +lbmemberhealth_alarm_template = ''' +{ + "heat_template_version" : "newton", + "description" : "Loadbalancer member health alarm test", + "parameters" : {}, + "resources" : { + "test_loadbalancer_member_health_alarm": { + "type": "OS::Aodh::LBMemberHealthAlarm", + "properties": { + "description": "Something something dark side", + "alarm_actions": ["trust+heat://"], + "repeat_actions": false, + "pool": "12345", + "stack": "13579", + "autoscaling_group_id": "02468" + } + }, + "signal_handler" : { + "type" : "SignalResourceType" + } + } +} +''' FakeAodhAlarm = {'other_attrs': 'val', 'alarm_id': 'foo'} @@ -709,3 +734,101 @@ class EventAlarmTest(common.HeatTestCase): res.client().alarm.get.return_value = FakeAodhAlarm scheduler.TaskRunner(res.create)() self.assertEqual(FakeAodhAlarm, res.FnGetAtt('show')) + + +class LBMemberHealthAlarmTest(common.HeatTestCase): + + def setUp(self): + super(LBMemberHealthAlarmTest, self).setUp() + self.fa = mock.Mock() + self.patchobject( + octavia.OctaviaClientPlugin, 'get_pool').return_value = "9999" + + def create_stack(self, template=None): + + if template is None: + template = lbmemberhealth_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(lbmemberhealth_alarm_template) + self.stack = utils.parse_stack(snippet) + res = self.stack['test_loadbalancer_member_health_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_loadbalancer_member_health_alarm'] + + self.patchobject(aodh.AodhClientPlugin, 'client', + return_value=self.fa) + self.patchobject(self.fa.alarm, 'delete') + rsrc.resource_id = '12345' + + 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_loadbalancer_member_health_alarm'] + + update_props = copy.deepcopy(rsrc.properties.data) + update_props.update({ + "enabled": True, + "description": "", + "insufficient_data_actions": [], + "alarm_actions": [], + "ok_actions": ["signal_handler"], + "pool": "0000", + "autoscaling_group_id": "2222" + }) + + 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/add-aodh-lbmemberhealth-alarm-c59502aac1944b8b.yaml b/releasenotes/notes/add-aodh-lbmemberhealth-alarm-c59502aac1944b8b.yaml new file mode 100644 index 0000000000..6a03fc302b --- /dev/null +++ b/releasenotes/notes/add-aodh-lbmemberhealth-alarm-c59502aac1944b8b.yaml @@ -0,0 +1,4 @@ +--- +features: + - OS::Aodh::LBMemberHealthAlarm resource plugin is added to manage + Aodh loadbalancer_member_health alarm. \ No newline at end of file