From 0c50b7d23f5cd5c3cd0b35a563a6b602986d1c09 Mon Sep 17 00:00:00 2001 From: Mike Spreitzer Date: Sat, 11 Oct 2014 22:30:20 +0000 Subject: [PATCH] Add query property to threshold alarms. Heat had fallen behind the evolution of the Ceilometer API. The full generality of the Ceilometer API for creating alarms was not available through Heat templates. In particular, the template author could stipulate only matching metadata in the query for Samples; other very interesting attributes, such as resource_id, could not be referenced in alarm properties. This change introduces a new property for OS::Ceilometer::Alarm, namely "query". The template can now specify a query property instead of a matching_metadata property, and can thus reference anything that can be referenced in a Ceilometer query. The old matching_metadata property remains, and its constraints on which samples to accept are combined with those from the query (if any). Note also that the python-ceilometerclient has a lot of backward compatibility logic --- including accepting matching_metadata. This change adds all that logic into OS::Ceilometer::Alarm, so that it becomes a proper client of the current Ceilometer API. Closes-Bug: #1326721 Change-Id: I0667db868c6f827867a5a20e4a3fa22fcad1a6a1 --- heat/engine/resources/ceilometer/alarm.py | 82 +++++++++++++++++++---- heat/tests/test_ceilometer_alarm.py | 55 ++++++++++++--- 2 files changed, 114 insertions(+), 23 deletions(-) diff --git a/heat/engine/resources/ceilometer/alarm.py b/heat/engine/resources/ceilometer/alarm.py index e69642d9f..8e6269ca4 100644 --- a/heat/engine/resources/ceilometer/alarm.py +++ b/heat/engine/resources/ceilometer/alarm.py @@ -116,12 +116,21 @@ class CeilometerAlarm(resource.Resource): PROPERTIES = ( COMPARISON_OPERATOR, EVALUATION_PERIODS, METER_NAME, PERIOD, - STATISTIC, THRESHOLD, MATCHING_METADATA, + STATISTIC, THRESHOLD, MATCHING_METADATA, QUERY, ) = ( 'comparison_operator', 'evaluation_periods', 'meter_name', 'period', - 'statistic', 'threshold', 'matching_metadata', + 'statistic', 'threshold', 'matching_metadata', 'query', ) + QUERY_FACTOR_FIELDS = ( + QF_FIELD, QF_OP, QF_VALUE, + ) = ( + 'field', 'op', 'value', + ) + + QF_OP_VALS = constraints.AllowedValues(['le', 'ge', 'eq', + 'lt', 'gt', 'ne']) + properties_schema = { COMPARISON_OPERATOR: properties.Schema( properties.Schema.STRING, @@ -166,41 +175,89 @@ class CeilometerAlarm(resource.Resource): properties.Schema.MAP, _('Meter should match this resource metadata (key=value) ' 'additionally to the meter_name.'), - default={} + default={}, + update_allowed=True ), + QUERY: properties.Schema( + properties.Schema.LIST, + _('A list of query factors, each comparing ' + 'a Sample attribute with a value. ' + 'Implicitly combined with matching_metadata, if any.'), + update_allowed=True, + support_status=support.SupportStatus(version='2015.1'), + schema=properties.Schema( + properties.Schema.MAP, + schema={ + QF_FIELD: properties.Schema( + properties.Schema.STRING, + _('Name of attribute to compare. ' + 'Names of the form metadata.user_metadata.X ' + 'or metadata.metering.X are equivalent to what ' + 'you can address through matching_metadata; ' + 'the former for Nova meters, ' + 'the latter for all others. ' + 'To see the attributes of your Samples, ' + 'use `ceilometer --debug sample-list`. ') + ), + QF_OP: properties.Schema( + properties.Schema.STRING, + _('Comparison operator'), + constraints=[QF_OP_VALS] + ), + QF_VALUE: properties.Schema( + properties.Schema.STRING, + _('String value with which to compare') + ) + } + ) + ) } properties_schema.update(common_properties_schema) default_client_name = 'ceilometer' def cfn_to_ceilometer(self, stack, properties): + """Apply all relevant compatibility xforms.""" + kwargs = actions_to_urls(stack, properties) - if self.MATCHING_METADATA not in properties: - return kwargs + kwargs['type'] = 'threshold' + rule = {} + for field in ['period', 'evaluation_periods', 'threshold', + 'statistic', 'comparison_operator', 'meter_name']: + if field in kwargs: + rule[field] = kwargs[field] + del kwargs[field] + mmd = properties.get(self.MATCHING_METADATA) or {} + query = properties.get(self.QUERY) or [] + if kwargs.get(self.METER_NAME) in NOVA_METERS: prefix = 'user_metadata.' else: prefix = 'metering.' - # make sure we have matching_metadata that looks like this: - # matching_metadata: {metadata.$prefix.x} - kwargs[self.MATCHING_METADATA] = {} - for m_k, m_v in six.iteritems(properties.get( - self.MATCHING_METADATA, {})): + # make sure the matching_metadata appears in the query like this: + # {field: metadata.$prefix.x, ...} + for m_k, m_v in six.iteritems(mmd): if m_k.startswith('metadata.%s' % prefix): key = m_k elif m_k.startswith(prefix): key = 'metadata.%s' % m_k else: key = 'metadata.%s%s' % (prefix, m_k) - kwargs[self.MATCHING_METADATA][key] = m_v + query.append(dict(field=key, op='eq', value=m_v)) + if self.MATCHING_METADATA in kwargs: + del kwargs[self.MATCHING_METADATA] + if self.QUERY in kwargs: + del kwargs[self.QUERY] + if query: + rule['query'] = query + kwargs['threshold_rule'] = rule return kwargs def handle_create(self): props = self.cfn_to_ceilometer(self.stack, self.properties) props['name'] = self.physical_resource_name() - alarm = self.ceilometer().alarms.create(**props) self.resource_id_set(alarm.alarm_id) @@ -218,6 +275,7 @@ class CeilometerAlarm(resource.Resource): def handle_update(self, json_snippet, tmpl_diff, prop_diff): if prop_diff: kwargs = {'alarm_id': self.resource_id} + kwargs.update(self.properties) kwargs.update(prop_diff) alarms_client = self.ceilometer().alarms alarms_client.update(**self.cfn_to_ceilometer(self.stack, kwargs)) diff --git a/heat/tests/test_ceilometer_alarm.py b/heat/tests/test_ceilometer_alarm.py index 5518516de..cbfba372c 100644 --- a/heat/tests/test_ceilometer_alarm.py +++ b/heat/tests/test_ceilometer_alarm.py @@ -161,15 +161,31 @@ class CeilometerAlarmTest(HeatTestCase): al['ok_actions'] = None al['repeat_actions'] = True al['enabled'] = True - al['evaluation_periods'] = 1 - al['period'] = 60 - al['threshold'] = 50 - if 'matching_metadata' in al: - al['matching_metadata'] = dict( - ('metadata.metering.%s' % k, v) - for k, v in al['matching_metadata'].items()) + rule = dict( + period=60, + evaluation_periods=1, + threshold=50) + for field in ['period', 'evaluation_periods', 'threshold']: + del al[field] + for field in ['statistic', 'comparison_operator', 'meter_name']: + rule[field] = al[field] + del al[field] + if 'query' in al and al['query']: + query = al['query'] else: - al['matching_metadata'] = {} + query = [] + if 'query' in al: + del al['query'] + if 'matching_metadata' in al and al['matching_metadata']: + for k, v in al['matching_metadata'].items(): + key = 'metadata.metering.' + k + query.append(dict(field=key, op='eq', value=v)) + if 'matching_metadata' in al: + del al['matching_metadata'] + if query: + rule['query'] = query + al['threshold_rule'] = rule + al['type'] = 'threshold' self.m.StubOutWithMock(self.fa.alarms, 'create') self.fa.alarms.create(**al).AndReturn(FakeCeilometerAlarm()) return stack @@ -184,15 +200,30 @@ class CeilometerAlarmTest(HeatTestCase): properties = t['Resources']['MEMAlarmHigh']['Properties'] properties['alarm_actions'] = ['signal_handler'] properties['matching_metadata'] = {'a': 'v'} + properties['query'] = [dict(field='b', op='eq', value='w')] self.stack = self.create_stack(template=json.dumps(t)) self.m.StubOutWithMock(self.fa.alarms, 'update') schema = schemata(alarm.CeilometerAlarm.properties_schema) + exns = ['period', 'evaluation_periods', 'threshold', + 'statistic', 'comparison_operator', 'meter_name', + 'matching_metadata', 'query'] al2 = dict((k, mox.IgnoreArg()) - for k, s in schema.items() if s.update_allowed) + for k, s in schema.items() + if s.update_allowed and k not in exns) al2['alarm_id'] = mox.IgnoreArg() - del al2['enabled'] - del al2['repeat_actions'] + al2['type'] = 'threshold' + al2['threshold_rule'] = dict( + meter_name=properties['meter_name'], + period=90, + evaluation_periods=2, + threshold=39, + statistic='max', + comparison_operator='lt', + query=[ + dict(field='c', op='ne', value='z'), + dict(field='metadata.metering.x', op='eq', value='y') + ]) self.fa.alarms.update(**al2).AndReturn(None) self.m.ReplayAll() @@ -212,6 +243,8 @@ class CeilometerAlarmTest(HeatTestCase): 'insufficient_data_actions': [], 'alarm_actions': [], 'ok_actions': ['signal_handler'], + 'matching_metadata': {'x': 'y'}, + 'query': [dict(field='c', op='ne', value='z')] }) snippet = rsrc_defn.ResourceDefinition(rsrc.name, rsrc.type(),