From 428e394e5a4c3d98fbe5f1dc3ab1273af86aaa79 Mon Sep 17 00:00:00 2001 From: Lingxian Kong Date: Tue, 26 Nov 2019 12:10:14 +1300 Subject: [PATCH] Support threshold type alarm again Add threshold type alarm back. Gnocchi is not actively maintained currently but there are still users running Ceilometer in production and relying on Ceilometer for auditing and billing. Change-Id: I94ea998affbdd9f5535431f3ba713e2d4662b253 --- aodhclient/tests/functional/test_alarm.py | 171 ++++++++++++++++++ aodhclient/tests/unit/test_alarm_cli.py | 23 +++ aodhclient/v2/alarm.py | 5 +- aodhclient/v2/alarm_cli.py | 44 ++++- doc/source/shell.rst | 21 ++- ...-add-threshold-alarm-47e012620fd611ea.yaml | 5 + 6 files changed, 254 insertions(+), 15 deletions(-) create mode 100644 releasenotes/notes/ussuri-add-threshold-alarm-47e012620fd611ea.yaml diff --git a/aodhclient/tests/functional/test_alarm.py b/aodhclient/tests/functional/test_alarm.py index ac3ebbb..39c44e9 100644 --- a/aodhclient/tests/functional/test_alarm.py +++ b/aodhclient/tests/functional/test_alarm.py @@ -184,6 +184,158 @@ class AodhClientTest(base.ClientTestBase): self.assertNotIn(ALARM_ID, [r['alarm_id'] for r in self.parser.listing(result)]) + def test_threshold_scenario(self): + PROJECT_ID = uuidutils.generate_uuid() + + # CREATE + result = self.aodh(u'alarm', + params=(u"create --type threshold --name " + "test_threshold_scenario " + "-m meter_name --threshold 5 " + "--project-id %s" % PROJECT_ID)) + alarm = self.details_multiple(result)[0] + ALARM_ID = alarm['alarm_id'] + self.assertEqual('test_threshold_scenario', alarm['name']) + self.assertEqual('meter_name', alarm['meter_name']) + self.assertEqual('5.0', alarm['threshold']) + + # CREATE WITH --TIME-CONSTRAINT + result = self.aodh( + u'alarm', + params=(u"create --type threshold --name alarm_tc " + "-m meter_name --threshold 5 " + "--time-constraint " + "name=cons1;start='0 11 * * *';duration=300 " + "--time-constraint " + "name=cons2;start='0 23 * * *';duration=600 " + "--project-id %s" % PROJECT_ID)) + alarm = self.details_multiple(result)[0] + self.assertEqual('alarm_tc', alarm['name']) + self.assertEqual('meter_name', alarm['meter_name']) + self.assertEqual('5.0', alarm['threshold']) + self.assertIsNotNone(alarm['time_constraints']) + + # CREATE FAIL MISSING PARAM + self.assertRaises(exceptions.CommandFailed, + self.aodh, u'alarm', + params=(u"create --type threshold --name " + "test_threshold_scenario " + "--project-id %s" % PROJECT_ID)) + + # UPDATE + result = self.aodh( + 'alarm', params=("update %s --severity critical --threshold 10" + % ALARM_ID)) + alarm_updated = self.details_multiple(result)[0] + self.assertEqual(ALARM_ID, alarm_updated["alarm_id"]) + self.assertEqual('critical', alarm_updated['severity']) + self.assertEqual('10.0', alarm_updated["threshold"]) + + # GET + result = self.aodh( + 'alarm', params="show %s" % ALARM_ID) + alarm_show = self.details_multiple(result)[0] + self.assertEqual(ALARM_ID, alarm_show["alarm_id"]) + self.assertEqual(PROJECT_ID, alarm_show["project_id"]) + self.assertEqual('test_threshold_scenario', alarm_show['name']) + self.assertEqual('meter_name', alarm_show['meter_name']) + self.assertEqual('10.0', alarm_show['threshold']) + + # GET BY NAME + result = self.aodh( + 'alarm', params="show --name test_threshold_scenario") + alarm_show = self.details_multiple(result)[0] + self.assertEqual(ALARM_ID, alarm_show["alarm_id"]) + self.assertEqual(PROJECT_ID, alarm_show["project_id"]) + self.assertEqual('test_threshold_scenario', alarm_show['name']) + self.assertEqual('meter_name', alarm_show['meter_name']) + self.assertEqual('10.0', alarm_show['threshold']) + + # GET BY NAME AND ID ERROR + self.assertRaises(exceptions.CommandFailed, + self.aodh, u'alarm', + params=(u"show %s --name test_threshold_scenario" % + ALARM_ID)) + + # LIST + result = self.aodh('alarm', params="list") + self.assertIn(ALARM_ID, + [r['alarm_id'] for r in self.parser.listing(result)]) + output_colums = ['alarm_id', 'type', 'name', 'state', 'severity', + 'enabled'] + for alarm_list in self.parser.listing(result): + self.assertEqual(sorted(output_colums), sorted(alarm_list.keys())) + if alarm_list["alarm_id"] == ALARM_ID: + self.assertEqual('test_threshold_scenario', alarm_list['name']) + + # LIST WITH PAGINATION + # list with limit + result = self.aodh('alarm', + params="list --limit 1") + alarm_list = self.parser.listing(result) + self.assertEqual(1, len(alarm_list)) + # list with sort with key=name dir=asc + result = self.aodh('alarm', + params="list --sort name:asc") + names = [r['name'] for r in self.parser.listing(result)] + sorted_name = sorted(names) + self.assertEqual(sorted_name, names) + # list with sort with key=name dir=asc and key=alarm_id dir=asc + result = self.aodh(u'alarm', + params=(u"create --type threshold --name " + "test_threshold_scenario " + "-m meter_name --threshold 5 " + "--project-id %s" % PROJECT_ID)) + created_alarm_id = self.details_multiple(result)[0]['alarm_id'] + result = self.aodh('alarm', + params="list --sort name:asc --sort alarm_id:asc") + alarm_list = self.parser.listing(result) + ids_with_same_name = [] + names = [] + for alarm in alarm_list: + names.append(['alarm_name']) + if alarm['name'] == 'test_threshold_scenario': + ids_with_same_name.append(alarm['alarm_id']) + sorted_ids = sorted(ids_with_same_name) + sorted_names = sorted(names) + self.assertEqual(sorted_names, names) + self.assertEqual(sorted_ids, ids_with_same_name) + # list with sort with key=name dir=desc and with the marker equal to + # the alarm_id of the test_threshold_scenario we created for this test. + result = self.aodh('alarm', + params="list --sort name:desc " + "--marker %s" % created_alarm_id) + self.assertIn('alarm_tc', + [r['name'] for r in self.parser.listing(result)]) + self.aodh('alarm', params="delete %s" % created_alarm_id) + + # LIST WITH QUERY + result = self.aodh('alarm', + params=("list --query project_id=%s" % PROJECT_ID)) + alarm_list = self.parser.listing(result)[0] + self.assertEqual(ALARM_ID, alarm_list["alarm_id"]) + self.assertEqual('test_threshold_scenario', alarm_list['name']) + + # DELETE + result = self.aodh('alarm', params="delete %s" % ALARM_ID) + self.assertEqual("", result) + + # GET FAIL + result = self.aodh('alarm', params="show %s" % ALARM_ID, + fail_ok=True, merge_stderr=True) + expected = "Alarm %s not found (HTTP 404)" % ALARM_ID + self.assertFirstLineStartsWith(result.splitlines(), expected) + + # DELETE FAIL + result = self.aodh('alarm', params="delete %s" % ALARM_ID, + fail_ok=True, merge_stderr=True) + self.assertFirstLineStartsWith(result.splitlines(), expected) + + # LIST DOES NOT HAVE ALARM + result = self.aodh('alarm', params="list") + self.assertNotIn(ALARM_ID, + [r['alarm_id'] for r in self.parser.listing(result)]) + def test_composite_scenario(self): project_id = uuidutils.generate_uuid() @@ -298,6 +450,25 @@ class AodhClientTest(base.ClientTestBase): test(params) self.aodh('alarm', params='delete %s' % alarm['alarm_id']) + def test_threshold_alarm_create_show_query(self): + params = ('create --type threshold --name alarm-multiple-query ' + '-m cpu_util --threshold 90 --query "project_id=123;' + 'resource_id=456"') + expected_lines = { + 'query': 'project_id = 123 AND', + '': 'resource_id = 456' + } + self._test_alarm_create_show_query(params, expected_lines) + + params = ('create --type threshold --name alarm-single-query ' + '-m cpu_util --threshold 90 --query project_id=123') + expected_lines = {'query': 'project_id = 123'} + self._test_alarm_create_show_query(params, expected_lines) + + params = ('create --type threshold --name alarm-no-query ' + '-m cpu_util --threshold 90') + self._test_alarm_create_show_query(params, {'query': ''}) + def test_event_alarm_create_show_query(self): params = ('create --type event --name alarm-multiple-query ' '--query "traits.project_id=789;traits.resource_id=012"') diff --git a/aodhclient/tests/unit/test_alarm_cli.py b/aodhclient/tests/unit/test_alarm_cli.py index a88dace..0745c84 100644 --- a/aodhclient/tests/unit/test_alarm_cli.py +++ b/aodhclient/tests/unit/test_alarm_cli.py @@ -49,6 +49,22 @@ class CliAlarmCreateTest(testtools.TestCase): '--threshold, --resource-id, --resource-type and ' '--aggregation-method') + @mock.patch.object(argparse.ArgumentParser, 'error') + def test_validate_args_threshold(self, mock_arg): + # Cover the test case of the method _validate_args for + # threshold + parser = self.cli_alarm_create.get_parser('aodh alarm create') + test_parsed_args = parser.parse_args([ + '--name', 'threshold_test', + '--type', 'threshold', + '--threshold', '80' + ]) + self.cli_alarm_create._validate_args(test_parsed_args) + mock_arg.assert_called_once_with( + 'Threshold alarm requires -m/--meter-name and ' + '--threshold parameters. Meter name can be ' + 'found in Ceilometer') + @mock.patch.object(argparse.ArgumentParser, 'error') def test_validate_args_composite(self, mock_arg): # Cover the test case of the method _validate_args for @@ -156,6 +172,13 @@ class CliAlarmCreateTest(testtools.TestCase): 'type': '', 'value': 'fake-resource-id'}] }, + 'threshold_rule': {'comparison_operator': 'le', + 'evaluation_periods': 60, + 'query': [{'field': 'resource', + 'op': 'eq', + 'type': '', + 'value': 'fake-resource-id'}], + 'threshold': 80.0}, 'gnocchi_resources_threshold_rule': { 'granularity': '60', 'metric': 'cpu', diff --git a/aodhclient/v2/alarm.py b/aodhclient/v2/alarm.py index 7428223..1e3dc7d 100644 --- a/aodhclient/v2/alarm.py +++ b/aodhclient/v2/alarm.py @@ -115,7 +115,10 @@ class AlarmManager(base.Manager): else: self._clean_rules(alarm_update['type'], alarm_update) - if 'event_rule' in alarm_update: + if 'threshold_rule' in alarm_update: + alarm['threshold_rule'].update(alarm_update.get('threshold_rule')) + alarm_update.pop('threshold_rule') + elif 'event_rule' in alarm_update: if ('type' in alarm_update and alarm_update['type'] != alarm['type']): alarm.pop('%s_rule' % alarm['type'], None) diff --git a/aodhclient/v2/alarm_cli.py b/aodhclient/v2/alarm_cli.py index 893a27b..162279b 100644 --- a/aodhclient/v2/alarm_cli.py +++ b/aodhclient/v2/alarm_cli.py @@ -24,7 +24,7 @@ from aodhclient import exceptions from aodhclient.i18n import _ from aodhclient import utils -ALARM_TYPES = ['event', 'composite', +ALARM_TYPES = ['event', 'composite', 'threshold', 'gnocchi_resources_threshold', 'gnocchi_aggregation_by_metrics_threshold', 'gnocchi_aggregation_by_resources_threshold', @@ -33,7 +33,7 @@ ALARM_STATES = ['ok', 'alarm', 'insufficient data'] ALARM_SEVERITY = ['low', 'moderate', 'critical'] ALARM_OPERATORS = ['lt', 'le', 'eq', 'ne', 'ge', 'gt'] ALARM_OP_MAP = dict(zip(ALARM_OPERATORS, ('<', '<=', '=', '!=', '>=', '>'))) - +STATISTICS = ['max', 'min', 'avg', 'sum', 'count'] ALARM_LIST_COLS = ['alarm_id', 'type', 'name', 'state', 'severity', 'enabled'] @@ -103,7 +103,7 @@ def _format_alarm(alarm): alarm["time_constraints"] = jsonutils.dumps(alarm["time_constraints"], sort_keys=True, indent=2) - # only works for event alarm + # only works for threshold and event alarm if isinstance(alarm.get('query'), list): query_rows = [] for q in alarm['query']: @@ -265,7 +265,7 @@ class CliAlarmCreate(show.ShowOne): common_group = parser.add_argument_group('common alarm rules') common_group.add_argument( '--query', metavar='', dest='query', - help="For alarms of type event: " + help="For alarms of type threshold or event: " "key[op]data_type::value; list. data_type is optional, " "but if supplied must be string, integer, float, or boolean. " 'For alarms of ' @@ -284,11 +284,26 @@ class CliAlarmCreate(show.ShowOne): '--threshold', type=float, metavar='', dest='threshold', help='Threshold to evaluate against.') + # For event type alarm event_group = parser.add_argument_group('event alarm') event_group.add_argument( '--event-type', metavar='', dest='event_type', help='Event type to evaluate against') + # For Ceilometer threshold type alarm + threshold_group = parser.add_argument_group('threshold alarm') + threshold_group.add_argument( + '-m', '--meter-name', metavar='', + dest='meter_name', help='Meter to evaluate against') + threshold_group.add_argument( + '--period', type=int, metavar='', dest='period', + help='Length of each period (seconds) to evaluate over.') + threshold_group.add_argument( + '--statistic', metavar='', dest='statistic', + choices=STATISTICS, + help='Statistic to evaluate, one of: ' + str(STATISTICS)) + + # For common Gnocchi threshold type alarm gnocchi_common_group = parser.add_argument_group( 'common gnocchi alarm rules') gnocchi_common_group.add_argument( @@ -313,15 +328,19 @@ class CliAlarmCreate(show.ShowOne): '--resource-id', metavar='', dest='resource_id', help='The id of a resource.') + # For composite type alarm composite_group = parser.add_argument_group('composite alarm') composite_group.add_argument( '--composite-rule', metavar='', dest='composite_rule', type=jsonutils.loads, help='Composite threshold rule with JSON format, the form can ' - 'be a nested dict which combine gnocchi rules by ' + 'be a nested dict which combine threshold/gnocchi rules by ' '"and", "or". For example, the form is like: ' - '{"or":[RULE1, RULE2, {"and": [RULE3, RULE4]}]}.' + '{"or":[RULE1, RULE2, {"and": [RULE3, RULE4]}]}, The ' + 'RULEx can be basic threshold rules but must include a ' + '"type" field, like this: {"threshold": 0.8,' + '"meter_name":"cpu_util","type":"threshold"}' ) loadbalancer_member_health_group = parser.add_argument_group( @@ -366,7 +385,12 @@ class CliAlarmCreate(show.ShowOne): raise argparse.ArgumentTypeError(msg) def _validate_args(self, parsed_args): - if (parsed_args.type == 'gnocchi_resources_threshold' and + if (parsed_args.type == 'threshold' and + not (parsed_args.meter_name and parsed_args.threshold)): + self.parser.error('Threshold alarm requires -m/--meter-name and ' + '--threshold parameters. Meter name can be ' + 'found in Ceilometer') + elif (parsed_args.type == 'gnocchi_resources_threshold' and not (parsed_args.metrics and parsed_args.threshold is not None and parsed_args.resource_id and parsed_args.resource_type and parsed_args.aggregation_method)): @@ -408,8 +432,12 @@ class CliAlarmCreate(show.ShowOne): 'alarm_actions', 'ok_actions', 'insufficient_data_actions', 'time_constraints', 'repeat_actions']) - if parsed_args.type == 'event' and parsed_args.query: + if parsed_args.type in ('threshold', 'event') and parsed_args.query: parsed_args.query = utils.cli_to_array(parsed_args.query) + alarm['threshold_rule'] = utils.dict_from_parsed_args( + parsed_args, ['meter_name', 'period', 'evaluation_periods', + 'statistic', 'comparison_operator', 'threshold', + 'query']) alarm['event_rule'] = utils.dict_from_parsed_args( parsed_args, ['event_type', 'query']) alarm['gnocchi_resources_threshold_rule'] = ( diff --git a/doc/source/shell.rst b/doc/source/shell.rst index 2dcc053..f76b825 100644 --- a/doc/source/shell.rst +++ b/doc/source/shell.rst @@ -64,25 +64,34 @@ command. Examples -------- -Create an alarm:: +Create a Ceilometer threshold alarm:: - aodh alarm create -t gnocchi_resources_threshold --name alarm1 \ + openstack alarm create --name alarm1 --description 'CPU High Average' \ + --type threshold --meter-name cpu_util \ + --threshold 5 --comparison-operator gt --statistic avg \ + --period 60 --evaluation-periods 3 \ + --query "metadata.user_metadata.stack=$heat_stack_id" \ + --alarm-action 'log://' + +Create a Gnocchi threshold alarm:: + + openstack alarm create -t gnocchi_resources_threshold --name alarm1 \ --metric cpu_util --threshold 5 --resource_id \ --resource_type generic --aggregation_method mean --project-id List alarms:: - aodh alarm list + openstack alarm list List alarm with query parameters:: - aodh alarm list --query "state=alarm and type=gnocchi_resources_threshold" + openstack alarm list --query "state=alarm and type=gnocchi_resources_threshold" Show an alarm's history:: - aodh alarm-history show + openstack alarm-history show Search alarm history data:: - aodh alarm-history search --query 'timestamp>"2016-03-09T01:22:35"' + openstack alarm-history search --query 'timestamp>"2016-03-09T01:22:35"' diff --git a/releasenotes/notes/ussuri-add-threshold-alarm-47e012620fd611ea.yaml b/releasenotes/notes/ussuri-add-threshold-alarm-47e012620fd611ea.yaml new file mode 100644 index 0000000..5cddb49 --- /dev/null +++ b/releasenotes/notes/ussuri-add-threshold-alarm-47e012620fd611ea.yaml @@ -0,0 +1,5 @@ +--- +features: + - Add threshold type alarm back. Gnocchi is not actively maintained currently + but there are still users running Ceilometer in production and relying on + Ceilometer for auditing and billing. \ No newline at end of file