summaryrefslogtreecommitdiff
path: root/heat
diff options
context:
space:
mode:
Diffstat (limited to 'heat')
-rw-r--r--heat/engine/clients/os/monasca.py3
-rw-r--r--heat/engine/resources/openstack/monasca/alarm_definition.py205
-rw-r--r--heat/tests/openstack/monasca/test_alarm_definition.py266
3 files changed, 474 insertions, 0 deletions
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)