Merge "Adds alarm time constraint support to ceilometer CLI"

This commit is contained in:
Jenkins
2014-03-26 14:32:20 +00:00
committed by Gerrit Code Review
6 changed files with 249 additions and 7 deletions

View File

@@ -154,6 +154,27 @@ def args_array_to_dict(kwargs, key_to_convert):
return kwargs
def args_array_to_list_of_dicts(kwargs, key_to_convert):
"""Converts ['a=1;b=2','c=3;d=4'] to [{a:1,b:2},{c:3,d:4}]
"""
values_to_convert = kwargs.get(key_to_convert)
if values_to_convert:
try:
kwargs[key_to_convert] = []
for lst in values_to_convert:
pairs = lst.split(";")
dct = dict()
for pair in pairs:
kv = pair.split("=", 1)
dct[kv[0]] = kv[1].strip(" \"'") # strip spaces and quotes
kwargs[key_to_convert].append(dct)
except Exception:
raise exc.CommandError(
'%s must be a list of key1=value1;key2=value2;... not "%s"' % (
key_to_convert, values_to_convert))
return kwargs
def key_with_slash_to_nested_dict(kwargs):
nested_kwargs = {}
for k in list(kwargs):

View File

@@ -14,6 +14,7 @@
# under the License.
import itertools
import mock
import six
import sys
@@ -117,6 +118,33 @@ class UtilsTest(test_utils.BaseTestCase):
'other': 'value'
})
def test_args_array_to_list_of_dicts(self):
starts = ['0 11 * * *', '"0 11 * * *"', '\'0 11 * * *\'']
timezones = [None, 'US/Eastern', '"US/Eastern"', '\'US/Eastern\'']
descs = [None, 'de sc', '"de sc"', '\'de sc\'']
for start, tz, desc in itertools.product(starts, timezones, descs):
my_args = {
'time_constraints': ['name=const1;start=%s;duration=1'
% start],
'other': 'value'
}
expected = {
'time_constraints': [dict(name='const1',
start='0 11 * * *',
duration='1')],
'other': 'value'
}
if tz:
my_args['time_constraints'][0] += ';timezone=%s' % tz
expected['time_constraints'][0]['timezone'] = 'US/Eastern'
if desc:
my_args['time_constraints'][0] += ';description=%s' % desc
expected['time_constraints'][0]['description'] = 'de sc'
cleaned = utils.args_array_to_list_of_dicts(my_args,
'time_constraints')
self.assertEqual(expected, cleaned)
def test_key_with_slash_to_nested_dict(self):
my_args = {
'combination_rule/alarm_ids': ['id1', 'id2'],

View File

@@ -40,6 +40,16 @@ AN_ALARM = {u'alarm_actions': [u'http://site:8000/alarm'],
u'threshold': 200.0,
u'comparison_operator': 'gt',
},
u'time_constraints': [{u'name': u'cons1',
u'description': u'desc1',
u'start': u'0 11 * * *',
u'duration': 300,
u'timezone': u''},
{u'name': u'cons2',
u'description': u'desc2',
u'start': u'0 23 * * *',
u'duration': 600,
u'timezone': ''}],
u'timestamp': u'2013-05-09T13:41:23.085000',
u'enabled': True,
u'alarm_id': u'alarm-id',
@@ -54,6 +64,8 @@ CREATE_ALARM = copy.deepcopy(AN_ALARM)
del CREATE_ALARM['timestamp']
del CREATE_ALARM['state_timestamp']
del CREATE_ALARM['alarm_id']
CREATE_ALARM_WITHOUT_TC = copy.deepcopy(CREATE_ALARM)
del CREATE_ALARM_WITHOUT_TC['time_constraints']
DELTA_ALARM = {u'alarm_actions': ['url1', 'url2']}
DELTA_ALARM_RULE = {u'comparison_operator': u'lt',
u'threshold': 42.1,
@@ -61,11 +73,21 @@ DELTA_ALARM_RULE = {u'comparison_operator': u'lt',
u'query': [{u'field': u'key_name',
u'op': u'eq',
u'value': u'key_value'}]}
DELTA_ALARM_TC = [{u'name': u'cons1',
u'duration': 500}]
DELTA_ALARM['time_constraints'] = DELTA_ALARM_TC
UPDATED_ALARM = copy.deepcopy(AN_ALARM)
UPDATED_ALARM.update(DELTA_ALARM)
UPDATED_ALARM['threshold_rule'].update(DELTA_ALARM_RULE)
DELTA_ALARM['remove_time_constraints'] = 'cons2'
UPDATED_ALARM['time_constraints'] = [{u'name': u'cons1',
u'description': u'desc1',
u'start': u'0 11 * * *',
u'duration': 500,
u'timezone': u''}]
DELTA_ALARM['threshold_rule'] = DELTA_ALARM_RULE
UPDATE_ALARM = copy.deepcopy(UPDATED_ALARM)
UPDATE_ALARM['remove_time_constraints'] = 'cons2'
del UPDATE_ALARM['user_id']
del UPDATE_ALARM['project_id']
del UPDATE_ALARM['name']
@@ -101,6 +123,9 @@ DELTA_LEGACY_ALARM = {u'alarm_actions': ['url1', 'url2'],
u'comparison_operator': u'lt',
u'meter_name': u'foobar',
u'threshold': 42.1}
DELTA_LEGACY_ALARM['time_constraints'] = [{u'name': u'cons1',
u'duration': 500}]
DELTA_LEGACY_ALARM['remove_time_constraints'] = 'cons2'
UPDATED_LEGACY_ALARM = copy.deepcopy(AN_LEGACY_ALARM)
UPDATED_LEGACY_ALARM.update(DELTA_LEGACY_ALARM)
UPDATE_LEGACY_ALARM = copy.deepcopy(UPDATED_LEGACY_ALARM)
@@ -348,7 +373,7 @@ class AlarmLegacyManagerTest(testtools.TestCase):
def test_create(self):
alarm = self.mgr.create(**CREATE_LEGACY_ALARM)
expect = [
('POST', '/v2/alarms', {}, CREATE_ALARM),
('POST', '/v2/alarms', {}, CREATE_ALARM_WITHOUT_TC),
]
self.assertEqual(self.api.calls, expect)
self.assertTrue(alarm)
@@ -360,7 +385,7 @@ class AlarmLegacyManagerTest(testtools.TestCase):
del create['meter_name']
alarm = self.mgr.create(**create)
expect = [
('POST', '/v2/alarms', {}, CREATE_ALARM),
('POST', '/v2/alarms', {}, CREATE_ALARM_WITHOUT_TC),
]
self.assertEqual(self.api.calls, expect)
self.assertTrue(alarm)
@@ -392,3 +417,41 @@ class AlarmLegacyManagerTest(testtools.TestCase):
self.assertEqual(alarm.alarm_id, 'alarm-id')
for (key, value) in six.iteritems(UPDATED_ALARM):
self.assertEqual(getattr(alarm, key), value)
class AlarmTimeConstraintTest(testtools.TestCase):
def setUp(self):
super(AlarmTimeConstraintTest, self).setUp()
self.api = utils.FakeAPI(fixtures)
self.mgr = alarms.AlarmManager(self.api)
def test_add_new(self):
new_constraint = dict(name='cons3',
start='0 0 * * *',
duration=500)
kwargs = dict(time_constraints=[new_constraint])
self.mgr.update(alarm_id='alarm-id', **kwargs)
actual = self.api.calls[1][3]['time_constraints']
expected = AN_ALARM[u'time_constraints'] + [new_constraint]
self.assertEqual(expected, actual)
def test_update_existing(self):
updated_constraint = dict(name='cons2',
duration=500)
kwargs = dict(time_constraints=[updated_constraint])
self.mgr.update(alarm_id='alarm-id', **kwargs)
actual = self.api.calls[1][3]['time_constraints']
expected = [AN_ALARM[u'time_constraints'][0], dict(name='cons2',
description='desc2',
start='0 23 * * *',
duration=500,
timezone='')]
self.assertEqual(expected, actual)
def test_remove(self):
kwargs = dict(remove_time_constraints=['cons2'])
self.mgr.update(alarm_id='alarm-id', **kwargs)
actual = self.api.calls[1][3]['time_constraints']
expected = [AN_ALARM[u'time_constraints'][0]]
self.assertEqual(expected, actual)

View File

@@ -163,6 +163,16 @@ class ShellAlarmCommandTest(utils.BaseTestCase):
"value": "INSTANCE_ID",
"op": "eq"}],
"comparison_operator": "gt"},
"time_constraints": [{"name": "cons1",
"description": "desc1",
"start": "0 11 * * *",
"duration": 300,
"timezone": ""},
{"name": "cons2",
"description": "desc2",
"start": "0 23 * * *",
"duration": 600,
"timezone": ""}],
"alarm_id": ALARM_ID,
"state": "insufficient data",
"insufficient_data_actions": [],
@@ -281,6 +291,36 @@ class ShellAlarmCommandTest(utils.BaseTestCase):
sys.stdout.close()
sys.stdout = orig
def test_alarm_create_time_constraints(self):
shell = base_shell.CeilometerShell()
argv = ['alarm-threshold-create',
'--name', 'cpu_high',
'--meter-name', 'cpu_util',
'--threshold', '70.0',
'--time-constraint',
'name=cons1;start="0 11 * * *";duration=300',
'--time-constraint',
'name=cons2;start="0 23 * * *";duration=600',
]
_, args = shell.parse_args(argv)
orig = sys.stdout
sys.stdout = six.StringIO()
alarm = alarms.Alarm(mock.Mock(), self.ALARM)
self.cc.alarms.create.return_value = alarm
try:
ceilometer_shell.do_alarm_threshold_create(self.cc, args)
_, kwargs = self.cc.alarms.create.call_args
time_constraints = [dict(name='cons1', start='0 11 * * *',
duration='300'),
dict(name='cons2', start='0 23 * * *',
duration='600')]
self.assertEqual(time_constraints, kwargs['time_constraints'])
finally:
sys.stdout.close()
sys.stdout = orig
class ShellSampleListCommandTest(utils.BaseTestCase):

