From ae514b04a95cadb3ac1819a9097dbee694f4596b Mon Sep 17 00:00:00 2001 From: Ondrej Mular Date: Tue, 21 Jun 2016 15:23:07 +0200 Subject: [PATCH] bz1315371-01-add support for pacemaker alerts --- .pylintrc | 2 +- pcs/alert.py | 237 +++++++++ pcs/app.py | 6 + pcs/cli/common/env.py | 1 + pcs/cli/common/lib_wrapper.py | 28 +- pcs/cli/common/middleware.py | 2 +- pcs/common/report_codes.py | 6 + pcs/config.py | 4 + pcs/lib/cib/alert.py | 281 +++++++++++ pcs/lib/cib/nvpair.py | 90 ++++ pcs/lib/cib/test/test_alert.py | 931 +++++++++++++++++++++++++++++++++++ pcs/lib/cib/test/test_nvpair.py | 206 ++++++++ pcs/lib/cib/tools.py | 127 +++++ pcs/lib/commands/alert.py | 169 +++++++ pcs/lib/commands/test/test_alert.py | 639 ++++++++++++++++++++++++ pcs/lib/commands/test/test_ticket.py | 2 +- pcs/lib/env.py | 32 +- pcs/lib/pacemaker.py | 17 +- pcs/lib/reports.py | 91 ++++ pcs/pcs.8 | 25 + pcs/test/resources/cib-empty-2.5.xml | 10 + pcs/test/test_alert.py | 363 ++++++++++++++ pcs/test/test_lib_cib_tools.py | 181 ++++++- pcs/test/test_lib_env.py | 140 +++++- pcs/test/test_lib_pacemaker.py | 24 +- pcs/test/test_resource.py | 6 + pcs/test/test_stonith.py | 3 + pcs/test/tools/color_text_runner.py | 10 + pcs/usage.py | 43 ++ pcs/utils.py | 9 +- 30 files changed, 3649 insertions(+), 36 deletions(-) create mode 100644 pcs/alert.py create mode 100644 pcs/lib/cib/alert.py create mode 100644 pcs/lib/cib/nvpair.py create mode 100644 pcs/lib/cib/test/test_alert.py create mode 100644 pcs/lib/cib/test/test_nvpair.py create mode 100644 pcs/lib/commands/alert.py create mode 100644 pcs/lib/commands/test/test_alert.py create mode 100644 pcs/test/resources/cib-empty-2.5.xml create mode 100644 pcs/test/test_alert.py diff --git a/.pylintrc b/.pylintrc index 661f3d2..e378e6a 100644 --- a/.pylintrc +++ b/.pylintrc @@ -92,7 +92,7 @@ dummy-variables-rgx=_$|dummy [FORMAT] # Maximum number of lines in a module -max-module-lines=4571 +max-module-lines=4577 # Maximum number of characters on a single line. max-line-length=1291 diff --git a/pcs/alert.py b/pcs/alert.py new file mode 100644 index 0000000..d3a6e28 --- /dev/null +++ b/pcs/alert.py @@ -0,0 +1,237 @@ +from __future__ import ( + absolute_import, + division, + print_function, + unicode_literals, +) + +import sys + +from pcs import ( + usage, + utils, +) +from pcs.cli.common.errors import CmdLineInputError +from pcs.cli.common.parse_args import prepare_options +from pcs.cli.common.console_report import indent +from pcs.lib.errors import LibraryError + + +def alert_cmd(*args): + argv = args[1] + if not argv: + sub_cmd = "config" + else: + sub_cmd = argv.pop(0) + try: + if sub_cmd == "help": + usage.alert(argv) + elif sub_cmd == "create": + alert_add(*args) + elif sub_cmd == "update": + alert_update(*args) + elif sub_cmd == "remove": + alert_remove(*args) + elif sub_cmd == "config" or sub_cmd == "show": + print_alert_config(*args) + elif sub_cmd == "recipient": + recipient_cmd(*args) + else: + raise CmdLineInputError() + except LibraryError as e: + utils.process_library_reports(e.args) + except CmdLineInputError as e: + utils.exit_on_cmdline_input_errror(e, "alert", sub_cmd) + + +def recipient_cmd(*args): + argv = args[1] + + if not argv: + usage.alert(["recipient"]) + sys.exit(1) + + sub_cmd = argv.pop(0) + try: + if sub_cmd == "help": + usage.alert(["recipient"]) + elif sub_cmd == "add": + recipient_add(*args) + elif sub_cmd == "update": + recipient_update(*args) + elif sub_cmd == "remove": + recipient_remove(*args) + except CmdLineInputError as e: + utils.exit_on_cmdline_input_errror( + e, "alert", "recipient {0}".format(sub_cmd) + ) + + +def parse_cmd_sections(arg_list, section_list): + output = dict([(section, []) for section in section_list + ["main"]]) + cur_section = "main" + for arg in arg_list: + if arg in section_list: + cur_section = arg + continue + output[cur_section].append(arg) + + return output + + +def ensure_only_allowed_options(parameter_dict, allowed_list): + for arg, value in parameter_dict.items(): + if arg not in allowed_list: + raise CmdLineInputError( + "Unexpected parameter '{0}={1}'".format(arg, value) + ) + + +def alert_add(lib, argv, modifiers): + if not argv: + raise CmdLineInputError() + + sections = parse_cmd_sections(argv, ["options", "meta"]) + main_args = prepare_options(sections["main"]) + ensure_only_allowed_options(main_args, ["id", "description", "path"]) + + lib.alert.create_alert( + main_args.get("id", None), + main_args.get("path", None), + prepare_options(sections["options"]), + prepare_options(sections["meta"]), + main_args.get("description", None) + ) + + +def alert_update(lib, argv, modifiers): + if not argv: + raise CmdLineInputError() + + alert_id = argv[0] + + sections = parse_cmd_sections(argv[1:], ["options", "meta"]) + main_args = prepare_options(sections["main"]) + ensure_only_allowed_options(main_args, ["description", "path"]) + + lib.alert.update_alert( + alert_id, + main_args.get("path", None), + prepare_options(sections["options"]), + prepare_options(sections["meta"]), + main_args.get("description", None) + ) + + +def alert_remove(lib, argv, modifiers): + if len(argv) != 1: + raise CmdLineInputError() + + lib.alert.remove_alert(argv[0]) + + +def recipient_add(lib, argv, modifiers): + if len(argv) < 2: + raise CmdLineInputError() + + alert_id = argv[0] + recipient_value = argv[1] + + sections = parse_cmd_sections(argv[2:], ["options", "meta"]) + main_args = prepare_options(sections["main"]) + ensure_only_allowed_options(main_args, ["description"]) + + lib.alert.add_recipient( + alert_id, + recipient_value, + prepare_options(sections["options"]), + prepare_options(sections["meta"]), + main_args.get("description", None) + ) + + +def recipient_update(lib, argv, modifiers): + if len(argv) < 2: + raise CmdLineInputError() + + alert_id = argv[0] + recipient_value = argv[1] + + sections = parse_cmd_sections(argv[2:], ["options", "meta"]) + main_args = prepare_options(sections["main"]) + ensure_only_allowed_options(main_args, ["description"]) + + lib.alert.update_recipient( + alert_id, + recipient_value, + prepare_options(sections["options"]), + prepare_options(sections["meta"]), + main_args.get("description", None) + ) + + +def recipient_remove(lib, argv, modifiers): + if len(argv) != 2: + raise CmdLineInputError() + + lib.alert.remove_recipient(argv[0], argv[1]) + + +def _nvset_to_str(nvset_obj): + output = [] + for nvpair_obj in nvset_obj: + output.append("{key}={value}".format( + key=nvpair_obj["name"], value=nvpair_obj["value"] + )) + return " ".join(output) + + +def __description_attributes_to_str(obj): + output = [] + if obj.get("description"): + output.append("Description: {desc}".format(desc=obj["description"])) + if obj.get("instance_attributes"): + output.append("Options: {attributes}".format( + attributes=_nvset_to_str(obj["instance_attributes"]) + )) + if obj.get("meta_attributes"): + output.append("Meta options: {attributes}".format( + attributes=_nvset_to_str(obj["meta_attributes"]) + )) + return output + + +def _alert_to_str(alert): + content = [] + content.extend(__description_attributes_to_str(alert)) + + recipients = [] + for recipient in alert.get("recipient_list", []): + recipients.extend( _recipient_to_str(recipient)) + + if recipients: + content.append("Recipients:") + content.extend(indent(recipients, 1)) + + return ["Alert: {alert_id} (path={path})".format( + alert_id=alert["id"], path=alert["path"] + )] + indent(content, 1) + + +def _recipient_to_str(recipient): + return ["Recipient: {value}".format(value=recipient["value"])] + indent( + __description_attributes_to_str(recipient), 1 + ) + + +def print_alert_config(lib, argv, modifiers): + if argv: + raise CmdLineInputError() + + print("Alerts:") + alert_list = lib.alert.get_all_alerts() + if alert_list: + for alert in alert_list: + print("\n".join(indent(_alert_to_str(alert), 1))) + else: + print(" No alerts defined") diff --git a/pcs/app.py b/pcs/app.py index 3c4865f..3758ee4 100644 --- a/pcs/app.py +++ b/pcs/app.py @@ -27,6 +27,7 @@ from pcs import ( stonith, usage, utils, + alert, ) from pcs.cli.common import completion @@ -193,6 +194,11 @@ def main(argv=None): argv, utils.get_modificators() ), + "alert": lambda args: alert.alert_cmd( + utils.get_library_wrapper(), + args, + utils.get_modificators() + ), } if command not in cmd_map: usage.main() diff --git a/pcs/cli/common/env.py b/pcs/cli/common/env.py index f407981..2ba4f70 100644 --- a/pcs/cli/common/env.py +++ b/pcs/cli/common/env.py @@ -8,6 +8,7 @@ from __future__ import ( class Env(object): def __init__(self): self.cib_data = None + self.cib_upgraded = False self.user = None self.groups = None self.corosync_conf_data = None diff --git a/pcs/cli/common/lib_wrapper.py b/pcs/cli/common/lib_wrapper.py index 909b435..2ba5602 100644 --- a/pcs/cli/common/lib_wrapper.py +++ b/pcs/cli/common/lib_wrapper.py @@ -19,6 +19,7 @@ from pcs.lib.commands import ( quorum, qdevice, sbd, + alert, ) from pcs.cli.common.reports import ( LibraryReportProcessorToConsole as LibraryReportProcessorToConsole, @@ -42,6 +43,14 @@ def cli_env_to_lib_env(cli_env): cli_env.auth_tokens_getter, ) +def lib_env_to_cli_env(lib_env, cli_env): + if not lib_env.is_cib_live: + cli_env.cib_data = lib_env._get_cib_xml() + cli_env.cib_upgraded = lib_env.cib_upgraded + if not lib_env.is_corosync_conf_live: + cli_env.corosync_conf_data = lib_env.get_corosync_conf_data() + return cli_env + def bind(cli_env, run_with_middleware, run_library_command): def run(cli_env, *args, **kwargs): lib_env = cli_env_to_lib_env(cli_env) @@ -50,10 +59,7 @@ def bind(cli_env, run_with_middleware, run_library_command): #midlewares needs finish its work and they see only cli_env #so we need reflect some changes to cli_env - if not lib_env.is_cib_live: - cli_env.cib_data = lib_env.get_cib_xml() - if not lib_env.is_corosync_conf_live: - cli_env.corosync_conf_data = lib_env.get_corosync_conf_data() + lib_env_to_cli_env(lib_env, cli_env) return lib_call_result return partial(run_with_middleware, run, cli_env) @@ -140,6 +146,20 @@ def load_module(env, middleware_factory, name): "get_local_sbd_config": sbd.get_local_sbd_config, } ) + if name == "alert": + return bind_all( + env, + middleware.build(middleware_factory.cib), + { + "create_alert": alert.create_alert, + "update_alert": alert.update_alert, + "remove_alert": alert.remove_alert, + "add_recipient": alert.add_recipient, + "update_recipient": alert.update_recipient, + "remove_recipient": alert.remove_recipient, + "get_all_alerts": alert.get_all_alerts, + } + ) raise Exception("No library part '{0}'".format(name)) diff --git a/pcs/cli/common/middleware.py b/pcs/cli/common/middleware.py index 16618e1..e53e138 100644 --- a/pcs/cli/common/middleware.py +++ b/pcs/cli/common/middleware.py @@ -34,7 +34,7 @@ def cib(use_local_cib, load_cib_content, write_cib): result_of_next = next_in_line(env, *args, **kwargs) if use_local_cib: - write_cib(env.cib_data) + write_cib(env.cib_data, env.cib_upgraded) return result_of_next return apply diff --git a/pcs/common/report_codes.py b/pcs/common/report_codes.py index 927df35..bda982a 100644 --- a/pcs/common/report_codes.py +++ b/pcs/common/report_codes.py @@ -20,11 +20,17 @@ SKIP_OFFLINE_NODES = "SKIP_OFFLINE_NODES" AGENT_GENERAL_ERROR = "AGENT_GENERAL_ERROR" AGENT_NOT_FOUND = "AGENT_NOT_FOUND" BAD_CLUSTER_STATE_FORMAT = 'BAD_CLUSTER_STATE_FORMAT' +CIB_ALERT_NOT_FOUND = "CIB_ALERT_NOT_FOUND" +CIB_ALERT_RECIPIENT_ALREADY_EXISTS = "CIB_ALERT_RECIPIENT_ALREADY_EXISTS" +CIB_ALERT_RECIPIENT_NOT_FOUND = "CIB_ALERT_RECIPIENT_NOT_FOUND" CIB_CANNOT_FIND_MANDATORY_SECTION = "CIB_CANNOT_FIND_MANDATORY_SECTION" CIB_LOAD_ERROR_BAD_FORMAT = "CIB_LOAD_ERROR_BAD_FORMAT" CIB_LOAD_ERROR = "CIB_LOAD_ERROR" CIB_LOAD_ERROR_SCOPE_MISSING = "CIB_LOAD_ERROR_SCOPE_MISSING" CIB_PUSH_ERROR = "CIB_PUSH_ERROR" +CIB_UPGRADE_FAILED = "CIB_UPGRADE_FAILED" +CIB_UPGRADE_FAILED_TO_MINIMAL_REQUIRED_VERSION = "CIB_UPGRADE_FAILED_TO_MINIMAL_REQUIRED_VERSION" +CIB_UPGRADE_SUCCESSFUL = "CIB_UPGRADE_SUCCESSFUL" CLUSTER_RESTART_REQUIRED_TO_APPLY_CHANGES = "CLUSTER_RESTART_REQUIRED_TO_APPLY_CHANGES" CMAN_BROADCAST_ALL_RINGS = 'CMAN_BROADCAST_ALL_RINGS' CMAN_UDPU_RESTART_REQUIRED = 'CMAN_UDPU_RESTART_REQUIRED' diff --git a/pcs/config.py b/pcs/config.py index 51de822..4659c5b 100644 --- a/pcs/config.py +++ b/pcs/config.py @@ -38,6 +38,7 @@ from pcs import ( stonith, usage, utils, + alert, ) from pcs.lib.errors import LibraryError from pcs.lib.commands import quorum as lib_quorum @@ -123,6 +124,9 @@ def config_show_cib(): ticket_command.show(lib, [], modificators) print() + alert.print_alert_config(lib, [], modificators) + + print() del utils.pcs_options["--all"] print("Resources Defaults:") resource.show_defaults("rsc_defaults", indent=" ") diff --git a/pcs/lib/cib/alert.py b/pcs/lib/cib/alert.py new file mode 100644 index 0000000..6b72996 --- /dev/null +++ b/pcs/lib/cib/alert.py @@ -0,0 +1,281 @@ +from __future__ import ( + absolute_import, + division, + print_function, + unicode_literals, +) + +from lxml import etree + +from pcs.lib import reports +from pcs.lib.errors import LibraryError +from pcs.lib.cib.nvpair import update_nvset, get_nvset +from pcs.lib.cib.tools import ( + check_new_id_applicable, + get_sub_element, + find_unique_id, + get_alerts, +) + + +def update_instance_attributes(tree, element, attribute_dict): + """ + Updates instance attributes of element. Returns updated instance + attributes element. + + tree -- cib etree node + element -- parent element of instance attributes + attribute_dict -- dictionary of nvpairs + """ + return update_nvset("instance_attributes", tree, element, attribute_dict) + + +def update_meta_attributes(tree, element, attribute_dict): + """ + Updates meta attributes of element. Returns updated meta attributes element. + + tree -- cib etree node + element -- parent element of meta attributes + attribute_dict -- dictionary of nvpairs + """ + return update_nvset("meta_attributes", tree, element, attribute_dict) + + +def _update_optional_attribute(element, attribute, value): + """ + Update optional attribute of element. Remove existing element if value + is empty. + + element -- parent element of specified attribute + attribute -- attribute to be updated + value -- new value + """ + if value is None: + return + if value: + element.set(attribute, value) + elif attribute in element.attrib: + del element.attrib[attribute] + + +def get_alert_by_id(tree, alert_id): + """ + Returns alert element with specified id. + Raises AlertNotFound if alert with specified id doesn't exist. + + tree -- cib etree node + alert_id -- id of alert + """ + alert = get_alerts(tree).find("./alert[@id='{0}']".format(alert_id)) + if alert is None: + raise LibraryError(reports.cib_alert_not_found(alert_id)) + return alert + + +def get_recipient(alert, recipient_value): + """ + Returns recipient element with value recipient_value which belong to + specified alert. + Raises RecipientNotFound if recipient doesn't exist. + + alert -- parent element of required recipient + recipient_value -- value of recipient + """ + recipient = alert.find( + "./recipient[@value='{0}']".format(recipient_value) + ) + if recipient is None: + raise LibraryError(reports.cib_alert_recipient_not_found( + alert.get("id"), recipient_value + )) + return recipient + + +def create_alert(tree, alert_id, path, description=""): + """ + Create new alert element. Returns newly created element. + Raises LibraryError if element with specified id already exists. + + tree -- cib etree node + alert_id -- id of new alert, it will be generated if it is None + path -- path to script + description -- description + """ + if alert_id: + check_new_id_applicable(tree, "alert-id", alert_id) + else: + alert_id = find_unique_id(tree, "alert") + + alert = etree.SubElement(get_alerts(tree), "alert", id=alert_id, path=path) + if description: + alert.set("description", description) + + return alert + + +def update_alert(tree, alert_id, path, description=None): + """ + Update existing alert. Return updated alert element. + Raises AlertNotFound if alert with specified id doesn't exist. + + tree -- cib etree node + alert_id -- id of alert to be updated + path -- new value of path, stay unchanged if None + description -- new value of description, stay unchanged if None, remove + if empty + """ + alert = get_alert_by_id(tree, alert_id) + if path: + alert.set("path", path) + _update_optional_attribute(alert, "description", description) + return alert + + +def remove_alert(tree, alert_id): + """ + Remove alert with specified id. + Raises AlertNotFound if alert with specified id doesn't exist. + + tree -- cib etree node + alert_id -- id of alert which should be removed + """ + alert = get_alert_by_id(tree, alert_id) + alert.getparent().remove(alert) + + +def add_recipient( + tree, + alert_id, + recipient_value, + description="" +): + """ + Add recipient to alert with specified id. Returns added recipient element. + Raises AlertNotFound if alert with specified id doesn't exist. + Raises LibraryError if recipient already exists. + + tree -- cib etree node + alert_id -- id of alert which should be parent of new recipient + recipient_value -- value of recipient + description -- description of recipient + """ + alert = get_alert_by_id(tree, alert_id) + + recipient = alert.find( + "./recipient[@value='{0}']".format(recipient_value) + ) + if recipient is not None: + raise LibraryError(reports.cib_alert_recipient_already_exists( + alert_id, recipient_value + )) + + recipient = etree.SubElement( + alert, + "recipient", + id=find_unique_id(tree, "{0}-recipient".format(alert_id)), + value=recipient_value + ) + + if description: + recipient.set("description", description) + + return recipient + + +def update_recipient(tree, alert_id, recipient_value, description): + """ + Update specified recipient. Returns updated recipient element. + Raises AlertNotFound if alert with specified id doesn't exist. + Raises RecipientNotFound if recipient doesn't exist. + + tree -- cib etree node + alert_id -- id of alert, parent element of recipient + recipient_value -- recipient value + description -- description, if empty it will be removed, stay unchanged + if None + """ + recipient = get_recipient( + get_alert_by_id(tree, alert_id), recipient_value + ) + _update_optional_attribute(recipient, "description", description) + return recipient + + +def remove_recipient(tree, alert_id, recipient_value): + """ + Remove specified recipient. + Raises AlertNotFound if alert with specified id doesn't exist. + Raises RecipientNotFound if recipient doesn't exist. + + tree -- cib etree node + alert_id -- id of alert, parent element of recipient + recipient_value -- recipient value + """ + recipient = get_recipient( + get_alert_by_id(tree, alert_id), recipient_value + ) + recipient.getparent().remove(recipient) + + +def get_all_recipients(alert): + """ + Returns list of all recipient of specified alert. Format: + [ + { + "id": , + "value": , + "description": , + "instance_attributes": , + "meta_attributes": + } + ] + + alert -- parent element of recipients to return + """ + recipient_list = [] + for recipient in alert.findall("./recipient"): + recipient_list.append({ + "id": recipient.get("id"), + "value": recipient.get("value"), + "description": recipient.get("description", ""), + "instance_attributes": get_nvset( + get_sub_element(recipient, "instance_attributes") + ), + "meta_attributes": get_nvset( + get_sub_element(recipient, "meta_attributes") + ) + }) + return recipient_list + + +def get_all_alerts(tree): + """ + Returns list of all alerts specified in tree. Format: + [ + { + "id": , + "path": , + "description": , + "instance_attributes": , + "meta_attributes": , + "recipients_list": + } + ] + + tree -- cib etree node + """ + alert_list = [] + for alert in get_alerts(tree).findall("./alert"): + alert_list.append({ + "id": alert.get("id"), + "path": alert.get("path"), + "description": alert.get("description", ""), + "instance_attributes": get_nvset( + get_sub_element(alert, "instance_attributes") + ), + "meta_attributes": get_nvset( + get_sub_element(alert, "meta_attributes") + ), + "recipient_list": get_all_recipients(alert) + }) + return alert_list diff --git a/pcs/lib/cib/nvpair.py b/pcs/lib/cib/nvpair.py new file mode 100644 index 0000000..d1a0cae --- /dev/null +++ b/pcs/lib/cib/nvpair.py @@ -0,0 +1,90 @@ +from __future__ import ( + absolute_import, + division, + print_function, + unicode_literals, +) + +from lxml import etree + +from pcs.lib.cib.tools import ( + get_sub_element, + find_unique_id, +) + + +def update_nvpair(tree, element, name, value): + """ + Update nvpair, create new if it doesn't yet exist or remove existing + nvpair if value is empty. Returns created/updated/removed nvpair element. + + tree -- cib etree node + element -- element in which nvpair should be added/updated/removed + name -- name of nvpair + value -- value of nvpair + """ + nvpair = element.find("./nvpair[@name='{0}']".format(name)) + if nvpair is None: + if not value: + return None + nvpair_id = find_unique_id( + tree, "{0}-{1}".format(element.get("id"), name) + ) + nvpair = etree.SubElement( + element, "nvpair", id=nvpair_id, name=name, value=value + ) + else: + if value: + nvpair.set("value", value) + else: + # remove nvpair if value is empty + element.remove(nvpair) + return nvpair + + +def update_nvset(tag_name, tree, element, attribute_dict): + """ + This method updates nvset specified by tag_name. If specified nvset + doesn't exist it will be created. Returns updated nvset element or None if + attribute_dict is empty. + + tag_name -- tag name of nvset element + tree -- cib etree node + element -- parent element of nvset + attribute_dict -- dictionary of nvpairs + """ + if not attribute_dict: + return None + + attributes = get_sub_element(element, tag_name, find_unique_id( + tree, "{0}-{1}".format(element.get("id"), tag_name) + ), 0) + + for name, value in sorted(attribute_dict.items()): + update_nvpair(tree, attributes, name, value) + + return attributes + + +def get_nvset(nvset): + """ + Returns nvset element as list of nvpairs with format: + [ + { + "id": , + "name": , + "value": + }, + ... + ] + + nvset -- nvset element + """ + nvpair_list = [] + for nvpair in nvset.findall("./nvpair"): + nvpair_list.append({ + "id": nvpair.get("id"), + "name": nvpair.get("name"), + "value": nvpair.get("value", "") + }) + return nvpair_list diff --git a/pcs/lib/cib/test/test_alert.py b/pcs/lib/cib/test/test_alert.py new file mode 100644 index 0000000..c387aaf --- /dev/null +++ b/pcs/lib/cib/test/test_alert.py @@ -0,0 +1,931 @@ +from __future__ import ( + absolute_import, + division, + print_function, + unicode_literals, +) + +from unittest import TestCase + +from lxml import etree + +from pcs.common import report_codes +from pcs.lib.cib import alert +from pcs.lib.errors import ReportItemSeverity as severities +from pcs.test.tools.assertions import( + assert_raise_library_error, + assert_xml_equal, +) +from pcs.test.tools.pcs_mock import mock + + +@mock.patch("pcs.lib.cib.alert.update_nvset") +class UpdateInstanceAttributesTest(TestCase): + def test_success(self, mock_update_nvset): + ret_val = etree.Element("nvset") + tree = etree.Element("tree") + element = etree.Element("element") + attributes = {"a": 1} + mock_update_nvset.return_value = ret_val + self.assertEqual( + alert.update_instance_attributes(tree, element, attributes), + ret_val + ) + mock_update_nvset.assert_called_once_with( + "instance_attributes", tree, element, attributes + ) + + +@mock.patch("pcs.lib.cib.alert.update_nvset") +class UpdateMetaAttributesTest(TestCase): + def test_success(self, mock_update_nvset): + ret_val = etree.Element("nvset") + tree = etree.Element("tree") + element = etree.Element("element") + attributes = {"a": 1} + mock_update_nvset.return_value = ret_val + self.assertEqual( + alert.update_meta_attributes(tree, element, attributes), + ret_val + ) + mock_update_nvset.assert_called_once_with( + "meta_attributes", tree, element, attributes + ) + + +class UpdateOptionalAttributeTest(TestCase): + def test_add(self): + element = etree.Element("element") + alert._update_optional_attribute(element, "attr", "value1") + self.assertEqual(element.get("attr"), "value1") + + def test_update(self): + element = etree.Element("element", attr="value") + alert._update_optional_attribute(element, "attr", "value1") + self.assertEqual(element.get("attr"), "value1") + + def test_remove(self): + element = etree.Element("element", attr="value") + alert._update_optional_attribute(element, "attr", "") + self.assertTrue(element.get("attr") is None) + + +class GetAlertByIdTest(TestCase): + def test_found(self): + xml = """ + + + + + + + + + """ + assert_xml_equal( + '', + etree.tostring( + alert.get_alert_by_id(etree.XML(xml), "alert-2") + ).decode() + ) + + def test_different_place(self): + xml = """ + + + + + + + + + """ + assert_raise_library_error( + lambda: alert.get_alert_by_id(etree.XML(xml), "alert-2"), + ( + severities.ERROR, + report_codes.CIB_ALERT_NOT_FOUND, + {"alert": "alert-2"} + ) + ) + + def test_not_exist(self): + xml = """ + + + + + + + + """ + assert_raise_library_error( + lambda: alert.get_alert_by_id(etree.XML(xml), "alert-2"), + ( + severities.ERROR, + report_codes.CIB_ALERT_NOT_FOUND, + {"alert": "alert-2"} + ) + ) + + +class GetRecipientTest(TestCase): + def setUp(self): + self.xml = etree.XML( + """ + + + + + + + + + """ + ) + + def test_exist(self): + assert_xml_equal( + '', + etree.tostring(alert.get_recipient(self.xml, "value2")).decode() + ) + + def test_different_place(self): + assert_raise_library_error( + lambda: alert.get_recipient(self.xml, "value4"), + ( + severities.ERROR, + report_codes.CIB_ALERT_RECIPIENT_NOT_FOUND, + { + "alert": "alert-1", + "recipient": "value4" + } + ) + ) + + def test_not_recipient(self): + assert_raise_library_error( + lambda: alert.get_recipient(self.xml, "value3"), + ( + severities.ERROR, + report_codes.CIB_ALERT_RECIPIENT_NOT_FOUND, + { + "alert": "alert-1", + "recipient": "value3" + } + ) + ) + + +class CreateAlertTest(TestCase): + def setUp(self): + self.tree = etree.XML( + """ + + + + + + + + """ + ) + + def test_no_alerts(self): + tree = etree.XML( + """ + + + + """ + ) + assert_xml_equal( + '', + etree.tostring( + alert.create_alert(tree, "my-alert", "/test/path") + ).decode() + ) + assert_xml_equal( + """ + + + + + + + + """, + etree.tostring(tree).decode() + ) + + def test_alerts_exists(self): + assert_xml_equal( + '', + etree.tostring( + alert.create_alert(self.tree, "my-alert", "/test/path") + ).decode() + ) + assert_xml_equal( + """ + + + + + + + + + """, + etree.tostring(self.tree).decode() + ) + + def test_alerts_exists_with_description(self): + assert_xml_equal( + '', + etree.tostring(alert.create_alert( + self.tree, "my-alert", "/test/path", "nothing" + )).decode() + ) + assert_xml_equal( + """ + + + + + + + + + """, + etree.tostring(self.tree).decode() + ) + + def test_invalid_id(self): + assert_raise_library_error( + lambda: alert.create_alert(self.tree, "1alert", "/path"), + ( + severities.ERROR, + report_codes.INVALID_ID, + { + "id": "1alert", + "id_description": "alert-id", + "invalid_character": "1", + "reason": "invalid first character" + } + ) + ) + + def test_id_exists(self): + assert_raise_library_error( + lambda: alert.create_alert(self.tree, "alert", "/path"), + ( + severities.ERROR, + report_codes.ID_ALREADY_EXISTS, + {"id": "alert"} + ) + ) + + def test_no_id(self): + assert_xml_equal( + '', + etree.tostring( + alert.create_alert(self.tree, None, "/test/path") + ).decode() + ) + assert_xml_equal( + """ + + + + + + + + + """, + etree.tostring(self.tree).decode() + ) + + +class UpdateAlertTest(TestCase): + def setUp(self): + self.tree = etree.XML( + """ + + + + + + + + + """ + ) + + def test_update_path(self): + assert_xml_equal( + '', + etree.tostring( + alert.update_alert(self.tree, "alert", "/test/path") + ).decode() + ) + assert_xml_equal( + """ + + + + + + + + + """, + etree.tostring(self.tree).decode() + ) + + def test_remove_path(self): + assert_xml_equal( + '', + etree.tostring(alert.update_alert(self.tree, "alert", "")).decode() + ) + assert_xml_equal( + """ + + + + + + + + + """, + etree.tostring(self.tree).decode() + ) + + def test_update_description(self): + assert_xml_equal( + '', + etree.tostring( + alert.update_alert(self.tree, "alert", None, "desc") + ).decode() + ) + assert_xml_equal( + """ + + + + + + + + + """, + etree.tostring(self.tree).decode() + ) + + def test_remove_description(self): + assert_xml_equal( + '', + etree.tostring( + alert.update_alert(self.tree, "alert1", None, "") + ).decode() + ) + assert_xml_equal( + """ + + + + + + + + + """, + etree.tostring(self.tree).decode() + ) + + def test_id_not_exists(self): + assert_raise_library_error( + lambda: alert.update_alert(self.tree, "alert0", "/test"), + ( + severities.ERROR, + report_codes.CIB_ALERT_NOT_FOUND, + {"alert": "alert0"} + ) + ) + + +class RemoveAlertTest(TestCase): + def setUp(self): + self.tree = etree.XML( + """ + + + + + + + + + """ + ) + + def test_success(self): + alert.remove_alert(self.tree, "alert") + assert_xml_equal( + """ + + + + + + + + """, + etree.tostring(self.tree).decode() + ) + + def test_not_existing_id(self): + assert_raise_library_error( + lambda: alert.remove_alert(self.tree, "not-existing-id"), + ( + severities.ERROR, + report_codes.CIB_ALERT_NOT_FOUND, + {"alert": "not-existing-id"} + ) + ) + + +class AddRecipientTest(TestCase): + def setUp(self): + self.tree = etree.XML( + """ + + + + + + + + + + """ + ) + + def test_success(self): + assert_xml_equal( + '', + etree.tostring( + alert.add_recipient(self.tree, "alert", "value1") + ).decode() + ) + assert_xml_equal( + """ + + + + + + + + + + + """, + etree.tostring(self.tree).decode() + ) + + def test_recipient_exist(self): + assert_raise_library_error( + lambda: alert.add_recipient(self.tree, "alert", "test_val"), + ( + severities.ERROR, + report_codes.CIB_ALERT_RECIPIENT_ALREADY_EXISTS, + { + "recipient": "test_val", + "alert": "alert" + } + ) + ) + + def test_alert_not_exist(self): + assert_raise_library_error( + lambda: alert.add_recipient(self.tree, "alert1", "test_val"), + ( + severities.ERROR, + report_codes.CIB_ALERT_NOT_FOUND, + {"alert": "alert1"} + ) + ) + + def test_with_description(self): + assert_xml_equal( + """ + + """, + etree.tostring(alert.add_recipient( + self.tree, "alert", "value1", "desc" + )).decode() + ) + assert_xml_equal( + """ + + + + + + + + + + + """, + etree.tostring(self.tree).decode() + ) + + +class UpdateRecipientTest(TestCase): + def setUp(self): + self.tree = etree.XML( + """ + + + + + + + + + + + """ + ) + + def test_add_description(self): + assert_xml_equal( + """ + + """, + etree.tostring(alert.update_recipient( + self.tree, "alert", "test_val", "description" + )).decode() + ) + assert_xml_equal( + """ + + + + + + + + + + + """, + etree.tostring(self.tree).decode() + ) + + def test_update_description(self): + assert_xml_equal( + """ + + """, + etree.tostring(alert.update_recipient( + self.tree, "alert", "value1", "description" + )).decode() + ) + assert_xml_equal( + """ + + + + + + + + + + + """, + etree.tostring(self.tree).decode() + ) + + def test_remove_description(self): + assert_xml_equal( + """ + + """, + etree.tostring( + alert.update_recipient(self.tree, "alert", "value1", "") + ).decode() + ) + assert_xml_equal( + """ + + + + + + + + + + + """, + etree.tostring(self.tree).decode() + ) + + def test_alert_not_exists(self): + assert_raise_library_error( + lambda: alert.update_recipient(self.tree, "alert1", "test_val", ""), + ( + severities.ERROR, + report_codes.CIB_ALERT_NOT_FOUND, + {"alert": "alert1"} + ) + ) + + def test_recipient_not_exists(self): + assert_raise_library_error( + lambda: alert.update_recipient(self.tree, "alert", "unknown", ""), + ( + severities.ERROR, + report_codes.CIB_ALERT_RECIPIENT_NOT_FOUND, + { + "alert": "alert", + "recipient": "unknown" + } + ) + ) + + +class RemoveRecipientTest(TestCase): + def setUp(self): + self.tree = etree.XML( + """ + + + + + + + + + + + """ + ) + + def test_success(self): + alert.remove_recipient(self.tree, "alert", "val") + assert_xml_equal( + """ + + + + + + + + + + """, + etree.tostring(self.tree).decode() + ) + + def test_alert_not_exists(self): + assert_raise_library_error( + lambda: alert.remove_recipient(self.tree, "alert1", "test_val"), + ( + severities.ERROR, + report_codes.CIB_ALERT_NOT_FOUND, + {"alert": "alert1"} + ) + ) + + def test_recipient_not_exists(self): + assert_raise_library_error( + lambda: alert.remove_recipient(self.tree, "alert", "unknown"), + ( + severities.ERROR, + report_codes.CIB_ALERT_RECIPIENT_NOT_FOUND, + { + "alert": "alert", + "recipient": "unknown" + } + ) + ) + + +class GetAllRecipientsTest(TestCase): + def test_success(self): + alert_obj = etree.XML( + """ + + + + + + + + + + + + + """ + ) + self.assertEqual( + [ + { + "id": "alert-recipient", + "value": "test_val", + "description": "", + "instance_attributes": [ + { + "id": "nvset-name1-value1", + "name": "name1", + "value": "value1" + }, + { + "id": "nvset-name2-value2", + "name": "name2", + "value": "value2" + } + ], + "meta_attributes": [ + { + "id": "nvset-name3", + "name": "name3", + "value": "" + } + ] + }, + { + "id": "alert-recipient-1", + "value": "value1", + "description": "desc", + "instance_attributes": [], + "meta_attributes": [] + } + ], + alert.get_all_recipients(alert_obj) + ) + + +class GetAllAlertsTest(TestCase): + def test_success(self): + alerts = etree.XML( + """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + """ + ) + self.assertEqual( + [ + { + "id": "alert", + "path": "/path", + "description": "", + "instance_attributes": [], + "meta_attributes": [], + "recipient_list": [ + { + "id": "alert-recipient", + "value": "test_val", + "description": "", + "instance_attributes": [ + { + "id": "instance_attributes-name1-value1", + "name": "name1", + "value": "value1" + }, + { + "id": "instance_attributes-name2-value2", + "name": "name2", + "value": "value2" + } + ], + "meta_attributes": [ + { + "id": "meta_attributes-name3", + "name": "name3", + "value": "" + } + ] + }, + { + "id": "alert-recipient-1", + "value": "value1", + "description": "desc", + "instance_attributes": [], + "meta_attributes": [] + } + ] + }, + { + "id": "alert1", + "path": "/test/path", + "description": "desc", + "instance_attributes": [ + { + "id": "alert1-name1-value1", + "name": "name1", + "value": "value1" + }, + { + "id": "alert1-name2-value2", + "name": "name2", + "value": "value2" + } + ], + "meta_attributes": [ + { + "id": "alert1-name3", + "name": "name3", + "value": "" + } + ], + "recipient_list": [] + } + ], + alert.get_all_alerts(alerts) + ) diff --git a/pcs/lib/cib/test/test_nvpair.py b/pcs/lib/cib/test/test_nvpair.py new file mode 100644 index 0000000..6907f25 --- /dev/null +++ b/pcs/lib/cib/test/test_nvpair.py @@ -0,0 +1,206 @@ +from __future__ import ( + absolute_import, + division, + print_function, + unicode_literals, +) + +from unittest import TestCase + +from lxml import etree + +from pcs.lib.cib import nvpair +from pcs.test.tools.assertions import assert_xml_equal + + +class UpdateNvpairTest(TestCase): + def setUp(self): + self.nvset = etree.Element("nvset", id="nvset") + etree.SubElement( + self.nvset, "nvpair", id="nvset-attr", name="attr", value="1" + ) + etree.SubElement( + self.nvset, "nvpair", id="nvset-attr2", name="attr2", value="2" + ) + etree.SubElement( + self.nvset, "notnvpair", id="nvset-test", name="test", value="0" + ) + + def test_update(self): + assert_xml_equal( + "", + etree.tostring( + nvpair.update_nvpair(self.nvset, self.nvset, "attr", "10") + ).decode() + ) + assert_xml_equal( + """ + + + + + + """, + etree.tostring(self.nvset).decode() + ) + + def test_add(self): + assert_xml_equal( + "", + etree.tostring( + nvpair.update_nvpair(self.nvset, self.nvset, "test", "0") + ).decode() + ) + assert_xml_equal( + """ + + + + + + + """, + etree.tostring(self.nvset).decode() + ) + + def test_remove(self): + assert_xml_equal( + "", + etree.tostring( + nvpair.update_nvpair(self.nvset, self.nvset, "attr2", "") + ).decode() + ) + assert_xml_equal( + """ + + + + + """, + etree.tostring(self.nvset).decode() + ) + + def test_remove_not_existing(self): + self.assertTrue( + nvpair.update_nvpair(self.nvset, self.nvset, "attr3", "") is None + ) + assert_xml_equal( + """ + + + + + + """, + etree.tostring(self.nvset).decode() + ) + + +class UpdateNvsetTest(TestCase): + def setUp(self): + self.root = etree.Element("root", id="root") + self.nvset = etree.SubElement(self.root, "nvset", id="nvset") + etree.SubElement( + self.nvset, "nvpair", id="nvset-attr", name="attr", value="1" + ) + etree.SubElement( + self.nvset, "nvpair", id="nvset-attr2", name="attr2", value="2" + ) + etree.SubElement( + self.nvset, "notnvpair", id="nvset-test", name="test", value="0" + ) + + def test_None(self): + self.assertTrue( + nvpair.update_nvset("nvset", self.root, self.root, None) is None + ) + + def test_empty(self): + self.assertTrue( + nvpair.update_nvset("nvset", self.root, self.root, {}) is None + ) + + def test_existing(self): + self.assertEqual( + self.nvset, + nvpair.update_nvset("nvset", self.root, self.root, { + "attr": "10", + "new_one": "20", + "test": "0", + "attr2": "" + }) + ) + assert_xml_equal( + """ + + + + + + + """, + etree.tostring(self.nvset).decode() + ) + + def test_new(self): + root = etree.Element("root", id="root") + assert_xml_equal( + """ + + + + + + """, + etree.tostring(nvpair.update_nvset("nvset", root, root, { + "attr": "10", + "new_one": "20", + "test": "0", + "attr2": "" + })).decode() + ) + assert_xml_equal( + """ + + + + + + + + """, + etree.tostring(root).decode() + ) + + +class GetNvsetTest(TestCase): + def test_success(self): + nvset = etree.XML( + """ + + + + + + """ + ) + self.assertEqual( + [ + { + "id": "nvset-name1", + "name": "name1", + "value": "value1" + }, + { + "id": "nvset-name2", + "name": "name2", + "value": "value2" + }, + { + "id": "nvset-name3", + "name": "name3", + "value": "" + } + ], + nvpair.get_nvset(nvset) + ) diff --git a/pcs/lib/cib/tools.py b/pcs/lib/cib/tools.py index dfe31fc..b59d50d 100644 --- a/pcs/lib/cib/tools.py +++ b/pcs/lib/cib/tools.py @@ -5,8 +5,12 @@ from __future__ import ( unicode_literals, ) +import os +import re +import tempfile from lxml import etree +from pcs import settings from pcs.lib import reports from pcs.lib.errors import LibraryError from pcs.lib.pacemaker_values import validate_id @@ -71,6 +75,15 @@ def get_acls(tree): acls = etree.SubElement(get_configuration(tree), "acls") return acls + +def get_alerts(tree): + """ + Return 'alerts' element from tree, create a new one if missing + tree -- cib etree node + """ + return get_sub_element(get_configuration(tree), "alerts") + + def get_constraints(tree): """ Return 'constraint' element from tree @@ -87,3 +100,117 @@ def find_parent(element, tag_names): def export_attributes(element): return dict((key, value) for key, value in element.attrib.items()) + + +def get_sub_element(element, sub_element_tag, new_id=None, new_index=None): + """ + Returns sub-element sub_element_tag of element. It will create new + element if such doesn't exist yet. Id of new element will be new_if if + it's not None. new_index specify where will be new element added, if None + it will be appended. + + element -- parent element + sub_element_tag -- tag of wanted element + new_id -- id of new element + new_index -- index for new element + """ + sub_element = element.find("./{0}".format(sub_element_tag)) + if sub_element is None: + sub_element = etree.Element(sub_element_tag) + if new_id: + sub_element.set("id", new_id) + if new_index is None: + element.append(sub_element) + else: + element.insert(new_index, sub_element) + return sub_element + + +def get_pacemaker_version_by_which_cib_was_validated(cib): + """ + Return version of pacemaker which validated specified cib as tree. + Version is returned as tuple of integers: (, , ). + Raises LibraryError on any failure. + + cib -- cib etree + """ + version = cib.get("validate-with") + if version is None: + raise LibraryError(reports.cib_load_error_invalid_format()) + + regexp = re.compile( + r"pacemaker-(?P\d+)\.(?P\d+)(\.(?P\d+))?" + ) + match = regexp.match(version) + if not match: + raise LibraryError(reports.cib_load_error_invalid_format()) + return ( + int(match.group("major")), + int(match.group("minor")), + int(match.group("rev") or 0) + ) + + +def upgrade_cib(cib, runner): + """ + Upgrade CIB to the latest schema of installed pacemaker. Returns upgraded + CIB as string. + Raises LibraryError on any failure. + + cib -- cib etree + runner -- CommandRunner + """ + temp_file = tempfile.NamedTemporaryFile("w+", suffix=".pcs") + temp_file.write(etree.tostring(cib).decode()) + temp_file.flush() + output, retval = runner.run( + [ + os.path.join(settings.pacemaker_binaries, "cibadmin"), + "--upgrade", + "--force" + ], + env_extend={"CIB_file": temp_file.name} + ) + + if retval != 0: + temp_file.close() + LibraryError(reports.cib_upgrade_failed(output)) + + try: + temp_file.seek(0) + return etree.fromstring(temp_file.read()) + except (EnvironmentError, etree.XMLSyntaxError, etree.DocumentInvalid) as e: + LibraryError(reports.cib_upgrade_failed(str(e))) + finally: + temp_file.close() + + +def ensure_cib_version(runner, cib, version): + """ + This method ensures that specified cib is verified by pacemaker with + version 'version' or newer. If cib doesn't correspond to this version, + method will try to upgrade cib. + Returns cib which was verified by pacemaker version 'version' or later. + Raises LibraryError on any failure. + + runner -- CommandRunner + cib -- cib tree + version -- tuple of integers (, , ) + """ + current_version = get_pacemaker_version_by_which_cib_was_validated( + cib + ) + if current_version >= version: + return None + + upgraded_cib = upgrade_cib(cib, runner) + current_version = get_pacemaker_version_by_which_cib_was_validated( + upgraded_cib + ) + + if current_version >= version: + return upgraded_cib + + raise LibraryError(reports.unable_to_upgrade_cib_to_required_version( + current_version, version + )) diff --git a/pcs/lib/commands/alert.py b/pcs/lib/commands/alert.py new file mode 100644 index 0000000..7371fbc --- /dev/null +++ b/pcs/lib/commands/alert.py @@ -0,0 +1,169 @@ +from __future__ import ( + absolute_import, + division, + print_function, + unicode_literals, +) + +from pcs.lib import reports +from pcs.lib.cib import alert +from pcs.lib.errors import LibraryError + + +REQUIRED_CIB_VERSION = (2, 5, 0) + + +def create_alert( + lib_env, + alert_id, + path, + instance_attribute_dict, + meta_attribute_dict, + description=None +): + """ + Create new alert. + Raises LibraryError if path is not specified, or any other failure. + + lib_env -- LibraryEnvironment + alert_id -- id of alert to be created, if None it will be generated + path -- path to script for alert + instance_attribute_dict -- dictionary of instance attributes + meta_attribute_dict -- dictionary of meta attributes + description -- alert description description + """ + if not path: + raise LibraryError(reports.required_option_is_missing("path")) + + cib = lib_env.get_cib(REQUIRED_CIB_VERSION) + + alert_el = alert.create_alert(cib, alert_id, path, description) + alert.update_instance_attributes(cib, alert_el, instance_attribute_dict) + alert.update_meta_attributes(cib, alert_el, meta_attribute_dict) + + lib_env.push_cib(cib) + + +def update_alert( + lib_env, + alert_id, + path, + instance_attribute_dict, + meta_attribute_dict, + description=None +): + """ + Update existing alert with specified id. + + lib_env -- LibraryEnvironment + alert_id -- id of alert to be updated + path -- new path, if None old value will stay unchanged + instance_attribute_dict -- dictionary of instance attributes to update + meta_attribute_dict -- dictionary of meta attributes to update + description -- new description, if empty string, old description will be + deleted, if None old value will stay unchanged + """ + cib = lib_env.get_cib(REQUIRED_CIB_VERSION) + + alert_el = alert.update_alert(cib, alert_id, path, description) + alert.update_instance_attributes(cib, alert_el, instance_attribute_dict) + alert.update_meta_attributes(cib, alert_el, meta_attribute_dict) + + lib_env.push_cib(cib) + + +def remove_alert(lib_env, alert_id): + """ + Remove alert with specified id. + + lib_env -- LibraryEnvironment + alert_id -- id of alert which should be removed + """ + cib = lib_env.get_cib(REQUIRED_CIB_VERSION) + alert.remove_alert(cib, alert_id) + lib_env.push_cib(cib) + + +def add_recipient( + lib_env, + alert_id, + recipient_value, + instance_attribute_dict, + meta_attribute_dict, + description=None +): + """ + Add new recipient to alert witch id alert_id. + + lib_env -- LibraryEnvironment + alert_id -- id of alert to which new recipient should be added + recipient_value -- value of new recipient + instance_attribute_dict -- dictionary of instance attributes to update + meta_attribute_dict -- dictionary of meta attributes to update + description -- recipient description + """ + if not recipient_value: + raise LibraryError( + reports.required_option_is_missing("value") + ) + + cib = lib_env.get_cib(REQUIRED_CIB_VERSION) + recipient = alert.add_recipient( + cib, alert_id, recipient_value, description + ) + alert.update_instance_attributes(cib, recipient, instance_attribute_dict) + alert.update_meta_attributes(cib, recipient, meta_attribute_dict) + + lib_env.push_cib(cib) + + +def update_recipient( + lib_env, + alert_id, + recipient_value, + instance_attribute_dict, + meta_attribute_dict, + description=None +): + """ + Update existing recipient. + + lib_env -- LibraryEnvironment + alert_id -- id of alert to which recipient belong + recipient_value -- recipient to be updated + instance_attribute_dict -- dictionary of instance attributes to update + meta_attribute_dict -- dictionary of meta attributes to update + description -- new description, if empty string, old description will be + deleted, if None old value will stay unchanged + """ + cib = lib_env.get_cib(REQUIRED_CIB_VERSION) + recipient = alert.update_recipient( + cib, alert_id, recipient_value, description + ) + alert.update_instance_attributes(cib, recipient, instance_attribute_dict) + alert.update_meta_attributes(cib, recipient, meta_attribute_dict) + + lib_env.push_cib(cib) + + +def remove_recipient(lib_env, alert_id, recipient_value): + """ + Remove existing recipient. + + lib_env -- LibraryEnvironment + alert_id -- id of alert to which recipient belong + recipient_value -- recipient to be removed + """ + cib = lib_env.get_cib(REQUIRED_CIB_VERSION) + alert.remove_recipient(cib, alert_id, recipient_value) + lib_env.push_cib(cib) + + +def get_all_alerts(lib_env): + """ + Returns list of all alerts. See docs of pcs.lib.cib.alert.get_all_alerts for + description of data format. + + lib_env -- LibraryEnvironment + """ + return alert.get_all_alerts(lib_env.get_cib()) diff --git a/pcs/lib/commands/test/test_alert.py b/pcs/lib/commands/test/test_alert.py new file mode 100644 index 0000000..34813df --- /dev/null +++ b/pcs/lib/commands/test/test_alert.py @@ -0,0 +1,639 @@ +from __future__ import ( + absolute_import, + division, + print_function, + unicode_literals, +) + +import logging +from lxml import etree + +from unittest import TestCase + +from pcs.test.tools.pcs_mock import mock +from pcs.test.tools.assertions import ( + assert_raise_library_error, + assert_xml_equal, +) +from pcs.test.tools.custom_mock import MockLibraryReportProcessor + +from pcs.common import report_codes +from pcs.lib.errors import ReportItemSeverity as Severities +from pcs.lib.env import LibraryEnvironment +from pcs.lib.external import CommandRunner + +import pcs.lib.commands.alert as cmd_alert + + +@mock.patch("pcs.lib.cib.tools.upgrade_cib") +class CreateAlertTest(TestCase): + def setUp(self): + self.mock_log = mock.MagicMock(spec_set=logging.Logger) + self.mock_run = mock.MagicMock(spec_set=CommandRunner) + self.mock_rep = MockLibraryReportProcessor() + self.mock_env = LibraryEnvironment( + self.mock_log, self.mock_rep, cib_data="" + ) + + def test_no_path(self, mock_upgrade_cib): + assert_raise_library_error( + lambda: cmd_alert.create_alert( + self.mock_env, None, None, None, None + ), + ( + Severities.ERROR, + report_codes.REQUIRED_OPTION_IS_MISSING, + {"option_name": "path"} + ) + ) + self.assertEqual(0, mock_upgrade_cib.call_count) + + def test_upgrade_needed(self, mock_upgrade_cib): + self.mock_env._push_cib_xml( + """ + + + + + """ + ) + mock_upgrade_cib.return_value = etree.XML( + """ + + + + + """ + ) + cmd_alert.create_alert( + self.mock_env, + "my-alert", + "/my/path", + { + "instance": "value", + "another": "val" + }, + {"meta1": "val1"}, + "my description" + ) + assert_xml_equal( + """ + + + + + + + + + + + + + + + + """, + self.mock_env._get_cib_xml() + ) + self.assertEqual(1, mock_upgrade_cib.call_count) + + +class UpdateAlertTest(TestCase): + def setUp(self): + self.mock_log = mock.MagicMock(spec_set=logging.Logger) + self.mock_run = mock.MagicMock(spec_set=CommandRunner) + self.mock_rep = MockLibraryReportProcessor() + self.mock_env = LibraryEnvironment( + self.mock_log, self.mock_rep, cib_data="" + ) + + def test_update_all(self): + self.mock_env._push_cib_xml( + """ + + + + + + + + + + + + + + + + """ + ) + cmd_alert.update_alert( + self.mock_env, + "my-alert", + "/another/one", + { + "instance": "", + "my-attr": "its_val" + }, + {"meta1": "val2"}, + "" + ) + assert_xml_equal( + """ + + + + + + + + + + + + + + + + """, + self.mock_env._get_cib_xml() + ) + + def test_update_instance_attribute(self): + self.mock_env._push_cib_xml( + """ + + + + + + + + + + + + """ + ) + cmd_alert.update_alert( + self.mock_env, + "my-alert", + None, + {"instance": "new_val"}, + {}, + None + ) + assert_xml_equal( + """ + + + + + + + + + + + + """, + self.mock_env._get_cib_xml() + ) + + def test_alert_doesnt_exist(self): + self.mock_env._push_cib_xml( + """ + + + + + + + + """ + ) + assert_raise_library_error( + lambda: cmd_alert.update_alert( + self.mock_env, "unknown", "test", {}, {}, None + ), + ( + Severities.ERROR, + report_codes.CIB_ALERT_NOT_FOUND, + {"alert": "unknown"} + ) + ) + + +class RemoveAlertTest(TestCase): + def setUp(self): + self.mock_log = mock.MagicMock(spec_set=logging.Logger) + self.mock_run = mock.MagicMock(spec_set=CommandRunner) + self.mock_rep = MockLibraryReportProcessor() + cib = """ + + + + + + + + + """ + self.mock_env = LibraryEnvironment( + self.mock_log, self.mock_rep, cib_data=cib + ) + + def test_success(self): + cmd_alert.remove_alert(self.mock_env, "alert") + assert_xml_equal( + """ + + + + + + + + """, + self.mock_env._get_cib_xml() + ) + + def test_not_existing_alert(self): + assert_raise_library_error( + lambda: cmd_alert.remove_alert(self.mock_env, "unknown"), + ( + Severities.ERROR, + report_codes.CIB_ALERT_NOT_FOUND, + {"alert": "unknown"} + ) + ) + + +class AddRecipientTest(TestCase): + def setUp(self): + self.mock_log = mock.MagicMock(spec_set=logging.Logger) + self.mock_run = mock.MagicMock(spec_set=CommandRunner) + self.mock_rep = MockLibraryReportProcessor() + cib = """ + + + + + + + + + + """ + self.mock_env = LibraryEnvironment( + self.mock_log, self.mock_rep, cib_data=cib + ) + + def test_alert_not_found(self): + assert_raise_library_error( + lambda: cmd_alert.add_recipient( + self.mock_env, "unknown", "recipient", {}, {} + ), + ( + Severities.ERROR, + report_codes.CIB_ALERT_NOT_FOUND, + {"alert": "unknown"} + ) + ) + + def test_value_not_defined(self): + assert_raise_library_error( + lambda: cmd_alert.add_recipient( + self.mock_env, "unknown", "", {}, {} + ), + ( + Severities.ERROR, + report_codes.REQUIRED_OPTION_IS_MISSING, + {"option_name": "value"} + ) + ) + + def test_recipient_already_exists(self): + assert_raise_library_error( + lambda: cmd_alert.add_recipient( + self.mock_env, "alert", "value1", {}, {} + ), + ( + Severities.ERROR, + report_codes.CIB_ALERT_RECIPIENT_ALREADY_EXISTS, + { + "recipient": "value1", + "alert": "alert" + } + ) + ) + + def test_success(self): + cmd_alert.add_recipient( + self.mock_env, + "alert", + "value", + {"attr1": "val1"}, + { + "attr2": "val2", + "attr1": "val1" + } + ) + assert_xml_equal( + """ + + + + + + + + + + + + + + + + + + + """, + self.mock_env._get_cib_xml() + ) + + +class UpdateRecipientTest(TestCase): + def setUp(self): + self.mock_log = mock.MagicMock(spec_set=logging.Logger) + self.mock_run = mock.MagicMock(spec_set=CommandRunner) + self.mock_rep = MockLibraryReportProcessor() + cib = """ + + + + + + + + + + + + + + + + + + + """ + self.mock_env = LibraryEnvironment( + self.mock_log, self.mock_rep, cib_data=cib + ) + + def test_alert_not_found(self): + assert_raise_library_error( + lambda: cmd_alert.update_recipient( + self.mock_env, "unknown", "recipient", {}, {} + ), + ( + Severities.ERROR, + report_codes.CIB_ALERT_NOT_FOUND, + {"alert": "unknown"} + ) + ) + + def test_recipient_not_found(self): + assert_raise_library_error( + lambda: cmd_alert.update_recipient( + self.mock_env, "alert", "recipient", {}, {} + ), + ( + Severities.ERROR, + report_codes.CIB_ALERT_RECIPIENT_NOT_FOUND, + { + "recipient": "recipient", + "alert": "alert" + } + ) + ) + + def test_update_all(self): + cmd_alert.update_recipient( + self.mock_env, + "alert", + "value", + {"attr1": "value"}, + { + "attr1": "", + "attr3": "new_val" + }, + "desc" + ) + assert_xml_equal( + """ + + + + + + + + + + + + + + + + + + + """, + self.mock_env._get_cib_xml() + ) + + +class RemoveRecipientTest(TestCase): + def setUp(self): + self.mock_log = mock.MagicMock(spec_set=logging.Logger) + self.mock_run = mock.MagicMock(spec_set=CommandRunner) + self.mock_rep = MockLibraryReportProcessor() + cib = """ + + + + + + + + + + + """ + self.mock_env = LibraryEnvironment( + self.mock_log, self.mock_rep, cib_data=cib + ) + + def test_alert_not_found(self): + assert_raise_library_error( + lambda: cmd_alert.remove_recipient( + self.mock_env, "unknown", "recipient" + ), + ( + Severities.ERROR, + report_codes.CIB_ALERT_NOT_FOUND, + {"alert": "unknown"} + ) + ) + + def test_recipient_not_found(self): + assert_raise_library_error( + lambda: cmd_alert.remove_recipient( + self.mock_env, "alert", "recipient" + ), + ( + Severities.ERROR, + report_codes.CIB_ALERT_RECIPIENT_NOT_FOUND, + { + "recipient": "recipient", + "alert": "alert" + } + ) + ) + + def test_success(self): + cmd_alert.remove_recipient(self.mock_env, "alert", "value1") + assert_xml_equal( + """ + + + + + + + + + + """, + self.mock_env._get_cib_xml() + ) + + +@mock.patch("pcs.lib.cib.alert.get_all_alerts") +class GetAllAlertsTest(TestCase): + def setUp(self): + self.mock_log = mock.MagicMock(spec_set=logging.Logger) + self.mock_run = mock.MagicMock(spec_set=CommandRunner) + self.mock_rep = MockLibraryReportProcessor() + self.mock_env = LibraryEnvironment( + self.mock_log, self.mock_rep, cib_data='' + ) + + def test_success(self, mock_alerts): + mock_alerts.return_value = [{"id": "alert"}] + self.assertEqual( + [{"id": "alert"}], + cmd_alert.get_all_alerts(self.mock_env) + ) + self.assertEqual(1, mock_alerts.call_count) diff --git a/pcs/lib/commands/test/test_ticket.py b/pcs/lib/commands/test/test_ticket.py index a22a014..751001b 100644 --- a/pcs/lib/commands/test/test_ticket.py +++ b/pcs/lib/commands/test/test_ticket.py @@ -44,7 +44,7 @@ class CreateTest(TestCase): }) assert_xml_equal( - env.get_cib_xml(), + env._get_cib_xml(), str(cib.append_to_first_tag_name( 'constraints', """ [= ...]] Add specified utilization options to specified node. If node is not specified, shows utilization of all nodes. If utilization options are not specified, shows utilization of specified node. Utilization option should be in format name=value, value has to be integer. Options may be removed by setting an option without a value. Example: pcs node utilization node1 cpu=4 ram= +.SS "alert" +.TP +[config|show] +Show all configured alerts. +.TP +create path= [id=] [description=] [options [