]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Add a Ceilometer alarm resource
authorAngus Salkeld <asalkeld@redhat.com>
Fri, 26 Jul 2013 06:13:25 +0000 (16:13 +1000)
committerAngus Salkeld <asalkeld@redhat.com>
Fri, 26 Jul 2013 06:13:25 +0000 (16:13 +1000)
Note: this gets the signed url from the resources capable of getting
signals by calling Fn::GetAtt('AlarmUrl')

blueprint watch-ceilometer
Change-Id: If8822f7c9bfc2113b6ee57e1faff2ab4f8ff3b16

heat/engine/clients.py
heat/engine/resource.py
heat/engine/resources/autoscaling.py
heat/engine/resources/ceilometer/__init__.py [new file with mode: 0644]
heat/engine/resources/ceilometer/alarm.py [new file with mode: 0644]
heat/engine/resources/instance.py
heat/tests/test_ceilometer_alarm.py [new file with mode: 0644]
requirements.txt

index 05cbcaf7c7252b5dc43a35c86dafcace9397d2cd..388943a24ff7d892d307e85cd8257623df905584 100644 (file)
@@ -39,6 +39,12 @@ except ImportError:
     cinderclient = None
     logger.info('cinderclient not available')
 
+try:
+    from ceilometerclient.v2 import client as ceilometerclient
+except ImportError:
+    ceilometerclient = None
+    logger.info('ceilometerclient not available')
+
 
 cloud_opts = [
     cfg.StrOpt('cloud_backend',
@@ -60,6 +66,7 @@ class OpenStackClients(object):
         self._swift = None
         self._quantum = None
         self._cinder = None
+        self._ceilometer = None
 
     @property
     def auth_token(self):
@@ -174,6 +181,30 @@ class OpenStackClients(object):
 
         return self._cinder
 
+    def ceilometer(self):
+        if ceilometerclient is None:
+            return None
+        if self._ceilometer:
+            return self._ceilometer
+
+        if self.auth_token is None:
+            logger.error("Ceilometer connection failed, no auth_token!")
+            return None
+        con = self.context
+        args = {
+            'auth_url': con.auth_url,
+            'service_type': 'metering',
+            'project_id': con.tenant,
+            'token': self.auth_token,
+            'endpoint': self.url_for(service_type='metering'),
+        }
+
+        client = ceilometerclient.Client(**args)
+
+        self._ceilometer = client
+        return self._ceilometer
+
+
 if cfg.CONF.cloud_backend:
     cloud_backend_module = importutils.import_module(cfg.CONF.cloud_backend)
     Clients = cloud_backend_module.Clients
index f390e328ccddd4b989c4baddf7b65cc0ae46b142..33d0527825f7e91330946b016c2d788a7b06056a 100644 (file)
@@ -327,6 +327,9 @@ class Resource(object):
     def cinder(self):
         return self.stack.clients.cinder()
 
+    def ceilometer(self):
+        return self.stack.clients.ceilometer()
+
     def _do_action(self, action, pre_func=None):
         '''
         Perform a transition to a new state via a specified action
index d634b69e5b8dd5908bfe2a1ccb3607d9f44052f6..f1ae64ca9b19b97f808802301e6897ce4048477a 100644 (file)
@@ -467,6 +467,22 @@ class ScalingPolicy(signal_responder.SignalResponder, CooldownMixin):
                                          self.name)
 
     def handle_signal(self, details=None):
+        # ceilometer sends details like this:
+        # {u'state': u'alarm', u'reason': u'...'})
+        # in this policy we currently assume that this gets called
+        # only when there is an alarm. But the template writer can
+        # put the policy in all the alarm notifiers (nodata, and ok).
+        #
+        # our watchrule has upper case states so lower() them all.
+        if details is None:
+            alarm_state = 'alarm'
+        else:
+            alarm_state = details.get('state', 'alarm').lower()
+
+        logger.info('%s Alarm, new state %s' % (self.name, alarm_state))
+
+        if alarm_state != 'alarm':
+            return
         if self._cooldown_inprogress():
             logger.info("%s NOT performing scaling action, cooldown %s" %
                         (self.name, self.properties['Cooldown']))
diff --git a/heat/engine/resources/ceilometer/__init__.py b/heat/engine/resources/ceilometer/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/heat/engine/resources/ceilometer/alarm.py b/heat/engine/resources/ceilometer/alarm.py
new file mode 100644 (file)
index 0000000..85a5677
--- /dev/null
@@ -0,0 +1,112 @@
+# 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,
+    }
index 2bc4bc0e28058f36bb361bd93c856386b6f23de7..1ac08554828bb4ce003d32dc4d6c95716738a85a 100644 (file)
@@ -57,8 +57,17 @@ class Restarter(signal_responder.SignalResponder):
         return None
 
     def handle_signal(self, details=None):
-        victim = self._find_resource(self.properties['InstanceId'])
+        if details is None:
+            alarm_state = 'alarm'
+        else:
+            alarm_state = details.get('state', 'alarm').lower()
+
+        logger.info('%s Alarm, new state %s' % (self.name, alarm_state))
 
+        if alarm_state != 'alarm':
+            return
+
+        victim = self._find_resource(self.properties['InstanceId'])
         if victim is None:
             logger.info('%s Alarm, can not find instance %s' %
                        (self.name, self.properties['InstanceId']))
diff --git a/heat/tests/test_ceilometer_alarm.py b/heat/tests/test_ceilometer_alarm.py
new file mode 100644 (file)
index 0000000..c83f0eb
--- /dev/null
@@ -0,0 +1,242 @@
+# 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()
index 200108a46d74c383a42e1e8122e49e523800aa4e..21a4c0ebee773d59dce507d68649f7bc462555c1 100644 (file)
@@ -19,6 +19,7 @@ WebOb==1.2.3
 python-keystoneclient>=0.2.1
 python-swiftclient>=1.2
 python-quantumclient>=2.2.0
+python-ceilometerclient>=1.0.1
 python-cinderclient>=1.0.4
 PyYAML>=3.1.0
 oslo.config>=1.1.0