View File

@@ -36,7 +36,8 @@ UPDATABLE_ATTRIBUTES = [
'threshold_rule',
'combination_rule',
]
CREATION_ATTRIBUTES = UPDATABLE_ATTRIBUTES + ['project_id', 'user_id']
CREATION_ATTRIBUTES = UPDATABLE_ATTRIBUTES + ['project_id', 'user_id',
'time_constraints']
class Alarm(base.Resource):
@@ -111,6 +112,26 @@ class AlarmManager(base.Manager):
del kwargs['matching_metadata']
kwargs['threshold_rule']['query'] = query
@staticmethod
def _merge_time_constraints(existing_tcs, kwargs):
new_tcs = kwargs.get('time_constraints', [])
if not existing_tcs:
updated_tcs = new_tcs
else:
updated_tcs = [dict(tc) for tc in existing_tcs]
for tc in new_tcs:
for i, old_tc in enumerate(updated_tcs):
if old_tc['name'] == tc['name']: # if names match, merge
utils.merge_nested_dict(updated_tcs[i], tc)
break
else:
updated_tcs.append(tc)
tcs_to_remove = kwargs.get('remove_time_constraints', [])
for tc in updated_tcs:
if tc['name'] in tcs_to_remove:
updated_tcs.remove(tc)
return updated_tcs
def create(self, **kwargs):
self._compat_legacy_alarm_kwargs(kwargs, create=True)
new = dict((key, value) for (key, value) in kwargs.items()
@@ -120,6 +141,8 @@ class AlarmManager(base.Manager):
def update(self, alarm_id, **kwargs):
self._compat_legacy_alarm_kwargs(kwargs)
updated = self.get(alarm_id).to_dict()
updated['time_constraints'] = self._merge_time_constraints(
updated.get('time_constraints', []), kwargs)
kwargs = dict((k, v) for k, v in kwargs.items()
if k in updated and k in UPDATABLE_ATTRIBUTES)
utils.merge_nested_dict(updated, kwargs, depth=1)

View File

@@ -182,6 +182,24 @@ def alarm_rule_formatter(alarm):
return _display_rule(alarm.type, alarm.rule)
def _display_time_constraints(time_constraints):
if time_constraints:
return ', '.join('%(name)s at %(start)s %(timezone)s for %(duration)ss'
% {
'name': tc['name'],
'start': tc['start'],
'duration': tc['duration'],
'timezone': tc.get('timezone', '')
}
for tc in time_constraints)
else:
return 'None'
def time_constraints_formatter(alarm):
return _display_time_constraints(alarm.time_constraints)
def _infer_type(detail):
if 'type' in detail:
return detail['type']
@@ -205,6 +223,10 @@ def alarm_change_detail_formatter(change):
detail[k]))
else:
fields.append('%s: %s' % (k, detail[k]))
if 'time_constraints' in detail:
fields.append('time_constraints: %s' %
_display_time_constraints(
detail['time_constraints']))
elif change.type == 'rule change':
for k, v in six.iteritems(detail):
if k == 'rule':
@@ -224,11 +246,13 @@ def do_alarm_list(cc, args={}):
# omit action initially to keep output width sane
# (can switch over to vertical formatting when available from CLIFF)
field_labels = ['Alarm ID', 'Name', 'State', 'Enabled', 'Continuous',
'Alarm condition']
'Alarm condition', 'Time constraints']
fields = ['alarm_id', 'name', 'state', 'enabled', 'repeat_actions',
'rule']
utils.print_list(alarms, fields, field_labels,
formatters={'rule': alarm_rule_formatter}, sortby=0)
'rule', 'time_constraints']
utils.print_list(
alarms, fields, field_labels,
formatters={'rule': alarm_rule_formatter,
'time_constraints': time_constraints_formatter}, sortby=0)
def alarm_query_formater(alarm):
@@ -239,6 +263,17 @@ def alarm_query_formater(alarm):
return r' AND\n'.join(qs)
def alarm_time_constraints_formatter(alarm):
time_constraints = []
for tc in alarm.time_constraints:
lines = []
for k in ['name', 'description', 'start', 'duration', 'timezone']:
if k in tc and tc[k]:
lines.append(r'%s: %s' % (k, tc[k]))
time_constraints.append('{' + r',\n '.join(lines) + '}')
return '[' + r',\n '.join(time_constraints) + ']'
def _display_alarm(alarm):
fields = ['name', 'description', 'type',
'state', 'enabled', 'alarm_id', 'user_id', 'project_id',
@@ -248,6 +283,8 @@ def _display_alarm(alarm):
data.update(alarm.rule)
if alarm.type == 'threshold':
data['query'] = alarm_query_formater(alarm)
if alarm.time_constraints:
data['time_constraints'] = alarm_time_constraints_formatter(alarm)
utils.print_dict(data, wrap=72)
@@ -293,6 +330,18 @@ def common_alarm_arguments(create=False):
metavar='<Webhook URL>', action='append', default=None,
help=('URL to invoke when state transitions to '
'insufficient_data. May be used multiple times.'))
@utils.arg('--time-constraint', dest='time_constraints',
metavar='<Time Constraint>', action='append',
default=None,
help=('Only evaluate the alarm if the time at evaluation '
'is within this time constraint. Start point(s) of '
'the constraint are specified with a cron expression '
', whereas its duration is given in seconds. '
'Can be specified multiple times for multiple '
'time constraints, format is: '
'name=<CONSTRAINT_NAME>;start=<CRON>;'
'duration=<SECONDS>;[description=<DESCRIPTION>;'
'[timezone=<IANA Timezone>]]'))
@functools.wraps(func)
def _wrapped(*args, **kwargs):
return func(*args, **kwargs)
@@ -326,6 +375,7 @@ def common_alarm_arguments(create=False):
def do_alarm_create(cc, args={}):
'''Create a new alarm (Deprecated). Use alarm-threshold-create instead.'''
fields = dict(filter(lambda x: not (x[1] is None), vars(args).items()))
fields = utils.args_array_to_list_of_dicts(fields, "time_constraints")
fields = utils.args_array_to_dict(fields, "matching_metadata")
alarm = cc.alarms.create(**fields)
_display_alarm(alarm)
@@ -363,6 +413,7 @@ def do_alarm_create(cc, args={}):
def do_alarm_threshold_create(cc, args={}):
'''Create a new alarm based on computed statistics.'''
fields = dict(filter(lambda x: not (x[1] is None), vars(args).items()))
fields = utils.args_array_to_list_of_dicts(fields, 'time_constraints')
fields = utils.key_with_slash_to_nested_dict(fields)
fields['type'] = 'threshold'
if 'query' in fields['threshold_rule']:
@@ -388,6 +439,7 @@ def do_alarm_threshold_create(cc, args={}):
def do_alarm_combination_create(cc, args={}):
'''Create a new alarm based on state of other alarms.'''
fields = dict(filter(lambda x: not (x[1] is None), vars(args).items()))
fields = utils.args_array_to_list_of_dicts(fields, 'time_constraints')
fields = utils.key_with_slash_to_nested_dict(fields)
fields['type'] = 'combination'
alarm = cc.alarms.create(**fields)
@@ -397,6 +449,10 @@ def do_alarm_combination_create(cc, args={}):
@utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>', required=True,
help='ID of the alarm to update.')
@common_alarm_arguments()
@utils.arg('--remove-time-constraint', action='append',
metavar='<Constraint names>',
dest='remove_time_constraints',
help='Name or list of names of the time constraints to remove.')
@utils.arg('--period', type=int, metavar='<PERIOD>',
help='Length of each period (seconds) to evaluate over.')
@utils.arg('--evaluation-periods', type=int, metavar='<COUNT>',
@@ -421,6 +477,7 @@ def do_alarm_combination_create(cc, args={}):
def do_alarm_update(cc, args={}):
'''Update an existing alarm (Deprecated).'''
fields = dict(filter(lambda x: not (x[1] is None), vars(args).items()))
fields = utils.args_array_to_list_of_dicts(fields, "time_constraints")
fields = utils.args_array_to_dict(fields, "matching_metadata")
fields.pop('alarm_id')
try:
@@ -433,6 +490,10 @@ def do_alarm_update(cc, args={}):
@utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>', required=True,
help='ID of the alarm to update.')
@common_alarm_arguments()
@utils.arg('--remove-time-constraint', action='append',
metavar='<Constraint names>',
dest='remove_time_constraints',
help='Name or list of names of the time constraints to remove.')
@utils.arg('-m', '--meter-name', metavar='<METRIC>',
dest='threshold_rule/meter_name',
help='Metric to evaluate against.')
@@ -463,6 +524,7 @@ def do_alarm_update(cc, args={}):
def do_alarm_threshold_update(cc, args={}):
'''Update an existing alarm based on computed statistics.'''
fields = dict(filter(lambda x: not (x[1] is None), vars(args).items()))
fields = utils.args_array_to_list_of_dicts(fields, 'time_constraints')
fields = utils.key_with_slash_to_nested_dict(fields)
fields.pop('alarm_id')
fields['type'] = 'threshold'
@@ -479,6 +541,10 @@ def do_alarm_threshold_update(cc, args={}):
@utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>', required=True,
help='ID of the alarm to update.')
@common_alarm_arguments()
@utils.arg('--remove-time-constraint', action='append',
metavar='<Constraint names>',
dest='remove_time_constraints',
help='Name or list of names of the time constraints to remove.')
@utils.arg('--alarm_ids', action='append', metavar='<ALARM IDS>',
dest='combination_rule/alarm_ids',
help='List of alarm id.')
@@ -493,6 +559,7 @@ def do_alarm_threshold_update(cc, args={}):
def do_alarm_combination_update(cc, args={}):
'''Update an existing alarm based on state of other alarms.'''
fields = dict(filter(lambda x: not (x[1] is None), vars(args).items()))
fields = utils.args_array_to_list_of_dicts(fields, 'time_constraints')
fields = utils.key_with_slash_to_nested_dict(fields)
fields.pop('alarm_id')
fields['type'] = 'combination'