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
This commit is contained in:
Mike Spreitzer 2014-10-11 22:30:20 +00:00
parent f16471a97f
commit 0c50b7d23f
2 changed files with 114 additions and 23 deletions

View File

@ -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))

View File

@ -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(),