--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=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
+# 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.engine import resource
+
+
+class CeilometerAlarm(resource.Resource):
+
+ properties_schema = {'comparison_operator': {'Type': 'String',
+ 'Required': True,
+ 'AllowedValues': ['ge',
+ 'gt',
+ 'eq',
+ 'ne',
+ 'lt',
+ 'le']},
+ 'evaluation_periods': {'Type': 'String',
+ 'Required': True},
+ 'counter_name': {'Type': 'String',
+ 'Required': True},
+ 'period': {'Type': 'String',
+ 'Required': True},
+ 'statistic': {'Type': 'String',
+ 'Required': True,
+ 'AllowedValues': ['count',
+ 'avg',
+ 'sum',
+ 'min',
+ 'max']},
+ 'threshold': {'Type': 'String',
+ 'Required': True},
+ 'alarm_actions': {'Type': 'List'},
+ 'ok_actions': {'Type': 'List'},
+ 'insufficient_data_actions': {'Type': 'List'},
+ 'description': {'Type': 'String'},
+ 'source': {'Type': 'String'},
+ 'matching_metadata': {'Type': 'Map'}}
+
+ update_allowed_keys = ('Properties',)
+ # allow the properties that affect the watch calculation.
+ # note: when using in-instance monitoring you can only change the
+ # metric name if you re-configure the instance too.
+ update_allowed_properties = ('comparison_operator', 'description',
+ 'evaluation_periods', 'period', 'statistic',
+ 'alarm_actions', 'ok_actions',
+ 'insufficient_data_actions', 'threshold')
+
+ def _actions_to_urls(self, props):
+ kwargs = {}
+ for k, v in iter(props.items()):
+ if k.endswith('_actions') and v is not None:
+ kwargs[k] = []
+ for act in v:
+ # if the action is a resource name
+ # we ask the destination resource for an alarm url.
+ # the template writer should really do this in the
+ # template if possible with:
+ # {Fn::GetAtt: ['MyAction', 'AlarmUrl']}
+ if act in self.stack:
+ url = self.stack[act].FnGetAtt('AlarmUrl')
+ kwargs[k].append(url)
+ else:
+ kwargs[k].append(act)
+ else:
+ kwargs[k] = v
+ return kwargs
+
+ def handle_create(self):
+ props = self._actions_to_urls(self.parsed_template('Properties'))
+ props['name'] = self.physical_resource_name()
+ props['enabled'] = True
+
+ alarm = self.ceilometer().alarms.create(**props)
+ self.resource_id_set(alarm.alarm_id)
+
+ def handle_update(self, json_snippet, tmpl_diff, prop_diff):
+ if prop_diff:
+ kwargs = {'alarm_id': self.resource_id}
+ kwargs.update(prop_diff)
+ self.ceilometer().alarms.update(**self._actions_to_urls(kwargs))
+
+ def handle_suspend(self):
+ if self.resource_id is not None:
+ self.ceilometer().alarms.update(alarm_id=self.resource_id,
+ enabled=False)
+
+ def handle_resume(self):
+ if self.resource_id is not None:
+ self.ceilometer().alarms.update(alarm_id=self.resource_id,
+ enabled=True)
+
+ def handle_delete(self):
+ if self.resource_id is not None:
+ self.ceilometer().alarms.delete(self.resource_id)
+
+
+def resource_mapping():
+ return {
+ 'OS::Metering::Alarm': CeilometerAlarm,
+ }
--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=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
+# 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 copy
+import json
+import mox
+import testtools
+import uuid
+
+from oslo.config import cfg
+
+from heat.tests import fakes
+from heat.tests import generic_resource
+from heat.tests.common import HeatTestCase
+from heat.tests.utils import setup_dummy_db
+from heat.tests.utils import stack_delete_after
+
+from heat.common import context
+from heat.common import template_format
+
+from heat.openstack.common.importutils import try_import
+
+from heat.engine import parser
+from heat.engine import resource
+from heat.engine import scheduler
+from heat.engine.resources.ceilometer import alarm
+
+ceilometerclient = try_import('ceilometerclient.v2')
+
+alarm_template = '''
+{
+ "AWSTemplateFormatVersion" : "2010-09-09",
+ "Description" : "Alarm Test",
+ "Parameters" : {},
+ "Resources" : {
+ "MEMAlarmHigh": {
+ "Type": "OS::Metering::Alarm",
+ "Properties": {
+ "description": "Scale-up if MEM > 50% for 1 minute",
+ "counter_name": "MemoryUtilization",
+ "statistic": "avg",
+ "period": "60",
+ "evaluation_periods": "1",
+ "threshold": "50",
+ "alarm_actions": [],
+ "matching_metadata": {},
+ "comparison_operator": "gt"
+ }
+ },
+ "signal_handler" : {
+ "Type" : "SignalResourceType"
+ }
+ }
+}
+'''
+
+
+class FakeCeilometerAlarm(object):
+ alarm_id = 'foo'
+
+
+class FakeCeilometerAlarms(object):
+ def create(self, **kwargs):
+ pass
+
+ def update(self, **kwargs):
+ pass
+
+ def delete(self, alarm_id):
+ pass
+
+
+class FakeCeilometerClient(object):
+ alarms = FakeCeilometerAlarms()
+
+
+class UUIDStub(object):
+ def __init__(self, value):
+ self.value = value
+
+ def __enter__(self):
+ self.uuid4 = uuid.uuid4
+ uuid_stub = lambda: self.value
+ uuid.uuid4 = uuid_stub
+
+ def __exit__(self, *exc_info):
+ uuid.uuid4 = self.uuid4
+
+
+class CeilometerAlarmTest(HeatTestCase):
+ def setUp(self):
+ super(CeilometerAlarmTest, self).setUp()
+ setup_dummy_db()
+
+ resource._register_class('SignalResourceType',
+ generic_resource.SignalResource)
+
+ cfg.CONF.set_default('heat_waitcondition_server_url',
+ 'http://127.0.0.1:8000/v1/waitcondition')
+
+ self.stack_id = 'STACKABCD1234'
+ self.fc = fakes.FakeKeystoneClient()
+ self.fa = FakeCeilometerClient()
+
+ # Note tests creating a stack should be decorated with @stack_delete_after
+ # to ensure the stack is properly cleaned up
+ def create_stack(self, stack_name='test_stack',
+ template=None):
+ if template is None:
+ template = alarm_template
+ temp = template_format.parse(template)
+ template = parser.Template(temp)
+ ctx = context.get_admin_context()
+ ctx.tenant_id = 'test_tenant'
+ stack = parser.Stack(ctx, stack_name, template,
+ disable_rollback=True)
+
+ # Stub out the stack ID so we have a known value
+ with UUIDStub(self.stack_id):
+ stack.store()
+
+ self.m.StubOutWithMock(resource.Resource, 'keystone')
+ resource.Resource.keystone().MultipleTimes().AndReturn(
+ self.fc)
+
+ self.m.StubOutWithMock(alarm.CeilometerAlarm, 'ceilometer')
+ alarm.CeilometerAlarm.ceilometer().MultipleTimes().AndReturn(
+ self.fa)
+
+ al = copy.deepcopy(temp['Resources']['MEMAlarmHigh']['Properties'])
+ al['description'] = mox.IgnoreArg()
+ al['enabled'] = mox.IgnoreArg()
+ al['name'] = mox.IgnoreArg()
+ al['alarm_actions'] = mox.IgnoreArg()
+ self.m.StubOutWithMock(self.fa.alarms, 'create')
+ self.fa.alarms.create(**al).AndReturn(FakeCeilometerAlarm())
+ return stack
+
+ @testtools.skipIf(ceilometerclient is None, 'ceilometerclient unavailable')
+ @stack_delete_after
+ def test_mem_alarm_high_update_no_replace(self):
+ '''
+ Make sure that we can change the update-able properties
+ without replacing the Alarm rsrc.
+ '''
+ #short circuit the alarm's references
+ t = template_format.parse(alarm_template)
+ properties = t['Resources']['MEMAlarmHigh']['Properties']
+ properties['alarm_actions'] = ['signal_handler']
+ properties['matching_metadata'] = {'a': 'v'}
+
+ self.stack = self.create_stack(template=json.dumps(t))
+ self.m.StubOutWithMock(self.fa.alarms, 'update')
+ al2 = {}
+ for k in alarm.CeilometerAlarm.update_allowed_properties:
+ al2[k] = mox.IgnoreArg()
+ al2['alarm_id'] = mox.IgnoreArg()
+ self.fa.alarms.update(**al2).AndReturn(None)
+
+ self.m.ReplayAll()
+ self.stack.create()
+ rsrc = self.stack['MEMAlarmHigh']
+
+ snippet = copy.deepcopy(rsrc.parsed_template())
+ snippet['Properties']['comparison_operator'] = 'lt'
+ snippet['Properties']['description'] = 'fruity'
+ snippet['Properties']['evaluation_periods'] = '2'
+ snippet['Properties']['period'] = '90'
+ snippet['Properties']['statistic'] = 'max'
+ snippet['Properties']['threshold'] = '39'
+ snippet['Properties']['insufficient_data_actions'] = []
+ snippet['Properties']['alarm_actions'] = []
+ snippet['Properties']['ok_actions'] = ['signal_handler']
+
+ self.assertEqual(None, rsrc.update(snippet))
+
+ self.m.VerifyAll()
+
+ @testtools.skipIf(ceilometerclient is None, 'ceilometerclient unavailable')
+ @stack_delete_after
+ def test_mem_alarm_high_update_replace(self):
+ '''
+ Make sure that the Alarm resource IS replaced when non-update-able
+ properties are changed.
+ '''
+ t = template_format.parse(alarm_template)
+ properties = t['Resources']['MEMAlarmHigh']['Properties']
+ properties['alarm_actions'] = ['signal_handler']
+ properties['matching_metadata'] = {'a': 'v'}
+
+ self.stack = self.create_stack(template=json.dumps(t))
+
+ self.m.ReplayAll()
+ self.stack.create()
+ rsrc = self.stack['MEMAlarmHigh']
+
+ snippet = copy.deepcopy(rsrc.parsed_template())
+ snippet['Properties']['counter_name'] = 'temp'
+
+ self.assertRaises(resource.UpdateReplace,
+ rsrc.update, snippet)
+
+ self.m.VerifyAll()
+
+ @testtools.skipIf(ceilometerclient is None, 'ceilometerclient unavailable')
+ @stack_delete_after
+ def test_mem_alarm_suspend_resume(self):
+ """
+ Make sure that the Alarm resource gets disabled on suspend
+ and reenabled on resume.
+ """
+ self.stack = self.create_stack()
+
+ self.m.StubOutWithMock(self.fa.alarms, 'update')
+ al_suspend = {'alarm_id': mox.IgnoreArg(),
+ 'enabled': False}
+ self.fa.alarms.update(**al_suspend).AndReturn(None)
+ al_resume = {'alarm_id': mox.IgnoreArg(),
+ 'enabled': True}
+ self.fa.alarms.update(**al_resume).AndReturn(None)
+ self.m.ReplayAll()
+
+ self.stack.create()
+ rsrc = self.stack['MEMAlarmHigh']
+ scheduler.TaskRunner(rsrc.suspend)()
+ self.assertTrue((rsrc.SUSPEND, rsrc.COMPLETE), rsrc.state)
+ scheduler.TaskRunner(rsrc.resume)()
+ self.assertTrue((rsrc.RESUME, rsrc.COMPLETE), rsrc.state)
+
+ self.m.VerifyAll()