From ff6acbe5c607943897b73981f3b427bc56d1ff4f Mon Sep 17 00:00:00 2001 From: Kanagaraj Manickam Date: Fri, 17 Jul 2015 10:31:45 +0530 Subject: [PATCH] Monasca Alarm definition resource plugin Adds resource plugin and unit testcases for monasca alarm definition. implements blueprint support-monasca-alarm-notification Co-authored-by: Sergey Kraynev Change-Id: Ie7ada35c83d3d5b4b0fd6524f2a217615862f16e --- heat/engine/clients/os/monasca.py | 3 + .../openstack/monasca/alarm_definition.py | 205 ++++++++++++++ .../monasca/test_alarm_definition.py | 266 ++++++++++++++++++ 3 files changed, 474 insertions(+) create mode 100644 heat/engine/resources/openstack/monasca/alarm_definition.py create mode 100644 heat/tests/openstack/monasca/test_alarm_definition.py diff --git a/heat/engine/clients/os/monasca.py b/heat/engine/clients/os/monasca.py index 376ac3bf2..a762c079f 100644 --- a/heat/engine/clients/os/monasca.py +++ b/heat/engine/clients/os/monasca.py @@ -54,6 +54,9 @@ class MonascaClientPlugin(client_plugin.ClientPlugin): def is_not_found(self, ex): return isinstance(ex, monasca_exc.NotFound) + def is_un_processable(self, ex): + return isinstance(ex, monasca_exc.HTTPUnProcessable) + def get_notification(self, notification): try: return self.client().notifications.get( diff --git a/heat/engine/resources/openstack/monasca/alarm_definition.py b/heat/engine/resources/openstack/monasca/alarm_definition.py new file mode 100644 index 000000000..67593156a --- /dev/null +++ b/heat/engine/resources/openstack/monasca/alarm_definition.py @@ -0,0 +1,205 @@ +# +# 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. + +from heat.common.i18n import _ +from heat.engine import clients +from heat.engine import constraints +from heat.engine import properties +from heat.engine import resource +from heat.engine import support + + +class MonascaAlarmDefinition(resource.Resource): + """Heat Template Resource for Monasca Alarm definition.""" + + support_status = support.SupportStatus( + version='5.0.0', + status=support.UNSUPPORTED) + + default_client_name = 'monasca' + + SEVERITY_LEVELS = ( + LOW, MEDIUM, HIGH, CRITICAL + ) = ( + 'low', 'medium', 'high', 'critical' + ) + + PROPERTIES = ( + NAME, DESCRIPTION, EXPRESSION, MATCH_BY, SEVERITY, + OK_ACTIONS, ALARM_ACTIONS, UNDETERMINED_ACTIONS, + ACTIONS_ENABLED + ) = ( + 'name', 'description', 'expression', 'match_by', 'severity', + 'ok_actions', 'alarm_actions', 'undetermined_actions', + 'actions_enabled' + ) + + properties_schema = { + NAME: properties.Schema( + properties.Schema.STRING, + _('Name of the alarm. By default, physical resource name is ' + 'used.'), + update_allowed=True + ), + DESCRIPTION: properties.Schema( + properties.Schema.STRING, + _('Description of the alarm.'), + update_allowed=True + ), + EXPRESSION: properties.Schema( + properties.Schema.STRING, + _('Expression of the alarm to evaluate.'), + update_allowed=True, + required=True + ), + MATCH_BY: properties.Schema( + properties.Schema.LIST, + _('The metric dimensions to match to the alarm dimensions. ' + 'One or more dimension key names separated by a comma.') + ), + SEVERITY: properties.Schema( + properties.Schema.STRING, + _('Severity of the alarm.'), + update_allowed=True, + constraints=[constraints.AllowedValues( + SEVERITY_LEVELS + )], + default=LOW + ), + OK_ACTIONS: properties.Schema( + properties.Schema.LIST, + _('The notification methods to use when an alarm state is OK.'), + update_allowed=True, + schema=properties.Schema( + properties.Schema.STRING, + _('Monasca notification'), + constraints=[constraints.CustomConstraint( + 'monasca.notification') + ] + ) + ), + ALARM_ACTIONS: properties.Schema( + properties.Schema.LIST, + _('The notification methods to use when an alarm state is ALARM.'), + update_allowed=True, + schema=properties.Schema( + properties.Schema.STRING, + _('Monasca notification'), + constraints=[constraints.CustomConstraint( + 'monasca.notification') + ] + ) + ), + UNDETERMINED_ACTIONS: properties.Schema( + properties.Schema.LIST, + _('The notification methods to use when an alarm state is ' + 'UNDETERMINED.'), + update_allowed=True, + schema=properties.Schema( + properties.Schema.STRING, + _('Monasca notification'), + constraints=[constraints.CustomConstraint( + 'monasca.notification') + ] + ) + ), + ACTIONS_ENABLED: properties.Schema( + properties.Schema.BOOLEAN, + _('Whether to enable the actions or not.'), + update_allowed=True, + default=True, + ), + } + + def handle_create(self): + args = dict( + name=(self.properties[self.NAME] or + self.physical_resource_name()), + description=self.properties[self.DESCRIPTION], + expression=self.properties[self.EXPRESSION], + match_by=self.properties[self.MATCH_BY], + severity=self.properties[self.SEVERITY], + ok_actions=self.properties[self.OK_ACTIONS], + alarm_actions=self.properties[self.ALARM_ACTIONS], + undetermined_actions=self.properties[ + self.UNDETERMINED_ACTIONS] + ) + + alarm = self.client().alarm_definitions.create(**args) + self.resource_id_set(alarm['id']) + + # Monasca enables action by default + actions_enabled = self.properties[self.ACTIONS_ENABLED] + if not actions_enabled: + self.client().alarm_definitions.patch( + alarm_id=self.resource_id, + actions_enabled=actions_enabled + ) + + def handle_update(self, prop_diff, json_snippet=None, tmpl_diff=None): + args = dict(alarm_id=self.resource_id) + + if prop_diff.get(self.NAME): + args['name'] = prop_diff.get(self.NAME) + + if prop_diff.get(self.DESCRIPTION): + args['description'] = prop_diff.get(self.DESCRIPTION) + + if prop_diff.get(self.EXPRESSION): + args['expression'] = prop_diff.get(self.EXPRESSION) + + if prop_diff.get(self.SEVERITY): + args['severity'] = prop_diff.get(self.SEVERITY) + + if prop_diff.get(self.OK_ACTIONS): + args['ok_actions'] = prop_diff.get(self.OK_ACTIONS) + + if prop_diff.get(self.ALARM_ACTIONS): + args['alarm_actions'] = prop_diff.get(self.ALARM_ACTIONS) + + if prop_diff.get(self.UNDETERMINED_ACTIONS): + args['undetermined_actions'] = prop_diff.get( + self.UNDETERMINED_ACTIONS + ) + + if prop_diff.get(self.ACTIONS_ENABLED): + args['actions_enabled'] = prop_diff.get(self.ACTIONS_ENABLED) + + if len(args) > 1: + try: + self.client().alarm_definitions.patch(**args) + except Exception as ex: + if self.client_plugin().is_un_processable(ex): + # Monasca does not allow to update the sub expression + raise resource.UpdateReplace(resource_name=self.name) + + def handle_delete(self): + if self.resource_id is not None: + try: + self.client().alarm_definitions.delete( + alarm_id=self.resource_id) + except Exception as ex: + self.client_plugin().ignore_not_found(ex) + + +def resource_mapping(): + return { + 'OS::Monasca::AlarmDefinition': MonascaAlarmDefinition + } + + +def available_resource_mapping(): + if not clients.has_client(MonascaAlarmDefinition.default_client_name): + return {} + + return resource_mapping() diff --git a/heat/tests/openstack/monasca/test_alarm_definition.py b/heat/tests/openstack/monasca/test_alarm_definition.py new file mode 100644 index 000000000..989f8837d --- /dev/null +++ b/heat/tests/openstack/monasca/test_alarm_definition.py @@ -0,0 +1,266 @@ +# +# 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 + +from heat.engine.clients.os import monasca as client_plugin +from heat.engine import resource +from heat.engine.resources.openstack.monasca import alarm_definition +from heat.engine import stack +from heat.engine import template +from heat.tests import common +from heat.tests import utils + + +sample_template = { + 'heat_template_version': '2015-10-15', + 'resources': { + 'test_resource': { + 'type': 'OS::Monasca::AlarmDefinition', + 'properties': { + 'name': 'sample_alarm_id', + 'description': 'sample alarm def', + 'expression': 'sample expression', + 'match_by': ['match_by'], + 'severity': 'low', + 'ok_actions': ['sample_notification'], + 'alarm_actions': ['sample_notification'], + 'undetermined_actions': ['sample_notification'], + 'actions_enabled': False + } + } + } +} + +RESOURCE_TYPE = 'OS::Monasca::AlarmDefinition' + + +class MonascaAlarmDefinition(alarm_definition.MonascaAlarmDefinition): + ''' + Monasca service is not available by default. So, this class overrides + the is_service_available to return True + ''' + @classmethod + def is_service_available(cls, context): + return True + + +class MonascaAlarmDefinitionTest(common.HeatTestCase): + + def setUp(self): + super(MonascaAlarmDefinitionTest, self).setUp() + + self.ctx = utils.dummy_context() + # As monascaclient is not part of requirements.txt, RESOURCE_TYPE is + # not registered by default. For testing, its registered here + resource._register_class(RESOURCE_TYPE, + MonascaAlarmDefinition) + self.stack = stack.Stack( + self.ctx, 'test_stack', + template.Template(sample_template) + ) + + self.test_resource = self.stack['test_resource'] + + # Mock client plugin + self.test_client_plugin = mock.MagicMock() + self.test_resource.client_plugin = mock.MagicMock( + return_value=self.test_client_plugin) + self.test_client_plugin.get_notification.return_value = ( + 'sample_notification' + ) + + # Mock client + self.test_client = mock.MagicMock() + self.test_resource.client = mock.MagicMock( + return_value=self.test_client) + + def _get_mock_resource(self): + value = dict(id='477e8273-60a7-4c41-b683-fdb0bc7cd152') + + return value + + def test_resource_handle_create(self): + mock_alarm_create = self.test_client.alarm_definitions.create + mock_alarm_patch = self.test_client.alarm_definitions.patch + mock_resource = self._get_mock_resource() + mock_alarm_create.return_value = mock_resource + + # validate the properties + self.assertEqual( + 'sample_alarm_id', + self.test_resource.properties.get( + alarm_definition.MonascaAlarmDefinition.NAME)) + self.assertEqual( + 'sample alarm def', + self.test_resource.properties.get( + alarm_definition.MonascaAlarmDefinition.DESCRIPTION)) + self.assertEqual( + 'sample expression', + self.test_resource.properties.get( + alarm_definition.MonascaAlarmDefinition.EXPRESSION)) + self.assertEqual( + ['match_by'], + self.test_resource.properties.get( + alarm_definition.MonascaAlarmDefinition.MATCH_BY)) + self.assertEqual( + 'low', + self.test_resource.properties.get( + alarm_definition.MonascaAlarmDefinition.SEVERITY)) + self.assertEqual( + ['sample_notification'], + self.test_resource.properties.get( + alarm_definition.MonascaAlarmDefinition.OK_ACTIONS)) + self.assertEqual( + ['sample_notification'], + self.test_resource.properties.get( + alarm_definition.MonascaAlarmDefinition.ALARM_ACTIONS)) + self.assertEqual( + ['sample_notification'], + self.test_resource.properties.get( + alarm_definition.MonascaAlarmDefinition.UNDETERMINED_ACTIONS)) + self.assertEqual( + False, + self.test_resource.properties.get( + alarm_definition.MonascaAlarmDefinition.ACTIONS_ENABLED)) + + self.test_resource.data_set = mock.Mock() + self.test_resource.handle_create() + # validate physical resource id + self.assertEqual(mock_resource['id'], self.test_resource.resource_id) + + args = dict( + name='sample_alarm_id', + description='sample alarm def', + expression='sample expression', + match_by=['match_by'], + severity='low', + ok_actions=['sample_notification'], + alarm_actions=['sample_notification'], + undetermined_actions=['sample_notification'] + ) + + mock_alarm_create.assert_called_once_with(**args) + mock_alarm_patch.assert_called_once_with( + alarm_id=self.test_resource.resource_id, + actions_enabled=False) + + def test_resource_handle_update(self): + mock_alarm_patch = self.test_client.alarm_definitions.patch + self.test_resource.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151' + + prop_diff = { + alarm_definition.MonascaAlarmDefinition.NAME: + 'name-updated', + alarm_definition.MonascaAlarmDefinition.DESCRIPTION: + 'description-updated', + alarm_definition.MonascaAlarmDefinition.EXPRESSION: + 'expression-updated', + alarm_definition.MonascaAlarmDefinition.ACTIONS_ENABLED: + True, + alarm_definition.MonascaAlarmDefinition.SEVERITY: + 'medium', + alarm_definition.MonascaAlarmDefinition.OK_ACTIONS: + ['sample_notification'], + alarm_definition.MonascaAlarmDefinition.ALARM_ACTIONS: + ['sample_notification'], + alarm_definition.MonascaAlarmDefinition.UNDETERMINED_ACTIONS: + ['sample_notification']} + + self.test_resource.handle_update(json_snippet=None, + tmpl_diff=None, + prop_diff=prop_diff) + + args = dict( + alarm_id=self.test_resource.resource_id, + name='name-updated', + description='description-updated', + expression='expression-updated', + actions_enabled=True, + severity='medium', + ok_actions=['sample_notification'], + alarm_actions=['sample_notification'], + undetermined_actions=['sample_notification'] + ) + + mock_alarm_patch.assert_called_once_with(**args) + + def test_resource_handle_update_sub_expression(self): + ''' + Monasca does not allow to update the metrics in the expression though + it allows to update the metrics measurement condition range. Monasca + client raises HTTPUnProcessable in this case + + so UpdateReplace is raised to re-create the alarm-definition with + updated new expression. + ''' + # TODO(skraynev): remove it when monasca client will be + # merged in global requirements + class HTTPUnProcessable(Exception): + pass + + client_plugin.monasca_exc = mock.Mock() + client_plugin.monasca_exc.HTTPUnProcessable = HTTPUnProcessable + mock_alarm_patch = self.test_client.alarm_definitions.patch + mock_alarm_patch.side_effect = ( + client_plugin.monasca_exc.HTTPUnProcessable) + self.test_resource.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151' + + prop_diff = {alarm_definition.MonascaAlarmDefinition.EXPRESSION: + 'expression-updated'} + + self.assertRaises(resource.UpdateReplace, + self.test_resource.handle_update, + json_snippet=None, + tmpl_diff=None, + prop_diff=prop_diff) + + def test_resource_handle_delete(self): + mock_alarm_delete = self.test_client.alarm_definitions.delete + self.test_resource.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151' + mock_alarm_delete.return_value = None + + self.assertIsNone(self.test_resource.handle_delete()) + mock_alarm_delete.assert_called_once_with( + alarm_id=self.test_resource.resource_id + ) + + def test_resource_handle_delete_resource_id_is_none(self): + self.test_resource.resource_id = None + self.assertIsNone(self.test_resource.handle_delete()) + + def test_resource_handle_delete_not_found(self): + # TODO(skraynev): remove it when monasca client will be + # merged in global requirements + class NotFound(Exception): + pass + + client_plugin.monasca_exc = mock.Mock() + client_plugin.monasca_exc.NotFound = NotFound + + self.test_resource.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151' + mock_alarm_delete = self.test_client.alarm_definitions.delete + mock_alarm_delete.side_effect = client_plugin.monasca_exc.NotFound + self.assertIsNone(self.test_resource.handle_delete()) + self.assertEqual(1, + self.test_client_plugin.ignore_not_found.call_count) + e_type = type(self.test_client_plugin.ignore_not_found.call_args[0][0]) + self.assertEqual(type(client_plugin.monasca_exc.NotFound()), e_type) + + def test_resource_mapping(self): + mapping = alarm_definition.resource_mapping() + self.assertEqual(1, len(mapping)) + self.assertEqual(alarm_definition.MonascaAlarmDefinition, + mapping[RESOURCE_TYPE]) + self.assertIsInstance(self.test_resource, + alarm_definition.MonascaAlarmDefinition)