From ce01f564651bee07f91be8dc7f6dfca561216625 Mon Sep 17 00:00:00 2001 From: Mehdi Abaakouk Date: Tue, 24 Sep 2013 11:09:05 +0200 Subject: [PATCH 1/8] Improve the CM shell client alarm visualisation This change aim to get a better alarm representation in shell. In alarm-list: * it add a short sentence to describe the alarm rule * it remove project_id/user_id because it always show the same id for all alarm for end-user In alarm-show, it show alarm rule attributes as alarm properties instead of a unparsable json in the rule property. example of short sentence for column 'Alarm condition': * combinated states (AND) of 8babd6a2-c457-42d0-9eb5-cdfb3cb50203, d0e11a94-8f59-48a9-8f6d-b0d68aaac8d0 * cpu_util >= 50.0 during 1 x 60s Change-Id: If4df2dc08f9f4cb7796fd98308c7d62e311d1138 --- ceilometerclient/common/utils.py | 6 ++-- ceilometerclient/v2/shell.py | 48 ++++++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/ceilometerclient/common/utils.py b/ceilometerclient/common/utils.py index ea79dbf..eac860f 100644 --- a/ceilometerclient/common/utils.py +++ b/ceilometerclient/common/utils.py @@ -63,17 +63,19 @@ def print_dict(d, dict_property="Property", wrap=0): # convert dict to str to check length if isinstance(v, dict): v = str(v) - if wrap > 0: - v = textwrap.fill(str(v), wrap) # if value has a newline, add in multiple rows # e.g. fault with stacktrace if v and isinstance(v, basestring) and r'\n' in v: lines = v.strip().split(r'\n') col1 = k for line in lines: + if wrap > 0: + line = textwrap.fill(str(line), wrap) pt.add_row([col1, line]) col1 = '' else: + if wrap > 0: + v = textwrap.fill(str(v), wrap) pt.add_row([k, v]) print pt.get_string() diff --git a/ceilometerclient/v2/shell.py b/ceilometerclient/v2/shell.py index 05f7f96..8423bd4 100644 --- a/ceilometerclient/v2/shell.py +++ b/ceilometerclient/v2/shell.py @@ -28,6 +28,9 @@ ALARM_STATES = ['ok', 'alarm', 'insufficient_data'] ALARM_OPERATORS = ['lt', 'le', 'eq', 'ne', 'ge', 'gt'] ALARM_COMBINATION_OPERATORS = ['and', 'or'] STATISTICS = ['max', 'min', 'avg', 'sum', 'count'] +OPERATORS_STRING = dict(gt='>', ge='>=', + lt='<', le="<=", + eq='==', ne='!=') @utils.arg('-q', '--query', metavar='', @@ -128,6 +131,28 @@ def do_meter_list(cc, args={}): sortby=0) +def alarm_rule_formatter(alarm): + if alarm.type == 'threshold': + return ('%(meter_name)s %(comparison_operator)s ' + '%(threshold)s during %(evaluation_periods)s x %(period)ss' % + { + 'meter_name': alarm.rule['meter_name'], + 'threshold': alarm.rule['threshold'], + 'evaluation_periods': alarm.rule['evaluation_periods'], + 'period': alarm.rule['period'], + 'comparison_operator': OPERATORS_STRING.get( + alarm.rule['comparison_operator']) + }) + elif alarm.type == 'combination': + return ('combinated states (%(operator)s) of %(alarms)s' % { + 'operator': alarm.rule['operator'].upper(), + 'alarms': ", ".join(alarm.rule['alarm_ids'])}) + else: + # just dump all + return "\n".join(["%s: %s" % (f, v) + for f, v in alarm.rule.iteritems()]) + + @utils.arg('-q', '--query', metavar='', help='key[op]value; list.') def do_alarm_list(cc, args={}): @@ -135,20 +160,31 @@ def do_alarm_list(cc, args={}): alarms = cc.alarms.list(q=options.cli_to_array(args.query)) # omit action initially to keep output width sane # (can switch over to vertical formatting when available from CLIFF) - field_labels = ['Name', 'Description', 'State', 'Enabled', 'Continuous', - 'Alarm ID', 'User ID', 'Project ID'] - fields = ['name', 'description', 'state', 'enabled', 'repeat_actions', - 'alarm_id', 'user_id', 'project_id'] + field_labels = ['Alarm ID', 'Name', 'State', 'Enabled', 'Continuous', + 'Alarm condition'] + fields = ['alarm_id', 'name', 'state', 'enabled', 'repeat_actions', + 'rule'] utils.print_list(alarms, fields, field_labels, - sortby=0) + formatters={'rule': alarm_rule_formatter}, sortby=0) + + +def alarm_query_formater(alarm): + qs = [] + for q in alarm.rule['query']: + qs.append('%s %s %s' % ( + q['field'], OPERATORS_STRING.get(q['op']), q['value'])) + return r' AND\n'.join(qs) def _display_alarm(alarm): - fields = ['name', 'description', 'type', 'rule', + fields = ['name', 'description', 'type', 'state', 'enabled', 'alarm_id', 'user_id', 'project_id', 'alarm_actions', 'ok_actions', 'insufficient_data_actions', 'repeat_actions'] data = dict([(f, getattr(alarm, f, '')) for f in fields]) + data.update(alarm.rule) + if alarm.type == 'threshold': + data['query'] = alarm_query_formater(alarm) utils.print_dict(data, wrap=72) From b961738765976e77711d909eec1ecc402fa8a484 Mon Sep 17 00:00:00 2001 From: Cyril Roelandt Date: Wed, 25 Sep 2013 12:49:58 +0000 Subject: [PATCH 2/8] Help messages: specify which options are required Closes-Bug: #1223283 Change-Id: I080fa73bd45a1f9f442dbcdfa65fdc24e30521da --- ceilometerclient/common/utils.py | 7 +++++++ ceilometerclient/tests/test_utils.py | 19 +++++++++++++++++++ ceilometerclient/v2/shell.py | 26 ++++++++------------------ 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/ceilometerclient/common/utils.py b/ceilometerclient/common/utils.py index eac860f..ef76dcc 100644 --- a/ceilometerclient/common/utils.py +++ b/ceilometerclient/common/utils.py @@ -27,6 +27,13 @@ from ceilometerclient.openstack.common import importutils # Decorator for cli-args def arg(*args, **kwargs): def _decorator(func): + if 'help' in kwargs: + if 'default' in kwargs: + kwargs['help'] += " Defaults to %s." % kwargs['default'] + required = kwargs.get('required', False) + if required: + kwargs['help'] += " Required." + # Because of the sematics of decorator composition if we just append # to the options list positional options will appear to be backwards. func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs)) diff --git a/ceilometerclient/tests/test_utils.py b/ceilometerclient/tests/test_utils.py index c002dc6..0e209b0 100644 --- a/ceilometerclient/tests/test_utils.py +++ b/ceilometerclient/tests/test_utils.py @@ -74,3 +74,22 @@ class UtilsTest(test_utils.BaseTestCase): 'statictic': 'avg', 'comparison_operator': 'or'}, }) + + def test_arg(self): + @utils.arg(help="not_required_no_default.") + def not_required_no_default(): + pass + _, args = not_required_no_default.__dict__['arguments'][0] + self.assertEqual(args['help'], "not_required_no_default.") + + @utils.arg(required=True, help="required_no_default.") + def required_no_default(): + pass + _, args = required_no_default.__dict__['arguments'][0] + self.assertEqual(args['help'], "required_no_default. Required.") + + @utils.arg(default=42, help="not_required_default.") + def not_required_default(): + pass + _, args = not_required_default.__dict__['arguments'][0] + self.assertEqual(args['help'], "not_required_default. Defaults to 42.") diff --git a/ceilometerclient/v2/shell.py b/ceilometerclient/v2/shell.py index 8423bd4..bcd5657 100644 --- a/ceilometerclient/v2/shell.py +++ b/ceilometerclient/v2/shell.py @@ -35,7 +35,7 @@ OPERATORS_STRING = dict(gt='>', ge='>=', @utils.arg('-q', '--query', metavar='', help='key[op]value; list.') -@utils.arg('-m', '--meter', metavar='', +@utils.arg('-m', '--meter', metavar='', required=True, help='Name of meter to show samples for.') @utils.arg('-p', '--period', metavar='', help='Period in seconds over which to group samples.') @@ -44,8 +44,6 @@ def do_statistics(cc, args): fields = {'meter_name': args.meter, 'q': options.cli_to_array(args.query), 'period': args.period} - if args.meter is None: - raise exc.CommandError('Meter name not provided (-m )') try: statistics = cc.statistics.list(**fields) except exc.HTTPNotFound: @@ -62,14 +60,12 @@ def do_statistics(cc, args): @utils.arg('-q', '--query', metavar='', help='key[op]value; list.') -@utils.arg('-m', '--meter', metavar='', +@utils.arg('-m', '--meter', metavar='', required=True, help='Name of meter to show samples for.') def do_sample_list(cc, args): '''List the samples for this meters.''' fields = {'meter_name': args.meter, 'q': options.cli_to_array(args.query)} - if args.meter is None: - raise exc.CommandError('Meter name not provided (-m )') try: samples = cc.samples.list(**fields) except exc.HTTPNotFound: @@ -89,15 +85,15 @@ def do_sample_list(cc, args): @utils.arg('--user-id', metavar='', help='User to associate with sample ' '(only settable by admin users)') -@utils.arg('-r', '--resource-id', metavar='', +@utils.arg('-r', '--resource-id', metavar='', required=True, help='ID of the resource.') @utils.arg('-m', '--meter-name', metavar='', help='the meter name') -@utils.arg('--meter-type', metavar='', +@utils.arg('--meter-type', metavar='', required=True, help='the meter type') -@utils.arg('--meter-unit', metavar='', +@utils.arg('--meter-unit', metavar='', required=True, help='the meter unit') -@utils.arg('--sample-volume', metavar='', +@utils.arg('--sample-volume', metavar='', required=True, help='The sample volume') @utils.arg('--resource-metadata', metavar='', help='resource metadata') @@ -188,12 +184,10 @@ def _display_alarm(alarm): utils.print_dict(data, wrap=72) -@utils.arg('-a', '--alarm_id', metavar='', +@utils.arg('-a', '--alarm_id', metavar='', required=True, help='ID of the alarm to show.') def do_alarm_show(cc, args={}): '''Show an alarm.''' - if args.alarm_id is None: - raise exc.CommandError('Alarm ID not provided (-a )') try: alarm = cc.alarms.get(args.alarm_id) except exc.HTTPNotFound: @@ -416,8 +410,6 @@ def do_alarm_combination_update(cc, args={}): help='ID of the alarm to delete.') def do_alarm_delete(cc, args={}): '''Delete an alarm.''' - if args.alarm_id is None: - raise exc.CommandError('Alarm ID not provided (-a )') try: cc.alarms.delete(args.alarm_id) except exc.HTTPNotFound: @@ -462,12 +454,10 @@ def do_resource_list(cc, args={}): sortby=1) -@utils.arg('-r', '--resource_id', metavar='', +@utils.arg('-r', '--resource_id', metavar='', required=True, help='ID of the resource to show.') def do_resource_show(cc, args={}): '''Show the resource.''' - if args.resource_id is None: - raise exc.CommandError('Resource id not provided (-r )') try: resource = cc.resources.get(args.resource_id) except exc.HTTPNotFound: From 544e6217e58a9eaee67e98f27cb9385ee412ba79 Mon Sep 17 00:00:00 2001 From: Stefano Zilli Date: Fri, 13 Sep 2013 09:08:19 +0200 Subject: [PATCH 3/8] Added support to --os-cacert Closes-Bug: #1224343 Change-Id: Ib0549d4496c47900c81cc970b99bcff25cad0040 --- ceilometerclient/client.py | 6 +++++- ceilometerclient/common/http.py | 16 ++++++++-------- ceilometerclient/shell.py | 12 +++++++++--- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/ceilometerclient/client.py b/ceilometerclient/client.py index c65c214..e65c627 100644 --- a/ceilometerclient/client.py +++ b/ceilometerclient/client.py @@ -21,6 +21,7 @@ def _get_ksclient(**kwargs): * username: name of user * password: user's password * auth_url: endpoint to authenticate against + * cacert: path of CA TLS certificate * insecure: allow insecure SSL (no cert verification) * tenant_{name|id}: name or ID of tenant """ @@ -30,6 +31,7 @@ def _get_ksclient(**kwargs): tenant_name=kwargs.get('tenant_name'), auth_url=kwargs.get('auth_url'), region_name=kwargs.get('region_name'), + cacert=kwargs.get('cacert'), insecure=kwargs.get('insecure')) @@ -52,6 +54,7 @@ def get_client(api_version, **kwargs): * os_username: name of user * os_password: user's password * os_auth_url: endpoint to authenticate against + * os_cacert: path of CA TLS certificate * insecure: allow insecure SSL (no cert verification) * os_tenant_{name|id}: name or ID of tenant """ @@ -72,6 +75,7 @@ def get_client(api_version, **kwargs): 'region_name': kwargs.get('os_region_name'), 'service_type': kwargs.get('os_service_type'), 'endpoint_type': kwargs.get('os_endpoint_type'), + 'cacert': kwargs.get('os_cacert'), 'insecure': kwargs.get('insecure'), } _ksclient = _get_ksclient(**ks_kwargs) @@ -86,7 +90,7 @@ def get_client(api_version, **kwargs): 'token': token, 'insecure': kwargs.get('insecure'), 'timeout': kwargs.get('timeout'), - 'ca_file': kwargs.get('ca_file'), + 'cacert': kwargs.get('cacert'), 'cert_file': kwargs.get('cert_file'), 'key_file': kwargs.get('key_file'), } diff --git a/ceilometerclient/common/http.py b/ceilometerclient/common/http.py index a0e03f4..b53bb98 100644 --- a/ceilometerclient/common/http.py +++ b/ceilometerclient/common/http.py @@ -63,7 +63,7 @@ class HTTPClient(object): if parts.scheme == 'https': _class = VerifiedHTTPSConnection - _kwargs['ca_file'] = kwargs.get('ca_file', None) + _kwargs['ca_cert'] = kwargs.get('cacert', None) _kwargs['cert_file'] = kwargs.get('cert_file', None) _kwargs['key_file'] = kwargs.get('key_file', None) _kwargs['insecure'] = kwargs.get('insecure', False) @@ -93,7 +93,7 @@ class HTTPClient(object): conn_params_fmt = [ ('key_file', '--key %s'), ('cert_file', '--cert %s'), - ('ca_file', '--cacert %s'), + ('cacert', '--cacert %s'), ] for (key, fmt) in conn_params_fmt: value = self.connection_params[2].get(key) @@ -215,21 +215,21 @@ class VerifiedHTTPSConnection(httplib.HTTPSConnection): """ def __init__(self, host, port, key_file=None, cert_file=None, - ca_file=None, timeout=None, insecure=False): + cacert=None, timeout=None, insecure=False): httplib.HTTPSConnection.__init__(self, host, port, key_file=key_file, cert_file=cert_file) self.key_file = key_file self.cert_file = cert_file - if ca_file is not None: - self.ca_file = ca_file + if cacert is not None: + self.cacert = cacert else: - self.ca_file = self.get_system_ca_file() + self.cacert = self.get_system_ca_file() self.timeout = timeout self.insecure = insecure def connect(self): """Connect to a host on a given (SSL) port. - If ca_file is pointing somewhere, use it to check Server Certificate. + If cacert is pointing somewhere, use it to check Server Certificate. Redefined/copied and extended from httplib.py:1105 (Python 2.6.x). This is needed to pass cert_reqs=ssl.CERT_REQUIRED as parameter to @@ -245,7 +245,7 @@ class VerifiedHTTPSConnection(httplib.HTTPSConnection): if self.insecure is True: kwargs = {'cert_reqs': ssl.CERT_NONE} else: - kwargs = {'cert_reqs': ssl.CERT_REQUIRED, 'ca_certs': self.ca_file} + kwargs = {'cert_reqs': ssl.CERT_REQUIRED, 'ca_certs': self.cacert} if self.cert_file: kwargs['certfile'] = self.cert_file diff --git a/ceilometerclient/shell.py b/ceilometerclient/shell.py index 3ea531a..b6d3939 100644 --- a/ceilometerclient/shell.py +++ b/ceilometerclient/shell.py @@ -76,11 +76,17 @@ class CeilometerShell(object): ' This option is not necessary if your key is ' 'prepended to your cert file.') - parser.add_argument('--ca-file', - help='Path of CA SSL certificate(s) used to verify' - ' the remote server certificate. Without this ' + parser.add_argument('--os-cacert', + metavar='', + dest='os_cacert', + default=utils.env('OS_CACERT'), + help='Path of CA TLS certificate(s) used to verify' + 'the remote server\'s certificate. Without this ' 'option ceilometer looks for the default system ' 'CA certificates.') + parser.add_argument('--ca-file', + dest='os_cacert', + help='DEPRECATED! Use --os-cacert.') parser.add_argument('--timeout', default=600, From 66ef360c1480899bcdf6ad7af8f2d581b532c5e6 Mon Sep 17 00:00:00 2001 From: Mehdi Abaakouk Date: Thu, 26 Sep 2013 09:50:37 +0200 Subject: [PATCH 4/8] Allow to update an alarm partially The patch allow to only modify a part of an alarm instead of force to set the full alarm description. This permit to an application that have been written code around alarm with ceilometerclient 1.0.4 and ceilometer pre havana-3. To update alarm without any change with this and ceilometer >= havana-3 (ie: heat). Fixes bug #1231303 Change-Id: I20250131d05d20bfadbca450dfe6b8237f4b7183 --- ceilometerclient/common/utils.py | 9 +++ ceilometerclient/tests/test_utils.py | 31 ++++++++ ceilometerclient/tests/v2/test_alarms.py | 24 +++++- ceilometerclient/v2/alarms.py | 37 ++++----- ceilometerclient/v2/shell.py | 96 ++++++++++++------------ 5 files changed, 129 insertions(+), 68 deletions(-) diff --git a/ceilometerclient/common/utils.py b/ceilometerclient/common/utils.py index ef76dcc..f28a3ea 100644 --- a/ceilometerclient/common/utils.py +++ b/ceilometerclient/common/utils.py @@ -160,6 +160,15 @@ def key_with_slash_to_nested_dict(kwargs): return kwargs +def merge_nested_dict(dest, source, depth=0): + for (key, value) in source.iteritems(): + if isinstance(value, dict) and depth: + merge_nested_dict(dest[key], value, + depth=(depth - 1)) + else: + dest[key] = value + + def exit(msg=''): if msg: print >> sys.stderr, msg diff --git a/ceilometerclient/tests/test_utils.py b/ceilometerclient/tests/test_utils.py index 0e209b0..e0626af 100644 --- a/ceilometerclient/tests/test_utils.py +++ b/ceilometerclient/tests/test_utils.py @@ -93,3 +93,34 @@ class UtilsTest(test_utils.BaseTestCase): pass _, args = not_required_default.__dict__['arguments'][0] self.assertEqual(args['help'], "not_required_default. Defaults to 42.") + + def test_merge_nested_dict(self): + dest = {'key': 'value', + 'nested': {'key2': 'value2', + 'key3': 'value3', + 'nested2': {'key': 'value', + 'some': 'thing'}}} + source = {'key': 'modified', + 'nested': {'key3': 'modified3', + 'nested2': {'key5': 'value5'}}} + utils.merge_nested_dict(dest, source, depth=1) + + self.assertEqual(dest, {'key': 'modified', + 'nested': {'key2': 'value2', + 'key3': 'modified3', + 'nested2': {'key5': 'value5'}}}) + + def test_merge_nested_dict_no_depth(self): + dest = {'key': 'value', + 'nested': {'key2': 'value2', + 'key3': 'value3', + 'nested2': {'key': 'value', + 'some': 'thing'}}} + source = {'key': 'modified', + 'nested': {'key3': 'modified3', + 'nested2': {'key5': 'value5'}}} + utils.merge_nested_dict(dest, source) + + self.assertEqual(dest, {'key': 'modified', + 'nested': {'key3': 'modified3', + 'nested2': {'key5': 'value5'}}}) diff --git a/ceilometerclient/tests/v2/test_alarms.py b/ceilometerclient/tests/v2/test_alarms.py index 82279f5..f64ded1 100644 --- a/ceilometerclient/tests/v2/test_alarms.py +++ b/ceilometerclient/tests/v2/test_alarms.py @@ -61,6 +61,7 @@ DELTA_ALARM_RULE = {u'comparison_operator': u'lt', UPDATED_ALARM = copy.deepcopy(AN_ALARM) UPDATED_ALARM.update(DELTA_ALARM) UPDATED_ALARM['threshold_rule'].update(DELTA_ALARM_RULE) +DELTA_ALARM['threshold_rule'] = DELTA_ALARM_RULE UPDATE_ALARM = copy.deepcopy(UPDATED_ALARM) del UPDATE_ALARM['user_id'] del UPDATE_ALARM['project_id'] @@ -215,7 +216,20 @@ class AlarmManagerTest(testtools.TestCase): def test_update(self): alarm = self.mgr.update(alarm_id='alarm-id', **UPDATE_ALARM) expect = [ - ('PUT', '/v2/alarms/alarm-id', {}, UPDATE_ALARM), + ('GET', '/v2/alarms/alarm-id', {}, None), + ('PUT', '/v2/alarms/alarm-id', {}, UPDATED_ALARM), + ] + self.assertEqual(self.api.calls, expect) + self.assertTrue(alarm) + self.assertEqual(alarm.alarm_id, 'alarm-id') + for (key, value) in UPDATED_ALARM.iteritems(): + self.assertEqual(getattr(alarm, key), value) + + def test_update_delta(self): + alarm = self.mgr.update(alarm_id='alarm-id', **DELTA_ALARM) + expect = [ + ('GET', '/v2/alarms/alarm-id', {}, None), + ('PUT', '/v2/alarms/alarm-id', {}, UPDATED_ALARM), ] self.assertEqual(self.api.calls, expect) self.assertTrue(alarm) @@ -276,9 +290,10 @@ class AlarmLegacyManagerTest(testtools.TestCase): self.assertTrue(alarm) def test_update(self): - alarm = self.mgr.update(alarm_id='alarm-id', **UPDATE_LEGACY_ALARM) + alarm = self.mgr.update(alarm_id='alarm-id', **DELTA_LEGACY_ALARM) expect = [ - ('PUT', '/v2/alarms/alarm-id', {}, UPDATE_ALARM), + ('GET', '/v2/alarms/alarm-id', {}, None), + ('PUT', '/v2/alarms/alarm-id', {}, UPDATED_ALARM), ] self.assertEqual(self.api.calls, expect) self.assertTrue(alarm) @@ -293,7 +308,8 @@ class AlarmLegacyManagerTest(testtools.TestCase): del updated['meter_name'] alarm = self.mgr.update(alarm_id='alarm-id', **updated) expect = [ - ('PUT', '/v2/alarms/alarm-id', {}, UPDATE_ALARM), + ('GET', '/v2/alarms/alarm-id', {}, None), + ('PUT', '/v2/alarms/alarm-id', {}, UPDATED_ALARM), ] self.assertEqual(self.api.calls, expect) self.assertTrue(alarm) diff --git a/ceilometerclient/v2/alarms.py b/ceilometerclient/v2/alarms.py index d1ce7c6..71cc0ac 100644 --- a/ceilometerclient/v2/alarms.py +++ b/ceilometerclient/v2/alarms.py @@ -19,6 +19,7 @@ import warnings from ceilometerclient.common import base +from ceilometerclient.common import utils from ceilometerclient.v2 import options @@ -34,7 +35,7 @@ UPDATABLE_ATTRIBUTES = [ 'repeat_actions', 'threshold_rule', 'combination_rule', - ] +] CREATION_ATTRIBUTES = UPDATABLE_ATTRIBUTES + ['project_id', 'user_id'] @@ -67,12 +68,12 @@ class AlarmManager(base.Manager): return None @classmethod - def _compat_legacy_alarm_kwargs(cls, kwargs): - cls._compat_counter_rename_kwargs(kwargs) - cls._compat_alarm_before_rule_type_kwargs(kwargs) + def _compat_legacy_alarm_kwargs(cls, kwargs, create=False): + cls._compat_counter_rename_kwargs(kwargs, create) + cls._compat_alarm_before_rule_type_kwargs(kwargs, create) @staticmethod - def _compat_counter_rename_kwargs(kwargs): + def _compat_counter_rename_kwargs(kwargs, create=False): # NOTE(jd) Compatibility with Havana-2 API if 'counter_name' in kwargs: warnings.warn("counter_name has been renamed to meter_name", @@ -80,40 +81,40 @@ class AlarmManager(base.Manager): kwargs['meter_name'] = kwargs['counter_name'] @staticmethod - def _compat_alarm_before_rule_type_kwargs(kwargs): + def _compat_alarm_before_rule_type_kwargs(kwargs, create=False): # NOTE(sileht) Compatibility with Havana-3 API - if kwargs.get('type'): - return - warnings.warn("alarm without type set is deprecated", - DeprecationWarning) + if create and 'type' not in kwargs: + warnings.warn("alarm without type set is deprecated", + DeprecationWarning) + kwargs['type'] = 'threshold' - kwargs['type'] = 'threshold' - kwargs['threshold_rule'] = {} for field in ['period', 'evaluation_periods', 'threshold', 'statistic', 'comparison_operator', 'meter_name']: if field in kwargs: - kwargs['threshold_rule'][field] = kwargs[field] + kwargs.setdefault('threshold_rule', {})[field] = kwargs[field] del kwargs[field] - query = [] if 'matching_metadata' in kwargs: + query = [] for key in kwargs['matching_metadata']: query.append({'field': key, 'op': 'eq', 'value': kwargs['matching_metadata'][key]}) del kwargs['matching_metadata'] - kwargs['threshold_rule']['query'] = query + kwargs['threshold_rule']['query'] = query def create(self, **kwargs): - self._compat_legacy_alarm_kwargs(kwargs) + self._compat_legacy_alarm_kwargs(kwargs, create=True) new = dict((key, value) for (key, value) in kwargs.items() if key in CREATION_ATTRIBUTES) return self._create(self._path(), new) def update(self, alarm_id, **kwargs): self._compat_legacy_alarm_kwargs(kwargs) - updated = dict((key, value) for (key, value) in kwargs.items() - if key in UPDATABLE_ATTRIBUTES) + updated = self.get(alarm_id).to_dict() + 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) return self._update(self._path(alarm_id), updated) def delete(self, alarm_id): diff --git a/ceilometerclient/v2/shell.py b/ceilometerclient/v2/shell.py index bcd5657..60fa107 100644 --- a/ceilometerclient/v2/shell.py +++ b/ceilometerclient/v2/shell.py @@ -196,45 +196,49 @@ def do_alarm_show(cc, args={}): _display_alarm(alarm) -def common_alarm_arguments(func): - @utils.arg('--name', metavar='', required=True, - help='Name of the alarm (must be unique per tenant)') - @utils.arg('--project-id', metavar='', - help='Tenant to associate with alarm ' - '(only settable by admin users)') - @utils.arg('--user-id', metavar='', - help='User to associate with alarm ' - '(only settable by admin users)') - @utils.arg('--description', metavar='', - help='Free text description of the alarm') - @utils.arg('--state', metavar='', - help='State of the alarm, one of: ' + str(ALARM_STATES)) - @utils.arg('--enabled', type=utils.string_to_bool, metavar='{True|False}', - help='True if alarm evaluation/actioning is enabled') - @utils.arg('--alarm-action', dest='alarm_actions', - metavar='', action='append', default=None, - help=('URL to invoke when state transitions to alarm. ' - 'May be used multiple times.')) - @utils.arg('--ok-action', dest='ok_actions', - metavar='', action='append', default=None, - help=('URL to invoke when state transitions to OK. ' - 'May be used multiple times.')) - @utils.arg('--insufficient-data-action', dest='insufficient_data_actions', - metavar='', action='append', default=None, - help=('URL to invoke when state transitions to unkown. ' - 'May be used multiple times.')) - @utils.arg('--repeat-actions', dest='repeat_actions', - metavar='{True|False}', type=utils.string_to_bool, - default=False, - help=('True if actions should be repeatedly notified ' - 'while alarm remains in target state')) - @functools.wraps(func) - def _wrapper(*args, **kwargs): - return func(*args, **kwargs) +def common_alarm_arguments(create=False): + def _wrapper(func): + @utils.arg('--name', metavar='', required=create, + help='Name of the alarm (must be unique per tenant)') + @utils.arg('--project-id', metavar='', + help='Tenant to associate with alarm ' + '(only settable by admin users)') + @utils.arg('--user-id', metavar='', + help='User to associate with alarm ' + '(only settable by admin users)') + @utils.arg('--description', metavar='', + help='Free text description of the alarm') + @utils.arg('--state', metavar='', + help='State of the alarm, one of: ' + str(ALARM_STATES)) + @utils.arg('--enabled', type=utils.string_to_bool, + metavar='{True|False}', + help='True if alarm evaluation/actioning is enabled') + @utils.arg('--alarm-action', dest='alarm_actions', + metavar='', action='append', default=None, + help=('URL to invoke when state transitions to alarm. ' + 'May be used multiple times.')) + @utils.arg('--ok-action', dest='ok_actions', + metavar='', action='append', default=None, + help=('URL to invoke when state transitions to OK. ' + 'May be used multiple times.')) + @utils.arg('--insufficient-data-action', + dest='insufficient_data_actions', + metavar='', action='append', default=None, + help=('URL to invoke when state transitions to unkown. ' + 'May be used multiple times.')) + @utils.arg('--repeat-actions', dest='repeat_actions', + metavar='{True|False}', type=utils.string_to_bool, + default=False, + help=('True if actions should be repeatedly notified ' + 'while alarm remains in target state')) + @functools.wraps(func) + def _wrapped(*args, **kwargs): + return func(*args, **kwargs) + return _wrapped return _wrapper -@common_alarm_arguments +@common_alarm_arguments(create=True) @utils.arg('--period', type=int, metavar='', help='Length of each period (seconds) to evaluate over') @utils.arg('--evaluation-periods', type=int, metavar='', @@ -259,7 +263,7 @@ def do_alarm_create(cc, args={}): _display_alarm(alarm) -@common_alarm_arguments +@common_alarm_arguments(create=True) @utils.arg('--meter-name', metavar='', required=True, dest='threshold_rule/meter_name', help='Metric to evaluate against') @@ -294,7 +298,7 @@ def do_alarm_threshold_create(cc, args={}): _display_alarm(alarm) -@common_alarm_arguments +@common_alarm_arguments(create=True) @utils.arg('--alarm_ids', action='append', metavar='', required=True, dest='combination_rule/alarm_ids', help='List of alarm id') @@ -313,18 +317,18 @@ def do_alarm_combination_create(cc, args={}): @utils.arg('-a', '--alarm_id', metavar='', required=True, help='ID of the alarm to update.') -@common_alarm_arguments +@common_alarm_arguments() @utils.arg('--period', type=int, metavar='', help='Length of each period (seconds) to evaluate over') @utils.arg('--evaluation-periods', type=int, metavar='', help='Number of periods to evaluate over') -@utils.arg('--meter-name', metavar='', required=True, +@utils.arg('--meter-name', metavar='', help='Metric to evaluate against') @utils.arg('--statistic', metavar='', help='Statistic to evaluate, one of: ' + str(STATISTICS)) @utils.arg('--comparison-operator', metavar='', help='Operator to compare with, one of: ' + str(ALARM_OPERATORS)) -@utils.arg('--threshold', type=float, metavar='', required=True, +@utils.arg('--threshold', type=float, metavar='', help='Threshold to evaluate against') @utils.arg('--matching-metadata', dest='matching_metadata', metavar='', action='append', default=None, @@ -344,9 +348,9 @@ def do_alarm_update(cc, args={}): @utils.arg('-a', '--alarm_id', metavar='', required=True, help='ID of the alarm to update.') -@common_alarm_arguments +@common_alarm_arguments() @utils.arg('--meter-name', metavar='', - dest='threshold_rule/meter_name', required=True, + dest='threshold_rule/meter_name', help='Metric to evaluate against') @utils.arg('--period', type=int, metavar='', dest='threshold_rule/period', @@ -360,7 +364,7 @@ def do_alarm_update(cc, args={}): @utils.arg('--comparison-operator', metavar='', dest='threshold_rule/comparison_operator', help='Operator to compare with, one of: ' + str(ALARM_OPERATORS)) -@utils.arg('--threshold', type=float, metavar='', required=True, +@utils.arg('--threshold', type=float, metavar='', dest='threshold_rule/threshold', help='Threshold to evaluate against') @utils.arg('-q', '--query', metavar='', @@ -385,9 +389,9 @@ def do_alarm_threshold_update(cc, args={}): @utils.arg('-a', '--alarm_id', metavar='', required=True, help='ID of the alarm to update.') -@common_alarm_arguments +@common_alarm_arguments() @utils.arg('--alarm_ids', action='append', metavar='', - dest='combination_rule/alarm_ids', required=True, + dest='combination_rule/alarm_ids', help='List of alarm id') @utils.arg('---operator', metavar='', dest='combination_rule/operator', From a99a8c628faa33768961952b59b8175c1a108780 Mon Sep 17 00:00:00 2001 From: OpenStack Jenkins Date: Tue, 1 Oct 2013 16:14:33 +0000 Subject: [PATCH 5/8] Updated from global requirements Change-Id: I71d6d8aad148b240f8ab1325d3c3ff2996ebf642 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2a0786a..70c2b3f 100644 --- a/setup.py +++ b/setup.py @@ -18,5 +18,5 @@ import setuptools setuptools.setup( - setup_requires=['pbr>=0.5.21,<1.0'], + setup_requires=['pbr'], pbr=True) From 7c8a676b43e9f82276041afbae15d739dc2c1ab4 Mon Sep 17 00:00:00 2001 From: Eoghan Glynn Date: Wed, 2 Oct 2013 09:30:27 +0000 Subject: [PATCH 6/8] Fix shell.do_alarm_get_state to get as opposed to set Otherwise the CLI fails when attempting to set a state arg that doesn't exist. (Simple copy'n'paste error in the original code). Change-Id: Iab117177805449ddec9d03656a95a0cbbbbd58bb --- ceilometerclient/tests/v2/test_shell.py | 39 +++++++++++++++++++++++++ ceilometerclient/v2/shell.py | 6 ++-- test-requirements.txt | 1 + 3 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 ceilometerclient/tests/v2/test_shell.py diff --git a/ceilometerclient/tests/v2/test_shell.py b/ceilometerclient/tests/v2/test_shell.py new file mode 100644 index 0000000..c758619 --- /dev/null +++ b/ceilometerclient/tests/v2/test_shell.py @@ -0,0 +1,39 @@ +# 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 mock + +from ceilometerclient.tests import utils +from ceilometerclient.v2 import shell as ceilometer_shell + + +class ShellAlarmStateCommandsTest(utils.BaseTestCase): + + ALARM_ID = 'foobar' + + def setUp(self): + super(ShellAlarmStateCommandsTest, self).setUp() + self.cc = mock.Mock() + self.cc.alarms = mock.Mock() + self.args = mock.Mock() + self.args.alarm_id = self.ALARM_ID + + def test_alarm_state_get(self): + ceilometer_shell.do_alarm_get_state(self.cc, self.args) + self.cc.alarms.get_state.assert_called_once_with(self.ALARM_ID) + self.assertFalse(self.cc.alarms.set_state.called) + + def test_alarm_state_set(self): + self.args.state = 'ok' + ceilometer_shell.do_alarm_set_state(self.cc, self.args) + self.cc.alarms.set_state.assert_called_once_with(self.ALARM_ID, 'ok') + self.assertFalse(self.cc.alarms.get_state.called) diff --git a/ceilometerclient/v2/shell.py b/ceilometerclient/v2/shell.py index bcd5657..76aea34 100644 --- a/ceilometerclient/v2/shell.py +++ b/ceilometerclient/v2/shell.py @@ -423,10 +423,9 @@ def do_alarm_delete(cc, args={}): def do_alarm_set_state(cc, args={}): '''Set the state of an alarm.''' try: - cc.alarms.set_state(args.alarm_id, args.state) + state = cc.alarms.set_state(args.alarm_id, args.state) except exc.HTTPNotFound: raise exc.CommandError('Alarm not found: %s' % args.alarm_id) - state = cc.alarms.get_state(args.alarm_id) utils.print_dict({'state': state}, wrap=72) @@ -435,10 +434,9 @@ def do_alarm_set_state(cc, args={}): def do_alarm_get_state(cc, args={}): '''Get the state of an alarm.''' try: - cc.alarms.set_state(args.alarm_id, args.state) + state = cc.alarms.get_state(args.alarm_id) except exc.HTTPNotFound: raise exc.CommandError('Alarm not found: %s' % args.alarm_id) - state = cc.alarms.get_state(args.alarm_id) utils.print_dict({'state': state}, wrap=72) diff --git a/test-requirements.txt b/test-requirements.txt index a3e4bfc..26843ce 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,6 +6,7 @@ hacking>=0.5.6,<0.8 coverage>=3.6 discover fixtures>=0.3.14 +mock>=1.0 mox>=0.5.3 python-subunit sphinx>=1.1.2 From fc201b3805dda16fad66d7c0d0e47b1e32612874 Mon Sep 17 00:00:00 2001 From: Eoghan Glynn Date: Wed, 2 Oct 2013 09:59:47 +0000 Subject: [PATCH 7/8] Use standard CLI object-verb ordering for alarm-{g|s}set-state The openstack CLIs all use an object-followed-by-verb ordering convention when construct command names. Follow a similar convention for commands to get and set alarm state. Change-Id: I34e0f450019556c80476df782d4c86dca08bdc9d --- ceilometerclient/tests/v2/test_shell.py | 4 ++-- ceilometerclient/v2/shell.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ceilometerclient/tests/v2/test_shell.py b/ceilometerclient/tests/v2/test_shell.py index c758619..6d5da90 100644 --- a/ceilometerclient/tests/v2/test_shell.py +++ b/ceilometerclient/tests/v2/test_shell.py @@ -28,12 +28,12 @@ class ShellAlarmStateCommandsTest(utils.BaseTestCase): self.args.alarm_id = self.ALARM_ID def test_alarm_state_get(self): - ceilometer_shell.do_alarm_get_state(self.cc, self.args) + ceilometer_shell.do_alarm_state_get(self.cc, self.args) self.cc.alarms.get_state.assert_called_once_with(self.ALARM_ID) self.assertFalse(self.cc.alarms.set_state.called) def test_alarm_state_set(self): self.args.state = 'ok' - ceilometer_shell.do_alarm_set_state(self.cc, self.args) + ceilometer_shell.do_alarm_state_set(self.cc, self.args) self.cc.alarms.set_state.assert_called_once_with(self.ALARM_ID, 'ok') self.assertFalse(self.cc.alarms.get_state.called) diff --git a/ceilometerclient/v2/shell.py b/ceilometerclient/v2/shell.py index 76aea34..a2ae667 100644 --- a/ceilometerclient/v2/shell.py +++ b/ceilometerclient/v2/shell.py @@ -420,7 +420,7 @@ def do_alarm_delete(cc, args={}): help='ID of the alarm state to set.') @utils.arg('--state', metavar='', required=True, help='State of the alarm, one of: ' + str(ALARM_STATES)) -def do_alarm_set_state(cc, args={}): +def do_alarm_state_set(cc, args={}): '''Set the state of an alarm.''' try: state = cc.alarms.set_state(args.alarm_id, args.state) @@ -431,7 +431,7 @@ def do_alarm_set_state(cc, args={}): @utils.arg('-a', '--alarm_id', metavar='', required=True, help='ID of the alarm state to show.') -def do_alarm_get_state(cc, args={}): +def do_alarm_state_get(cc, args={}): '''Get the state of an alarm.''' try: state = cc.alarms.get_state(args.alarm_id) From c3283ec1e861f29a53c59c1a09f2a1bc21a713e4 Mon Sep 17 00:00:00 2001 From: Eoghan Glynn Date: Wed, 2 Oct 2013 16:18:06 +0000 Subject: [PATCH 8/8] Add support for new alarm-history command Expose the new alarm history API via the CLI. Change-Id: I5757d309dd7143dfab83985fb51ee6d320478e3b --- ceilometerclient/tests/v2/test_alarms.py | 81 +++++++++++++++++++- ceilometerclient/tests/v2/test_shell.py | 98 ++++++++++++++++++++++++ ceilometerclient/v2/alarms.py | 13 ++++ ceilometerclient/v2/shell.py | 78 ++++++++++++++++--- 4 files changed, 256 insertions(+), 14 deletions(-) diff --git a/ceilometerclient/tests/v2/test_alarms.py b/ceilometerclient/tests/v2/test_alarms.py index f64ded1..0c2fa72 100644 --- a/ceilometerclient/tests/v2/test_alarms.py +++ b/ceilometerclient/tests/v2/test_alarms.py @@ -20,7 +20,7 @@ import copy import testtools from ceilometerclient.tests import utils -import ceilometerclient.v2.alarms +from ceilometerclient.v2 import alarms AN_ALARM = {u'alarm_actions': [u'http://site:8000/alarm'], u'ok_actions': [u'http://site:8000/ok'], @@ -108,6 +108,47 @@ del UPDATE_LEGACY_ALARM['alarm_id'] del UPDATE_LEGACY_ALARM['timestamp'] del UPDATE_LEGACY_ALARM['state_timestamp'] +FULL_DETAIL = ('{"alarm_actions": [], ' + '"user_id": "8185aa72421a4fd396d4122cba50e1b5", ' + '"name": "scombo", ' + '"timestamp": "2013-10-03T08:58:33.647912", ' + '"enabled": true, ' + '"state_timestamp": "2013-10-03T08:58:33.647912", ' + '"rule": {"operator": "or", "alarm_ids": ' + '["062cc907-3a9f-4867-ab3b-fa83212b39f7"]}, ' + '"alarm_id": "alarm-id, ' + '"state": "insufficient data", ' + '"insufficient_data_actions": [], ' + '"repeat_actions": false, ' + '"ok_actions": [], ' + '"project_id": "57d04f24d0824b78b1ea9bcecedbda8f", ' + '"type": "combination", ' + '"description": "Combined state of alarms ' + '062cc907-3a9f-4867-ab3b-fa83212b39f7"}') +ALARM_HISTORY = [{'on_behalf_of': '57d04f24d0824b78b1ea9bcecedbda8f', + 'user_id': '8185aa72421a4fd396d4122cba50e1b5', + 'event_id': 'c74a8611-6553-4764-a860-c15a6aabb5d0', + 'timestamp': '2013-10-03T08:59:28.326000', + 'detail': '{"state": "alarm"}', + 'alarm_id': 'alarm-id', + 'project_id': '57d04f24d0824b78b1ea9bcecedbda8f', + 'type': 'state transition'}, + {'on_behalf_of': '57d04f24d0824b78b1ea9bcecedbda8f', + 'user_id': '8185aa72421a4fd396d4122cba50e1b5', + 'event_id': 'c74a8611-6553-4764-a860-c15a6aabb5d0', + 'timestamp': '2013-10-03T08:59:28.326000', + 'detail': '{"description": "combination of one"}', + 'alarm_id': 'alarm-id', + 'project_id': '57d04f24d0824b78b1ea9bcecedbda8f', + 'type': 'rule change'}, + {'on_behalf_of': '57d04f24d0824b78b1ea9bcecedbda8f', + 'user_id': '8185aa72421a4fd396d4122cba50e1b5', + 'event_id': '4fd7df9e-190d-4471-8884-dc5a33d5d4bb', + 'timestamp': '2013-10-03T08:58:33.647000', + 'detail': FULL_DETAIL, + 'alarm_id': 'alarm-id', + 'project_id': '57d04f24d0824b78b1ea9bcecedbda8f', + 'type': 'creation'}] fixtures = { '/v2/alarms': @@ -159,6 +200,20 @@ fixtures = { None, ), }, + '/v2/alarms/alarm-id/history': + { + 'GET': ( + {}, + ALARM_HISTORY, + ), + }, + '/v2/alarms/alarm-id/history?q.op=&q.value=NOW&q.field=timestamp': + { + 'GET': ( + {}, + ALARM_HISTORY, + ), + }, } @@ -167,7 +222,7 @@ class AlarmManagerTest(testtools.TestCase): def setUp(self): super(AlarmManagerTest, self).setUp() self.api = utils.FakeAPI(fixtures) - self.mgr = ceilometerclient.v2.alarms.AlarmManager(self.api) + self.mgr = alarms.AlarmManager(self.api) def test_list_all(self): alarms = list(self.mgr.list()) @@ -261,13 +316,33 @@ class AlarmManagerTest(testtools.TestCase): self.assertEqual(self.api.calls, expect) self.assertTrue(deleted is None) + def _do_test_get_history(self, q, url): + history = self.mgr.get_history(q=q, alarm_id='alarm-id') + expect = [('GET', url, {}, None)] + self.assertEqual(self.api.calls, expect) + for i in xrange(len(history)): + change = history[i] + self.assertTrue(isinstance(change, alarms.AlarmChange)) + for k, v in ALARM_HISTORY[i].iteritems(): + self.assertEqual(getattr(change, k), v) + + def test_get_all_history(self): + url = '/v2/alarms/alarm-id/history' + self._do_test_get_history(None, url) + + def test_get_constrained_history(self): + q = [dict(field='timestamp', value='NOW')] + url = ('/v2/alarms/alarm-id/history' + '?q.op=&q.value=NOW&q.field=timestamp') + self._do_test_get_history(q, url) + class AlarmLegacyManagerTest(testtools.TestCase): def setUp(self): super(AlarmLegacyManagerTest, self).setUp() self.api = utils.FakeAPI(fixtures) - self.mgr = ceilometerclient.v2.alarms.AlarmManager(self.api) + self.mgr = alarms.AlarmManager(self.api) def test_create(self): alarm = self.mgr.create(**CREATE_LEGACY_ALARM) diff --git a/ceilometerclient/tests/v2/test_shell.py b/ceilometerclient/tests/v2/test_shell.py index 6d5da90..402343b 100644 --- a/ceilometerclient/tests/v2/test_shell.py +++ b/ceilometerclient/tests/v2/test_shell.py @@ -10,9 +10,15 @@ # License for the specific language governing permissions and limitations # under the License. +import cStringIO import mock +import re +import sys + +from testtools import matchers from ceilometerclient.tests import utils +from ceilometerclient.v2 import alarms from ceilometerclient.v2 import shell as ceilometer_shell @@ -37,3 +43,95 @@ class ShellAlarmStateCommandsTest(utils.BaseTestCase): ceilometer_shell.do_alarm_state_set(self.cc, self.args) self.cc.alarms.set_state.assert_called_once_with(self.ALARM_ID, 'ok') self.assertFalse(self.cc.alarms.get_state.called) + + +class ShellAlarmHistoryCommandTest(utils.BaseTestCase): + + ALARM_ID = '768ff714-8cfb-4db9-9753-d484cb33a1cc' + FULL_DETAIL = ('{"alarm_actions": [], ' + '"user_id": "8185aa72421a4fd396d4122cba50e1b5", ' + '"name": "scombo", ' + '"timestamp": "2013-10-03T08:58:33.647912", ' + '"enabled": true, ' + '"state_timestamp": "2013-10-03T08:58:33.647912", ' + '"rule": {"operator": "or", "alarm_ids": ' + '["062cc907-3a9f-4867-ab3b-fa83212b39f7"]}, ' + '"alarm_id": "768ff714-8cfb-4db9-9753-d484cb33a1cc", ' + '"state": "insufficient data", ' + '"insufficient_data_actions": [], ' + '"repeat_actions": false, ' + '"ok_actions": [], ' + '"project_id": "57d04f24d0824b78b1ea9bcecedbda8f", ' + '"type": "combination", ' + '"description": "Combined state of alarms ' + '062cc907-3a9f-4867-ab3b-fa83212b39f7"}') + ALARM_HISTORY = [{'on_behalf_of': '57d04f24d0824b78b1ea9bcecedbda8f', + 'user_id': '8185aa72421a4fd396d4122cba50e1b5', + 'event_id': 'c74a8611-6553-4764-a860-c15a6aabb5d0', + 'timestamp': '2013-10-03T08:59:28.326000', + 'detail': '{"state": "alarm"}', + 'alarm_id': '768ff714-8cfb-4db9-9753-d484cb33a1cc', + 'project_id': '57d04f24d0824b78b1ea9bcecedbda8f', + 'type': 'state transition'}, + {'on_behalf_of': '57d04f24d0824b78b1ea9bcecedbda8f', + 'user_id': '8185aa72421a4fd396d4122cba50e1b5', + 'event_id': 'c74a8611-6553-4764-a860-c15a6aabb5d0', + 'timestamp': '2013-10-03T08:59:28.326000', + 'detail': '{"description": "combination of one"}', + 'alarm_id': '768ff714-8cfb-4db9-9753-d484cb33a1cc', + 'project_id': '57d04f24d0824b78b1ea9bcecedbda8f', + 'type': 'rule change'}, + {'on_behalf_of': '57d04f24d0824b78b1ea9bcecedbda8f', + 'user_id': '8185aa72421a4fd396d4122cba50e1b5', + 'event_id': '4fd7df9e-190d-4471-8884-dc5a33d5d4bb', + 'timestamp': '2013-10-03T08:58:33.647000', + 'detail': FULL_DETAIL, + 'alarm_id': '768ff714-8cfb-4db9-9753-d484cb33a1cc', + 'project_id': '57d04f24d0824b78b1ea9bcecedbda8f', + 'type': 'creation'}] + TIMESTAMP_RE = (' +\| (\d{4})-(\d{2})-(\d{2})T' + '(\d{2})\:(\d{2})\:(\d{2})\.(\d{6}) \| +') + + def setUp(self): + super(ShellAlarmHistoryCommandTest, self).setUp() + self.cc = mock.Mock() + self.cc.alarms = mock.Mock() + self.args = mock.Mock() + self.args.alarm_id = self.ALARM_ID + + def _do_test_alarm_history(self, raw_query=None, parsed_query=None): + self.args.query = raw_query + orig = sys.stdout + sys.stdout = cStringIO.StringIO() + history = [alarms.AlarmChange(mock.Mock(), change) + for change in self.ALARM_HISTORY] + self.cc.alarms.get_history.return_value = history + + try: + ceilometer_shell.do_alarm_history(self.cc, self.args) + self.cc.alarms.get_history.assert_called_once_with( + q=parsed_query, + alarm_id=self.ALARM_ID + ) + out = sys.stdout.getvalue() + required = [ + '.*creation%sname: scombo.*' % self.TIMESTAMP_RE, + '.*rule change%sdescription: combination of one.*' % + self.TIMESTAMP_RE, + '.*state transition%sstate: alarm.*' % self.TIMESTAMP_RE, + ] + for r in required: + self.assertThat(out, matchers.MatchesRegex(r, re.DOTALL)) + finally: + sys.stdout.close() + sys.stdout = orig + + def test_alarm_all_history(self): + self._do_test_alarm_history() + + def test_alarm_constrained_history(self): + parsed_query = [dict(field='timestamp', + value='2013-10-03T08:59:28', + op='gt')] + self._do_test_alarm_history(raw_query='timestamp>2013-10-03T08:59:28', + parsed_query=parsed_query) diff --git a/ceilometerclient/v2/alarms.py b/ceilometerclient/v2/alarms.py index 71cc0ac..42f398e 100644 --- a/ceilometerclient/v2/alarms.py +++ b/ceilometerclient/v2/alarms.py @@ -51,6 +51,14 @@ class Alarm(base.Resource): return super(Alarm, self).__getattr__(k) +class AlarmChange(base.Resource): + def __repr__(self): + return "" % self._info + + def __getattr__(self, k): + return super(AlarmChange, self).__getattr__(k) + + class AlarmManager(base.Manager): resource_class = Alarm @@ -130,3 +138,8 @@ class AlarmManager(base.Manager): resp, body = self.api.json_request('GET', "%s/state" % self._path(alarm_id)) return body + + def get_history(self, alarm_id, q=None): + path = '%s/history' % self._path(alarm_id) + url = options.build_url(path, q) + return self._list(url, obj_class=AlarmChange) diff --git a/ceilometerclient/v2/shell.py b/ceilometerclient/v2/shell.py index e90be02..24b15b9 100644 --- a/ceilometerclient/v2/shell.py +++ b/ceilometerclient/v2/shell.py @@ -127,26 +127,63 @@ def do_meter_list(cc, args={}): sortby=0) -def alarm_rule_formatter(alarm): - if alarm.type == 'threshold': +def _display_rule(type, rule): + if type == 'threshold': return ('%(meter_name)s %(comparison_operator)s ' '%(threshold)s during %(evaluation_periods)s x %(period)ss' % { - 'meter_name': alarm.rule['meter_name'], - 'threshold': alarm.rule['threshold'], - 'evaluation_periods': alarm.rule['evaluation_periods'], - 'period': alarm.rule['period'], + 'meter_name': rule['meter_name'], + 'threshold': rule['threshold'], + 'evaluation_periods': rule['evaluation_periods'], + 'period': rule['period'], 'comparison_operator': OPERATORS_STRING.get( - alarm.rule['comparison_operator']) + rule['comparison_operator']) }) - elif alarm.type == 'combination': + elif type == 'combination': return ('combinated states (%(operator)s) of %(alarms)s' % { - 'operator': alarm.rule['operator'].upper(), - 'alarms': ", ".join(alarm.rule['alarm_ids'])}) + 'operator': rule['operator'].upper(), + 'alarms': ", ".join(rule['alarm_ids'])}) else: # just dump all return "\n".join(["%s: %s" % (f, v) - for f, v in alarm.rule.iteritems()]) + for f, v in rule.iteritems()]) + + +def alarm_rule_formatter(alarm): + return _display_rule(alarm.type, alarm.rule) + + +def _infer_type(detail): + if 'type' in detail: + return detail['type'] + elif 'meter_name' in detail['rule']: + return 'threshold' + elif 'alarms' in detail['rule']: + return 'combination' + else: + return 'unknown' + + +def alarm_change_detail_formatter(change): + detail = json.loads(change.detail) + fields = [] + if change.type == 'state transition': + fields.append('state: %s' % detail['state']) + elif change.type == 'creation' or change.type == 'deletion': + for k in ['name', 'description', 'type', 'rule']: + if k == 'rule': + fields.append('rule: %s' % _display_rule(detail['type'], + detail[k])) + else: + fields.append('%s: %s' % (k, detail[k])) + elif change.type == 'rule change': + for k, v in detail.iteritems(): + if k == 'rule': + fields.append('rule: %s' % _display_rule(_infer_type(detail), + v)) + else: + fields.append('%s: %s' % (k, v)) + return '\n'.join(fields) @utils.arg('-q', '--query', metavar='', @@ -444,6 +481,25 @@ def do_alarm_state_get(cc, args={}): utils.print_dict({'state': state}, wrap=72) +@utils.arg('-a', '--alarm_id', metavar='', required=True, + help='ID of the alarm for which history is shown.') +@utils.arg('-q', '--query', metavar='', + help='key[op]value; list.') +def do_alarm_history(cc, args={}): + '''Display the change history of an alarm.''' + kwargs = dict(alarm_id=args.alarm_id, + q=options.cli_to_array(args.query)) + try: + history = cc.alarms.get_history(**kwargs) + except exc.HTTPNotFound: + raise exc.CommandError('Alarm not found: %s' % args.alarm_id) + field_labels = ['Type', 'Timestamp', 'Detail'] + fields = ['type', 'timestamp', 'detail'] + utils.print_list(history, fields, field_labels, + formatters={'detail': alarm_change_detail_formatter}, + sortby=1) + + @utils.arg('-q', '--query', metavar='', help='key[op]value; list.') def do_resource_list(cc, args={}):