diff --git a/aodh/cmd/alarm_conversion.py b/aodh/cmd/alarm_conversion.py new file mode 100644 index 000000000..e8805e2d4 --- /dev/null +++ b/aodh/cmd/alarm_conversion.py @@ -0,0 +1,146 @@ +# +# 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. + +"""A tool for converting combination alarms to composite alarms. +""" + +import datetime +import uuid + +import argparse +from oslo_log import log + +from aodh.i18n import _LI, _LW +from aodh import service +from aodh import storage +from aodh.storage import models + +LOG = log.getLogger(__name__) + + +class DependentAlarmNotFound(Exception): + """The dependent alarm is not found.""" + + def __init__(self, com_alarm_id, dependent_alarm_id): + self.com_alarm_id = com_alarm_id + self.dependent_alarm_id = dependent_alarm_id + + +class UnsupportedSubAlarmType(Exception): + """Unsupported sub-alarm type.""" + + def __init__(self, sub_alarm_id, sub_alarm_type): + self.sub_alarm_id = sub_alarm_id + self.sub_alarm_type = sub_alarm_type + + +def _generate_composite_rule(conn, combin_alarm): + alarm_ids = combin_alarm.rule['alarm_ids'] + com_op = combin_alarm.rule['operator'] + LOG.info(_LI('Start converting combination alarm %(alarm)s, it depends on ' + 'alarms: %(alarm_ids)s'), + {'alarm': combin_alarm.alarm_id, 'alarm_ids': str(alarm_ids)}) + threshold_rules = [] + for alarm_id in alarm_ids: + try: + sub_alarm = list(conn.get_alarms(alarm_id=alarm_id))[0] + except IndexError: + raise DependentAlarmNotFound(combin_alarm.alarm_id, alarm_id) + if sub_alarm.type in ('threshold', 'gnocchi_resources_threshold', + 'gnocchi_aggregation_by_metrics_threshold', + 'gnocchi_aggregation_by_resources_threshold'): + sub_alarm.rule.update(type=sub_alarm.type) + threshold_rules.append(sub_alarm.rule) + elif sub_alarm.type == 'combination': + threshold_rules.append(_generate_composite_rule(conn, sub_alarm)) + else: + raise UnsupportedSubAlarmType(alarm_id, sub_alarm.type) + else: + return {com_op: threshold_rules} + + +def get_parser(): + parser = argparse.ArgumentParser( + description='for converting combination alarms to composite alarms.') + parser.add_argument( + '--delete-combination-alarm', + default=False, + type=bool, + help='Delete the combination alarm when conversion is done.', + ) + parser.add_argument( + '--alarm-id', + default=None, + type=str, + help='Only convert the alarm specified by this option.', + ) + return parser + + +def conversion(): + confirm = raw_input("This tool is used for converting the combination " + "alarms to composite alarms, please type 'yes' to " + "confirm: ") + if confirm != 'yes': + print("Alarm conversion aborted!") + return + args = get_parser().parse_args() + conf = service.prepare_service() + conn = storage.get_connection_from_config(conf) + combination_alarms = list(conn.get_alarms(alarm_type='combination', + alarm_id=args.alarm_id or None)) + count = 0 + for alarm in combination_alarms: + new_name = 'From-combination: %s' % alarm.alarm_id + n_alarm = list(conn.get_alarms(name=new_name, alarm_type='composite')) + if n_alarm: + LOG.warning(_LW('Alarm %(alarm)s has been already converted as ' + 'composite alarm: %(n_alarm_id)s, skipped.'), + {'alarm': alarm.alarm_id, + 'n_alarm_id': n_alarm[0].alarm_id}) + continue + try: + composite_rule = _generate_composite_rule(conn, alarm) + except DependentAlarmNotFound as e: + LOG.warning(_LW('The dependent alarm %(dep_alarm)s of alarm %' + '(com_alarm)s not found, skipped.'), + {'com_alarm': e.com_alarm_id, + 'dep_alarm': e.dependent_alarm_id}) + continue + except UnsupportedSubAlarmType as e: + LOG.warning(_LW('Alarm conversion from combination to composite ' + 'only support combination alarms depending ' + 'threshold alarms, the type of alarm %(alarm)s ' + 'is: %(type)s, skipped.'), + {'alarm': e.sub_alarm_id, 'type': e.sub_alarm_type}) + continue + new_alarm = models.Alarm(**alarm.as_dict()) + new_alarm.alarm_id = str(uuid.uuid4()) + new_alarm.name = new_name + new_alarm.type = 'composite' + new_alarm.description = ('composite alarm converted from combination ' + 'alarm: %s' % alarm.alarm_id) + new_alarm.rule = composite_rule + new_alarm.timestamp = datetime.datetime.now() + conn.create_alarm(new_alarm) + LOG.info(_LI('End Converting combination alarm %(s_alarm)s to ' + 'composite alarm %(d_alarm)s'), + {'s_alarm': alarm.alarm_id, 'd_alarm': new_alarm.alarm_id}) + count += 1 + if args.delete_combination_alarm: + for alarm in combination_alarms: + LOG.info(_LI('Deleting the combination alarm %s...'), + alarm.alarm_id) + conn.delete_alarm(alarm.alarm_id) + LOG.info(_LI('%s combination alarms have been converted to composite ' + 'alarms.'), count) diff --git a/aodh/tests/functional/api/v2/test_alarm_scenarios.py b/aodh/tests/functional/api/v2/test_alarm_scenarios.py index 7eb1b5e77..7e0712897 100644 --- a/aodh/tests/functional/api/v2/test_alarm_scenarios.py +++ b/aodh/tests/functional/api/v2/test_alarm_scenarios.py @@ -25,6 +25,7 @@ from oslo_serialization import jsonutils import six from six import moves +from aodh.cmd import alarm_conversion from aodh import messaging from aodh.storage import models from aodh.tests import constants @@ -3383,3 +3384,66 @@ class TestPaginationQuery(TestAlarmsBase): key=lambda d: (d['event_id'], d['timestamp']), reverse=True) self.assertEqual(sorted_data, data) + + +class TestCombinationCompositeConversion(TestAlarmsBase): + def setUp(self): + super(TestCombinationCompositeConversion, self).setUp() + alarms = default_alarms(self.auth_headers) + for alarm in alarms: + self.alarm_conn.create_alarm(alarm) + com_parameters = alarms[3].as_dict() + com_parameters.update(dict(name='name5', alarm_id='e', description='e', + rule=dict(alarm_ids=['b', 'c'], + operator='and'))) + combin1 = models.Alarm(**com_parameters) + self.alarm_conn.create_alarm(combin1) + com_parameters.update(dict(name='name6', alarm_id='f', description='f', + rule=dict(alarm_ids=['d', 'e'], + operator='and'))) + combin2 = models.Alarm(**com_parameters) + self.alarm_conn.create_alarm(combin2) + + def test_conversion_without_combination_deletion(self): + data = self.get_json('/alarms', headers=self.auth_headers) + self.assertEqual(6, len(data)) + url = '/alarms?q.field=type&q.op=eq&q.value=combination' + combination_alarms = self.get_json(url, headers=self.auth_headers) + self.assertEqual(3, len(combination_alarms)) + test_args = alarm_conversion.get_parser().parse_args([]) + with mock.patch('__builtin__.raw_input', return_value='yes'): + with mock.patch('argparse.ArgumentParser.parse_args', + return_value=test_args): + alarm_conversion.conversion() + url = '/alarms?q.field=type&q.op=eq&q.value=composite' + composite_alarms = self.get_json(url, headers=self.auth_headers) + self.assertEqual(3, len(composite_alarms)) + url = '/alarms?q.field=type&q.op=eq&q.value=combination' + combination_alarms = self.get_json(url, headers=self.auth_headers) + self.assertEqual(3, len(combination_alarms)) + + def test_conversion_with_combination_deletion(self): + test_args = alarm_conversion.get_parser().parse_args( + ['--delete-combination-alarm', 'True']) + with mock.patch('__builtin__.raw_input', return_value='yes'): + with mock.patch('argparse.ArgumentParser.parse_args', + return_value=test_args): + alarm_conversion.conversion() + url = '/alarms?q.field=type&q.op=eq&q.value=composite' + composite_alarms = self.get_json(url, headers=self.auth_headers) + self.assertEqual(3, len(composite_alarms)) + url = '/alarms?q.field=type&q.op=eq&q.value=combination' + combination_alarms = self.get_json(url, headers=self.auth_headers) + self.assertEqual(0, len(combination_alarms)) + + def test_conversion_with_alarm_specified(self): + test_args = alarm_conversion.get_parser().parse_args( + ['--alarm-id', 'e']) + with mock.patch('__builtin__.raw_input', return_value='yes'): + with mock.patch('argparse.ArgumentParser.parse_args', + return_value=test_args): + alarm_conversion.conversion() + url = '/alarms?q.field=type&q.op=eq&q.value=composite' + composite_alarms = self.get_json(url, headers=self.auth_headers) + self.assertEqual(1, len(composite_alarms)) + self.assertEqual('From-combination: e', composite_alarms[0]['name']) diff --git a/releasenotes/notes/support-combination-to-composite-conversion-3e688a6b7d01a57e.yaml b/releasenotes/notes/support-combination-to-composite-conversion-3e688a6b7d01a57e.yaml new file mode 100644 index 000000000..9499545d9 --- /dev/null +++ b/releasenotes/notes/support-combination-to-composite-conversion-3e688a6b7d01a57e.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - > + Add a tool for converting combination alarms to composite alarms, + since we have deprecated the combination alarm support and recommend + to use composite alarm to perform multiple conditions alarming. diff --git a/setup.cfg b/setup.cfg index f555976c1..d62838c00 100644 --- a/setup.cfg +++ b/setup.cfg @@ -115,6 +115,7 @@ console_scripts = aodh-notifier = aodh.cmd.alarm:notifier aodh-listener = aodh.cmd.alarm:listener aodh-data-migration = aodh.cmd.data_migration:main + aodh-combination-alarm-conversion = aodh.cmd.alarm_conversion:conversion oslo.config.opts = aodh = aodh.opts:list_opts