deb-heat/heat/tests/test_ceilometer_alarm.py
Mike Spreitzer 0c50b7d23f 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
2014-10-14 02:05:41 +00:00

530 lines
18 KiB
Python

#
# 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
from ceilometerclient import exc as ceilometerclient_exc
import mox
from oslo.config import cfg
import six
from heat.common import exception
from heat.common import template_format
from heat.engine.clients.os import ceilometer
from heat.engine import parser
from heat.engine.properties import schemata
from heat.engine import resource
from heat.engine.resources.ceilometer import alarm
from heat.engine import rsrc_defn
from heat.engine import scheduler
from heat.tests.common import HeatTestCase
from heat.tests import generic_resource
from heat.tests import utils
alarm_template = '''
{
"AWSTemplateFormatVersion" : "2010-09-09",
"Description" : "Alarm Test",
"Parameters" : {},
"Resources" : {
"MEMAlarmHigh": {
"Type": "OS::Ceilometer::Alarm",
"Properties": {
"description": "Scale-up if MEM > 50% for 1 minute",
"meter_name": "MemoryUtilization",
"statistic": "avg",
"period": "60",
"evaluation_periods": "1",
"threshold": "50",
"alarm_actions": [],
"matching_metadata": {},
"comparison_operator": "gt"
}
},
"signal_handler" : {
"Type" : "SignalResourceType"
}
}
}
'''
not_string_alarm_template = '''
{
"AWSTemplateFormatVersion" : "2010-09-09",
"Description" : "Alarm Test",
"Parameters" : {},
"Resources" : {
"MEMAlarmHigh": {
"Type": "OS::Ceilometer::Alarm",
"Properties": {
"description": "Scale-up if MEM > 50% for 1 minute",
"meter_name": "MemoryUtilization",
"statistic": "avg",
"period": 60,
"evaluation_periods": 1,
"threshold": 50,
"alarm_actions": [],
"matching_metadata": {},
"comparison_operator": "gt"
}
},
"signal_handler" : {
"Type" : "SignalResourceType"
}
}
}
'''
combination_alarm_template = '''
{
"AWSTemplateFormatVersion" : "2010-09-09",
"Description" : "Combination Alarm Test",
"Resources" : {
"CombinAlarm": {
"Type": "OS::Ceilometer::CombinationAlarm",
"Properties": {
"description": "Do stuff in combination",
"alarm_ids": ["alarm1", "alarm2"],
"operator": "and",
"alarm_actions": [],
}
}
}
}
'''
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 CeilometerAlarmTest(HeatTestCase):
def setUp(self):
super(CeilometerAlarmTest, self).setUp()
resource._register_class('SignalResourceType',
generic_resource.SignalResource)
cfg.CONF.set_default('heat_waitcondition_server_url',
'http://server.test:8000/v1/waitcondition')
self.stub_keystoneclient()
self.fa = FakeCeilometerClient()
def create_stack(self, template=None):
if template is None:
template = alarm_template
temp = template_format.parse(template)
template = parser.Template(temp)
ctx = utils.dummy_context()
ctx.tenant_id = 'test_tenant'
stack = parser.Stack(ctx, utils.random_name(), template,
disable_rollback=True)
stack.store()
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['name'] = mox.IgnoreArg()
al['alarm_actions'] = mox.IgnoreArg()
al['insufficient_data_actions'] = None
al['ok_actions'] = None
al['repeat_actions'] = True
al['enabled'] = True
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:
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
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'}
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 and k not in exns)
al2['alarm_id'] = mox.IgnoreArg()
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()
self.stack.create()
rsrc = self.stack['MEMAlarmHigh']
props = copy.copy(rsrc.properties.data)
props.update({
'comparison_operator': 'lt',
'description': 'fruity',
'evaluation_periods': '2',
'period': '90',
'enabled': True,
'repeat_actions': True,
'statistic': 'max',
'threshold': '39',
'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(),
props)
scheduler.TaskRunner(rsrc.update, snippet)()
self.m.VerifyAll()
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']
props = copy.copy(rsrc.properties.data)
props['meter_name'] = 'temp'
snippet = rsrc_defn.ResourceDefinition(rsrc.name,
rsrc.type(),
props)
updater = scheduler.TaskRunner(rsrc.update, snippet)
self.assertRaises(resource.UpdateReplace, updater)
self.m.VerifyAll()
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.assertEqual((rsrc.SUSPEND, rsrc.COMPLETE), rsrc.state)
scheduler.TaskRunner(rsrc.resume)()
self.assertEqual((rsrc.RESUME, rsrc.COMPLETE), rsrc.state)
self.m.VerifyAll()
def test_mem_alarm_high_correct_int_parameters(self):
self.stack = self.create_stack(not_string_alarm_template)
self.m.ReplayAll()
self.stack.create()
rsrc = self.stack['MEMAlarmHigh']
self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state)
self.assertIsNone(rsrc.validate())
self.assertIsInstance(rsrc.properties['evaluation_periods'], int)
self.assertIsInstance(rsrc.properties['period'], int)
self.assertIsInstance(rsrc.properties['threshold'], int)
self.m.VerifyAll()
def test_no_matching_metadata(self):
"""Make sure that we can pass in an empty matching_metadata."""
t = template_format.parse(alarm_template)
properties = t['Resources']['MEMAlarmHigh']['Properties']
properties['alarm_actions'] = ['signal_handler']
del properties['matching_metadata']
self.stack = self.create_stack(template=json.dumps(t))
self.m.ReplayAll()
self.stack.create()
rsrc = self.stack['MEMAlarmHigh']
self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state)
self.assertIsNone(rsrc.validate())
self.m.VerifyAll()
def test_mem_alarm_high_not_correct_string_parameters(self):
snippet = template_format.parse(not_string_alarm_template)
for p in ('period', 'evaluation_periods'):
snippet['Resources']['MEMAlarmHigh']['Properties'][p] = '60a'
stack = utils.parse_stack(snippet)
resource_defns = stack.t.resource_definitions(stack)
rsrc = alarm.CeilometerAlarm(
'MEMAlarmHigh', resource_defns['MEMAlarmHigh'], stack)
error = self.assertRaises(exception.StackValidationFailed,
rsrc.validate)
self.assertEqual(
"Property error : MEMAlarmHigh: %s Value '60a' is not an "
"integer" % p, six.text_type(error))
def test_mem_alarm_high_not_integer_parameters(self):
snippet = template_format.parse(not_string_alarm_template)
for p in ('period', 'evaluation_periods'):
snippet['Resources']['MEMAlarmHigh']['Properties'][p] = [60]
stack = utils.parse_stack(snippet)
resource_defns = stack.t.resource_definitions(stack)
rsrc = alarm.CeilometerAlarm(
'MEMAlarmHigh', resource_defns['MEMAlarmHigh'], stack)
error = self.assertRaises(exception.StackValidationFailed,
rsrc.validate)
self.assertEqual(
"Property error : MEMAlarmHigh: %s int() argument must be "
"a string or a number, not 'list'" % p, six.text_type(error))
def test_mem_alarm_high_check_not_required_parameters(self):
snippet = template_format.parse(not_string_alarm_template)
snippet['Resources']['MEMAlarmHigh']['Properties'].pop('meter_name')
stack = utils.parse_stack(snippet)
resource_defns = stack.t.resource_definitions(stack)
rsrc = alarm.CeilometerAlarm(
'MEMAlarmHigh', resource_defns['MEMAlarmHigh'], stack)
error = self.assertRaises(exception.StackValidationFailed,
rsrc.validate)
self.assertEqual(
"Property error : MEMAlarmHigh: Property meter_name not assigned",
six.text_type(error))
for p in ('period', 'evaluation_periods', 'statistic',
'comparison_operator'):
snippet = template_format.parse(not_string_alarm_template)
snippet['Resources']['MEMAlarmHigh']['Properties'].pop(p)
stack = utils.parse_stack(snippet)
resource_defns = stack.t.resource_definitions(stack)
rsrc = alarm.CeilometerAlarm(
'MEMAlarmHigh', resource_defns['MEMAlarmHigh'], stack)
self.assertIsNone(rsrc.validate())
def test_delete_alarm_not_found(self):
t = template_format.parse(alarm_template)
self.stack = self.create_stack(template=json.dumps(t))
self.m.StubOutWithMock(self.fa.alarms, 'delete')
self.fa.alarms.delete('foo').AndRaise(
ceilometerclient_exc.HTTPNotFound())
self.m.ReplayAll()
self.stack.create()
rsrc = self.stack['MEMAlarmHigh']
scheduler.TaskRunner(rsrc.delete)()
self.assertEqual((rsrc.DELETE, rsrc.COMPLETE), rsrc.state)
self.m.VerifyAll()
class CombinationAlarmTest(HeatTestCase):
def setUp(self):
super(CombinationAlarmTest, self).setUp()
self.fc = FakeCeilometerClient()
self.m.StubOutWithMock(ceilometer.CeilometerClientPlugin, '_create')
def create_alarm(self):
ceilometer.CeilometerClientPlugin._create().AndReturn(
self.fc)
self.m.StubOutWithMock(self.fc.alarms, 'create')
self.fc.alarms.create(
alarm_actions=[],
description=u'Do stuff in combination',
enabled=True,
insufficient_data_actions=None,
ok_actions=None,
name=mox.IgnoreArg(), type='combination',
combination_rule={'alarm_ids': [u'alarm1', u'alarm2'],
'operator': u'and', 'repeat_actions': True}
).AndReturn(FakeCeilometerAlarm())
snippet = template_format.parse(combination_alarm_template)
stack = utils.parse_stack(snippet)
resource_defns = stack.t.resource_definitions(stack)
return alarm.CombinationAlarm(
'CombinAlarm', resource_defns['CombinAlarm'], stack)
def test_create(self):
rsrc = self.create_alarm()
self.m.ReplayAll()
scheduler.TaskRunner(rsrc.create)()
self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state)
self.assertEqual('foo', rsrc.resource_id)
self.m.VerifyAll()
def test_invalid_alarm_list(self):
snippet = template_format.parse(combination_alarm_template)
snippet['Resources']['CombinAlarm']['Properties']['alarm_ids'] = []
stack = utils.parse_stack(snippet)
resource_defns = stack.t.resource_definitions(stack)
rsrc = alarm.CombinationAlarm(
'CombinAlarm', resource_defns['CombinAlarm'], stack)
error = self.assertRaises(exception.StackValidationFailed,
rsrc.validate)
self.assertEqual(
"Property error : CombinAlarm: alarm_ids length (0) is out of "
"range (min: 1, max: None)", six.text_type(error))
def test_update(self):
rsrc = self.create_alarm()
self.m.StubOutWithMock(self.fc.alarms, 'update')
self.fc.alarms.update(
alarm_id='foo',
combination_rule={'alarm_ids': [u'alarm1', u'alarm3']})
self.m.ReplayAll()
scheduler.TaskRunner(rsrc.create)()
update_template = copy.deepcopy(rsrc.t)
update_template['Properties']['alarm_ids'] = ['alarm1', 'alarm3']
scheduler.TaskRunner(rsrc.update, update_template)()
self.assertEqual((rsrc.UPDATE, rsrc.COMPLETE), rsrc.state)
self.m.VerifyAll()
def test_suspend(self):
rsrc = self.create_alarm()
self.m.StubOutWithMock(self.fc.alarms, 'update')
self.fc.alarms.update(alarm_id='foo', enabled=False)
self.m.ReplayAll()
scheduler.TaskRunner(rsrc.create)()
scheduler.TaskRunner(rsrc.suspend)()
self.assertEqual((rsrc.SUSPEND, rsrc.COMPLETE), rsrc.state)
self.m.VerifyAll()
def test_resume(self):
rsrc = self.create_alarm()
self.m.StubOutWithMock(self.fc.alarms, 'update')
self.fc.alarms.update(alarm_id='foo', enabled=True)
self.m.ReplayAll()
scheduler.TaskRunner(rsrc.create)()
rsrc.state_set(rsrc.SUSPEND, rsrc.COMPLETE)
scheduler.TaskRunner(rsrc.resume)()
self.assertEqual((rsrc.RESUME, rsrc.COMPLETE), rsrc.state)
self.m.VerifyAll()
def test_delete(self):
rsrc = self.create_alarm()
self.m.StubOutWithMock(self.fc.alarms, 'delete')
self.fc.alarms.delete('foo')
self.m.ReplayAll()
scheduler.TaskRunner(rsrc.create)()
scheduler.TaskRunner(rsrc.delete)()
self.assertEqual((rsrc.DELETE, rsrc.COMPLETE), rsrc.state)
self.m.VerifyAll()
def test_delete_not_found(self):
rsrc = self.create_alarm()
self.m.StubOutWithMock(self.fc.alarms, 'delete')
self.fc.alarms.delete('foo').AndRaise(
ceilometerclient_exc.HTTPNotFound())
self.m.ReplayAll()
scheduler.TaskRunner(rsrc.create)()
scheduler.TaskRunner(rsrc.delete)()
self.assertEqual((rsrc.DELETE, rsrc.COMPLETE), rsrc.state)
self.m.VerifyAll()