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:
parent
f16471a97f
commit
0c50b7d23f
@ -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))
|
||||
|
@ -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(),
|
||||
|
Loading…
Reference in New Issue
Block a user