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
This commit is contained in:
Lingxian Kong 2019-11-26 12:10:14 +13:00
parent ec0ee160cf
commit 428e394e5a
6 changed files with 254 additions and 15 deletions

View File

@ -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"')

View File

@ -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',

View File

@ -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)

View File

@ -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='<QUERY>', 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='<THRESHOLD>',
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='<EVENT_TYPE>',
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='<METER NAME>',
dest='meter_name', help='Meter to evaluate against')
threshold_group.add_argument(
'--period', type=int, metavar='<PERIOD>', dest='period',
help='Length of each period (seconds) to evaluate over.')
threshold_group.add_argument(
'--statistic', metavar='<STATISTIC>', 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='<RESOURCE_ID>',
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='<COMPOSITE_RULE>',
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'] = (

View File

@ -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 <RES_ID> \
--resource_type generic --aggregation_method mean --project-id <PROJ_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 <ALARM_ID>
openstack alarm-history show <ALARM_ID>
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"'

View File

@ -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.