From 5ae6f8552ccdae5acf07a0db5d3810410cb95303 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Wed, 24 Apr 2024 17:16:12 +0200 Subject: [PATCH 001/227] pin types-pycurl --- dev_requirements.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index aaad92567..0578bfcee 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -6,5 +6,8 @@ black==24.3.0 isort types-cryptography types-dataclasses -types-pycurl +# later versions remove type annotations from a few functions causing +# error: Call to untyped function "getinfo" in typed context [no-untyped-call] +# so we are stuck with this version until there's a fix +types-pycurl==7.45.2.20240311 types-python-dateutil From 14dde17cb8c13df047cc2b840e9f8244522d986d Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Thu, 25 Apr 2024 13:41:16 +0200 Subject: [PATCH 002/227] fix running pcsd from git * has been broken since fork method change to forkserver --- pcs/pcs.in | 47 ++++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/pcs/pcs.in b/pcs/pcs.in index d0fcd775d..09c637e9a 100755 --- a/pcs/pcs.in +++ b/pcs/pcs.in @@ -2,31 +2,32 @@ import os.path import sys -CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) +if __name__ == "__main__": + CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) -# We prevent to import some module from this dir instead of e.g. standard module. -# There is no reason to import anything from this module. -sys.path.remove(CURRENT_DIR) + # We prevent to import some module from this dir instead of e.g. standard module. + # There is no reason to import anything from this module. + sys.path.remove(CURRENT_DIR) -# Add pcs package. -PACKAGE_DIR = os.path.dirname(CURRENT_DIR) -BUNDLED_PACKAGES_DIR = os.path.join(PACKAGE_DIR, "@PCS_BUNDLED_DIR_LOCAL@", "packages") -sys.path.insert(0, BUNDLED_PACKAGES_DIR) -sys.path.insert(0, PACKAGE_DIR) + # Add pcs package. + PACKAGE_DIR = os.path.dirname(CURRENT_DIR) + BUNDLED_PACKAGES_DIR = os.path.join(PACKAGE_DIR, "@PCS_BUNDLED_DIR_LOCAL@", "packages") + sys.path.insert(0, BUNDLED_PACKAGES_DIR) + sys.path.insert(0, PACKAGE_DIR) -# pylint: disable=wrong-import-position -# we need settings to be imported from a path defined above -from pcs import settings + # pylint: disable=wrong-import-position + # we need settings to be imported from a path defined above + from pcs import settings -settings.pcsd_exec_location = os.path.join(PACKAGE_DIR, "pcsd") -settings.pcsd_gem_path = os.path.join(PACKAGE_DIR, "@PCSD_BUNDLED_DIR_ROOT_LOCAL@") -settings.pcs_data_dir = os.path.join(PACKAGE_DIR, "data") + settings.pcsd_exec_location = os.path.join(PACKAGE_DIR, "pcsd") + settings.pcsd_gem_path = os.path.join(PACKAGE_DIR, "@PCSD_BUNDLED_DIR_ROOT_LOCAL@") + settings.pcs_data_dir = os.path.join(PACKAGE_DIR, "data") -if "-d" in sys.argv: - from pcs.daemon.run import main - argv = sys.argv[:] - argv.remove("-d") - main(argv[1:]) -else: - from pcs import app - app.main(sys.argv[1:]) + if "-d" in sys.argv: + from pcs.daemon.run import main + argv = sys.argv[:] + argv.remove("-d") + main(argv[1:]) + else: + from pcs import app + app.main(sys.argv[1:]) From 2f4ebe9dfb2d9854e6ae05834e6062d245dae88d Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Thu, 16 May 2024 10:36:23 +0200 Subject: [PATCH 003/227] fix stdout wrapping to terminal width --- CHANGELOG.md | 3 +++ pcs/cli/common/output.py | 8 +++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a198d0f7e..a6ef6cc26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ ([RHEL-27492]) - Use different process creation method for multiprocessing module in order to avoid deadlock on process termination. ([ghissue#780], [RHEL-28749]) +- Do not wrap pcs output to terminal width if pcs's stdout is redirected + ([RHEL-36514]) ### Deprecated - Pcs produces warnings about [features planned to be removed in pacemaker 3](https://projects.clusterlabs.org/w/projects/pacemaker/pacemaker_3.0_changes/pacemaker_3.0_configuration_changes/): @@ -37,6 +39,7 @@ [RHEL-28749]: https://issues.redhat.com/browse/RHEL-28749 [RHEL-25854]: https://issues.redhat.com/browse/RHEL-25854 [RHEL-21051]: https://issues.redhat.com/browse/RHEL-21051 +[RHEL-36514]: https://issues.redhat.com/browse/RHEL-36514 ## [0.11.7] - 2024-01-11 diff --git a/pcs/cli/common/output.py b/pcs/cli/common/output.py index 179f7c03f..9dc0e1621 100644 --- a/pcs/cli/common/output.py +++ b/pcs/cli/common/output.py @@ -56,9 +56,11 @@ def format_wrap_for_terminal( trim -- number which will be substracted from terminal size. Can be used in cases lines will be indented later by this number of spaces. """ - if (sys.stdout is not None and sys.stdout.isatty()) or ( - sys.stderr is not None and sys.stderr.isatty() - ): + # This function is used for stdout only - we don't care about wrapping + # error messages and debug info. So it checks stdout and not stderr. + # Checking stderr would enable wrapping in case of 'pcs ... | grep ...' + # (stderr is connected to a terminal), which we don't want. (RHEL-36514) + if sys.stdout is not None and sys.stdout.isatty(): return format_wrap( text, # minimal line length is 40 From e89ff46f65e5036b6f603e8f40b7c9aada95eb95 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Thu, 23 May 2024 13:29:20 +0200 Subject: [PATCH 004/227] validate resource-discovery --- CHANGELOG.md | 2 + pcs/constraint.py | 47 ++++++++++++++++++++++- pcs_test/tier1/legacy/test_constraints.py | 21 ++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6ef6cc26..f97442555 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ avoid deadlock on process termination. ([ghissue#780], [RHEL-28749]) - Do not wrap pcs output to terminal width if pcs's stdout is redirected ([RHEL-36514]) +- Report an error when an invalid resource-discovery is specified ([RHEL-7701]) ### Deprecated - Pcs produces warnings about [features planned to be removed in pacemaker 3](https://projects.clusterlabs.org/w/projects/pacemaker/pacemaker_3.0_changes/pacemaker_3.0_configuration_changes/): @@ -34,6 +35,7 @@ [ghissue#772]: https://github.com/ClusterLabs/pcs/issues/772 [ghissue#780]: https://github.com/ClusterLabs/pcs/issues/780 [RHEL-2977]: https://issues.redhat.com/browse/RHEL-2977 +[RHEL-7701]: https://issues.redhat.com/browse/RHEL-7701 [RHEL-16231]: https://issues.redhat.com/browse/RHEL-16231 [RHEL-27492]: https://issues.redhat.com/browse/RHEL-27492 [RHEL-28749]: https://issues.redhat.com/browse/RHEL-28749 diff --git a/pcs/constraint.py b/pcs/constraint.py index f239b62e2..3e9e27e4f 100644 --- a/pcs/constraint.py +++ b/pcs/constraint.py @@ -46,6 +46,7 @@ get_all_constraints_ids, ) from pcs.common.pacemaker.resource.list import CibResourcesDto +from pcs.common.pacemaker.types import CibResourceDiscovery from pcs.common.reports import ReportItem from pcs.common.str_tools import ( format_list, @@ -972,7 +973,29 @@ def location_add(lib, argv, modifiers, skip_score_and_node_check=False): if argv: for arg in argv: if "=" in arg: - options.append(arg.split("=", 1)) + name, value = arg.split("=", 1) + if name == "resource-discovery": + if not modifiers.get("--force"): + allowed_discovery = list( + map( + str, + [ + CibResourceDiscovery.ALWAYS, + CibResourceDiscovery.EXCLUSIVE, + CibResourceDiscovery.NEVER, + ], + ) + ) + if value not in allowed_discovery: + utils.err( + ( + "invalid {0} value '{1}', allowed values are: " + "{2}, use --force to override" + ).format( + name, value, format_list(allowed_discovery) + ) + ) + options.append([name, value]) else: raise CmdLineInputError(f"bad option '{arg}'") if options[-1][0] != "resource-discovery" and not modifiers.get( @@ -1111,6 +1134,28 @@ def location_rule(lib, argv, modifiers): # If resource-discovery is specified, we use it with the rsc_location # element not the rule if resource_discovery: + if not modifiers.get("--force"): + allowed_discovery = list( + map( + str, + [ + CibResourceDiscovery.ALWAYS, + CibResourceDiscovery.EXCLUSIVE, + CibResourceDiscovery.NEVER, + ], + ) + ) + if resource_discovery not in allowed_discovery: + utils.err( + ( + "invalid {0} value '{1}', allowed values are: {2}, " + "use --force to override" + ).format( + "resource-discovery", + resource_discovery, + format_list(allowed_discovery), + ) + ) lc.setAttribute("resource-discovery", options.pop("resource-discovery")) constraints.appendChild(lc) diff --git a/pcs_test/tier1/legacy/test_constraints.py b/pcs_test/tier1/legacy/test_constraints.py index 4bd7a99e5..539e84b71 100644 --- a/pcs_test/tier1/legacy/test_constraints.py +++ b/pcs_test/tier1/legacy/test_constraints.py @@ -1403,6 +1403,21 @@ def test_constraint_resource_discovery_rules(self): self.assertEqual(stderr, "") self.assertEqual(retval, 0) + stdout, stderr, retval = pcs( + self.temp_cib.name, + ( + "constraint location crd1 rule resource-discovery=bad-value " + "opsrole2 ne controller2" + ).split(), + ) + self.assertEqual(stdout, "") + self.assertEqual( + stderr, + "Error: invalid resource-discovery value 'bad-value', allowed " + "values are: 'always', 'exclusive', 'never', use --force to override\n", + ) + self.assertEqual(retval, 1) + self.assert_pcs_success( "constraint --full".split(), stdout_full=outdent( @@ -1456,6 +1471,12 @@ def test_constraint_resource_discovery(self): self.assertEqual(stdout, "") self.assertEqual(retval, 0) + self.assert_pcs_fail( + "-- constraint location add id7 crd1 my_node2 score=-INFINITY resource-discovery=bad-value".split(), + "Error: invalid resource-discovery value 'bad-value', allowed " + "values are: 'always', 'exclusive', 'never', use --force to override\n", + ) + self.assert_pcs_success( "constraint --full".split(), stdout_full=outdent( From ca0be56e422aac6e14b9871a8ddf906c7e8efdb5 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Mon, 3 Jun 2024 17:45:54 +0200 Subject: [PATCH 005/227] Do not bundle platfrom specific ruby gems. * BUNDLE_FORCE_RUBY_PLATFORM: Ignore the current machine's platform and install only ruby platform gems. As a result, gems with native extensions will be compiled from source. --- Makefile.am | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile.am b/Makefile.am index f1a714e22..0cadd9312 100644 --- a/Makefile.am +++ b/Makefile.am @@ -129,6 +129,7 @@ if ENABLE_DOWNLOAD echo 'BUNDLE_TIMEOUT: 30' >> .bundle/config echo 'BUNDLE_RETRY: 30' >> .bundle/config echo 'BUNDLE_JOBS: 1' >> .bundle/config + echo 'BUNDLE_FORCE_RUBY_PLATFORM: "true"' >> .bundle/config $(BUNDLE) cp -rp $(PCSD_BUNDLED_DIR_LOCAL)/* $(PCSD_BUNDLED_DIR_ROOT_LOCAL)/ rm -rf $$(realpath $(PCSD_BUNDLED_DIR_LOCAL)/../) From af9510afb3ce53b3dd05136fdbb9f0a5cc048205 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Fri, 31 May 2024 16:00:06 +0200 Subject: [PATCH 006/227] fix booth destroy for arbitrators --- CHANGELOG.md | 3 + pcs/lib/commands/booth.py | 35 ++++--- pcs/lib/pacemaker/live.py | 4 + pcs_test/tier0/lib/commands/test_booth.py | 110 ++++++++++++++++++++-- 4 files changed, 132 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f97442555..90e907535 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ - Do not wrap pcs output to terminal width if pcs's stdout is redirected ([RHEL-36514]) - Report an error when an invalid resource-discovery is specified ([RHEL-7701]) +- 'pcs booth destroy' now works for nodes without a cluster (such as + arbitrators) ([RHEL-7737]) ### Deprecated - Pcs produces warnings about [features planned to be removed in pacemaker 3](https://projects.clusterlabs.org/w/projects/pacemaker/pacemaker_3.0_changes/pacemaker_3.0_configuration_changes/): @@ -36,6 +38,7 @@ [ghissue#780]: https://github.com/ClusterLabs/pcs/issues/780 [RHEL-2977]: https://issues.redhat.com/browse/RHEL-2977 [RHEL-7701]: https://issues.redhat.com/browse/RHEL-7701 +[RHEL-7737]: https://issues.redhat.com/browse/RHEL-7737 [RHEL-16231]: https://issues.redhat.com/browse/RHEL-16231 [RHEL-27492]: https://issues.redhat.com/browse/RHEL-27492 [RHEL-28749]: https://issues.redhat.com/browse/RHEL-28749 diff --git a/pcs/lib/commands/booth.py b/pcs/lib/commands/booth.py index c961705bd..f291a085b 100644 --- a/pcs/lib/commands/booth.py +++ b/pcs/lib/commands/booth.py @@ -58,6 +58,7 @@ ) from pcs.lib.interface.config import ParserErrorException from pcs.lib.node import get_existing_nodes_names +from pcs.lib.pacemaker.live import has_cib_xml from pcs.lib.resource_agent import ( ResourceAgentError, ResourceAgentFacade, @@ -165,20 +166,30 @@ def config_destroy( found_instance_name = booth_env.instance_name _ensure_live_env(env, booth_env) - booth_resource_list = resource.find_for_config( - get_resources(env.get_cib()), - booth_env.config_path, - ) - if booth_resource_list: - report_processor.report( - ReportItem.error( - reports.messages.BoothConfigIsUsed( - found_instance_name, - reports.const.BOOTH_CONFIG_USED_IN_CLUSTER_RESOURCE, - resource_name=str(booth_resource_list[0].get("id", "")), + if ( + has_cib_xml() + or env.service_manager.is_running("pacemaker") + or env.service_manager.is_running("pacemaker_remoted") + ): + # To allow destroying booth config on arbitrators, only check CIB if: + # * pacemaker is running and therefore we are able to get CIB + # * CIB is stored on disk - pcmk is not running but the node is in a + # cluster (don't checking corosync to cover remote and guest nodes) + # If CIB cannot be loaded in either case, fail with an error. + booth_resource_list = resource.find_for_config( + get_resources(env.get_cib()), + booth_env.config_path, + ) + if booth_resource_list: + report_processor.report( + ReportItem.error( + reports.messages.BoothConfigIsUsed( + found_instance_name, + reports.const.BOOTH_CONFIG_USED_IN_CLUSTER_RESOURCE, + resource_name=str(booth_resource_list[0].get("id", "")), + ) ) ) - ) # Only systemd is currently supported. Initd does not supports multiple # instances (here specified by name) if is_systemd(env.service_manager): diff --git a/pcs/lib/pacemaker/live.py b/pcs/lib/pacemaker/live.py index 301ce3436..43197ac10 100644 --- a/pcs/lib/pacemaker/live.py +++ b/pcs/lib/pacemaker/live.py @@ -151,6 +151,10 @@ def get_ticket_status_text(runner: CommandRunner) -> Tuple[str, str, int]: ### cib +def has_cib_xml() -> bool: + return os.path.exists(os.path.join(settings.cib_dir, "cib.xml")) + + def get_cib_xml_cmd_results( runner: CommandRunner, scope: Optional[str] = None ) -> tuple[str, str, int]: diff --git a/pcs_test/tier0/lib/commands/test_booth.py b/pcs_test/tier0/lib/commands/test_booth.py index 4e945216c..2957e378b 100644 --- a/pcs_test/tier0/lib/commands/test_booth.py +++ b/pcs_test/tier0/lib/commands/test_booth.py @@ -524,10 +524,13 @@ def test_success_default_instance(self): class ConfigDestroy(TestCase, FixtureMixin): + # pylint: disable=too-many-public-methods def setUp(self): self.env_assist, self.config = get_env_tools(self) + self.cib_path = os.path.join(settings.cib_dir, "cib.xml") def fixture_config_booth_not_used(self, instance_name="booth"): + self.config.fs.exists(self.cib_path, True) self.config.runner.cib.load() self.config.services.is_running( "booth", instance=instance_name, return_value=False @@ -536,6 +539,44 @@ def fixture_config_booth_not_used(self, instance_name="booth"): "booth", instance=instance_name, return_value=False ) + def fixture_config_booth_used( + self, + instance_name, + cib_exists=False, + pcmk_running=False, + pcmk_remote_running=False, + booth_running=False, + booth_enabled=False, + ): + cib_load_exception = False + self.config.fs.exists(self.cib_path, cib_exists) + if not cib_exists: + self.config.services.is_running( + "pacemaker", + return_value=pcmk_running, + name="services.is_running.pcmk", + ) + if not pcmk_running: + self.config.services.is_running( + "pacemaker_remoted", + return_value=pcmk_remote_running, + name="services.is_running.pcmk_remote", + ) + if cib_exists and not pcmk_running and not pcmk_remote_running: + self.config.runner.cib.load( + returncode=1, stderr="unable to get cib, pcmk is not running" + ) + cib_load_exception = True + elif pcmk_running or pcmk_remote_running: + self.config.runner.cib.load(resources=self.fixture_cib_resources()) + if not cib_load_exception: + self.config.services.is_running( + "booth", instance=instance_name, return_value=booth_running + ) + self.config.services.is_enabled( + "booth", instance=instance_name, return_value=booth_enabled + ) + def fixture_config_success(self, instance_name="booth"): self.fixture_config_booth_not_used(instance_name) self.config.raw_file.read( @@ -663,17 +704,29 @@ def test_not_live(self): expected_in_processor=False, ) - def test_booth_config_in_use(self): + def test_booth_config_in_use_cib_pcmk(self): instance_name = "booth" + self.fixture_config_booth_used(instance_name, pcmk_running=True) - self.config.runner.cib.load(resources=self.fixture_cib_resources()) - self.config.services.is_running( - "booth", instance=instance_name, return_value=True + self.env_assist.assert_raise_library_error( + lambda: commands.config_destroy(self.env_assist.get_env()), ) - self.config.services.is_enabled( - "booth", instance=instance_name, return_value=True + + self.env_assist.assert_reports( + [ + fixture.error( + reports.codes.BOOTH_CONFIG_IS_USED, + name=instance_name, + detail=reports.const.BOOTH_CONFIG_USED_IN_CLUSTER_RESOURCE, + resource_name="booth_resource", + ), + ] ) + def test_booth_config_in_use_cib_pcmk_remote(self): + instance_name = "booth" + self.fixture_config_booth_used(instance_name, pcmk_remote_running=True) + self.env_assist.assert_raise_library_error( lambda: commands.config_destroy(self.env_assist.get_env()), ) @@ -686,16 +739,57 @@ def test_booth_config_in_use(self): detail=reports.const.BOOTH_CONFIG_USED_IN_CLUSTER_RESOURCE, resource_name="booth_resource", ), + ] + ) + + def test_pcmk_not_running(self): + instance_name = "booth" + self.fixture_config_booth_used(instance_name, cib_exists=True) + + self.env_assist.assert_raise_library_error( + lambda: commands.config_destroy(self.env_assist.get_env()), + [ + fixture.error( + reports.codes.CIB_LOAD_ERROR, + reason="unable to get cib, pcmk is not running", + ) + ], + expected_in_processor=False, + ) + + def test_booth_config_in_use_systemd_running(self): + instance_name = "booth" + self.fixture_config_booth_used(instance_name, booth_running=True) + + self.env_assist.assert_raise_library_error( + lambda: commands.config_destroy(self.env_assist.get_env()), + ) + + self.env_assist.assert_reports( + [ fixture.error( reports.codes.BOOTH_CONFIG_IS_USED, name=instance_name, - detail=reports.const.BOOTH_CONFIG_USED_ENABLED_IN_SYSTEMD, + detail=reports.const.BOOTH_CONFIG_USED_RUNNING_IN_SYSTEMD, resource_name=None, ), + ] + ) + + def test_booth_config_in_use_systemd_enabled(self): + instance_name = "booth" + self.fixture_config_booth_used(instance_name, booth_enabled=True) + + self.env_assist.assert_raise_library_error( + lambda: commands.config_destroy(self.env_assist.get_env()), + ) + + self.env_assist.assert_reports( + [ fixture.error( reports.codes.BOOTH_CONFIG_IS_USED, name=instance_name, - detail=reports.const.BOOTH_CONFIG_USED_RUNNING_IN_SYSTEMD, + detail=reports.const.BOOTH_CONFIG_USED_ENABLED_IN_SYSTEMD, resource_name=None, ), ] From be17a9beeb9a55e0c10a34cce5672ebe793bf650 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Mon, 3 Jun 2024 17:12:56 +0200 Subject: [PATCH 007/227] validate sbd options --- CHANGELOG.md | 6 +- pcs/lib/commands/sbd.py | 38 +++++-- .../tier0/lib/commands/sbd/test_enable_sbd.py | 101 ++++++++++++++---- 3 files changed, 119 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90e907535..78a6dfcea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ - Report an error when an invalid resource-discovery is specified ([RHEL-7701]) - 'pcs booth destroy' now works for nodes without a cluster (such as arbitrators) ([RHEL-7737]) +- Validate SBD\_DELAY\_START and SBD\_STARTMODE options ([RHEL-17962]) ### Deprecated - Pcs produces warnings about [features planned to be removed in pacemaker 3](https://projects.clusterlabs.org/w/projects/pacemaker/pacemaker_3.0_changes/pacemaker_3.0_configuration_changes/): @@ -40,10 +41,11 @@ [RHEL-7701]: https://issues.redhat.com/browse/RHEL-7701 [RHEL-7737]: https://issues.redhat.com/browse/RHEL-7737 [RHEL-16231]: https://issues.redhat.com/browse/RHEL-16231 +[RHEL-17962]: https://issues.redhat.com/browse/RHEL-17962 +[RHEL-21051]: https://issues.redhat.com/browse/RHEL-21051 +[RHEL-25854]: https://issues.redhat.com/browse/RHEL-25854 [RHEL-27492]: https://issues.redhat.com/browse/RHEL-27492 [RHEL-28749]: https://issues.redhat.com/browse/RHEL-28749 -[RHEL-25854]: https://issues.redhat.com/browse/RHEL-25854 -[RHEL-21051]: https://issues.redhat.com/browse/RHEL-21051 [RHEL-36514]: https://issues.redhat.com/browse/RHEL-36514 diff --git a/pcs/lib/commands/sbd.py b/pcs/lib/commands/sbd.py index b50d0bf2c..6a197a7ce 100644 --- a/pcs/lib/commands/sbd.py +++ b/pcs/lib/commands/sbd.py @@ -1,6 +1,9 @@ +from typing import Any + from pcs import settings from pcs.common import reports from pcs.common.reports.item import ReportItem +from pcs.common.validate import is_integer from pcs.lib import ( sbd, validate, @@ -22,13 +25,13 @@ from pcs.lib.node import get_existing_nodes_names from pcs.lib.tools import environment_file_to_dict -UNSUPPORTED_SBD_OPTION_LIST = [ +_UNSUPPORTED_SBD_OPTION_LIST = [ "SBD_WATCHDOG_DEV", "SBD_OPTS", "SBD_PACEMAKER", "SBD_DEVICE", ] -ALLOWED_SBD_OPTION_LIST = [ +_ALLOWED_SBD_OPTION_LIST = [ "SBD_DELAY_START", "SBD_STARTMODE", "SBD_WATCHDOG_TIMEOUT", @@ -38,13 +41,14 @@ {"flush", "noflush"}, {"reboot", "off", "crashdump"}, ) +_STARTMODE_ALLOWED_VALUES = ["always", "clean"] def __tuple(set1, set2): return {f"{v1},{v2}" for v1 in set1 for v2 in set2} -TIMEOUT_ACTION_ALLOWED_VALUE_LIST = sorted( +_TIMEOUT_ACTION_ALLOWED_VALUE_LIST = sorted( _TIMEOUT_ACTION_ALLOWED_VALUES[0] | _TIMEOUT_ACTION_ALLOWED_VALUES[1] | __tuple( @@ -56,6 +60,15 @@ def __tuple(set1, set2): ) +class _ValueSbdDelayStart(validate.ValuePredicateBase): + def _is_valid(self, value: validate.TypeOptionValue) -> bool: + # 1 means yes, so we don't allow it to prevent confusion + return value in ["yes", "no"] or is_integer(value, 2) + + def _get_allowed_values(self) -> Any: + return "'yes', 'no' or an integer greater than 1" + + def _validate_sbd_options( sbd_config, allow_unknown_opts=False, allow_invalid_option_values=False ): @@ -68,16 +81,29 @@ def _validate_sbd_options( """ validators = [ validate.NamesIn( - ALLOWED_SBD_OPTION_LIST, - banned_name_list=UNSUPPORTED_SBD_OPTION_LIST, + _ALLOWED_SBD_OPTION_LIST, + banned_name_list=_UNSUPPORTED_SBD_OPTION_LIST, severity=reports.item.get_severity( reports.codes.FORCE, allow_unknown_opts ), ), + _ValueSbdDelayStart( + "SBD_DELAY_START", + severity=reports.item.get_severity( + reports.codes.FORCE, allow_invalid_option_values + ), + ), + validate.ValueIn( + "SBD_STARTMODE", + _STARTMODE_ALLOWED_VALUES, + severity=reports.item.get_severity( + reports.codes.FORCE, allow_invalid_option_values + ), + ), validate.ValueNonnegativeInteger("SBD_WATCHDOG_TIMEOUT"), validate.ValueIn( "SBD_TIMEOUT_ACTION", - TIMEOUT_ACTION_ALLOWED_VALUE_LIST, + _TIMEOUT_ACTION_ALLOWED_VALUE_LIST, severity=reports.item.get_severity( reports.codes.FORCE, allow_invalid_option_values ), diff --git a/pcs_test/tier0/lib/commands/sbd/test_enable_sbd.py b/pcs_test/tier0/lib/commands/sbd/test_enable_sbd.py index 4aee9bec1..238006a3f 100644 --- a/pcs_test/tier0/lib/commands/sbd/test_enable_sbd.py +++ b/pcs_test/tier0/lib/commands/sbd/test_enable_sbd.py @@ -6,8 +6,8 @@ from pcs.common import reports from pcs.common.reports import codes as report_codes from pcs.lib.commands.sbd import ( - ALLOWED_SBD_OPTION_LIST, - TIMEOUT_ACTION_ALLOWED_VALUE_LIST, + _ALLOWED_SBD_OPTION_LIST, + _TIMEOUT_ACTION_ALLOWED_VALUE_LIST, enable_sbd, ) from pcs.lib.corosync.config_parser import Parser @@ -808,18 +808,56 @@ def test_invalid_opt_values(self): default_watchdog="/dev/watchdog", watchdog_dict={}, sbd_options={ + "SBD_DELAY_START": "bad_delay", + "SBD_STARTMODE": "bad_startmode", + "SBD_WATCHDOG_TIMEOUT": "bad_timeout", "SBD_TIMEOUT_ACTION": "noflush,flush", + "UNKNOWN_OPT1": 1, }, ) ) self.env_assist.assert_reports( [ + fixture.error( + report_codes.INVALID_OPTIONS, + option_names=["UNKNOWN_OPT1"], + option_type=None, + allowed=sorted(_ALLOWED_SBD_OPTION_LIST), + allowed_patterns=[], + force_code=report_codes.FORCE, + ), + fixture.error( + report_codes.INVALID_OPTION_VALUE, + force_code=report_codes.FORCE, + option_name="SBD_DELAY_START", + option_value="bad_delay", + allowed_values="'yes', 'no' or an integer greater than 1", + cannot_be_empty=False, + forbidden_characters=None, + ), + fixture.error( + report_codes.INVALID_OPTION_VALUE, + force_code=report_codes.FORCE, + option_name="SBD_STARTMODE", + option_value="bad_startmode", + allowed_values=["always", "clean"], + cannot_be_empty=False, + forbidden_characters=None, + ), + fixture.error( + report_codes.INVALID_OPTION_VALUE, + option_name="SBD_WATCHDOG_TIMEOUT", + option_value="bad_timeout", + allowed_values="a non-negative integer", + cannot_be_empty=False, + forbidden_characters=None, + ), fixture.error( report_codes.INVALID_OPTION_VALUE, force_code=report_codes.FORCE, option_name="SBD_TIMEOUT_ACTION", option_value="noflush,flush", - allowed_values=TIMEOUT_ACTION_ALLOWED_VALUE_LIST, + allowed_values=_TIMEOUT_ACTION_ALLOWED_VALUE_LIST, cannot_be_empty=False, forbidden_characters=None, ), @@ -833,6 +871,9 @@ def test_invalid_opt_values_forced(self): default_watchdog="/dev/watchdog", watchdog_dict={}, sbd_options={ + "SBD_DELAY_START": "bad_delay", + "SBD_STARTMODE": "bad_startmode", + "SBD_WATCHDOG_TIMEOUT": "bad_timeout", "SBD_TIMEOUT_ACTION": "noflush,flush", "UNKNOWN_OPT1": 1, }, @@ -841,22 +882,46 @@ def test_invalid_opt_values_forced(self): ) self.env_assist.assert_reports( [ - fixture.warn( - report_codes.INVALID_OPTION_VALUE, - option_name="SBD_TIMEOUT_ACTION", - option_value="noflush,flush", - allowed_values=TIMEOUT_ACTION_ALLOWED_VALUE_LIST, - cannot_be_empty=False, - forbidden_characters=None, - ), fixture.error( report_codes.INVALID_OPTIONS, option_names=["UNKNOWN_OPT1"], option_type=None, - allowed=sorted(ALLOWED_SBD_OPTION_LIST), + allowed=sorted(_ALLOWED_SBD_OPTION_LIST), allowed_patterns=[], force_code=report_codes.FORCE, ), + fixture.warn( + report_codes.INVALID_OPTION_VALUE, + option_name="SBD_DELAY_START", + option_value="bad_delay", + allowed_values="'yes', 'no' or an integer greater than 1", + cannot_be_empty=False, + forbidden_characters=None, + ), + fixture.warn( + report_codes.INVALID_OPTION_VALUE, + option_name="SBD_STARTMODE", + option_value="bad_startmode", + allowed_values=["always", "clean"], + cannot_be_empty=False, + forbidden_characters=None, + ), + fixture.error( + report_codes.INVALID_OPTION_VALUE, + option_name="SBD_WATCHDOG_TIMEOUT", + option_value="bad_timeout", + allowed_values="a non-negative integer", + cannot_be_empty=False, + forbidden_characters=None, + ), + fixture.warn( + report_codes.INVALID_OPTION_VALUE, + option_name="SBD_TIMEOUT_ACTION", + option_value="noflush,flush", + allowed_values=_TIMEOUT_ACTION_ALLOWED_VALUE_LIST, + cannot_be_empty=False, + forbidden_characters=None, + ), ] ) @@ -880,7 +945,7 @@ def test_unknown_sbd_opts(self): report_codes.INVALID_OPTIONS, option_names=["UNKNOWN_OPT1", "UNKNOWN_OPT2"], option_type=None, - allowed=sorted(ALLOWED_SBD_OPTION_LIST), + allowed=sorted(_ALLOWED_SBD_OPTION_LIST), allowed_patterns=[], force_code=report_codes.FORCE, ), @@ -888,7 +953,7 @@ def test_unknown_sbd_opts(self): report_codes.INVALID_OPTIONS, option_names=["SBD_WATCHDOG_DEV"], option_type=None, - allowed=sorted(ALLOWED_SBD_OPTION_LIST), + allowed=sorted(_ALLOWED_SBD_OPTION_LIST), allowed_patterns=[], ), ] @@ -915,14 +980,14 @@ def test_unknown_sbd_opts_allowed(self): report_codes.INVALID_OPTIONS, option_names=["UNKNOWN_OPT1", "UNKNOWN_OPT2"], option_type=None, - allowed=sorted(ALLOWED_SBD_OPTION_LIST), + allowed=sorted(_ALLOWED_SBD_OPTION_LIST), allowed_patterns=[], ), fixture.error( report_codes.INVALID_OPTIONS, option_names=["SBD_WATCHDOG_DEV"], option_type=None, - allowed=sorted(ALLOWED_SBD_OPTION_LIST), + allowed=sorted(_ALLOWED_SBD_OPTION_LIST), allowed_patterns=[], ), ] @@ -1164,14 +1229,14 @@ def test_multiple_validation_failures(self): report_codes.INVALID_OPTIONS, option_names=["SBD_WATCHDOG_DEV"], option_type=None, - allowed=sorted(ALLOWED_SBD_OPTION_LIST), + allowed=sorted(_ALLOWED_SBD_OPTION_LIST), allowed_patterns=[], ), fixture.error( report_codes.INVALID_OPTIONS, option_names=["UNKNOWN_OPT1", "UNKNOWN_OPT2"], option_type=None, - allowed=sorted(ALLOWED_SBD_OPTION_LIST), + allowed=sorted(_ALLOWED_SBD_OPTION_LIST), allowed_patterns=[], force_code=report_codes.FORCE, ), From 86bfca8296c71bd8399f595bb50f339e67c0b08f Mon Sep 17 00:00:00 2001 From: Michal Pospisil Date: Thu, 27 Jun 2024 18:05:15 +0200 Subject: [PATCH 008/227] fix distro detection in Fedora CI --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index b4ef2682d..6ca44faaa 100644 --- a/configure.ac +++ b/configure.ac @@ -240,7 +240,7 @@ for i in $DISTRO $DISTROS; do DISTROEXT=debian break ;; - fedora|rhel|centos|centos-stream*|opensuse*) + fedora*|rhel|centos|centos-stream*|opensuse*) FOUND_DISTRO=1 CONFIGDIR="$sysconfdir/sysconfig" PCSLIBDIR="$LIBDIR" From 69541b064ccef67caf55208b5a772216e459c426 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Wed, 3 Jul 2024 16:10:03 +0200 Subject: [PATCH 009/227] remove ondrejmular from default reviewers --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b9b5ce55b..20f0c6ef8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,3 @@ # https://help.github.com/en/articles/about-code-owners # Default reviewers for everything -* @ondrejmular @tomjelinek +* @tomjelinek From 694c165b81af4847965afa1cdf61a1441e98dfc4 Mon Sep 17 00:00:00 2001 From: Michal Pospisil Date: Tue, 9 Jul 2024 14:39:15 +0200 Subject: [PATCH 010/227] Bumped to 0.11.8 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78a6dfcea..4fc45e47e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log -## [Unreleased] +## [0.11.8] - 2024-07-09 ### Added - Support for output formats `json` and `cmd` to resources/stonith defaults and From 700cfec62836bcb1c302b6435686554ece3e6846 Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Fri, 12 Jul 2024 12:35:45 +0200 Subject: [PATCH 011/227] add support for output formats for tags --- CHANGELOG.md | 9 + pcs/Makefile.am | 4 +- pcs/cli/common/lib_wrapper.py | 1 + pcs/cli/tag/command.py | 21 +-- pcs/cli/tag/output.py | 45 +++++ pcs/common/pacemaker/tag.py | 16 ++ pcs/config.py | 9 +- .../async_tasks/worker/command_mapping.py | 5 + pcs/lib/cib/tag.py | 11 ++ pcs/lib/commands/tag.py | 53 ++++-- pcs/pcs.8.in | 4 +- pcs/usage.py | 8 +- pcs_test/tier0/cli/tag/test_command.py | 49 +++--- .../tier0/lib/commands/tag/test_tag_config.py | 118 ++++++++++--- pcs_test/tier1/test_tag.py | 161 +++++++++++++++++- pcsd/capabilities.xml.in | 11 ++ 16 files changed, 426 insertions(+), 99 deletions(-) create mode 100644 pcs/cli/tag/output.py create mode 100644 pcs/common/pacemaker/tag.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fc45e47e..6dc92a13e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Change Log +## [Unreleased] + +### Added +- Support for output formats `json` and `cmd` to `pcs tag config` command +([RHEL-46284]) + +[RHEL-46284]: https://issues.redhat.com/browse/RHEL-46284 + + ## [0.11.8] - 2024-07-09 ### Added diff --git a/pcs/Makefile.am b/pcs/Makefile.am index 707a07743..c910bfd4d 100644 --- a/pcs/Makefile.am +++ b/pcs/Makefile.am @@ -102,8 +102,9 @@ EXTRA_DIST = \ cli/rule.py \ cli/status/command.py \ cli/status/__init__.py \ - cli/tag/command.py \ cli/tag/__init__.py \ + cli/tag/command.py \ + cli/tag/output.py \ cluster.py \ common/corosync_conf.py \ common/const.py \ @@ -145,6 +146,7 @@ EXTRA_DIST = \ common/pacemaker/resource/relations.py \ common/pacemaker/role.py \ common/pacemaker/rule.py \ + common/pacemaker/tag.py \ common/pacemaker/tools.py \ common/pacemaker/types.py \ common/permissions/__init__.py \ diff --git a/pcs/cli/common/lib_wrapper.py b/pcs/cli/common/lib_wrapper.py index 06a812d9e..6f667c708 100644 --- a/pcs/cli/common/lib_wrapper.py +++ b/pcs/cli/common/lib_wrapper.py @@ -530,6 +530,7 @@ def load_module(env, middleware_factory, name): { "config": tag.config, "create": tag.create, + "get_config_dto": tag.get_config_dto, "remove": tag.remove, "update": tag.update, }, diff --git a/pcs/cli/tag/command.py b/pcs/cli/tag/command.py index de7926e79..d74656f72 100644 --- a/pcs/cli/tag/command.py +++ b/pcs/cli/tag/command.py @@ -6,11 +6,8 @@ InputModifiers, group_by_keywords, ) -from pcs.cli.reports.output import ( - deprecation_warning, - print_to_stderr, -) -from pcs.common.str_tools import indent +from pcs.cli.reports.output import deprecation_warning +from pcs.cli.tag.output import print_config def tag_create(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: @@ -41,17 +38,11 @@ def tag_config(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file + * --output-format - supported formats: text, cmd, json """ - modifiers.ensure_only_supported("-f") - tag_list = lib.tag.config(argv) - if not tag_list: - print_to_stderr(" No tags defined") - return - lines = [] - for tag in tag_list: - lines.append(tag["tag_id"]) - lines.extend(indent(tag["idref_list"])) - print("\n".join(lines)) + modifiers.ensure_only_supported("-f", "--output-format") + tag_dto = lib.tag.get_config_dto(argv) + print_config(tag_dto, modifiers) def tag_remove(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: diff --git a/pcs/cli/tag/output.py b/pcs/cli/tag/output.py new file mode 100644 index 000000000..cbc7b4801 --- /dev/null +++ b/pcs/cli/tag/output.py @@ -0,0 +1,45 @@ +import json +import shlex + +from pcs.cli.common.output import lines_to_str +from pcs.cli.common.parse_args import ( + OUTPUT_FORMAT_VALUE_CMD, + OUTPUT_FORMAT_VALUE_JSON, + InputModifiers, +) +from pcs.common.interface import dto +from pcs.common.pacemaker.tag import CibTagListDto +from pcs.common.str_tools import indent + + +def tags_to_text(tags_dto: CibTagListDto) -> list[str]: + result = [] + for tag in tags_dto.tags: + result.append(tag.id) + result.extend(indent(list(tag.idref_list))) + return result + + +def tags_to_cmd(tags_dto: CibTagListDto) -> list[str]: + return [ + "pcs -- tag create {tag_id} {idref_list}".format( + tag_id=shlex.quote(tag.id), + idref_list=" ".join(shlex.quote(idref) for idref in tag.idref_list), + ) + for tag in tags_dto.tags + ] + + +def print_config(tags_dto: CibTagListDto, modifiers: InputModifiers) -> None: + output_format = modifiers.get_output_format() + if output_format == OUTPUT_FORMAT_VALUE_JSON: + print(json.dumps(dto.to_dict(tags_dto), indent=2)) + return + + if output_format == OUTPUT_FORMAT_VALUE_CMD: + print(";\n".join(tags_to_cmd(tags_dto))) + return + + result = lines_to_str(tags_to_text(tags_dto)) + if result: + print(result) diff --git a/pcs/common/pacemaker/tag.py b/pcs/common/pacemaker/tag.py new file mode 100644 index 000000000..63c55464f --- /dev/null +++ b/pcs/common/pacemaker/tag.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass +from typing import Sequence + +from pcs.common.interface.dto import DataTransferObject +from pcs.common.types import StringSequence + + +@dataclass(frozen=True) +class CibTagDto(DataTransferObject): + id: str + idref_list: StringSequence + + +@dataclass(frozen=True) +class CibTagListDto(DataTransferObject): + tags: Sequence[CibTagDto] diff --git a/pcs/config.py b/pcs/config.py index 1146b3803..e0919cc68 100644 --- a/pcs/config.py +++ b/pcs/config.py @@ -46,6 +46,7 @@ ResourcesConfigurationFacade, resources_to_text, ) +from pcs.cli.tag.output import tags_to_text from pcs.common.interface import dto from pcs.common.pacemaker.constraint import CibConstraintsDto from pcs.common.str_tools import indent @@ -220,15 +221,11 @@ def _config_show_cib_lines(lib, properties_facade=None): all_lines.append("") all_lines.extend(properties_lines) - tags = lib.tag.config([]) - if tags: + tag_lines = smart_wrap_text(tags_to_text(lib.tag.get_config_dto([]))) + if tag_lines: if all_lines: all_lines.append("") all_lines.append("Tags:") - tag_lines = [] - for tag in tags: - tag_lines.append(tag["tag_id"]) - tag_lines.extend(indent(tag["idref_list"])) all_lines.extend(indent(tag_lines, indent_step=1)) return all_lines diff --git a/pcs/daemon/async_tasks/worker/command_mapping.py b/pcs/daemon/async_tasks/worker/command_mapping.py index 8819744ad..6dc99164f 100644 --- a/pcs/daemon/async_tasks/worker/command_mapping.py +++ b/pcs/daemon/async_tasks/worker/command_mapping.py @@ -24,6 +24,7 @@ status, stonith, stonith_agent, + tag, ) from pcs.lib.permissions.config.types import PermissionAccessType as p @@ -384,6 +385,10 @@ class _Cmd: cmd=stonith.create_in_group, required_permission=p.WRITE, ), + "tag.get_config_dto": _Cmd( + cmd=tag.get_config_dto, + required_permission=p.READ, + ), # CMDs allowed in pcs_internal but not exposed via REST API: # "services.disable_service": Cmd(services.disable_service, # "services.enable_service": Cmd(services.enable_service, diff --git a/pcs/lib/cib/tag.py b/pcs/lib/cib/tag.py index cab36b3b6..0bf0fe6f1 100644 --- a/pcs/lib/cib/tag.py +++ b/pcs/lib/cib/tag.py @@ -15,6 +15,7 @@ from lxml.etree import _Element from pcs.common import reports +from pcs.common.pacemaker.tag import CibTagDto from pcs.common.reports import ( ReportItem, ReportItemList, @@ -658,6 +659,16 @@ def tag_element_to_dict( } +def tag_element_to_dto(tag_element: _Element) -> CibTagDto: + return CibTagDto( + str(tag_element.attrib["id"]), + [ + str(obj_ref.attrib["id"]) + for obj_ref in tag_element.findall(TAG_OBJREF) + ], + ) + + def expand_tag( some_or_tag_el: _Element, only_expand_types: Optional[StringCollection] = None, diff --git a/pcs/lib/commands/tag.py b/pcs/lib/commands/tag.py index 393f47b7a..04b7f900b 100644 --- a/pcs/lib/commands/tag.py +++ b/pcs/lib/commands/tag.py @@ -7,6 +7,7 @@ from lxml.etree import _Element +from pcs.common.pacemaker.tag import CibTagListDto from pcs.common.types import StringSequence from pcs.lib.cib import tag from pcs.lib.cib.tools import ( @@ -50,9 +51,25 @@ def create( tag.create_tag(tags_section, tag_id, idref_list) +def _get_tag_elements( + env: LibraryEnvironment, tag_filter: StringSequence +) -> list[_Element]: + tags_section: _Element = get_tags(env.get_cib()) + + if not tag_filter: + return tag.get_list_of_tag_elements(tags_section) + + tag_element_list, report_list = tag.find_tag_elements_by_ids( + tags_section, + tag_filter, + ) + if env.report_processor.report_list(report_list).has_errors: + raise LibraryError() + return tag_element_list + + def config( - env: LibraryEnvironment, - tag_filter: StringSequence, + env: LibraryEnvironment, tag_filter: StringSequence ) -> list[dict[str, Union[str, list[str]]]]: """ Get tags specified in tag_filter or if empty, then get all the tags @@ -61,21 +78,33 @@ def config( env -- provides all for communication with externals tag_filter -- list of tags we want to get """ - tags_section: _Element = get_tags(env.get_cib()) - if tag_filter: - tag_element_list, report_list = tag.find_tag_elements_by_ids( - tags_section, - tag_filter, - ) - if env.report_processor.report_list(report_list).has_errors: - raise LibraryError() - else: - tag_element_list = tag.get_list_of_tag_elements(tags_section) + tag_element_list = _get_tag_elements(env, tag_filter) + return [ tag.tag_element_to_dict(tag_element) for tag_element in tag_element_list ] +def get_config_dto( + env: LibraryEnvironment, tag_filter: StringSequence +) -> CibTagListDto: + """ + Get tags specified in tag_filter or if empty, then get all the tags + configured. + + env -- provides all for communication with externals + tag_filter -- list of tags we want to get + """ + tag_element_list = _get_tag_elements(env, tag_filter) + + return CibTagListDto( + [ + tag.tag_element_to_dto(tag_element) + for tag_element in tag_element_list + ] + ) + + def remove(env: LibraryEnvironment, tag_list: StringSequence) -> None: """ Remove specified tags from a cib. diff --git a/pcs/pcs.8.in b/pcs/pcs.8.in index 63c40b786..a67540cb5 100644 --- a/pcs/pcs.8.in +++ b/pcs/pcs.8.in @@ -1584,8 +1584,8 @@ destroy Permanently destroy disaster-recovery configuration on all sites. .SS "tag" .TP -[config|list [...]] -Display configured tags. +[config|list [...]] [@OUTPUT_FORMAT_SYNTAX_DOC@] +Display configured tags. @OUTPUT_FORMAT_DESC_DOC@ .TP create []... Create a tag containing the specified ids. diff --git a/pcs/usage.py b/pcs/usage.py index a4af30d46..3aab9ae98 100644 --- a/pcs/usage.py +++ b/pcs/usage.py @@ -3347,8 +3347,9 @@ def tag(args: Argv) -> str: Manage pacemaker tags. Commands: - [config|list [...]] + [config|list [...]] [{output_format_syntax}] Display configured tags. + {output_format_desc} create []... Create a tag containing the specified ids. @@ -3366,7 +3367,10 @@ def tag(args: Argv) -> str: of the added ids relatively to some id already existing in the tag. By adding ids to a tag they are already in and specifying --after or --before you can move the ids in the tag. -""" +""".format( + output_format_syntax=_output_format_syntax(cmd=True), + output_format_desc=_format_desc((" ", _output_format_desc())), + ) return sub_usage(args, output) diff --git a/pcs_test/tier0/cli/tag/test_command.py b/pcs_test/tier0/cli/tag/test_command.py index 830213c82..6bcfd405f 100644 --- a/pcs_test/tier0/cli/tag/test_command.py +++ b/pcs_test/tier0/cli/tag/test_command.py @@ -6,6 +6,10 @@ from pcs.cli.common.errors import CmdLineInputError from pcs.cli.tag import command +from pcs.common.pacemaker.tag import ( + CibTagDto, + CibTagListDto, +) from pcs.lib.errors import LibraryError from pcs_test.tools.misc import dict_to_modifiers @@ -45,39 +49,32 @@ def test_multiple_args(self): self.tag.create.assert_called_once_with("tagid", ["id1", "id2"]) -@mock.patch("pcs.cli.tag.command.print") +@mock.patch("pcs.cli.tag.output.print") class TagConfig(TestCase): - tag_dicts = [ - { - "tag_id": "tag1", - "idref_list": ["i1", "i2", "i3"], - }, - { - "tag_id": "tag2", - "idref_list": ["j1", "j2", "j3"], - }, + tag_dtos = [ + CibTagDto("tag1", ["i1", "i2", "i3"]), + CibTagDto("tag2", ["j1", "j2", "j3"]), ] def setUp(self): self.lib = mock.Mock(spec_set=["tag"]) - self.tag = mock.Mock(spec_set=["config"]) + self.tag = mock.Mock(spec_set=["get_config_dto"]) self.lib.tag = self.tag + self.lib_command = self.lib.tag.get_config_dto def _call_cmd(self, argv): command.tag_config(self.lib, argv, dict_to_modifiers({})) - @mock.patch("pcs.cli.tag.command.print_to_stderr") - def test_no_args_no_tags(self, mock_stderr_print, mock_print): - self.tag.config.return_value = [] + def test_no_args_no_tags(self, mock_print): + self.lib_command.return_value = CibTagListDto([]) self._call_cmd([]) - self.tag.config.assert_called_once_with([]) + self.lib_command.assert_called_once_with([]) mock_print.assert_not_called() - mock_stderr_print.assert_called_once_with(" No tags defined") def test_no_args_all_tags(self, mock_print): - self.tag.config.return_value = self.tag_dicts + self.lib_command.return_value = CibTagListDto(self.tag_dtos) self._call_cmd([]) - self.tag.config.assert_called_once_with([]) + self.lib_command.assert_called_once_with([]) mock_print.assert_called_once_with( dedent( """\ @@ -93,9 +90,9 @@ def test_no_args_all_tags(self, mock_print): ) def test_specified_tag(self, mock_print): - self.tag.config.return_value = self.tag_dicts[1:2] + self.lib_command.return_value = CibTagListDto(self.tag_dtos[1:2]) self._call_cmd(["tag2"]) - self.tag.config.assert_called_once_with(["tag2"]) + self.lib_command.assert_called_once_with(["tag2"]) mock_print.assert_called_once_with( dedent( """\ @@ -107,9 +104,9 @@ def test_specified_tag(self, mock_print): ) def test_specified_another_tag(self, mock_print): - self.tag.config.return_value = self.tag_dicts[0:1] + self.lib_command.return_value = CibTagListDto(self.tag_dtos[0:1]) self._call_cmd(["tag1"]) - self.tag.config.assert_called_once_with(["tag1"]) + self.lib_command.assert_called_once_with(["tag1"]) mock_print.assert_called_once_with( dedent( """\ @@ -121,9 +118,9 @@ def test_specified_another_tag(self, mock_print): ) def test_specified_more_tags_are_printed_in_given_order(self, mock_print): - self.tag.config.return_value = self.tag_dicts[::-1] + self.lib_command.return_value = CibTagListDto(self.tag_dtos[::-1]) self._call_cmd(["tag2", "tag1"]) - self.tag.config.assert_called_once_with(["tag2", "tag1"]) + self.lib_command.assert_called_once_with(["tag2", "tag1"]) mock_print.assert_called_once_with( dedent( """\ @@ -139,10 +136,10 @@ def test_specified_more_tags_are_printed_in_given_order(self, mock_print): ) def test_no_print_on_exception(self, mock_print): - self.tag.config.side_effect = LibraryError() + self.lib_command.side_effect = LibraryError() with self.assertRaises(LibraryError): self._call_cmd(["something"]) - self.tag.config.assert_called_once_with(["something"]) + self.lib_command.assert_called_once_with(["something"]) mock_print.assert_not_called() diff --git a/pcs_test/tier0/lib/commands/tag/test_tag_config.py b/pcs_test/tier0/lib/commands/tag/test_tag_config.py index eb50add22..d82a5070d 100644 --- a/pcs_test/tier0/lib/commands/tag/test_tag_config.py +++ b/pcs_test/tier0/lib/commands/tag/test_tag_config.py @@ -1,5 +1,9 @@ from unittest import TestCase +from pcs.common.pacemaker.tag import ( + CibTagDto, + CibTagListDto, +) from pcs.lib.commands import tag as cmd_tag from pcs_test.tier0.lib.commands.tag.tag_common import ( @@ -10,18 +14,7 @@ from pcs_test.tools.command_env import get_env_tools -class TestTagConfig(TestCase): - tag_dicts_list = [ - { - "tag_id": "tag1", - "idref_list": ["i1", "i2"], - }, - { - "tag_id": "tag2", - "idref_list": ["j1", "j2"], - }, - ] - +class TestTagConfigBase(TestCase): def setUp(self): self.env_assist, self.config = get_env_tools(self) self.config.runner.cib.load( @@ -34,12 +27,52 @@ def setUp(self): resources=fixture_resources_for_ids(), ) + def assert_not_found_reports(self): + self.env_assist.assert_reports( + [ + fixture.report_not_found( + _id, expected_types=["tag"], context_type="tags" + ) + for _id in ["nonexistent_tag1", "nonexistent_tag2"] + ] + ) + + def assert_unexpected_element_reports(self): + self.env_assist.assert_reports( + [ + fixture.report_unexpected_element( + "id1", + "primitive", + expected_types=["tag"], + ), + ] + ) + + +class TestTagConfig(TestTagConfigBase): + tag_dicts_list = [ + { + "tag_id": "tag1", + "idref_list": ["i1", "i2"], + }, + { + "tag_id": "tag2", + "idref_list": ["j1", "j2"], + }, + ] + def test_success_no_args(self): self.assertEqual( cmd_tag.config(self.env_assist.get_env(), []), self.tag_dicts_list, ) + def test_only_selected(self): + self.assertEqual( + cmd_tag.config(self.env_assist.get_env(), ["tag2"]), + [self.tag_dicts_list[1]], + ) + def test_tag_id_does_not_exist(self): self.env_assist.assert_raise_library_error( lambda: cmd_tag.config( @@ -51,25 +84,54 @@ def test_tag_id_does_not_exist(self): ], ) ) - self.env_assist.assert_reports( - [ - fixture.report_not_found( - _id, expected_types=["tag"], context_type="tags" - ) - for _id in ["nonexistent_tag1", "nonexistent_tag2"] - ] - ) + self.assert_not_found_reports() def test_not_a_tag_id(self): self.env_assist.assert_raise_library_error( lambda: cmd_tag.config(self.env_assist.get_env(), ["id1"]) ) - self.env_assist.assert_reports( - [ - fixture.report_unexpected_element( - "id1", - "primitive", - expected_types=["tag"], - ), - ] + self.assert_unexpected_element_reports() + + +class GetTagConfigDto(TestTagConfigBase): + tag_dto_list = [ + CibTagDto("tag1", ["i1", "i2"]), + CibTagDto("tag2", ["j1", "j2"]), + ] + + def test_success_no_args(self): + self.assertEqual( + cmd_tag.get_config_dto(self.env_assist.get_env(), []), + CibTagListDto(self.tag_dto_list), + ) + + def test_success_only_selected(self): + self.assertEqual( + cmd_tag.get_config_dto(self.env_assist.get_env(), ["tag2"]), + CibTagListDto([self.tag_dto_list[1]]), + ) + + def test_success_selected_order(self): + self.assertEqual( + cmd_tag.get_config_dto(self.env_assist.get_env(), ["tag2", "tag1"]), + CibTagListDto(self.tag_dto_list[::-1]), + ) + + def test_tag_id_does_not_exist(self): + self.env_assist.assert_raise_library_error( + lambda: cmd_tag.get_config_dto( + self.env_assist.get_env(), + [ + "nonexistent_tag1", + "tag2", + "nonexistent_tag2", + ], + ) + ) + self.assert_not_found_reports() + + def test_not_a_tag_id(self): + self.env_assist.assert_raise_library_error( + lambda: cmd_tag.get_config_dto(self.env_assist.get_env(), ["id1"]) ) + self.assert_unexpected_element_reports() diff --git a/pcs_test/tier1/test_tag.py b/pcs_test/tier1/test_tag.py index 0befc7b67..7a461ab17 100644 --- a/pcs_test/tier1/test_tag.py +++ b/pcs_test/tier1/test_tag.py @@ -1,16 +1,32 @@ +import json +import shlex from textwrap import dedent from unittest import TestCase from lxml import etree +from pcs.common.interface import dto +from pcs.common.pacemaker.tag import ( + CibTagDto, + CibTagListDto, +) +from pcs.common.types import StringSequence +from pcs.lib.cib.tools import get_resources + +from pcs_test.tools import fixture_cib from pcs_test.tools.cib import get_assert_pcs_effect_mixin from pcs_test.tools.misc import get_test_resource as rc from pcs_test.tools.misc import ( get_tmp_file, outdent, + write_data_to_tmpfile, write_file_to_tmpfile, ) from pcs_test.tools.pcs_runner import PcsRunner +from pcs_test.tools.xml import ( + XmlManipulation, + etree_to_str, +) empty_cib = rc("cib-empty.xml") tags_cib = rc("cib-tags.xml") @@ -167,14 +183,10 @@ class TagConfigListBase(TestTagMixin): def test_config_empty(self): write_file_to_tmpfile(empty_cib, self.temp_cib) - self.assert_pcs_success( - ["tag"], - stderr_full=" No tags defined\n", - ) + self.assert_pcs_success(["tag"], stderr_full="", stdout_full="") self.assert_pcs_success( - ["tag", self.command], - stderr_full=(self.deprecation_msg + " No tags defined\n"), + ["tag", self.command], stderr_full=self.deprecation_msg ) def test_config_tag_does_not_exist(self): @@ -231,7 +243,7 @@ def test_config_specified_tags(self): ) -class TagConfig( +class TagConfigText( TagConfigListBase, TestCase, ): @@ -249,6 +261,141 @@ class TagList( ) +class TagConfigJson(TestTagMixin, TestCase): + def test_config_empty(self): + expected_output = json.loads(json.dumps(dto.to_dict(CibTagListDto([])))) + write_file_to_tmpfile(empty_cib, self.temp_cib) + + stdout, stderr = self.assert_pcs_success_ignore_output( + ["tag", "--output-format=json"] + ) + self.assertEqual(json.loads(stdout), expected_output) + self.assertEqual(stderr, "") + + stdout, stderr = self.assert_pcs_success_ignore_output( + ["tag", "config", "--output-format=json"] + ) + self.assertEqual(json.loads(stdout), expected_output) + + def test_config_tag_does_not_exist(self): + self.assert_pcs_fail( + ["tag", "config", "notag2", "notag1", "--output-format=json"], + ( + "Error: tag 'notag2' does not exist\n" + "Error: tag 'notag1' does not exist\n" + "Error: Errors have occurred, therefore pcs is unable to " + "continue\n" + ), + ) + self.assert_resources_xml_in_cib(self.fixture_tags_xml()) + + def test_config_tags_defined(self): + stdout, stderr = self.assert_pcs_success_ignore_output( + ["tag", "config", "--output-format=json"] + ) + self.assertEqual( + json.loads(stdout), + json.loads( + json.dumps( + dto.to_dict( + CibTagListDto( + [ + CibTagDto("tag1", ["x1", "x2", "x3"]), + CibTagDto("tag2", ["y1", "x2"]), + CibTagDto("tag3", ["y2-clone"]), + CibTagDto( + "tag-mixed-stonith-devices-and-resources", + ["fence-rh-2", "y1", "fence-rh-1", "x3"], + ), + ] + ) + ) + ) + ), + ) + self.assertEqual(stderr, "") + + def test_config_specified_tags(self): + stdout, stderr = self.assert_pcs_success_ignore_output( + ["tag", "config", "tag2", "tag1", "--output-format=json"], + ) + self.assertEqual( + json.loads(stdout), + json.loads( + json.dumps( + dto.to_dict( + CibTagListDto( + [ + CibTagDto("tag2", ["y1", "x2"]), + CibTagDto("tag1", ["x1", "x2", "x3"]), + ] + ) + ) + ) + ), + ) + self.assertEqual(stderr, "") + + +class TagConfigCmd(TestCase): + def setUp(self): + self.old_cib = get_tmp_file("tier1_tag_cmd_old") + write_file_to_tmpfile(tags_cib, self.old_cib) + self.new_cib = get_tmp_file("tier1_tag_cmd_new") + write_data_to_tmpfile( + fixture_cib.modify_cib_file( + empty_cib, + resources=etree_to_str( + get_resources(XmlManipulation.from_file(tags_cib).tree) + ), + ), + self.new_cib, + ) + self.old_pcs_runner = PcsRunner(self.old_cib.name) + self.new_pcs_runner = PcsRunner(self.new_cib.name) + + def tearDown(self): + self.old_cib.close() + self.new_cib.close() + + def _test_commands(self, tag_filter: StringSequence): + stdout, _, retval = self.old_pcs_runner.run( + ["tag", "config", "--output-format=cmd"] + list(tag_filter) + ) + self.assertEqual(retval, 0) + + commands = [ + shlex.split(command)[1:] + for command in stdout.replace("\\\n", "").strip().split(";\n") + ] + + for cmd in commands: + stdout, stderr, retval = self.new_pcs_runner.run(cmd) + self.assertEqual( + retval, + 0, + ( + f"Command {cmd} exited with {retval}\nstdout:\n{stdout}\n" + f"stderr:\n{stderr}" + ), + ) + old_stdout, _, old_retval = self.old_pcs_runner.run( + ["tag", "config", "--output-format=json"] + list(tag_filter) + ) + new_stdout, _, new_retval = self.new_pcs_runner.run( + ["tag", "config", "--output-format=json"] + ) + self.assertEqual(old_retval, 0) + self.assertEqual(new_retval, 0) + self.assertEqual(json.loads(old_stdout), json.loads(new_stdout)) + + def test_success(self): + self._test_commands([]) + + def test_success_filter(self): + self._test_commands(["tag2", "tag1"]) + + class PcsConfigTagsTest(TestTagMixin, TestCase): config_template = dedent( """\ diff --git a/pcsd/capabilities.xml.in b/pcsd/capabilities.xml.in index 99d5af3f0..5586aa643 100644 --- a/pcsd/capabilities.xml.in +++ b/pcsd/capabilities.xml.in @@ -2332,6 +2332,17 @@ pcs commands: tag create, tag update + + + Display configured tags in various formats. + + pcs commands: + tag config --output-format=text|json|cmd + APIv2: + tag.get_config_dto + + + From 460e2f2c257f89593d21307df5825dfe9c5560c8 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Wed, 24 Jul 2024 14:38:31 +0200 Subject: [PATCH 012/227] tests: overhaul fence agents mocking * add a stonith_admin mock to support validating instance atributes by agents * rename agents to make it clear we're working with mocks and to show what each mock is used for * create custom metedata for agents to fit usecases in tests * tier1 tests do not require fence agents to be installed in the system, all is mocked now --- .gitignore | 1 + configure.ac | 1 + pcs_test/Makefile.am | 11 +- .../tier1/cib_resource/test_clone_unclone.py | 2 +- .../tier1/cib_resource/test_stonith_create.py | 169 ++- .../test_stonith_enable_disable.py | 39 +- pcs_test/tier1/legacy/test_resource.py | 8 +- pcs_test/tier1/legacy/test_stonith.py | 1007 +++++------------ pcs_test/tier1/test_status.py | 20 +- pcs_test/tools/bin_mock/__init__.py | 4 + .../stonith__fence_apc_metadata.xml | 277 ----- .../stonith__fence_ilo_metadata.xml | 269 ----- ...stonith__fence_pcsmock_action_metadata.xml | 31 + ...stonith__fence_pcsmock_method_metadata.xml | 32 + ...tonith__fence_pcsmock_minimal_metadata.xml | 22 + ...stonith__fence_pcsmock_params_metadata.xml | 107 ++ ...nith__fence_pcsmock_unfencing_metadata.xml | 22 + .../stonith__fence_scsi_metadata.xml | 182 --- .../stonith__fence_xvm_metadata.xml | 120 -- .../tools/bin_mock/pcmk/crm_resource_mock.py | 28 +- pcs_test/tools/bin_mock/pcmk/stonith_admin.in | 5 + .../tools/bin_mock/pcmk/stonith_admin_mock.py | 60 + 22 files changed, 710 insertions(+), 1707 deletions(-) delete mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_apc_metadata.xml delete mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_ilo_metadata.xml create mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_action_metadata.xml create mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_method_metadata.xml create mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_minimal_metadata.xml create mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_params_metadata.xml create mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_unfencing_metadata.xml delete mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_scsi_metadata.xml delete mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_xvm_metadata.xml create mode 100755 pcs_test/tools/bin_mock/pcmk/stonith_admin.in create mode 100644 pcs_test/tools/bin_mock/pcmk/stonith_admin_mock.py diff --git a/.gitignore b/.gitignore index b08138ea8..6b39468d6 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ pcs_test/smoke.sh pcs_test/tools/bin_mock/pcmk/crm_resource pcs_test/tools/bin_mock/pcmk/pacemaker_metadata pcs_test/tools/bin_mock/pcmk/pacemaker-fenced +pcs_test/tools/bin_mock/pcmk/stonith_admin pcs_test/resources/*.tmp pcs_test/resources/temp*.xml pcs_test/resources/temp* diff --git a/configure.ac b/configure.ac index 6ca44faaa..71bbd1b3a 100644 --- a/configure.ac +++ b/configure.ac @@ -629,6 +629,7 @@ AC_CONFIG_FILES([pcs_test/pcs_for_tests], [chmod +x pcs_test/pcs_for_tests]) AC_CONFIG_FILES([pcs_test/suite], [chmod +x pcs_test/suite]) AC_CONFIG_FILES([pcs_test/tools/bin_mock/pcmk/crm_resource], [chmod +x pcs_test/tools/bin_mock/pcmk/crm_resource]) AC_CONFIG_FILES([pcs_test/tools/bin_mock/pcmk/pacemaker-fenced], [chmod +x pcs_test/tools/bin_mock/pcmk/pacemaker-fenced]) +AC_CONFIG_FILES([pcs_test/tools/bin_mock/pcmk/stonith_admin], [chmod +x pcs_test/tools/bin_mock/pcmk/stonith_admin]) AC_CONFIG_FILES([pcsd/pcsd], [chmod +x pcsd/pcsd]) AC_CONFIG_FILES([scripts/pcsd.sh], [chmod +x scripts/pcsd.sh]) diff --git a/pcs_test/Makefile.am b/pcs_test/Makefile.am index 4ca7d1ee7..85dece70c 100644 --- a/pcs_test/Makefile.am +++ b/pcs_test/Makefile.am @@ -415,18 +415,19 @@ EXTRA_DIST = \ tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__remote_metadata.xml \ tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__Stateful_metadata.xml \ tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__SystemHealth_metadata.xml \ - tools/bin_mock/pcmk/crm_resource.d/stonith__fence_apc_metadata.xml \ - tools/bin_mock/pcmk/crm_resource.d/stonith__fence_ilo_metadata.xml \ - tools/bin_mock/pcmk/crm_resource.d/stonith__fence_scsi_metadata.xml \ - tools/bin_mock/pcmk/crm_resource.d/stonith__fence_xvm_metadata.xml \ + tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_action_metadata.xml \ + tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_method_metadata.xml \ + tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_minimal_metadata.xml \ + tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_params_metadata.xml \ + tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_unfencing_metadata.xml \ tools/bin_mock/pcmk/crm_resource.d/systemd__test@a__b_metadata.xml \ tools/bin_mock/pcmk/crm_resource_mock.py \ - tools/bin_mock/pcmk/pacemaker-fenced \ tools/bin_mock/pcmk/pacemaker_metadata.d/pacemaker_based.xml \ tools/bin_mock/pcmk/pacemaker_metadata.d/pacemaker_controld.xml \ tools/bin_mock/pcmk/pacemaker_metadata.d/pacemaker_fenced.xml \ tools/bin_mock/pcmk/pacemaker_metadata.d/pacemaker_schedulerd.xml \ tools/bin_mock/pcmk/pacemaker_metadata.py \ + tools/bin_mock/pcmk/stonith_admin_mock.py \ tools/case_analysis.py \ tools/check/__init__.py \ tools/check/test_misc.py \ diff --git a/pcs_test/tier1/cib_resource/test_clone_unclone.py b/pcs_test/tier1/cib_resource/test_clone_unclone.py index 9a4c01856..3f991c234 100644 --- a/pcs_test/tier1/cib_resource/test_clone_unclone.py +++ b/pcs_test/tier1/cib_resource/test_clone_unclone.py @@ -34,7 +34,7 @@ def _get_primitive_fixture( FIXTURE_CLONE = f"""{FIXTURE_PRIMITIVE_FOR_CLONE}""" FIXTURE_STONITH_FOR_CLONE = """ - + diff --git a/pcs_test/tier1/cib_resource/test_stonith_create.py b/pcs_test/tier1/cib_resource/test_stonith_create.py index a42c3dd62..9b43a1a23 100644 --- a/pcs_test/tier1/cib_resource/test_stonith_create.py +++ b/pcs_test/tier1/cib_resource/test_stonith_create.py @@ -1,5 +1,3 @@ -import re - from pcs_test.tier1.cib_resource.common import ResourceTest from pcs_test.tools.bin_mock import get_mock_settings from pcs_test.tools.misc import is_minimum_pacemaker_version @@ -12,12 +10,17 @@ class PlainStonith(ResourceTest): + def setUp(self): + super().setUp() + self.pcs_runner.mock_settings = get_mock_settings( + "crm_resource_exec", "stonith_admin_exec" + ) + def test_simplest(self): - self.pcs_runner.mock_settings = get_mock_settings("crm_resource_exec") self.assert_effect( - "stonith create S fence_xvm".split(), + "stonith create S fence_pcsmock_minimal".split(), """ - + - + @@ -117,16 +81,18 @@ def test_warning_when_not_valid_agent(self): """, - stderr_full=error, - stderr_regexp=error_re, + stderr_full=( + "Warning: Agent 'stonith:absent' is not installed or " + "does not provide valid metadata: " + "pcs mock error message: unable to load agent metadata\n" + ), ) def test_disabled_puts_target_role_stopped(self): - self.pcs_runner.mock_settings = get_mock_settings("crm_resource_exec") self.assert_effect( - "stonith create S fence_xvm --disabled".split(), + "stonith create S fence_pcsmock_minimal --disabled".split(), """ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + """ - self.assert_effect("stonith create S fence_xvm".split(), result_xml) + self.assert_effect( + "stonith create S fence_pcsmock_minimal".split(), result_xml + ) self.assert_effect("stonith enable S".split(), result_xml) class Disable(ResourceTest): + def setUp(self): + super().setUp() + self.pcs_runner.mock_settings = get_mock_settings( + "crm_resource_exec", "stonith_admin_exec" + ) + def test_disable_enabled_stonith(self): - self.pcs_runner.mock_settings = get_mock_settings("crm_resource_exec") self.assert_effect( - "stonith create S fence_xvm".split(), + "stonith create S fence_pcsmock_minimal".split(), """ - + - + - + """ self.assert_effect( - "stonith create S fence_xvm --disabled".split(), result_xml + "stonith create S fence_pcsmock_minimal --disabled".split(), + result_xml, ) self.assert_effect("stonith disable S".split(), result_xml) diff --git a/pcs_test/tier1/legacy/test_resource.py b/pcs_test/tier1/legacy/test_resource.py index c6709682b..ec7bed01e 100644 --- a/pcs_test/tier1/legacy/test_resource.py +++ b/pcs_test/tier1/legacy/test_resource.py @@ -138,8 +138,8 @@ def test_nonextisting_agent(self): "resource describe ocf:pacemaker:nonexistent".split(), ( "Error: Agent 'ocf:pacemaker:nonexistent' is not installed or does " - "not provide valid metadata: Metadata query for " - "ocf:pacemaker:nonexistent failed: Input/output error\n" + "not provide valid metadata: " + "pcs mock error message: unable to load agent metadata\n" + ERRORS_HAVE_OCCURRED ), ) @@ -5018,8 +5018,8 @@ def test_nonexisting_agent(self): agent = "ocf:pacemaker:nonexistent" message = ( f"Agent '{agent}' is not installed or does " - "not provide valid metadata: Metadata query for " - f"{agent} failed: Input/output error" + "not provide valid metadata: " + "pcs mock error message: unable to load agent metadata" ) self.assert_pcs_success( f"resource create --force D0 {agent}".split(), diff --git a/pcs_test/tier1/legacy/test_stonith.py b/pcs_test/tier1/legacy/test_stonith.py index 6d0bff244..d8d1f4e04 100644 --- a/pcs_test/tier1/legacy/test_stonith.py +++ b/pcs_test/tier1/legacy/test_stonith.py @@ -40,12 +40,12 @@ def setUp(self): def test_success(self): self.assert_pcs_success( - "stonith describe fence_apc".split(), + "stonith describe fence_pcsmock_params".split(), stdout_start=dedent( """\ - fence_apc - Fence agent for APC over telnet/ssh + fence_pcsmock_params - Mock agent for pcs tests - agent with various params - fence_apc is an I/O Fencing agent which can be used with the APC network power switch. It logs into device via telnet/ssh and reboots a specified outlet. Lengthy telnet/ssh connections should be avoided while a GFS cluster is running because the connection will block any necessary fencing actions. + This is an agent with params for pcs tests Stonith options: """ @@ -54,7 +54,7 @@ def test_success(self): def test_full(self): self.assert_pcs_success( - "stonith describe fence_apc --full".split(), + "stonith describe fence_pcsmock_params --full".split(), stdout_regexp=".*pcmk_list_retries.*", ) @@ -63,10 +63,9 @@ def test_nonextisting_agent(self): "stonith describe fence_noexist".split(), ( "Error: Agent 'stonith:fence_noexist' is not installed or does not " - "provide valid metadata: Agent fence_noexist not found or does " - "not support meta-data: Invalid argument (22), " - "Metadata query for stonith:fence_noexist failed: Input/output " - "error\n" + ERRORS_HAVE_OCCURRED + "provide valid metadata: " + "pcs mock error message: unable to load agent metadata\n" + + ERRORS_HAVE_OCCURRED ), ) @@ -85,17 +84,17 @@ def test_too_many_params(self): def test_pcsd_interface(self): self.maxDiff = None stdout, stderr, returncode = self.pcs_runner.run( - "stonith get_fence_agent_info stonith:fence_apc".split() + "stonith get_fence_agent_info stonith:fence_pcsmock_params".split() ) self.assertEqual( json.loads(stdout), { - "name": "stonith:fence_apc", + "name": "stonith:fence_pcsmock_params", "standard": "stonith", "provider": None, - "type": "fence_apc", - "shortdesc": "Fence agent for APC over telnet/ssh", - "longdesc": "fence_apc is an I/O Fencing agent which can be used with the APC network power switch. It logs into device via telnet/ssh and reboots a specified outlet. Lengthy telnet/ssh connections should be avoided while a GFS cluster is running because the connection will block any necessary fencing actions.", + "type": "fence_pcsmock_params", + "shortdesc": "Mock agent for pcs tests - agent with various params", + "longdesc": "This is an agent with params for pcs tests", "parameters": [ { "name": "action", @@ -115,81 +114,6 @@ def test_pcsd_interface(self): "unique_group": None, "reloadable": False, }, - { - "name": "cmd_prompt", - "shortdesc": "Force Python regex for command prompt", - "longdesc": None, - "type": "string", - "default": "['\\n>', '\\napc>']", - "enum_values": None, - "required": False, - "advanced": False, - "deprecated": True, - "deprecated_by": ["command_prompt"], - "deprecated_desc": None, - "unique_group": None, - "reloadable": False, - }, - { - "name": "command_prompt", - "shortdesc": "Force Python regex for command prompt", - "longdesc": None, - "type": "string", - "default": "['\\n>', '\\napc>']", - "enum_values": None, - "required": False, - "advanced": False, - "deprecated": False, - "deprecated_by": [], - "deprecated_desc": None, - "unique_group": None, - "reloadable": False, - }, - { - "name": "identity_file", - "shortdesc": "Identity file (private key) for SSH", - "longdesc": None, - "type": "string", - "default": None, - "enum_values": None, - "required": False, - "advanced": False, - "deprecated": False, - "deprecated_by": [], - "deprecated_desc": None, - "unique_group": None, - "reloadable": False, - }, - { - "name": "inet4_only", - "shortdesc": "Forces agent to use IPv4 addresses only", - "longdesc": None, - "type": "boolean", - "default": None, - "enum_values": None, - "required": False, - "advanced": False, - "deprecated": False, - "deprecated_by": [], - "deprecated_desc": None, - "unique_group": None, - "reloadable": False, - }, - { - "name": "inet6_only", - "shortdesc": "Forces agent to use IPv6 addresses only", - "longdesc": None, - "type": "boolean", - "default": None, - "enum_values": None, - "required": False, - "advanced": False, - "deprecated": False, - "deprecated_by": [], - "deprecated_desc": None, - "unique_group": None, - "reloadable": False, - }, { "name": "ip", "shortdesc": "IP address or hostname of fencing device", @@ -220,21 +144,6 @@ def test_pcsd_interface(self): "unique_group": None, "reloadable": False, }, - { - "name": "ipport", - "shortdesc": "TCP/UDP port to use for connection with device", - "longdesc": None, - "type": "integer", - "default": "23", - "enum_values": None, - "required": False, - "advanced": False, - "deprecated": False, - "deprecated_by": [], - "deprecated_desc": None, - "unique_group": None, - "reloadable": False, - }, { "name": "login", "shortdesc": "Login name", @@ -265,21 +174,6 @@ def test_pcsd_interface(self): "unique_group": None, "reloadable": False, }, - { - "name": "passwd_script", - "shortdesc": "Script to run to retrieve password", - "longdesc": None, - "type": "string", - "default": None, - "enum_values": None, - "required": False, - "advanced": False, - "deprecated": True, - "deprecated_by": ["password_script"], - "deprecated_desc": None, - "unique_group": None, - "reloadable": False, - }, { "name": "password", "shortdesc": "Login password or passphrase", @@ -295,51 +189,6 @@ def test_pcsd_interface(self): "unique_group": None, "reloadable": False, }, - { - "name": "password_script", - "shortdesc": "Script to run to retrieve password", - "longdesc": None, - "type": "string", - "default": None, - "enum_values": None, - "required": False, - "advanced": False, - "deprecated": False, - "deprecated_by": [], - "deprecated_desc": None, - "unique_group": None, - "reloadable": False, - }, - { - "name": "plug", - "shortdesc": "Physical plug number on device, UUID or identification of machine", - "longdesc": None, - "type": "string", - "default": None, - "enum_values": None, - "required": False, - "advanced": False, - "deprecated": False, - "deprecated_by": [], - "deprecated_desc": None, - "unique_group": None, - "reloadable": False, - }, - { - "name": "port", - "shortdesc": "Physical plug number on device, UUID or identification of machine", - "longdesc": None, - "type": "string", - "default": None, - "enum_values": None, - "required": False, - "advanced": False, - "deprecated": True, - "deprecated_by": ["plug"], - "deprecated_desc": None, - "unique_group": None, - "reloadable": False, - }, { "name": "secure", "shortdesc": "Use SSH connection", @@ -370,36 +219,6 @@ def test_pcsd_interface(self): "unique_group": None, "reloadable": False, }, - { - "name": "ssh_options", - "shortdesc": "SSH options to use", - "longdesc": None, - "type": "string", - "default": "-1 -c blowfish", - "enum_values": None, - "required": False, - "advanced": False, - "deprecated": False, - "deprecated_by": [], - "deprecated_desc": None, - "unique_group": None, - "reloadable": False, - }, - { - "name": "switch", - "shortdesc": "Physical switch number on device", - "longdesc": None, - "type": "string", - "default": None, - "enum_values": None, - "required": False, - "advanced": False, - "deprecated": False, - "deprecated_by": [], - "deprecated_desc": None, - "unique_group": None, - "reloadable": False, - }, { "name": "username", "shortdesc": "Login name", @@ -415,21 +234,6 @@ def test_pcsd_interface(self): "unique_group": None, "reloadable": False, }, - { - "name": "quiet", - "shortdesc": "Disable logging to stderr. Does not affect --verbose or --debug-file or logging to syslog.", - "longdesc": None, - "type": "boolean", - "default": None, - "enum_values": None, - "required": False, - "advanced": False, - "deprecated": False, - "deprecated_by": [], - "deprecated_desc": None, - "unique_group": None, - "reloadable": False, - }, { "name": "verbose", "shortdesc": "Verbose mode", @@ -475,141 +279,6 @@ def test_pcsd_interface(self): "unique_group": None, "reloadable": False, }, - { - "name": "separator", - "shortdesc": "Separator for CSV created by 'list' operation", - "longdesc": None, - "type": "string", - "default": ",", - "enum_values": None, - "required": False, - "advanced": False, - "deprecated": False, - "deprecated_by": [], - "deprecated_desc": None, - "unique_group": None, - "reloadable": False, - }, - { - "name": "delay", - "shortdesc": "Wait X seconds before fencing is started", - "longdesc": None, - "type": "second", - "default": "0", - "enum_values": None, - "required": False, - "advanced": False, - "deprecated": False, - "deprecated_by": [], - "deprecated_desc": None, - "unique_group": None, - "reloadable": False, - }, - { - "name": "login_timeout", - "shortdesc": "Wait X seconds for cmd prompt after login", - "longdesc": None, - "type": "second", - "default": "5", - "enum_values": None, - "required": False, - "advanced": False, - "deprecated": False, - "deprecated_by": [], - "deprecated_desc": None, - "unique_group": None, - "reloadable": False, - }, - { - "name": "power_timeout", - "shortdesc": "Test X seconds for status change after ON/OFF", - "longdesc": None, - "type": "second", - "default": "20", - "enum_values": None, - "required": False, - "advanced": False, - "deprecated": False, - "deprecated_by": [], - "deprecated_desc": None, - "unique_group": None, - "reloadable": False, - }, - { - "name": "power_wait", - "shortdesc": "Wait X seconds after issuing ON/OFF", - "longdesc": None, - "type": "second", - "default": "0", - "enum_values": None, - "required": False, - "advanced": False, - "deprecated": False, - "deprecated_by": [], - "deprecated_desc": None, - "unique_group": None, - "reloadable": False, - }, - { - "name": "shell_timeout", - "shortdesc": "Wait X seconds for cmd prompt after issuing command", - "longdesc": None, - "type": "second", - "default": "3", - "enum_values": None, - "required": False, - "advanced": False, - "deprecated": False, - "deprecated_by": [], - "deprecated_desc": None, - "unique_group": None, - "reloadable": False, - }, - { - "name": "retry_on", - "shortdesc": "Count of attempts to retry power on", - "longdesc": None, - "type": "integer", - "default": "1", - "enum_values": None, - "required": False, - "advanced": False, - "deprecated": False, - "deprecated_by": [], - "deprecated_desc": None, - "unique_group": None, - "reloadable": False, - }, - { - "name": "ssh_path", - "shortdesc": "Path to ssh binary", - "longdesc": None, - "type": "string", - "default": "/usr/bin/ssh", - "enum_values": None, - "required": False, - "advanced": False, - "deprecated": False, - "deprecated_by": [], - "deprecated_desc": None, - "unique_group": None, - "reloadable": False, - }, - { - "name": "telnet_path", - "shortdesc": "Path to telnet binary", - "longdesc": None, - "type": "string", - "default": "/usr/bin/telnet", - "enum_values": None, - "required": False, - "advanced": False, - "deprecated": False, - "deprecated_by": [], - "deprecated_desc": None, - "unique_group": None, - "reloadable": False, - }, { "name": "pcmk_host_argument", "shortdesc": "An alternate parameter to supply instead of 'port'", @@ -1018,17 +687,7 @@ def test_pcsd_interface(self): "on_target": False, }, { - "name": "status", - "timeout": None, - "interval": None, - "role": None, - "start-delay": None, - "OCF_CHECK_LEVEL": None, - "automatic": False, - "on_target": False, - }, - { - "name": "list", + "name": "metadata", "timeout": None, "interval": None, "role": None, @@ -1038,7 +697,7 @@ def test_pcsd_interface(self): "on_target": False, }, { - "name": "list-status", + "name": "status", "timeout": None, "interval": None, "role": None, @@ -1058,7 +717,7 @@ def test_pcsd_interface(self): "on_target": False, }, { - "name": "metadata", + "name": "list", "timeout": None, "interval": None, "role": None, @@ -1068,7 +727,7 @@ def test_pcsd_interface(self): "on_target": False, }, { - "name": "manpage", + "name": "list-status", "timeout": None, "interval": None, "role": None, @@ -1078,8 +737,8 @@ def test_pcsd_interface(self): "on_target": False, }, { - "name": "validate-all", - "timeout": None, + "name": "stop", + "timeout": "20s", "interval": None, "role": None, "start-delay": None, @@ -1088,7 +747,7 @@ def test_pcsd_interface(self): "on_target": False, }, { - "name": "stop", + "name": "start", "timeout": "20s", "interval": None, "role": None, @@ -1098,8 +757,8 @@ def test_pcsd_interface(self): "on_target": False, }, { - "name": "start", - "timeout": "20s", + "name": "validate-all", + "timeout": None, "interval": None, "role": None, "start-delay": None, @@ -1133,7 +792,9 @@ def setUp(self): self.temp_corosync_conf = get_tmp_file("tier1_test_stonith") write_file_to_tmpfile(rc("corosync.conf"), self.temp_corosync_conf) self.pcs_runner = PcsRunner(self.temp_cib.name) - self.pcs_runner.mock_settings = get_mock_settings("crm_resource_exec") + self.pcs_runner.mock_settings = get_mock_settings( + "crm_resource_exec", "stonith_admin_exec" + ) self.pcs_runner.mock_settings["corosync_conf_file"] = ( self.temp_corosync_conf.name ) @@ -1142,16 +803,14 @@ def tearDown(self): self.temp_cib.close() self.temp_corosync_conf.close() - @skip_unless_crm_rule() - def test_stonith_creation(self): + def test_stonith_creation_nonexistent_agent(self): self.assert_pcs_fail( "stonith create test1 fence_noexist".split(), ( "Error: Agent 'stonith:fence_noexist' is not installed or does not " - "provide valid metadata: Agent fence_noexist not found or does " - "not support meta-data: Invalid argument (22), " - "Metadata query for stonith:fence_noexist failed: Input/output " - "error, use --force to override\n" + ERRORS_HAVE_OCCURRED + "provide valid metadata: " + "pcs mock error message: unable to load agent metadata, " + "use --force to override\n" + ERRORS_HAVE_OCCURRED ), ) @@ -1159,67 +818,33 @@ def test_stonith_creation(self): "stonith create test1 fence_noexist --force".split(), stderr_full=( "Warning: Agent 'stonith:fence_noexist' is not installed or does not " - "provide valid metadata: Agent fence_noexist not found or does " - "not support meta-data: Invalid argument (22), " - "Metadata query for stonith:fence_noexist failed: Input/output " - "error\n" - ), - ) - - self.assert_pcs_fail( - "stonith create test2 fence_apc".split(), - ( - "Error: stonith option 'ip' or 'ipaddr' (deprecated) has to be " - "specified, use --force to override\n" - "Error: stonith option 'username' or 'login' (deprecated) has " - "to be specified, use --force to override\n" - + ERRORS_HAVE_OCCURRED + "provide valid metadata: " + "pcs mock error message: unable to load agent metadata\n" ), ) self.assert_pcs_success( - "stonith create test2 fence_apc --force".split(), - stderr_start=( - "Warning: stonith option 'ip' or 'ipaddr' (deprecated) has to be " - "specified\n" - "Warning: stonith option 'username' or 'login' (deprecated) has to " - "be specified\n" - ), - ) - - self.assert_pcs_fail( - "stonith create test3 fence_apc bad_argument=test".split(), - stderr_start=( - "Error: invalid stonith option 'bad_argument', allowed options are:" - ), - ) - - self.assert_pcs_fail( - "stonith create test9 fence_apc pcmk_status_action=xxx".split(), - ( - "Error: stonith option 'ip' or 'ipaddr' (deprecated) has to be " - "specified, use --force to override\n" - "Error: stonith option 'username' or 'login' (deprecated) has " - "to be specified, use --force to override\n" - + ERRORS_HAVE_OCCURRED + "stonith config".split(), + dedent( + """\ + Resource: test1 (class=stonith type=fence_noexist) + Operations: + monitor: test1-monitor-interval-60s + interval=60s + """ ), ) + def test_stonith_creation_pcmk_status_action(self): self.assert_pcs_success( - "stonith create test9 fence_apc pcmk_status_action=xxx --force".split(), - stderr_start=( - "Warning: stonith option 'ip' or 'ipaddr' (deprecated) has to be " - "specified\n" - "Warning: stonith option 'username' or 'login' (deprecated) has to " - "be specified\n" - ), + "stonith create test9 fence_pcsmock_minimal pcmk_status_action=xxx".split(), ) self.assert_pcs_success( "stonith config test9".split(), dedent( """\ - Resource: test9 (class=stonith type=fence_apc) + Resource: test9 (class=stonith type=fence_pcsmock_minimal) Attributes: test9-instance_attributes pcmk_status_action=xxx Operations: @@ -1229,34 +854,12 @@ def test_stonith_creation(self): ), ) - self.assert_pcs_success( - "stonith delete test9".split(), - stderr_full="Deleting Resource - test9\n", - ) - - self.assert_pcs_fail( - "stonith create test3 fence_ilo ip=test".split(), - ( - "Error: stonith option 'username' or 'login' (deprecated) has " - "to be specified, use --force to override\n" - + ERRORS_HAVE_OCCURRED - ), - ) - - self.assert_pcs_success( - "stonith create test3 fence_ilo ip=test --force".split(), - stderr_start=( - "Warning: stonith option 'username' or 'login' (deprecated) " - "has to be specified\n" - ), - ) - + def test_stonith_creation_pcmk_params(self): # Testing that pcmk_host_check, pcmk_host_list & pcmk_host_map are # allowed for stonith agents self.assert_pcs_success( ( - "stonith create apc-fencing fence_apc ip=morph-apc username=apc " - "password=apc switch=1 " + "stonith create fencing fence_pcsmock_minimal " "pcmk_host_map=buzz-01:1;buzz-02:2;buzz-03:3;buzz-04:4;buzz-05:5 " "pcmk_host_check=static-list " "pcmk_host_list=buzz-01,buzz-02,buzz-03,buzz-04,buzz-05" @@ -1264,77 +867,105 @@ def test_stonith_creation(self): ) self.assert_pcs_fail( - "resource config apc-fencing".split(), + "resource config fencing".split(), ( - "Warning: Unable to find resource 'apc-fencing'\n" + "Warning: Unable to find resource 'fencing'\n" "Error: No resource found\n" ), ) self.assert_pcs_success( - "stonith config apc-fencing".split(), + "stonith config fencing".split(), dedent( """\ - Resource: apc-fencing (class=stonith type=fence_apc) - Attributes: apc-fencing-instance_attributes - ip=morph-apc - password=apc + Resource: fencing (class=stonith type=fence_pcsmock_minimal) + Attributes: fencing-instance_attributes pcmk_host_check=static-list pcmk_host_list=buzz-01,buzz-02,buzz-03,buzz-04,buzz-05 pcmk_host_map=buzz-01:1;buzz-02:2;buzz-03:3;buzz-04:4;buzz-05:5 - switch=1 - username=apc Operations: - monitor: apc-fencing-monitor-interval-60s + monitor: fencing-monitor-interval-60s interval=60s """ ), ) + def test_stonith_creation_pcmk_host_list(self): + self.assert_pcs_success( + [ + "stonith", + "create", + "F1", + "fence_pcsmock_minimal", + "pcmk_host_list=nodea nodeb", + ], + ) + self.assert_pcs_success( - "stonith remove apc-fencing".split(), - stderr_full="Deleting Resource - apc-fencing\n", + "stonith config F1".split(), + dedent( + """\ + Resource: F1 (class=stonith type=fence_pcsmock_minimal) + Attributes: F1-instance_attributes + pcmk_host_list="nodea nodeb" + Operations: + monitor: F1-monitor-interval-60s + interval=60s + """ + ), ) + def test_stonith_creation(self): self.assert_pcs_fail( + "stonith create test2 fence_pcsmock_params".split(), ( - "stonith create apc-fencing fence_apc ip=morph-apc username=apc " - "--agent-validation" - ).split(), - stderr_start="Error: Validation result from agent", + "Error: stonith option 'ip' or 'ipaddr' (deprecated) has to be " + "specified, use --force to override\n" + "Error: stonith option 'username' or 'login' (deprecated) has " + "to be specified, use --force to override\n" + + ERRORS_HAVE_OCCURRED + ), ) self.assert_pcs_success( - ( - "stonith create apc-fencing fence_apc ip=morph-apc username=apc " - "--agent-validation --force" - ).split(), - stderr_start="Warning: Validation result from agent", + "stonith create test2 fence_pcsmock_params --force".split(), + stderr_start=( + "Warning: stonith option 'ip' or 'ipaddr' (deprecated) has to be " + "specified\n" + "Warning: stonith option 'username' or 'login' (deprecated) has to " + "be specified\n" + ), ) - self.assert_pcs_success( - "stonith remove apc-fencing".split(), - stderr_full="Deleting Resource - apc-fencing\n", + self.assert_pcs_fail( + "stonith create test3 fence_pcsmock_params bad_argument=test".split(), + stderr_start=( + "Error: invalid stonith option 'bad_argument', allowed options are:" + ), ) self.assert_pcs_fail( - "stonith update test3 bad_ipaddr=test username=login".split(), - stderr_regexp=( - "^Error: invalid stonith option 'bad_ipaddr', allowed options" - " are: [^\n]+, use --force to override\n$" + "stonith create test3 fence_pcsmock_params ip=test".split(), + ( + "Error: stonith option 'username' or 'login' (deprecated) has " + "to be specified, use --force to override\n" + + ERRORS_HAVE_OCCURRED ), ) self.assert_pcs_success( - "stonith update test3 username=testA --agent-validation".split(), - stderr_start="Warning: The resource was misconfigured before the update,", + "stonith create test3 fence_pcsmock_params ip=test --force".split(), + stderr_start=( + "Warning: stonith option 'username' or 'login' (deprecated) " + "has to be specified\n" + ), ) self.assert_pcs_success( "stonith config test2".split(), dedent( """\ - Resource: test2 (class=stonith type=fence_apc) + Resource: test2 (class=stonith type=fence_pcsmock_params) Operations: monitor: test2-monitor-interval-60s interval=60s @@ -1346,18 +977,13 @@ def test_stonith_creation(self): "stonith config".split(), dedent( """\ - Resource: test1 (class=stonith type=fence_noexist) - Operations: - monitor: test1-monitor-interval-60s - interval=60s - Resource: test2 (class=stonith type=fence_apc) + Resource: test2 (class=stonith type=fence_pcsmock_params) Operations: monitor: test2-monitor-interval-60s interval=60s - Resource: test3 (class=stonith type=fence_ilo) + Resource: test3 (class=stonith type=fence_pcsmock_params) Attributes: test3-instance_attributes ip=test - username=testA Operations: monitor: test3-monitor-interval-60s interval=60s @@ -1370,23 +996,16 @@ def test_stonith_creation(self): "stonith", "create", "test-fencing", - "fence_apc", + "fence_pcsmock_minimal", "pcmk_host_list=rhel7-node1 rhel7-node2", "op", "monitor", "interval=61s", - "--force", ], - stderr_start=( - "Warning: stonith option 'ip' or 'ipaddr' (deprecated) has to " - "be specified\n" - "Warning: stonith option 'username' or 'login' (deprecated) has to " - "be specified\n" - ), ) self.assert_pcs_success( - "config show".split(), + "config".split(), dedent( """\ Cluster Name: test99 @@ -1395,22 +1014,17 @@ def test_stonith_creation(self): Pacemaker Nodes: Stonith Devices: - Resource: test1 (class=stonith type=fence_noexist) - Operations: - monitor: test1-monitor-interval-60s - interval=60s - Resource: test2 (class=stonith type=fence_apc) + Resource: test2 (class=stonith type=fence_pcsmock_params) Operations: monitor: test2-monitor-interval-60s interval=60s - Resource: test3 (class=stonith type=fence_ilo) + Resource: test3 (class=stonith type=fence_pcsmock_params) Attributes: test3-instance_attributes ip=test - username=testA Operations: monitor: test3-monitor-interval-60s interval=60s - Resource: test-fencing (class=stonith type=fence_apc) + Resource: test-fencing (class=stonith type=fence_pcsmock_minimal) Attributes: test-fencing-instance_attributes pcmk_host_list="rhel7-node1 rhel7-node2" Operations: @@ -1420,10 +1034,81 @@ def test_stonith_creation(self): ), ) + def test_stonith_agent_validation(self): + self.pcs_runner.mock_settings = get_mock_settings( + "crm_resource_exec", "stonith_admin_exec" + ) + self.assert_pcs_fail( + ( + "stonith create fencing fence_pcsmock_params " + "ip=is_invalid=True username=apc " + "--agent-validation" + ).split(), + stderr_full=( + "Error: Validation result from agent (use --force to override):\n" + " pcsmock validation failure\n" + ERRORS_HAVE_OCCURRED + ), + ) + + self.assert_pcs_success( + ( + "stonith create fencing fence_pcsmock_params " + "ip=is_invalid=True username=apc " + "--agent-validation --force" + ).split(), + stderr_full=( + "Warning: Validation result from agent:\n" + " pcsmock validation failure\n" + ), + ) + + self.assert_pcs_success( + "stonith config".split(), + dedent( + """\ + Resource: fencing (class=stonith type=fence_pcsmock_params) + Attributes: fencing-instance_attributes + ip="is_invalid=True" + username=apc + Operations: + monitor: fencing-monitor-interval-60s + interval=60s + """ + ), + ) + + self.assert_pcs_fail( + "stonith update fencing bad_ipaddr=test username=login".split(), + stderr_regexp=( + "^Error: invalid stonith option 'bad_ipaddr', allowed options" + " are: [^\n]+, use --force to override\n$" + ), + ) + + self.assert_pcs_success( + "stonith update fencing username=testA --agent-validation".split(), + stderr_start="Warning: The resource was misconfigured before the update,", + ) + + self.assert_pcs_success( + "stonith config".split(), + dedent( + """\ + Resource: fencing (class=stonith type=fence_pcsmock_params) + Attributes: fencing-instance_attributes + ip="is_invalid=True" + username=testA + Operations: + monitor: fencing-monitor-interval-60s + interval=60s + """ + ), + ) + def test_stonith_create_requires_either_new_or_deprecated(self): # 'ipaddr' and 'login' are obsoleted by 'ip' and 'username' self.assert_pcs_fail( - "stonith create test2 fence_apc".split(), + "stonith create test2 fence_pcsmock_params".split(), ( "Error: stonith option 'ip' or 'ipaddr' (deprecated) has to be " "specified, use --force to override\n" @@ -1436,7 +1121,7 @@ def test_stonith_create_requires_either_new_or_deprecated(self): def test_stonith_create_deprecated_and_obsoleting(self): # 'ipaddr' and 'login' are obsoleted by 'ip' and 'username' self.assert_pcs_success( - "stonith create S fence_apc ip=i login=l password=1234".split(), + "stonith create S fence_pcsmock_params ip=i login=l password=1234".split(), stderr_full=( "Warning: stonith option 'login' is deprecated and might be " "removed in a future release, therefore it should not be " @@ -1447,7 +1132,7 @@ def test_stonith_create_deprecated_and_obsoleting(self): "stonith config S".split(), dedent( """\ - Resource: S (class=stonith type=fence_apc) + Resource: S (class=stonith type=fence_pcsmock_params) Attributes: S-instance_attributes ip=i login=l @@ -1466,7 +1151,7 @@ def test_stonith_create_both_deprecated_and_obsoleting(self): "stonith", "create", "S", - "fence_apc", + "fence_pcsmock_params", "ip=i1", "login=l", "ipaddr=i2", @@ -1486,7 +1171,7 @@ def test_stonith_create_both_deprecated_and_obsoleting(self): "stonith config S".split(), dedent( """\ - Resource: S (class=stonith type=fence_apc) + Resource: S (class=stonith type=fence_pcsmock_params) Attributes: S-instance_attributes ip=i1 ipaddr=i2 @@ -1501,65 +1186,62 @@ def test_stonith_create_both_deprecated_and_obsoleting(self): ) def test_stonith_create_provides_unfencing(self): - self.assert_pcs_success_ignore_output( - ("stonith", "create", "f1", "fence_scsi", "--force") + self.assert_pcs_success( + ("stonith", "create", "f1", "fence_pcsmock_unfencing") ) - self.assert_pcs_success_ignore_output( + self.assert_pcs_success( ( "stonith", "create", "f2", - "fence_scsi", + "fence_pcsmock_unfencing", "meta", "provides=unfencing", - "--force", ) ) - self.assert_pcs_success_ignore_output( + self.assert_pcs_success( ( "stonith", "create", "f3", - "fence_scsi", + "fence_pcsmock_unfencing", "meta", "provides=something", - "--force", ) ) - self.assert_pcs_success_ignore_output( + self.assert_pcs_success( ( "stonith", "create", "f4", - "fence_xvm", + "fence_pcsmock_minimal", "meta", "provides=something", - "--force", ) ) self.assert_pcs_success( "stonith config".split(), dedent( """\ - Resource: f1 (class=stonith type=fence_scsi) + Resource: f1 (class=stonith type=fence_pcsmock_unfencing) Meta Attributes: f1-meta_attributes provides=unfencing Operations: monitor: f1-monitor-interval-60s interval=60s - Resource: f2 (class=stonith type=fence_scsi) + Resource: f2 (class=stonith type=fence_pcsmock_unfencing) Meta Attributes: f2-meta_attributes provides=unfencing Operations: monitor: f2-monitor-interval-60s interval=60s - Resource: f3 (class=stonith type=fence_scsi) + Resource: f3 (class=stonith type=fence_pcsmock_unfencing) Meta Attributes: f3-meta_attributes provides=unfencing Operations: monitor: f3-monitor-interval-60s interval=60s - Resource: f4 (class=stonith type=fence_xvm) + Resource: f4 (class=stonith type=fence_pcsmock_minimal) Meta Attributes: f4-meta_attributes provides=something Operations: @@ -1571,7 +1253,7 @@ def test_stonith_create_provides_unfencing(self): def test_stonith_create_action(self): self.assert_pcs_fail( - "stonith create test fence_apc ip=i username=u action=a".split(), + "stonith create test fence_pcsmock_action action=a".split(), ( "Error: stonith option 'action' is deprecated and might be " "removed in a future release, therefore it should not be" @@ -1581,7 +1263,7 @@ def test_stonith_create_action(self): ) self.assert_pcs_success( - "stonith create test fence_apc ip=i username=u action=a --force".split(), + "stonith create test fence_pcsmock_action action=a --force".split(), stderr_start=( "Warning: stonith option 'action' is deprecated and might be " "removed in a future release, therefore it should not be " @@ -1593,11 +1275,9 @@ def test_stonith_create_action(self): "stonith config".split(), dedent( """\ - Resource: test (class=stonith type=fence_apc) + Resource: test (class=stonith type=fence_pcsmock_action) Attributes: test-instance_attributes action=a - ip=i - username=u Operations: monitor: test-monitor-interval-60s interval=60s @@ -1607,24 +1287,20 @@ def test_stonith_create_action(self): def test_stonith_create_action_empty(self): self.assert_pcs_fail( - "stonith create test fence_apc ip=i username=u action=".split(), + "stonith create test fence_pcsmock_action action=".split(), "Error: action cannot be empty\n" + ERRORS_HAVE_OCCURRED, ) def test_stonith_update_action(self): self.assert_pcs_success( - "stonith create test fence_apc ip=i username=u password=1234".split() + "stonith create test fence_pcsmock_action".split() ) self.assert_pcs_success( "stonith config".split(), dedent( """\ - Resource: test (class=stonith type=fence_apc) - Attributes: test-instance_attributes - ip=i - password=1234 - username=u + Resource: test (class=stonith type=fence_pcsmock_action) Operations: monitor: test-monitor-interval-60s interval=60s @@ -1655,12 +1331,9 @@ def test_stonith_update_action(self): "stonith config".split(), dedent( """\ - Resource: test (class=stonith type=fence_apc) + Resource: test (class=stonith type=fence_pcsmock_action) Attributes: test-instance_attributes action=a - ip=i - password=1234 - username=u Operations: monitor: test-monitor-interval-60s interval=60s @@ -1674,11 +1347,7 @@ def test_stonith_update_action(self): "stonith config".split(), dedent( """\ - Resource: test (class=stonith type=fence_apc) - Attributes: test-instance_attributes - ip=i - password=1234 - username=u + Resource: test (class=stonith type=fence_pcsmock_action) Operations: monitor: test-monitor-interval-60s interval=60s @@ -1697,74 +1366,29 @@ def test_stonith_fence_confirm(self): "Error: must specify one (and only one) node to confirm fenced\n", ) - def test_pcmk_host_list(self): - self.assert_pcs_success( - [ - "stonith", - "create", - "F1", - "fence_apc", - "pcmk_host_list=nodea nodeb", - "--force", - ], - stderr_start=( - "Warning: stonith option 'ip' or 'ipaddr' (deprecated) has to be " - "specified\n" - "Warning: stonith option 'username' or 'login' (deprecated) has to " - "be specified\n" - ), - ) - - self.assert_pcs_success( - "stonith config F1".split(), - dedent( - """\ - Resource: F1 (class=stonith type=fence_apc) - Attributes: F1-instance_attributes - pcmk_host_list="nodea nodeb" - Operations: - monitor: F1-monitor-interval-60s - interval=60s - """ - ), - ) - def test_stonith_delete_removes_level(self): shutil.copyfile(rc("cib-empty-with3nodes.xml"), self.temp_cib.name) - deprecated_warnings = ( - "Warning: stonith option 'ip' or 'ipaddr' (deprecated) has to be " - "specified\n" - "Warning: stonith option 'username' or 'login' (deprecated) has to " - "be specified\n" - ) self.assert_pcs_success( - "stonith create n1-ipmi fence_apc --force".split(), - stderr_start=deprecated_warnings, + "stonith create n1-ipmi fence_pcsmock_minimal".split(), ) self.assert_pcs_success( - "stonith create n2-ipmi fence_apc --force".split(), - stderr_start=deprecated_warnings, + "stonith create n2-ipmi fence_pcsmock_minimal".split(), ) self.assert_pcs_success( - "stonith create n1-apc1 fence_apc --force".split(), - stderr_start=deprecated_warnings, + "stonith create n1-apc1 fence_pcsmock_minimal".split(), ) self.assert_pcs_success( - "stonith create n1-apc2 fence_apc --force".split(), - stderr_start=deprecated_warnings, + "stonith create n1-apc2 fence_pcsmock_minimal".split(), ) self.assert_pcs_success( - "stonith create n2-apc1 fence_apc --force".split(), - stderr_start=deprecated_warnings, + "stonith create n2-apc1 fence_pcsmock_minimal".split(), ) self.assert_pcs_success( - "stonith create n2-apc2 fence_apc --force".split(), - stderr_start=deprecated_warnings, + "stonith create n2-apc2 fence_pcsmock_minimal".split(), ) self.assert_pcs_success( - "stonith create n2-apc3 fence_apc --force".split(), - stderr_start=deprecated_warnings, + "stonith create n2-apc3 fence_pcsmock_minimal".split(), ) self.assert_pcs_success_all( [ @@ -1780,13 +1404,13 @@ def test_stonith_delete_removes_level(self): ["stonith"], outdent( """\ - * n1-ipmi\t(stonith:fence_apc):\tStopped - * n2-ipmi\t(stonith:fence_apc):\tStopped - * n1-apc1\t(stonith:fence_apc):\tStopped - * n1-apc2\t(stonith:fence_apc):\tStopped - * n2-apc1\t(stonith:fence_apc):\tStopped - * n2-apc2\t(stonith:fence_apc):\tStopped - * n2-apc3\t(stonith:fence_apc):\tStopped + * n1-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped + * n2-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped + * n1-apc1\t(stonith:fence_pcsmock_minimal):\tStopped + * n1-apc2\t(stonith:fence_pcsmock_minimal):\tStopped + * n2-apc1\t(stonith:fence_pcsmock_minimal):\tStopped + * n2-apc2\t(stonith:fence_pcsmock_minimal):\tStopped + * n2-apc3\t(stonith:fence_pcsmock_minimal):\tStopped Fencing Levels: Target: rh7-1 @@ -1804,13 +1428,13 @@ def test_stonith_delete_removes_level(self): ["stonith"], outdent( """\ - n1-ipmi\t(stonith:fence_apc):\tStopped - n2-ipmi\t(stonith:fence_apc):\tStopped - n1-apc1\t(stonith:fence_apc):\tStopped - n1-apc2\t(stonith:fence_apc):\tStopped - n2-apc1\t(stonith:fence_apc):\tStopped - n2-apc2\t(stonith:fence_apc):\tStopped - n2-apc3\t(stonith:fence_apc):\tStopped + n1-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped + n2-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped + n1-apc1\t(stonith:fence_pcsmock_minimal):\tStopped + n1-apc2\t(stonith:fence_pcsmock_minimal):\tStopped + n2-apc1\t(stonith:fence_pcsmock_minimal):\tStopped + n2-apc2\t(stonith:fence_pcsmock_minimal):\tStopped + n2-apc3\t(stonith:fence_pcsmock_minimal):\tStopped Fencing Levels: Target: rh7-1 @@ -1833,12 +1457,12 @@ def test_stonith_delete_removes_level(self): ["stonith"], outdent( """\ - * n1-ipmi\t(stonith:fence_apc):\tStopped - * n2-ipmi\t(stonith:fence_apc):\tStopped - * n1-apc1\t(stonith:fence_apc):\tStopped - * n1-apc2\t(stonith:fence_apc):\tStopped - * n2-apc1\t(stonith:fence_apc):\tStopped - * n2-apc3\t(stonith:fence_apc):\tStopped + * n1-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped + * n2-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped + * n1-apc1\t(stonith:fence_pcsmock_minimal):\tStopped + * n1-apc2\t(stonith:fence_pcsmock_minimal):\tStopped + * n2-apc1\t(stonith:fence_pcsmock_minimal):\tStopped + * n2-apc3\t(stonith:fence_pcsmock_minimal):\tStopped Fencing Levels: Target: rh7-1 @@ -1856,12 +1480,12 @@ def test_stonith_delete_removes_level(self): ["stonith"], outdent( """\ - n1-ipmi\t(stonith:fence_apc):\tStopped - n2-ipmi\t(stonith:fence_apc):\tStopped - n1-apc1\t(stonith:fence_apc):\tStopped - n1-apc2\t(stonith:fence_apc):\tStopped - n2-apc1\t(stonith:fence_apc):\tStopped - n2-apc3\t(stonith:fence_apc):\tStopped + n1-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped + n2-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped + n1-apc1\t(stonith:fence_pcsmock_minimal):\tStopped + n1-apc2\t(stonith:fence_pcsmock_minimal):\tStopped + n2-apc1\t(stonith:fence_pcsmock_minimal):\tStopped + n2-apc3\t(stonith:fence_pcsmock_minimal):\tStopped Fencing Levels: Target: rh7-1 @@ -1884,11 +1508,11 @@ def test_stonith_delete_removes_level(self): ["stonith"], outdent( """\ - * n1-ipmi\t(stonith:fence_apc):\tStopped - * n2-ipmi\t(stonith:fence_apc):\tStopped - * n1-apc1\t(stonith:fence_apc):\tStopped - * n1-apc2\t(stonith:fence_apc):\tStopped - * n2-apc3\t(stonith:fence_apc):\tStopped + * n1-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped + * n2-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped + * n1-apc1\t(stonith:fence_pcsmock_minimal):\tStopped + * n1-apc2\t(stonith:fence_pcsmock_minimal):\tStopped + * n2-apc3\t(stonith:fence_pcsmock_minimal):\tStopped Fencing Levels: Target: rh7-1 @@ -1906,11 +1530,11 @@ def test_stonith_delete_removes_level(self): ["stonith"], outdent( """\ - n1-ipmi\t(stonith:fence_apc):\tStopped - n2-ipmi\t(stonith:fence_apc):\tStopped - n1-apc1\t(stonith:fence_apc):\tStopped - n1-apc2\t(stonith:fence_apc):\tStopped - n2-apc3\t(stonith:fence_apc):\tStopped + n1-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped + n2-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped + n1-apc1\t(stonith:fence_pcsmock_minimal):\tStopped + n1-apc2\t(stonith:fence_pcsmock_minimal):\tStopped + n2-apc3\t(stonith:fence_pcsmock_minimal):\tStopped Fencing Levels: Target: rh7-1 @@ -1933,10 +1557,10 @@ def test_stonith_delete_removes_level(self): ["stonith"], outdent( """\ - * n1-ipmi\t(stonith:fence_apc):\tStopped - * n2-ipmi\t(stonith:fence_apc):\tStopped - * n1-apc1\t(stonith:fence_apc):\tStopped - * n1-apc2\t(stonith:fence_apc):\tStopped + * n1-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped + * n2-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped + * n1-apc1\t(stonith:fence_pcsmock_minimal):\tStopped + * n1-apc2\t(stonith:fence_pcsmock_minimal):\tStopped Fencing Levels: Target: rh7-1 @@ -1953,10 +1577,10 @@ def test_stonith_delete_removes_level(self): ["stonith"], outdent( """\ - n1-ipmi\t(stonith:fence_apc):\tStopped - n2-ipmi\t(stonith:fence_apc):\tStopped - n1-apc1\t(stonith:fence_apc):\tStopped - n1-apc2\t(stonith:fence_apc):\tStopped + n1-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped + n2-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped + n1-apc1\t(stonith:fence_pcsmock_minimal):\tStopped + n1-apc2\t(stonith:fence_pcsmock_minimal):\tStopped Fencing Levels: Target: rh7-1 @@ -1978,9 +1602,9 @@ def test_stonith_delete_removes_level(self): ["stonith"], outdent( """\ - * n1-ipmi\t(stonith:fence_apc):\tStopped - * n2-ipmi\t(stonith:fence_apc):\tStopped - * n1-apc2\t(stonith:fence_apc):\tStopped + * n1-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped + * n2-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped + * n1-apc2\t(stonith:fence_pcsmock_minimal):\tStopped Fencing Levels: Target: rh7-1 @@ -1997,9 +1621,9 @@ def test_stonith_delete_removes_level(self): ["stonith"], outdent( """\ - n1-ipmi\t(stonith:fence_apc):\tStopped - n2-ipmi\t(stonith:fence_apc):\tStopped - n1-apc2\t(stonith:fence_apc):\tStopped + n1-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped + n2-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped + n1-apc2\t(stonith:fence_pcsmock_minimal):\tStopped Fencing Levels: Target: rh7-1 @@ -2021,8 +1645,8 @@ def test_stonith_delete_removes_level(self): ["stonith"], outdent( """\ - * n1-ipmi\t(stonith:fence_apc):\tStopped - * n2-ipmi\t(stonith:fence_apc):\tStopped + * n1-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped + * n2-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped Fencing Levels: Target: rh7-1 @@ -2038,8 +1662,8 @@ def test_stonith_delete_removes_level(self): ["stonith"], outdent( """\ - n1-ipmi\t(stonith:fence_apc):\tStopped - n2-ipmi\t(stonith:fence_apc):\tStopped + n1-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped + n2-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped Fencing Levels: Target: rh7-1 @@ -2058,11 +1682,8 @@ def test_no_stonith_warning(self): ) self.pcs_runner.corosync_conf_opt = None - self.assert_pcs_success_ignore_output( - ( - "stonith create test_stonith fence_apc ip=i username=u " - "pcmk_host_argument=node1 --force" - ).split() + self.assert_pcs_success( + "stonith create test_stonith fence_pcsmock_minimal".split() ) self.pcs_runner.corosync_conf_opt = self.temp_corosync_conf.name @@ -2092,16 +1713,16 @@ def test_no_stonith_warning(self): class StonithLevelTestCibFixture(CachedCibFixture): def _fixture_stonith_resource(self, name): - self.assert_pcs_success_ignore_output( + self._pcs_runner.mock_settings = get_mock_settings( + "crm_resource_exec", "stonith_admin_exec" + ) + self.assert_pcs_success( [ "stonith", "create", name, - "fence_apc", + "fence_pcsmock_minimal", "pcmk_host_list=rh7-1 rh7-2", - "ip=i", - "username=u", - "--force", ] ) @@ -2136,7 +1757,9 @@ def setUp(self): self.temp_cib = get_tmp_file("tier1_test_stonith_level") write_file_to_tmpfile(rc("cib-empty-withnodes.xml"), self.temp_cib) self.pcs_runner = PcsRunner(self.temp_cib.name) - self.pcs_runner.mock_settings = get_mock_settings("crm_resource_exec") + self.pcs_runner.mock_settings = get_mock_settings( + "crm_resource_exec", "stonith_admin_exec" + ) self.config = "" self.config_lines = [] @@ -2144,16 +1767,13 @@ def tearDown(self): self.temp_cib.close() def fixture_stonith_resource(self, name): - self.assert_pcs_success_ignore_output( + self.assert_pcs_success( [ "stonith", "create", name, - "fence_apc", + "fence_pcsmock_minimal", "pcmk_host_list=rh7-1 rh7-2", - "ip=i", - "username=u", - "--force", ] ) @@ -2597,17 +2217,17 @@ def test_all_possibilities(self): if PCMK_2_0_3_PLUS: result = outdent( """\ - * F1\t(stonith:fence_apc):\tStopped - * F2\t(stonith:fence_apc):\tStopped - * F3\t(stonith:fence_apc):\tStopped + * F1\t(stonith:fence_pcsmock_minimal):\tStopped + * F2\t(stonith:fence_pcsmock_minimal):\tStopped + * F3\t(stonith:fence_pcsmock_minimal):\tStopped """ ) else: result = outdent( """\ - F1\t(stonith:fence_apc):\tStopped - F2\t(stonith:fence_apc):\tStopped - F3\t(stonith:fence_apc):\tStopped + F1\t(stonith:fence_pcsmock_minimal):\tStopped + F2\t(stonith:fence_pcsmock_minimal):\tStopped + F3\t(stonith:fence_pcsmock_minimal):\tStopped """ ) self.assert_pcs_success( @@ -2628,27 +2248,21 @@ def test_all_possibilities(self): indent( dedent( """ - Resource: F1 (class=stonith type=fence_apc) + Resource: F1 (class=stonith type=fence_pcsmock_minimal) Attributes: F1-instance_attributes - ip=i pcmk_host_list="rh7-1 rh7-2" - username=u Operations: monitor: F1-monitor-interval-60s interval=60s - Resource: F2 (class=stonith type=fence_apc) + Resource: F2 (class=stonith type=fence_pcsmock_minimal) Attributes: F2-instance_attributes - ip=i pcmk_host_list="rh7-1 rh7-2" - username=u Operations: monitor: F2-monitor-interval-60s interval=60s - Resource: F3 (class=stonith type=fence_apc) + Resource: F3 (class=stonith type=fence_pcsmock_minimal) Attributes: F3-instance_attributes - ip=i pcmk_host_list="rh7-1 rh7-2" - username=u Operations: monitor: F3-monitor-interval-60s interval=60s @@ -3383,26 +2997,19 @@ def test_errors(self): class StonithUpdate(ResourceTest): - # added in fence-agents-all-4.11.0 - agent_secure_warning = ( - "(" - "Warning: Validation result from agent:\n" - " WARNING:root:Parse error: Ignoring option 'secure' because it does " - "not have value\n" - ")?" - ) - def setUp(self): super().setUp() - self.pcs_runner.mock_settings = get_mock_settings("crm_resource_exec") + self.pcs_runner.mock_settings = get_mock_settings( + "crm_resource_exec", "stonith_admin_exec" + ) self.fixture_create_stonith() def fixture_create_stonith(self): self.assert_effect( - "stonith create S fence_apc ip=i login=l ssh=0 debug=d password=1234".split(), + "stonith create S fence_pcsmock_params ip=i login=l ssh=0 debug=d password=1234".split(), """ - + """, - stderr_regexp=( + stderr_full=( "Warning: stonith option 'login' is deprecated and might be " "removed in a future release, therefore it should not " "be used, use 'username' instead\n" "Warning: stonith option 'debug' is deprecated and might be " "removed in a future release, therefore it should not " "be used, use 'debug_file' instead\n" - + self.agent_secure_warning ), ) @@ -3444,7 +3050,7 @@ def test_set_deprecated_param(self): "stonith update S debug=D".split(), """ - + """, - stderr_regexp=( + stderr_full=( "Warning: stonith option 'debug' is deprecated and might be " "removed in a future release, therefore it should not " "be used, use 'debug_file' instead\n" - + self.agent_secure_warning ), ) @@ -3483,7 +3088,7 @@ def test_unset_deprecated_param(self): "stonith update S debug=".split(), """ - + """, - stderr_regexp=self.agent_secure_warning, ) def test_unset_deprecated_required_param(self): @@ -3521,7 +3125,7 @@ def test_set_obsoleting_param(self): "stonith update S ssh=1".split(), """ - + - + - + """, - stderr_regexp=self.agent_secure_warning, ) def test_unset_obsoleting_required_set_deprecated(self): @@ -3625,7 +3228,7 @@ def test_unset_obsoleting_required_set_deprecated(self): "stonith update S ip= ipaddr=I".split(), """ - + """, - stderr_regexp=( + stderr_full=( "Warning: stonith option 'ipaddr' is deprecated and might be " "removed in a future release, therefore it should not " - "be used, use 'ip' instead\n" + self.agent_secure_warning + "be used, use 'ip' instead\n" ), ) @@ -3663,7 +3266,7 @@ def test_set_both_deprecated_and_obsoleting(self): "stonith update S ip=I1 ipaddr=I2".split(), """ - + """, - stderr_regexp=( + stderr_full=( "Warning: stonith option 'ipaddr' is deprecated and might be " "removed in a future release, therefore it should not " - "be used, use 'ip' instead\n" + self.agent_secure_warning + "be used, use 'ip' instead\n" ), ) diff --git a/pcs_test/tier1/test_status.py b/pcs_test/tier1/test_status.py index 4d4722082..f6780c60e 100644 --- a/pcs_test/tier1/test_status.py +++ b/pcs_test/tier1/test_status.py @@ -27,6 +27,9 @@ def setUp(self): self.temp_cib = get_tmp_file("tier0_statust_stonith_warning") write_file_to_tmpfile(self.empty_cib, self.temp_cib) self.pcs_runner = PcsRunner(self.temp_cib.name) + self.pcs_runner.mock_settings = get_mock_settings( + "crm_resource_exec", "stonith_admin_exec" + ) def tearDown(self): self.temp_cib.close() @@ -34,8 +37,7 @@ def tearDown(self): def fixture_stonith_action(self): self.assert_pcs_success( ( - "stonith create Sa fence_apc ip=i username=u action=reboot " - "--force" + "stonith create Sa fence_pcsmock_action action=reboot --force" ).split(), stderr_start=( "Warning: stonith option 'action' is deprecated and might be " @@ -45,15 +47,8 @@ def fixture_stonith_action(self): ) def fixture_stonith_cycle(self): - self.assert_pcs_success_ignore_output( - ( - "stonith", - "create", - "Sc", - "fence_ipmilan", - "method=cycle", - "--force", - ) + self.assert_pcs_success( + "stonith create Sc fence_pcsmock_method method=cycle".split() ) def fixture_resource(self): @@ -208,14 +203,13 @@ def test_warn_when_no_stonith(self): def test_no_stonith_warning_when_stonith_in_group(self): self.assert_pcs_success( - "stonith create S fence_xvm --group G".split(), + "stonith create S fence_pcsmock_minimal --group G".split(), stderr_full=( "Deprecation Warning: Option to group stonith resource is " "deprecated and will be removed in a future release.\n" ), ) self.pcs_runner.corosync_conf_opt = self.corosync_conf - self.pcs_runner.mock_settings = get_mock_settings("crm_resource_exec") if PCMK_2_0_3_PLUS: self.assert_pcs_success( ["status"], diff --git a/pcs_test/tools/bin_mock/__init__.py b/pcs_test/tools/bin_mock/__init__.py index 653bf81bf..9bef8e64f 100644 --- a/pcs_test/tools/bin_mock/__init__.py +++ b/pcs_test/tools/bin_mock/__init__.py @@ -8,10 +8,14 @@ PACEMAKER_FENCED_BIN = os.path.abspath( os.path.join(BIN_MOCK_DIR, "pcmk/pacemaker-fenced") ) +STONITH_ADMIN_BIN = os.path.abspath( + os.path.join(BIN_MOCK_DIR, "pcmk/stonith_admin") +) MOCK_SETTINGS = { "crm_resource_exec": CRM_RESOURCE_BIN, "pacemaker_fenced_exec": PACEMAKER_FENCED_BIN, + "stonith_admin_exec": STONITH_ADMIN_BIN, } diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_apc_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_apc_metadata.xml deleted file mode 100644 index 6d159b4fb..000000000 --- a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_apc_metadata.xml +++ /dev/null @@ -1,277 +0,0 @@ - - - fence_apc is an I/O Fencing agent which can be used with the APC network power switch. It logs into device via telnet/ssh and reboots a specified outlet. Lengthy telnet/ssh connections should be avoided while a GFS cluster is running because the connection will block any necessary fencing actions. - - - http://www.apc.com - - - - - - - Fencing action - - - - - - - Force Python regex for command prompt - - - - - - - Force Python regex for command prompt - - - - - - - Identity file (private key) for SSH - - - - - - - Forces agent to use IPv4 addresses only - - - - - - - Forces agent to use IPv6 addresses only - - - - - - - IP address or hostname of fencing device - - - - - - - IP address or hostname of fencing device - - - - - - - TCP/UDP port to use for connection with device - - - - - - - Login name - - - - - - - Login password or passphrase - - - - - - - Script to run to retrieve password - - - - - - - Login password or passphrase - - - - - - - Script to run to retrieve password - - - - - - - Physical plug number on device, UUID or identification of machine - - - - - - - Physical plug number on device, UUID or identification of machine - - - - - - - Use SSH connection - - - - - - - Use SSH connection - - - - - - - SSH options to use - - - - - - - Physical switch number on device - - - - - - - Login name - - - - - - - Disable logging to stderr. Does not affect --verbose or --debug-file or logging to syslog. - - - - - - - Verbose mode - - - - - - - Write debug information to given file - - - - - - - Write debug information to given file - - - - - - - Display version information and exit - - - - - - - Display help and exit - - - - - - - Separator for CSV created by 'list' operation - - - - - - - Wait X seconds before fencing is started - - - - - - - Wait X seconds for cmd prompt after login - - - - - - - Test X seconds for status change after ON/OFF - - - - - - - Wait X seconds after issuing ON/OFF - - - - - - - Wait X seconds for cmd prompt after issuing command - - - - - - - Count of attempts to retry power on - - - - - - - Path to ssh binary - - - - - - - Path to telnet binary - - - - - - - - - - - - - - - - - - - diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_ilo_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_ilo_metadata.xml deleted file mode 100644 index 5c6e1c3bb..000000000 --- a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_ilo_metadata.xml +++ /dev/null @@ -1,269 +0,0 @@ - - - - fence_ilo is an I/O Fencing agent used for HP servers with the Integrated Light Out (iLO) PCI card.The agent opens an SSL connection to the iLO card. Once the SSL connection is established, the agent is able to communicate with the iLO card through an XML stream. - - - http://www.hp.com - - - - - - - Fencing action - - - - - - - Forces agent to use IPv4 addresses only - - - - - - - Forces agent to use IPv6 addresses only - - - - - - - IP address or hostname of fencing device - - - - - - - IP address or hostname of fencing device - - - - - - - TCP/UDP port to use for connection with device - - - - - - - Login name - - - - - - - Disable TLS negotiation and force SSL3.0. This should only be used for devices that do not support TLS1.0 and up. - - - - - - - Login password or passphrase - - - - - - - Script to run to retrieve password - - - - - - - Login password or passphrase - - - - - - - Script to run to retrieve password - - - - - - - IP address or hostname of fencing device (together with --port-as-ip) - - - - - - - IP address or hostname of fencing device (together with --port-as-ip) - - - - - - - Force ribcl version to use - - - - - - - Force ribcl version to use - - - - - - - Use SSL connection with verifying certificate - - - - - - - Use SSL connection without verifying certificate - - - - - - - Use SSL connection with verifying certificate - - - - - - - Disable TLS negotiation and force TLS1.0. This should only be used for devices that do not support TLS1.1 and up. - - - - - - - Login name - - - - - - - Disable logging to stderr. Does not affect --verbose or --debug-file or logging to syslog. - - - - - - - Verbose mode - - - - - - - Write debug information to given file - - - - - - - Write debug information to given file - - - - - - - Display version information and exit - - - - - - - Display help and exit - - - - - - - Wait X seconds before fencing is started - - - - - - - Wait X seconds for cmd prompt after login - - - - - - - Make "port/plug" to be an alias to IP address - - - - - - - Test X seconds for status change after ON/OFF - - - - - - - Wait X seconds after issuing ON/OFF - - - - - - - Wait X seconds for cmd prompt after issuing command - - - - - - - Count of attempts to retry power on - - - - - - - Path to gnutls-cli binary - - - - - - - - - - - - - - - - - diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_action_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_action_metadata.xml new file mode 100644 index 000000000..fd40fa87d --- /dev/null +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_action_metadata.xml @@ -0,0 +1,31 @@ + + + This is an agent with action parameter for pcs tests + + + + + + + Fencing action (null, off, on, [reboot], status, list, list-status, monitor, validate-all, metadata) + + + + + + + + + + + + + + + + + + diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_method_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_method_metadata.xml new file mode 100644 index 000000000..cdccdd9c1 --- /dev/null +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_method_metadata.xml @@ -0,0 +1,32 @@ + + + This is an agent with method parameter for pcs tests + + + + + + + Method to fence + + + + + + + + + + + + + + + + + diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_minimal_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_minimal_metadata.xml new file mode 100644 index 000000000..c9fa8008b --- /dev/null +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_minimal_metadata.xml @@ -0,0 +1,22 @@ + + + This is a minimalistic agent for pcs tests + + + + + + + + + + + + + + + + diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_params_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_params_metadata.xml new file mode 100644 index 000000000..088ca985e --- /dev/null +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_params_metadata.xml @@ -0,0 +1,107 @@ + + + This is an agent with params for pcs tests + + + + + + + Fencing action + + + + + + + IP address or hostname of fencing device + + + + + + + IP address or hostname of fencing device + + + + + + + Login name + + + + + + + Login password or passphrase + + + + + + + Login password or passphrase + + + + + + + Use SSH connection + + + + + + + Use SSH connection + + + + + + + Login name + + + + + + + Verbose mode + + + + + + + Write debug information to given file + + + + + + + Write debug information to given file + + + + + + + + + + + + + + + + + diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_unfencing_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_unfencing_metadata.xml new file mode 100644 index 000000000..db2117441 --- /dev/null +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_unfencing_metadata.xml @@ -0,0 +1,22 @@ + + + This is an agent which provides unfencing for pcs tests + + + + + + + + + + + + + + + + diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_scsi_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_scsi_metadata.xml deleted file mode 100644 index 259a15932..000000000 --- a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_scsi_metadata.xml +++ /dev/null @@ -1,182 +0,0 @@ - - - fence_scsi is an I/O fencing agent that uses SCSI-3 persistent reservations to control access to shared storage devices. These devices must support SCSI-3 persistent reservations (SPC-3 or greater) as well as the "preempt-and-abort" subcommand. -The fence_scsi agent works by having each node in the cluster register a unique key with the SCSI device(s). Once registered, a single node will become the reservation holder by creating a "write exclusive, registrants only" reservation on the device(s). The result is that only registered nodes may write to the device(s). When a node failure occurs, the fence_scsi agent will remove the key belonging to the failed node from the device(s). The failed node will no longer be able to write to the device(s). A manual reboot is required. - - - - - - - - Fencing action - - - - - - - Use the APTPL flag for registrations. This option is only used for the 'on' action. - - - - - - - List of devices to use for current operation. Devices can be comma-separated list of raw devices (eg. /dev/sdc). Each device must support SCSI-3 persistent reservations. - - - - - - - Key to use for the current operation. This key should be unique to a node. For the "on" action, the key specifies the key use to register the local node. For the "off" action, this key specifies the key to be removed from the device(s). - - - - - - - Name of the node to be fenced. The node name is used to generate the key value used for the current operation. This option will be ignored when used with the -k option. - - - - - - - Name of the node to be fenced. The node name is used to generate the key value used for the current operation. This option will be ignored when used with the -k option. - - - - - - - Log output (stdout and stderr) to file - - - - - - - Disable logging to stderr. Does not affect --verbose or --debug-file or logging to syslog. - - - - - - - Verbose mode - - - - - - - Write debug information to given file - - - - - - - Write debug information to given file - - - - - - - Display version information and exit - - - - - - - Display help and exit - - - - - - - Wait X seconds before fencing is started - - - - - - - Wait X seconds for cmd prompt after login - - - - - - - Test X seconds for status change after ON/OFF - - - - - - - Wait X seconds after issuing ON/OFF - - - - - - - Wait X seconds for cmd prompt after issuing command - - - - - - - Count of attempts to retry power on - - - - - - - Path to corosync-cmapctl binary - - - - - - - Path to sg_persist binary - - - - - - - Path to sg_turs binary - - - - - - - Path to vgs binary - - - - - - - - - - - - - - - - diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_xvm_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_xvm_metadata.xml deleted file mode 100644 index d359854e3..000000000 --- a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_xvm_metadata.xml +++ /dev/null @@ -1,120 +0,0 @@ - - - fence_xvm is an I/O Fencing agent which can be used withvirtual machines. - - - - - - - Specify (stdin) or increment (command line) debug level - - - - - - - IP Family ([auto], ipv4, ipv6) - - - - - - - Multicast address (default=225.0.0.12 / ff05::3:1) - - - - - - - TCP, Multicast, or VMChannel IP port (default=1229) - - - - - - - Multicast retransmit time (in 1/10sec; default=20) - - - - - - - Authentication (none, sha1, [sha256], sha512) - - - - - - - Packet hash strength (none, sha1, [sha256], sha512) - - - - - - - Shared key file (default=/etc/cluster/fence_xvm.key) - - - - - - - Virtual Machine (domain name) to fence - - - - - - - Treat [domain] as UUID instead of domain name. This is provided for compatibility with older fence_xvmd installations. - - - - - - - Fencing action (null, off, on, [reboot], status, list, list-status, monitor, validate-all, metadata) - - - - - - - Fencing timeout (in seconds; default=30) - - - - - - - Fencing delay (in seconds; default=0) - - - - - - - Virtual Machine (domain name) to fence (deprecated; use port) - - - - - - - - - - - - - - - - - - - diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource_mock.py b/pcs_test/tools/bin_mock/pcmk/crm_resource_mock.py index f1d8d0691..58cf25e66 100644 --- a/pcs_test/tools/bin_mock/pcmk/crm_resource_mock.py +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource_mock.py @@ -26,7 +26,6 @@ def get_arg_values(argv, name): def main(): - # pylint: disable=too-many-branches argv = sys.argv[1:] if not argv: raise AssertionError() @@ -52,10 +51,11 @@ def main(): "ocf:pacemaker:remote", "ocf:pacemaker:Stateful", "ocf:pacemaker:SystemHealth", - "stonith:fence_apc", - "stonith:fence_ilo", - "stonith:fence_scsi", - "stonith:fence_xvm", + "stonith:fence_pcsmock_action", + "stonith:fence_pcsmock_method", + "stonith:fence_pcsmock_minimal", + "stonith:fence_pcsmock_params", + "stonith:fence_pcsmock_unfencing", "systemd:test@a:b", ) # known_agents_map = {item.lower()} @@ -63,21 +63,11 @@ def main(): write_local_file_to_stdout( "{}_metadata.xml".format(arg.replace(":", "__")) ) - elif arg == "ocf:pacemaker:nonexistent": - sys.stderr.write( - "Metadata query for ocf:pacemaker:nonexistent failed: " - "Input/output error\n" - ) - raise SystemExit(5) - elif arg == "stonith:fence_noexist": + else: sys.stderr.write( - "Agent fence_noexist not found or does not support meta-data: " - "Invalid argument (22)\nMetadata query for " - "stonith:fence_noexist failed: Input/output error\n" + "pcs mock error message: unable to load agent metadata" ) - raise SystemExit(5) - else: - raise AssertionError() + raise SystemExit(1) elif arg in option_file_map: if argv: raise AssertionError() @@ -100,7 +90,7 @@ def main(): is_invalid = "fake=is_invalid=True" in argv output = "" if is_invalid: - output = """Validation failure""" + output = """pcsmock validation failure""" stdout = """ diff --git a/pcs_test/tools/bin_mock/pcmk/stonith_admin.in b/pcs_test/tools/bin_mock/pcmk/stonith_admin.in new file mode 100755 index 000000000..bf48b5c40 --- /dev/null +++ b/pcs_test/tools/bin_mock/pcmk/stonith_admin.in @@ -0,0 +1,5 @@ +#!@PYTHON@ + +from stonith_admin_mock import main + +main() diff --git a/pcs_test/tools/bin_mock/pcmk/stonith_admin_mock.py b/pcs_test/tools/bin_mock/pcmk/stonith_admin_mock.py new file mode 100644 index 000000000..133097b52 --- /dev/null +++ b/pcs_test/tools/bin_mock/pcmk/stonith_admin_mock.py @@ -0,0 +1,60 @@ +import os.path +import sys +from textwrap import dedent + +CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) +DATA_DIR = os.path.join(CURRENT_DIR, "{}.d".format(sys.argv[0])) + + +def get_arg_values(argv, name): + values = [] + next_value = len(argv) + for i, value in enumerate(argv): + if value == name: + next_value = i + 1 + elif i == next_value: + values.append(value) + return values + + +def main(): + argv = sys.argv[1:] + if not argv: + raise AssertionError() + + arg = argv.pop(0) + + if arg == "--validate": + if get_arg_values(argv, "--output-as")[0] != "xml": + raise AssertionError() + is_invalid = False + for arg in argv: + if "=" in arg and arg.split("=", 1)[1] == "is_invalid=True": + is_invalid = True + break + output = "" + if is_invalid: + output = """pcsmock validation failure""" + cmd_str = " ".join(sys.argv) + agent_type = get_arg_values(argv, "--agent")[0] + stdout = dedent( + f""" + + + + {output} + + + + + """ + ) + sys.stdout.write(stdout) + if is_invalid: + raise SystemExit(1) + else: + raise AssertionError() + + +if __name__ == "__main__": + main() From 724f544a25bbf10576fa6eeb556460309a70c41c Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Tue, 23 Jul 2024 18:12:34 +0200 Subject: [PATCH 013/227] test: remove unused mock list of stonith agents --- pcs_test/Makefile.am | 1 - .../pcmk/crm_resource.d/list_agents_stonith | 46 ------------------- 2 files changed, 47 deletions(-) delete mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/list_agents_stonith diff --git a/pcs_test/Makefile.am b/pcs_test/Makefile.am index 85dece70c..24065031f 100644 --- a/pcs_test/Makefile.am +++ b/pcs_test/Makefile.am @@ -403,7 +403,6 @@ EXTRA_DIST = \ tools/bin_mock/__init__.py \ tools/bin_mock/pcmk/crm_resource.d/list_agents_ocf__heartbeat \ tools/bin_mock/pcmk/crm_resource.d/list_agents_ocf__pacemaker \ - tools/bin_mock/pcmk/crm_resource.d/list_agents_stonith \ tools/bin_mock/pcmk/crm_resource.d/list_ocf_providers \ tools/bin_mock/pcmk/crm_resource.d/list_standards \ tools/bin_mock/pcmk/crm_resource.d/lsb__network_metadata.xml \ diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/list_agents_stonith b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/list_agents_stonith deleted file mode 100644 index b807adca0..000000000 --- a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/list_agents_stonith +++ /dev/null @@ -1,46 +0,0 @@ -fence_xvm -fence_wti -fence_vmware_soap -fence_vmware_rest -fence_virt -fence_tripplite_snmp -fence_scsi -fence_sbd -fence_rsb -fence_rsa -fence_rhevm -fence_mpath -fence_kdump -fence_ipmilan -fence_ipdu -fence_intelmodular -fence_imm -fence_ilo_ssh -fence_ilo_mp -fence_ilo_moonshot -fence_ilo5_ssh -fence_ilo5 -fence_ilo4_ssh -fence_ilo4 -fence_ilo3_ssh -fence_ilo3 -fence_ilo2 -fence_ilo -fence_ifmib -fence_idrac -fence_ibmblade -fence_hpblade -fence_heuristics_ping -fence_evacuate -fence_eps -fence_emerson -fence_eaton_snmp -fence_drac5 -fence_compute -fence_cisco_ucs -fence_cisco_mds -fence_brocade -fence_bladecenter -fence_apc_snmp -fence_apc -fence_amt_ws From 8f200a627fe3d5c5e797a46d0407053fc1d55d12 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Thu, 6 Jun 2024 14:55:25 +0200 Subject: [PATCH 014/227] typehints for pcs.lib.pacemaker --- mypy.ini | 8 ++ pcs/lib/pacemaker/live.py | 142 +++++++++++++++++++----------------- pcs/lib/pacemaker/status.py | 2 +- pcs/lib/pacemaker/values.py | 2 +- 4 files changed, 84 insertions(+), 70 deletions(-) diff --git a/mypy.ini b/mypy.ini index 21a621350..988afbec5 100644 --- a/mypy.ini +++ b/mypy.ini @@ -153,6 +153,14 @@ disallow_untyped_calls = True disallow_untyped_defs = True disallow_untyped_calls = True +[mypy-pcs.lib.pacemaker.*] +disallow_untyped_defs = True +disallow_untyped_calls = True + +[mypy-pcs.lib.pacemaker.state] +disallow_untyped_defs = False +disallow_untyped_calls = False + [mypy-pcs.lib.permissions.*] disallow_untyped_defs = True disallow_untyped_calls = True diff --git a/pcs/lib/pacemaker/live.py b/pcs/lib/pacemaker/live.py index 43197ac10..c58f2e45e 100644 --- a/pcs/lib/pacemaker/live.py +++ b/pcs/lib/pacemaker/live.py @@ -1,11 +1,9 @@ import os.path import re from typing import ( - Dict, - List, Mapping, Optional, - Tuple, + Union, cast, ) @@ -53,12 +51,10 @@ class FenceHistoryCommandErrorException(Exception): ### status -def get_cluster_status_xml_raw(runner: CommandRunner) -> Tuple[str, str, int]: +def get_cluster_status_xml_raw(runner: CommandRunner) -> tuple[str, str, int]: """ Run pacemaker tool to get XML status. This function doesn't do any processing. Usually, using get_cluster_status_dom is preferred instead. - - runner -- a class for running external processes """ return runner.run( [ @@ -74,8 +70,6 @@ def get_cluster_status_xml_raw(runner: CommandRunner) -> Tuple[str, str, int]: def _get_cluster_status_xml(runner: CommandRunner) -> str: """ Get pacemaker XML status. Using get_cluster_status_dom is preferred instead. - - runner -- a class for running external processes """ stdout, stderr, retval = get_cluster_status_xml_raw(runner) if retval == 0: @@ -114,7 +108,7 @@ def get_cluster_status_text( runner: CommandRunner, hide_inactive_resources: bool, verbose: bool, -) -> Tuple[str, List[str]]: +) -> tuple[str, list[str]]: cmd = [settings.crm_mon_exec, "--one-shot"] if not hide_inactive_resources: cmd.append("--inactive") @@ -132,7 +126,7 @@ def get_cluster_status_text( reports.messages.CrmMonError(join_multilines([stderr, stdout])) ) ) - warnings: List[str] = [] + warnings: list[str] = [] if stderr.strip(): warnings = [ line @@ -143,7 +137,7 @@ def get_cluster_status_text( return stdout.strip(), warnings -def get_ticket_status_text(runner: CommandRunner) -> Tuple[str, str, int]: +def get_ticket_status_text(runner: CommandRunner) -> tuple[str, str, int]: stdout, stderr, retval = runner.run([settings.crm_ticket_exec, "--details"]) return stdout.strip(), stderr.strip(), retval @@ -197,7 +191,9 @@ def get_cib(xml: str) -> _Element: ) from e -def verify(runner, verbose=False): +def verify( + runner: CommandRunner, verbose: bool = False +) -> tuple[str, str, int, bool]: crm_verify_cmd = [settings.crm_verify_exec] # Currently, crm_verify can suggest up to two -V options but it accepts # more than two. We stick with two -V options if verbose mode was enabled. @@ -233,7 +229,7 @@ def verify(runner, verbose=False): return stdout, stderr, returncode, can_be_more_verbose -def replace_cib_configuration_xml(runner, xml): +def replace_cib_configuration_xml(runner: CommandRunner, xml: str) -> None: cmd = [ settings.cibadmin_exec, "--replace", @@ -249,11 +245,11 @@ def replace_cib_configuration_xml(runner, xml): ) -def replace_cib_configuration(runner, tree): +def replace_cib_configuration(runner: CommandRunner, tree: _Element) -> None: return replace_cib_configuration_xml(runner, etree_to_str(tree)) -def push_cib_diff_xml(runner, cib_diff_xml): +def push_cib_diff_xml(runner: CommandRunner, cib_diff_xml: str) -> None: cmd = [ settings.cibadmin_exec, "--patch", @@ -276,8 +272,6 @@ def diff_cibs_xml( """ Return xml diff of two CIBs - runner - reporter cib_old_xml -- original CIB cib_new_xml -- modified CIB """ @@ -317,21 +311,13 @@ def ensure_cib_version( cib: _Element, version: Version, fail_if_version_not_met: bool = True, -) -> Tuple[_Element, bool]: +) -> tuple[_Element, bool]: """ Make sure CIB complies to specified schema version (or newer), upgrade CIB if necessary. Raise on error. Raise if CIB cannot be upgraded enough to meet the required version unless fail_if_version_not_met is set to False. Return tuple(upgraded_cib, was_upgraded) - 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 -- runner - cib -- cib tree version -- required cib version fail_if_version_not_met -- allows a 'nice to have' cib upgrade """ @@ -364,10 +350,9 @@ def ensure_cib_version( ) -def _upgrade_cib(runner): +def _upgrade_cib(runner: CommandRunner) -> None: """ Upgrade CIB to the latest schema available locally or clusterwise. - CommandRunner runner """ stdout, stderr, retval = runner.run( [settings.cibadmin_exec, "--upgrade", "--force"] @@ -385,12 +370,13 @@ def _upgrade_cib(runner): ) -def simulate_cib_xml(runner, cib_xml): +def simulate_cib_xml( + runner: CommandRunner, cib_xml: str +) -> tuple[str, str, str]: """ Run crm_simulate to get effects the cib would have on the live cluster - CommandRunner runner -- runner - string cib_xml -- CIB XML to simulate + cib_xml -- CIB XML to simulate """ try: with ( @@ -426,12 +412,13 @@ def simulate_cib_xml(runner, cib_xml): ) from e -def simulate_cib(runner, cib): +def simulate_cib( + runner: CommandRunner, cib: _Element +) -> tuple[str, _Element, _Element]: """ Run crm_simulate to get effects the cib would have on the live cluster - CommandRunner runner -- runner - etree cib -- cib tree to simulate + cib -- cib tree to simulate """ cib_xml = etree_to_str(cib) try: @@ -456,9 +443,7 @@ def wait_for_idle(runner: CommandRunner, timeout: int) -> None: """ Run waiting command. Raise LibraryError if command failed. - runner -- preconfigured object for running external programs - timeout -- waiting timeout in seconds, wait indefinitely if non-positive - integer + timeout -- waiting timeout in seconds, wait indefinitely if less than 1 """ args = [settings.crm_resource_exec, "--wait"] if timeout > 0: @@ -488,7 +473,7 @@ def wait_for_idle(runner: CommandRunner, timeout: int) -> None: ### nodes -def get_local_node_name(runner): +def get_local_node_name(runner: CommandRunner) -> str: stdout, stderr, retval = runner.run([settings.crm_node_exec, "--name"]) if retval != 0: klass = ( @@ -506,7 +491,7 @@ def get_local_node_name(runner): return stdout.strip() -def get_local_node_status(runner): +def get_local_node_status(runner: CommandRunner) -> dict[str, Union[bool, str]]: try: cluster_status = ClusterState(get_cluster_status_dom(runner)) node_name = get_local_node_name(runner) @@ -514,7 +499,7 @@ def get_local_node_status(runner): return {"offline": True} for node_status in cluster_status.node_section.nodes: if node_status.attrs.name == node_name: - result = { + result: dict[str, Union[bool, str]] = { "offline": False, } for attr in ( @@ -539,7 +524,7 @@ def get_local_node_status(runner): ) -def remove_node(runner, node_name): +def remove_node(runner: CommandRunner, node_name: str) -> None: stdout, stderr, retval = runner.run( [ settings.crm_node_exec, @@ -569,7 +554,7 @@ def resource_cleanup( operation: Optional[str] = None, interval: Optional[str] = None, strict: bool = False, -): +) -> str: cmd = [settings.crm_resource_exec, "--cleanup"] if resource: cmd.extend(["--resource", resource]) @@ -602,7 +587,7 @@ def resource_refresh( node: Optional[str] = None, strict: bool = False, force: bool = False, -): +) -> str: if not force and not node and not resource: summary = ClusterState(get_cluster_status_dom(runner)).summary operations = summary.nodes.attrs.count * summary.resources.attrs.count @@ -638,7 +623,13 @@ def resource_refresh( return join_multilines([stdout, stderr]) -def resource_move(runner, resource_id, node=None, master=False, lifetime=None): +def resource_move( + runner: CommandRunner, + resource_id: str, + node: Optional[str] = None, + master: bool = False, + lifetime: Optional[str] = None, +) -> tuple[str, str, int]: return _resource_move_ban_clear( runner, "--move", @@ -649,7 +640,13 @@ def resource_move(runner, resource_id, node=None, master=False, lifetime=None): ) -def resource_ban(runner, resource_id, node=None, master=False, lifetime=None): +def resource_ban( + runner: CommandRunner, + resource_id: str, + node: Optional[str] = None, + master: bool = False, + lifetime: Optional[str] = None, +) -> tuple[str, str, int]: return _resource_move_ban_clear( runner, "--ban", @@ -661,8 +658,12 @@ def resource_ban(runner, resource_id, node=None, master=False, lifetime=None): def resource_unmove_unban( - runner, resource_id, node=None, master=False, expired=False -): + runner: CommandRunner, + resource_id: str, + node: Optional[str] = None, + master: bool = False, + expired: bool = False, +) -> tuple[str, str, int]: return _resource_move_ban_clear( runner, "--clear", @@ -673,21 +674,21 @@ def resource_unmove_unban( ) -def has_resource_unmove_unban_expired_support(runner): +def has_resource_unmove_unban_expired_support(runner: CommandRunner) -> bool: return _is_in_pcmk_tool_help( runner, settings.crm_resource_exec, ["--expired"] ) def _resource_move_ban_clear( - runner, - action, - resource_id, - node=None, - master=False, - lifetime=None, - expired=False, -): + runner: CommandRunner, + action: str, + resource_id: str, + node: Optional[str] = None, + master: bool = False, + lifetime: Optional[str] = None, + expired: bool = False, +) -> tuple[str, str, int]: command = [ settings.crm_resource_exec, action, @@ -723,22 +724,28 @@ def is_fence_history_supported_management(runner: CommandRunner) -> bool: ) -def fence_history_cleanup(runner, node=None): +def fence_history_cleanup( + runner: CommandRunner, node: Optional[str] = None +) -> str: return _run_fence_history_command(runner, "--cleanup", node) -def fence_history_text(runner, node=None): +def fence_history_text( + runner: CommandRunner, node: Optional[str] = None +) -> str: return _run_fence_history_command(runner, "--verbose", node) -def fence_history_update(runner): +def fence_history_update(runner: CommandRunner) -> str: # Pacemaker always prints "gather fencing-history from all nodes" even if a # node is specified. However, --history expects a value, so we must provide # it. Otherwise "--broadcast" would be considered a value of "--history". return _run_fence_history_command(runner, "--broadcast", node=None) -def _run_fence_history_command(runner, command, node=None): +def _run_fence_history_command( + runner: CommandRunner, command: str, node: Optional[str] = None +) -> str: stdout, stderr, retval = runner.run( [ settings.stonith_admin_exec, @@ -831,9 +838,9 @@ def _is_in_pcmk_tool_help( ) -def is_getting_resource_digest_supported(runner): +def is_getting_resource_digest_supported(runner: CommandRunner) -> bool: return _is_in_pcmk_tool_help( - runner, settings.crm_resource_exec, "--digests" + runner, settings.crm_resource_exec, ["--digests"] ) @@ -841,15 +848,14 @@ def get_resource_digests( runner: CommandRunner, resource_id: str, node_name: str, - resource_options: Dict[str, str], - crm_meta_attributes: Optional[Dict[str, Optional[str]]] = None, -) -> Dict[str, Optional[str]]: + resource_options: dict[str, str], + crm_meta_attributes: Optional[dict[str, Optional[str]]] = None, +) -> dict[str, Optional[str]]: """ Get set of digests for a resource using crm_resource utility. There are 3 types of digests: all, nonreloadable and nonprivate. Resource can have one or more digests types depending on the resource parameters. - runner -- command runner instance resource_id -- resource id node_name -- name of the node where resource is running resource_options -- resource options with updated values @@ -876,7 +882,7 @@ def get_resource_digests( ] stdout, stderr, retval = runner.run(command) - def error_exception(message): + def error_exception(message: str) -> LibraryError: return LibraryError( ReportItem.error( reports.messages.UnableToGetResourceOperationDigests(message) @@ -897,7 +903,7 @@ def error_exception(message): digests = {} for digest_type in ["all", "nonprivate", "nonreloadable"]: xpath_result = cast( - List[str], + list[str], dom.xpath( "./digests/digest[@type=$digest_type]/@hash", digest_type=digest_type, diff --git a/pcs/lib/pacemaker/status.py b/pcs/lib/pacemaker/status.py index 9b0930507..ed5eb0bad 100644 --- a/pcs/lib/pacemaker/status.py +++ b/pcs/lib/pacemaker/status.py @@ -39,7 +39,7 @@ def __init__(self, resource_id: str): class EmptyResourceIdError(ClusterStatusParsingError): - def __init__(self): + def __init__(self) -> None: super().__init__("") diff --git a/pcs/lib/pacemaker/values.py b/pcs/lib/pacemaker/values.py index 18025e215..22e074a47 100644 --- a/pcs/lib/pacemaker/values.py +++ b/pcs/lib/pacemaker/values.py @@ -93,7 +93,7 @@ def validate_id( id_candidate: str, description: Optional[str] = None, reporter: Union[None, List, ReportItemList] = None, -): +) -> None: """ Validate a pacemaker id, raise LibraryError on invalid id. From ccffba735128ea4be62aa33e7319114d8b26a8b0 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Wed, 31 Jul 2024 14:45:31 +0200 Subject: [PATCH 015/227] move resource restart to pcs.lib --- pcs/cli/common/lib_wrapper.py | 7 +- pcs/cli/routing/booth.py | 4 +- pcs/cli/routing/resource.py | 2 +- pcs/common/reports/codes.py | 7 + pcs/common/reports/messages.py | 66 +++++++ pcs/lib/commands/resource.py | 84 ++++++++- pcs/lib/pacemaker/live.py | 31 +++ pcs/resource.py | 54 ++---- pcs_test/Makefile.am | 3 +- pcs_test/tier0/cli/test_resource.py | 68 +++++++ .../tier0/common/reports/test_messages.py | 35 ++++ .../lib/commands/resource/test_restart.py | 177 ++++++++++++++++++ pcs_test/tier0/lib/pacemaker/test_live.py | 76 ++++++++ .../tools/command_env/config_runner_pcmk.py | 39 ++++ 14 files changed, 605 insertions(+), 48 deletions(-) create mode 100644 pcs_test/tier0/lib/commands/resource/test_restart.py diff --git a/pcs/cli/common/lib_wrapper.py b/pcs/cli/common/lib_wrapper.py index 6f667c708..acca19838 100644 --- a/pcs/cli/common/lib_wrapper.py +++ b/pcs/cli/common/lib_wrapper.py @@ -405,15 +405,16 @@ def load_module(env, middleware_factory, name): "enable": resource.enable, "get_configured_resources": resource.get_configured_resources, "get_failcounts": resource.get_failcounts, + "get_resource_relations_tree": ( + resource.get_resource_relations_tree + ), "group_add": resource.group_add, "is_any_resource_except_stonith": resource.is_any_resource_except_stonith, "is_any_stonith": resource.is_any_stonith, "manage": resource.manage, "move": resource.move, "move_autoclean": resource.move_autoclean, - "get_resource_relations_tree": ( - resource.get_resource_relations_tree - ), + "restart": resource.restart, "unmanage": resource.unmanage, "unmove_unban": resource.unmove_unban, }, diff --git a/pcs/cli/routing/booth.py b/pcs/cli/routing/booth.py index 811809f14..6c9d1908f 100644 --- a/pcs/cli/routing/booth.py +++ b/pcs/cli/routing/booth.py @@ -6,7 +6,7 @@ from pcs.cli.common.routing import create_router from pcs.resource import ( resource_remove, - resource_restart, + resource_restart_cmd, ) mapping = { @@ -30,7 +30,7 @@ # a function to pcs.lib "delete": command.get_remove_from_cluster(resource_remove), # type:ignore "remove": command.get_remove_from_cluster(resource_remove), # type:ignore - "restart": command.get_restart(resource_restart), # type:ignore + "restart": command.get_restart(resource_restart_cmd), # type:ignore "sync": command.sync, "pull": command.pull, "enable": command.enable, diff --git a/pcs/cli/routing/resource.py b/pcs/cli/routing/resource.py index b999a86ff..3e0ad6b6d 100644 --- a/pcs/cli/routing/resource.py +++ b/pcs/cli/routing/resource.py @@ -50,7 +50,7 @@ "enable": resource.resource_enable_cmd, "disable": resource.resource_disable_cmd, "safe-disable": resource.resource_safe_disable_cmd, - "restart": resource.resource_restart, + "restart": resource.resource_restart_cmd, "debug-start": partial( resource.resource_force_action, action="debug-start" ), diff --git a/pcs/common/reports/codes.py b/pcs/common/reports/codes.py index b28198e96..e6eb150a4 100644 --- a/pcs/common/reports/codes.py +++ b/pcs/common/reports/codes.py @@ -446,6 +446,13 @@ ) RESOURCE_REFRESH_ERROR = M("RESOURCE_REFRESH_ERROR") RESOURCE_REFRESH_TOO_TIME_CONSUMING = M("RESOURCE_REFRESH_TOO_TIME_CONSUMING") +RESOURCE_RESTART_ERROR = M("RESOURCE_RESTART_ERROR") +RESOURCE_RESTART_NODE_IS_FOR_MULTIINSTANCE_ONLY = M( + "RESOURCE_RESTART_NODE_IS_FOR_MULTIINSTANCE_ONLY" +) +RESOURCE_RESTART_USING_PARENT_RESOURCE = M( + "RESOURCE_RESTART_USING_PARENT_RESOURCE" +) RESOURCE_RUNNING_ON_NODES = M("RESOURCE_RUNNING_ON_NODES") RESOURCE_UNMOVE_UNBAN_PCMK_ERROR = M("RESOURCE_UNMOVE_UNBAN_PCMK_ERROR") RESOURCE_UNMOVE_UNBAN_PCMK_SUCCESS = M("RESOURCE_UNMOVE_UNBAN_PCMK_SUCCESS") diff --git a/pcs/common/reports/messages.py b/pcs/common/reports/messages.py index ccf32a032..9da13a9bd 100644 --- a/pcs/common/reports/messages.py +++ b/pcs/common/reports/messages.py @@ -3380,6 +3380,72 @@ def message(self) -> str: return "Cannot use 'mocked CIB' together with 'wait'" +@dataclass(frozen=True) +class ResourceRestartError(ReportItemMessage): + """ + An error occurred when restarting a resource in pacemaker + + reason -- error description + resource -- resource which has been restarted + node -- node where the resource has been restarted + """ + + reason: str + resource: str + node: Optional[str] = None + _code = codes.RESOURCE_RESTART_ERROR + + @property + def message(self) -> str: + return f"Unable to restart resource '{self.resource}':\n{self.reason}" + + +@dataclass(frozen=True) +class ResourceRestartNodeIsForMultiinstanceOnly(ReportItemMessage): + """ + Restart can be limited to a specified node only for multiinstance resources + + resource -- resource to be restarted + resource_type -- actual type of the resource + node -- node where the resource was to be restarted + """ + + resource: str + resource_type: str + node: str + _code = codes.RESOURCE_RESTART_NODE_IS_FOR_MULTIINSTANCE_ONLY + + @property + def message(self) -> str: + resource_type = _type_to_string(self.resource_type, article=True) + return ( + "Can only restart on a specific node for a clone or bundle, " + f"'{self.resource}' is {resource_type}" + ) + + +@dataclass(frozen=True) +class ResourceRestartUsingParentRersource(ReportItemMessage): + """ + Multiinstance parent is restarted instead of a specified primitive + + resource -- resource which has been asked to be restarted + parent -- parent resource to be restarted instead + """ + + resource: str + parent: str + _code = codes.RESOURCE_RESTART_USING_PARENT_RESOURCE + + @property + def message(self) -> str: + return ( + f"Restarting '{self.parent}' instead...\n" + "(If a resource is a clone or bundle, you must use the clone or " + "bundle instead)" + ) + + @dataclass(frozen=True) class ResourceCleanupError(ReportItemMessage): """ diff --git a/pcs/lib/commands/resource.py b/pcs/lib/commands/resource.py index 5e749bad3..9b30a04ec 100644 --- a/pcs/lib/commands/resource.py +++ b/pcs/lib/commands/resource.py @@ -66,6 +66,7 @@ push_cib_diff_xml, resource_ban, resource_move, + resource_restart, resource_unmove_unban, simulate_cib, ) @@ -101,7 +102,7 @@ @contextmanager def resource_environment( - env, + env: LibraryEnvironment, wait: WaitType = False, wait_for_resource_ids=None, resource_state_reporter=info_resource_state, @@ -2542,3 +2543,84 @@ def get_configured_resources(env: LibraryEnvironment) -> CibResourcesDto: ], bundles=bundles, ) + + +def restart( + env: LibraryEnvironment, + resource_id: str, + node: Optional[str] = None, + timeout: Optional[str] = None, +) -> None: + """ + Restart a resource + + resource_id -- id of the resource to be restarted + node -- name of the node to limit the restart to + timeout -- abort if the command doesn't finish in this time (integer + unit) + """ + cib = env.get_cib() + try: + resource_el = get_element_by_id(cib, resource_id) + except ElementNotFound as e: + env.report_processor.report( + ReportItem.error( + reports.messages.IdNotFound( + resource_id, expected_types=["resource"] + ) + ) + ) + raise LibraryError() from e + if not resource.common.is_resource(resource_el): + env.report_processor.report( + ReportItem.error( + reports.messages.IdBelongsToUnexpectedType( + resource_id, + expected_types=["resource"], + current_type=resource_el.tag, + ) + ) + ) + raise LibraryError() + + parent_resource_el = resource.clone.get_parent_any_clone(resource_el) + if parent_resource_el is None: + parent_resource_el = resource.bundle.get_parent_bundle(resource_el) + if parent_resource_el is not None: + env.report_processor.report( + reports.ReportItem.warning( + reports.messages.ResourceRestartUsingParentRersource( + str(resource_el.attrib["id"]), + str(parent_resource_el.attrib["id"]), + ) + ) + ) + resource_el = parent_resource_el + + if node and not ( + resource.clone.is_any_clone(resource_el) + or resource.bundle.is_bundle(resource_el) + ): + env.report_processor.report( + reports.ReportItem.error( + reports.messages.ResourceRestartNodeIsForMultiinstanceOnly( + str(resource_el.attrib["id"]), + resource_el.tag, + node, + ) + ) + ) + + if timeout is not None: + env.report_processor.report_list( + ValueTimeInterval("timeout").validate({"timeout": timeout}) + ) + + if env.report_processor.has_errors: + raise LibraryError() + + resource_restart( + env.cmd_runner(), + str(resource_el.attrib["id"]), + node=node, + timeout=timeout, + ) diff --git a/pcs/lib/pacemaker/live.py b/pcs/lib/pacemaker/live.py index c58f2e45e..707970d8f 100644 --- a/pcs/lib/pacemaker/live.py +++ b/pcs/lib/pacemaker/live.py @@ -547,6 +547,37 @@ def remove_node(runner: CommandRunner, node_name: str) -> None: ### resources +def resource_restart( + runner: CommandRunner, + resource: str, + node: Optional[str] = None, + timeout: Optional[str] = None, +) -> None: + """ + Ask pacemaker to restart a resource + + resource -- id of the resource to be restarted + node -- name of the node to limit the restart to + timeout -- abort if the command doesn't finish in this time (integer + unit) + """ + cmd = [settings.crm_resource_exec, "--restart", "--resource", resource] + if node: + cmd.extend(["--node", node]) + if timeout: + cmd.extend(["--timeout", timeout]) + + stdout, stderr, retval = runner.run(cmd) + + if retval != 0: + raise LibraryError( + ReportItem.error( + reports.messages.ResourceRestartError( + join_multilines([stderr, stdout]), resource, node + ) + ) + ) + + def resource_cleanup( runner: CommandRunner, resource: Optional[str] = None, diff --git a/pcs/resource.py b/pcs/resource.py index c2a5ff6c8..008b122fa 100644 --- a/pcs/resource.py +++ b/pcs/resource.py @@ -2793,56 +2793,30 @@ def resource_disable(argv: Argv) -> Optional[bool]: return None -def resource_restart(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: +def resource_restart_cmd( + lib: Any, argv: Argv, modifiers: InputModifiers +) -> None: """ Options: * --wait """ modifiers.ensure_only_supported("--wait") if not argv: - utils.err("You must specify a resource to restart") - _check_is_not_stonith(lib, [argv[0]]) - - dom = utils.get_cib_dom() - node = None - resource = argv.pop(0) - - real_res = utils.dom_get_resource_clone_ms_parent( - dom, resource - ) or utils.dom_get_resource_bundle_parent(dom, resource) - if real_res: - warn( - ( - "using {resource_id}... (if a resource is a clone or bundle " - "you must use the clone or bundle name)" - ).format(resource_id=real_res.getAttribute("id")) + raise CmdLineInputError( + "You must specify a resource to restart", + show_both_usage_and_message=True, ) - resource = real_res.getAttribute("id") - - args = ["crm_resource", "--restart", "--resource", resource] + resource = argv.pop(0) + node = argv.pop(0) if argv else None if argv: - node = argv.pop(0) - if not ( - utils.dom_get_clone(dom, resource) - or utils.dom_get_master(dom, resource) - or utils.dom_get_bundle(dom, resource) - ): - utils.err( - "can only restart on a specific node for a clone or bundle " - "resource" - ) - args.extend(["--node", node]) + raise CmdLineInputError() - if modifiers.is_specified("--wait"): - wait = modifiers.get("--wait") - if wait: - args.extend(["--timeout", str(wait)]) - else: - utils.err("You must specify the number of seconds to wait") + _check_is_not_stonith(lib, [resource]) + timeout = ( + modifiers.get("--wait") if modifiers.is_specified("--wait") else None + ) - output, retval = utils.run(args) - if retval != 0: - utils.err(output) + lib.resource.restart(resource, node, timeout) print_to_stderr(f"{resource} successfully restarted") diff --git a/pcs_test/Makefile.am b/pcs_test/Makefile.am index 24065031f..414b0ceb9 100644 --- a/pcs_test/Makefile.am +++ b/pcs_test/Makefile.am @@ -253,6 +253,7 @@ EXTRA_DIST = \ tier0/lib/commands/resource/test_resource_move_autoclean.py \ tier0/lib/commands/resource/test_resource_move_ban.py \ tier0/lib/commands/resource/test_resource_relations.py \ + tier0/lib/commands/resource/test_restart.py \ tier0/lib/commands/sbd/__init__.py \ tier0/lib/commands/sbd/test_disable_sbd.py \ tier0/lib/commands/sbd/test_enable_sbd.py \ @@ -268,8 +269,8 @@ EXTRA_DIST = \ tier0/lib/commands/test_acl.py \ tier0/lib/commands/test_alert.py \ tier0/lib/commands/test_booth.py \ - tier0/lib/commands/test_cib.py \ tier0/lib/commands/test_cib_options.py \ + tier0/lib/commands/test_cib.py \ tier0/lib/commands/test_cluster_property.py \ tier0/lib/commands/test_constraint_common.py \ tier0/lib/commands/test_constraint_order.py \ diff --git a/pcs_test/tier0/cli/test_resource.py b/pcs_test/tier0/cli/test_resource.py index 101e6fc43..afb731001 100644 --- a/pcs_test/tier0/cli/test_resource.py +++ b/pcs_test/tier0/cli/test_resource.py @@ -1234,3 +1234,71 @@ def test_monitor(self): self.resource.unmanage.assert_called_once_with( ["R1", "R2"], with_monitor=True ) + + +@mock.patch("pcs.resource.print_to_stderr") +class ResourceRestart(TestCase): + def setUp(self): + self.lib = mock.Mock(spec_set=["resource"]) + self.resource = mock.Mock(spec_set=["restart", "is_any_stonith"]) + self.resource.is_any_stonith.return_value = False + self.lib.resource = self.resource + + def test_no_args(self, mock_print): + with self.assertRaises(CmdLineInputError) as cm: + resource.resource_restart_cmd(self.lib, [], dict_to_modifiers({})) + self.assertEqual( + cm.exception.message, "You must specify a resource to restart" + ) + self.resource.restart.assert_not_called() + mock_print.assert_not_called() + + def test_one_arg(self, mock_print): + resource.resource_restart_cmd( + self.lib, ["resource"], dict_to_modifiers({}) + ) + self.resource.restart.assert_called_once_with("resource", None, None) + mock_print.assert_called_once_with("resource successfully restarted") + + def test_two_args(self, mock_print): + resource.resource_restart_cmd( + self.lib, ["resource", "node"], dict_to_modifiers({}) + ) + self.resource.restart.assert_called_once_with("resource", "node", None) + mock_print.assert_called_once_with("resource successfully restarted") + + def test_more_args(self, mock_print): + with self.assertRaises(CmdLineInputError) as cm: + resource.resource_restart_cmd( + self.lib, ["ono", "two", "three"], dict_to_modifiers({}) + ) + self.assertEqual(cm.exception.message, None) + self.resource.restart.assert_not_called() + mock_print.assert_not_called() + + def test_wait(self, mock_print): + resource.resource_restart_cmd( + self.lib, ["resource"], dict_to_modifiers({"wait": "10s"}) + ) + self.resource.restart.assert_called_once_with("resource", None, "10s") + mock_print.assert_called_once_with("resource successfully restarted") + + def test_all_options(self, mock_print): + resource.resource_restart_cmd( + self.lib, ["resource", "node"], dict_to_modifiers({"wait": "10s"}) + ) + self.resource.restart.assert_called_once_with("resource", "node", "10s") + mock_print.assert_called_once_with("resource successfully restarted") + + @mock.patch("pcs.resource.deprecation_warning") + def test_stonith(self, mock_deprecation, mock_print): + self.resource.is_any_stonith.return_value = True + resource.resource_restart_cmd( + self.lib, ["stonith"], dict_to_modifiers({}) + ) + self.resource.restart.assert_called_once_with("stonith", None, None) + mock_print.assert_called_once_with("stonith successfully restarted") + mock_deprecation.assert_called_once_with( + "Ability of this command to accept stonith resources is deprecated " + "and will be removed in a future release." + ) diff --git a/pcs_test/tier0/common/reports/test_messages.py b/pcs_test/tier0/common/reports/test_messages.py index 25e5daabb..f46bced1e 100644 --- a/pcs_test/tier0/common/reports/test_messages.py +++ b/pcs_test/tier0/common/reports/test_messages.py @@ -5847,3 +5847,38 @@ def test_multiple(self): "resource-bundle", ["resource-0", "resource-1"] ), ) + + +class ResourceRestartError(NameBuildTest): + def test_message(self) -> str: + self.assert_message_from_report( + "Unable to restart resource 'resourceId':\nerror description", + reports.ResourceRestartError("error description", "resourceId"), + ) + + +class ResourceRestartNodeIsForMultiinstanceOnly(NameBuildTest): + def test_message(self) -> str: + self.assert_message_from_report( + ( + "Can only restart on a specific node for a clone or bundle, " + "'resourceId' is a resource" + ), + reports.ResourceRestartNodeIsForMultiinstanceOnly( + "resourceId", "primitive", "node01" + ), + ) + + +class ResourceRestartUsingParentRersource(NameBuildTest): + def test_message(self) -> str: + self.assert_message_from_report( + ( + "Restarting 'parentId' instead...\n" + "(If a resource is a clone or bundle, you must use the clone " + "or bundle instead)" + ), + reports.ResourceRestartUsingParentRersource( + "resourceId", "parentId" + ), + ) diff --git a/pcs_test/tier0/lib/commands/resource/test_restart.py b/pcs_test/tier0/lib/commands/resource/test_restart.py new file mode 100644 index 000000000..9d61f5704 --- /dev/null +++ b/pcs_test/tier0/lib/commands/resource/test_restart.py @@ -0,0 +1,177 @@ +from unittest import TestCase + +from pcs.common import reports +from pcs.lib.commands import resource + +from pcs_test.tools import fixture +from pcs_test.tools.command_env import get_env_tools + + +class ResourceRestart(TestCase): + def setUp(self): + self.env_assist, self.config = get_env_tools(test_case=self) + resources_xml = """ + + + + + + + + + + + + + + + + + + """ + self.config.runner.cib.load(resources=resources_xml) + + def test_success(self): + self.config.runner.pcmk.resource_restart("R1") + resource.restart(self.env_assist.get_env(), "R1") + + def test_success_bundle_member(self): + self.config.runner.pcmk.resource_restart("B1") + resource.restart(self.env_assist.get_env(), "B1R1") + self.env_assist.assert_reports( + [ + fixture.warn( + reports.codes.RESOURCE_RESTART_USING_PARENT_RESOURCE, + resource="B1R1", + parent="B1", + ) + ] + ) + + def test_success_clone_member(self): + self.config.runner.pcmk.resource_restart("C1") + resource.restart(self.env_assist.get_env(), "C1R1") + self.env_assist.assert_reports( + [ + fixture.warn( + reports.codes.RESOURCE_RESTART_USING_PARENT_RESOURCE, + resource="C1R1", + parent="C1", + ) + ] + ) + + def test_success_clone_group_member(self): + self.config.runner.pcmk.resource_restart("C2") + resource.restart(self.env_assist.get_env(), "C2R1") + self.env_assist.assert_reports( + [ + fixture.warn( + reports.codes.RESOURCE_RESTART_USING_PARENT_RESOURCE, + resource="C2R1", + parent="C2", + ) + ] + ) + + def test_success_timeout(self): + self.config.runner.pcmk.resource_restart("R1", timeout="10") + resource.restart(self.env_assist.get_env(), "R1", timeout="10") + + def test_success_all_options(self): + self.config.runner.pcmk.resource_restart( + "C1", node="node1", timeout="10" + ) + resource.restart(self.env_assist.get_env(), "C1", "node1", "10") + + def test_bad_timeout(self): + self.env_assist.assert_raise_library_error( + lambda: resource.restart( + self.env_assist.get_env(), "R1", timeout="a while" + ) + ) + self.env_assist.assert_reports( + [ + fixture.error( + reports.codes.INVALID_OPTION_VALUE, + option_name="timeout", + option_value="a while", + allowed_values="time interval (e.g. 1, 2s, 3m, 4h, ...)", + cannot_be_empty=False, + forbidden_characters=None, + ) + ] + ) + + def test_resource_not_found(self): + self.env_assist.assert_raise_library_error( + lambda: resource.restart(self.env_assist.get_env(), "RX") + ) + self.env_assist.assert_reports( + [ + fixture.error( + reports.codes.ID_NOT_FOUND, + id="RX", + expected_types=["resource"], + context_type="", + context_id="", + ) + ] + ) + + def test_not_a_resource(self): + self.env_assist.assert_raise_library_error( + lambda: resource.restart(self.env_assist.get_env(), "R1-meta") + ) + self.env_assist.assert_reports( + [ + fixture.error( + reports.codes.ID_BELONGS_TO_UNEXPECTED_TYPE, + id="R1-meta", + expected_types=["resource"], + current_type="meta_attributes", + ) + ] + ) + + def test_stonith(self): + self.config.runner.pcmk.resource_restart("S1") + resource.restart(self.env_assist.get_env(), "S1") + + def test_node_not_multitinstance(self): + self.env_assist.assert_raise_library_error( + lambda: resource.restart( + self.env_assist.get_env(), "R1", node="node1" + ) + ) + self.env_assist.assert_reports( + [ + fixture.error( + reports.codes.RESOURCE_RESTART_NODE_IS_FOR_MULTIINSTANCE_ONLY, + resource="R1", + resource_type="primitive", + node="node1", + ) + ] + ) + + def test_node_multitinstance(self): + self.config.runner.pcmk.resource_restart("C1", node="node1") + resource.restart(self.env_assist.get_env(), "C1", "node1") + + def test_restart_error(self): + self.config.runner.pcmk.resource_restart( + "R1", stdout="some output", stderr="some error", returncode=1 + ) + self.env_assist.assert_raise_library_error( + lambda: resource.restart(self.env_assist.get_env(), "R1"), + [ + fixture.error( + reports.codes.RESOURCE_RESTART_ERROR, + reason="some error\nsome output", + resource="R1", + node=None, + ) + ], + expected_in_processor=False, + ) diff --git a/pcs_test/tier0/lib/pacemaker/test_live.py b/pcs_test/tier0/lib/pacemaker/test_live.py index 63ff9b843..5075e6447 100644 --- a/pcs_test/tier0/lib/pacemaker/test_live.py +++ b/pcs_test/tier0/lib/pacemaker/test_live.py @@ -1175,6 +1175,82 @@ def test_error(self): ) +class ResourceRestart(TestCase): + def setUp(self): + self.stdout = "expected output" + self.stderr = "expected stderr" + self.resource = "my_resource" + self.node = "my_node" + self.timeout = "10m" + self.env_assist, self.config = get_env_tools(test_case=self) + + def assert_output(self, real_output): + self.assertEqual(self.stdout + "\n" + self.stderr, real_output) + + def test_basic(self): + self.config.runner.pcmk.resource_restart( + self.resource, stdout=self.stdout, stderr=self.stderr + ) + env = self.env_assist.get_env() + lib.resource_restart(env.cmd_runner(), self.resource) + + def test_node(self): + self.config.runner.pcmk.resource_restart( + self.resource, + node=self.node, + stdout=self.stdout, + stderr=self.stderr, + ) + env = self.env_assist.get_env() + lib.resource_restart(env.cmd_runner(), self.resource, node=self.node) + + def test_timeout(self): + self.config.runner.pcmk.resource_restart( + self.resource, + timeout=self.timeout, + stdout=self.stdout, + stderr=self.stderr, + ) + env = self.env_assist.get_env() + lib.resource_restart( + env.cmd_runner(), self.resource, timeout=self.timeout + ) + + def test_all_options(self): + self.config.runner.pcmk.resource_restart( + self.resource, + node=self.node, + timeout=self.timeout, + stdout=self.stdout, + stderr=self.stderr, + ) + env = self.env_assist.get_env() + lib.resource_restart( + env.cmd_runner(), + self.resource, + node=self.node, + timeout=self.timeout, + ) + + def test_error(self): + self.config.runner.pcmk.resource_restart( + self.resource, stdout=self.stdout, stderr=self.stderr, returncode=1 + ) + env = self.env_assist.get_env() + self.env_assist.assert_raise_library_error( + lambda: lib.resource_restart(env.cmd_runner(), self.resource), + [ + fixture.error( + report_codes.RESOURCE_RESTART_ERROR, + reason=(self.stderr + "\n" + self.stdout), + resource=self.resource, + node=None, + ) + ], + expected_in_processor=False, + ) + + class ResourceCleanupTest(TestCase): def setUp(self): self.stdout = "expected output" diff --git a/pcs_test/tools/command_env/config_runner_pcmk.py b/pcs_test/tools/command_env/config_runner_pcmk.py index be1fa4485..83f134403 100644 --- a/pcs_test/tools/command_env/config_runner_pcmk.py +++ b/pcs_test/tools/command_env/config_runner_pcmk.py @@ -1,4 +1,5 @@ import os +from typing import Optional from pcs import settings @@ -527,6 +528,44 @@ def local_node_name( instead=instead, ) + def resource_restart( + self, + resource: str, + node: Optional[str] = None, + timeout: Optional[str] = None, + stdout: str = "", + stderr: str = "", + returncode: int = 0, + name: str = "runner.pcmk.restart", + ): + """ + Create a call for crm_resource --restart + + name -- the key of this call + resource -- the id of a resource to be restarted + node -- the name of the node where the resource should be restarted + timeout -- how long to wait for the resource to restart + stdout -- crm_resource's stdout + stderr -- crm_resource's stderr + returncode -- crm_resource's returncode + """ + cmd = ["crm_resource", "--restart"] + if resource: + cmd.extend(["--resource", resource]) + if node: + cmd.extend(["--node", node]) + if timeout: + cmd.extend(["--timeout", timeout]) + self.__calls.place( + name, + RunnerCall( + cmd, + stdout=stdout, + stderr=stderr, + returncode=returncode, + ), + ) + def resource_cleanup( self, name="runner.pcmk.cleanup", From 6de3c20553ac11d23dba476661daf151250e28e1 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Tue, 2 Jul 2024 15:35:23 +0200 Subject: [PATCH 016/227] stop using resource restart callback in booth restart --- pcs/cli/booth/command.py | 34 ++++++--------- pcs/cli/routing/booth.py | 7 +-- pcs/lib/booth/resource.py | 13 +++--- pcs/lib/commands/booth.py | 36 ++++++++-------- pcs_test/tier0/cli/test_booth.py | 36 ++++++---------- pcs_test/tier0/lib/commands/test_booth.py | 52 +++++++---------------- 6 files changed, 66 insertions(+), 112 deletions(-) diff --git a/pcs/cli/booth/command.py b/pcs/cli/booth/command.py index 4b0183025..7da5fc82c 100644 --- a/pcs/cli/booth/command.py +++ b/pcs/cli/booth/command.py @@ -236,28 +236,20 @@ def remove_from_cluster( return remove_from_cluster -def get_restart(resource_restart): # type:ignore - # TODO resource_restart is provisional hack until resources are not moved to - # lib - def restart(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: - """ - Options: - * --force - allow multiple - * --name - name of a booth instance - """ - modifiers.ensure_only_supported("--force", "--name") - if arg_list: - raise CmdLineInputError() - - lib.booth.restart( - lambda resource_id_list: resource_restart( - lib, resource_id_list, modifiers.get_subset("--force") - ), - instance_name=modifiers.get("--name"), - allow_multiple=modifiers.get("--force"), - ) +def restart(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: + """ + Options: + * --force - allow multiple + * --name - name of a booth instance + """ + modifiers.ensure_only_supported("--force", "--name") + if arg_list: + raise CmdLineInputError() - return restart + lib.booth.restart( + instance_name=modifiers.get("--name"), + allow_multiple=modifiers.get("--force"), + ) def sync(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: diff --git a/pcs/cli/routing/booth.py b/pcs/cli/routing/booth.py index 6c9d1908f..2cc330089 100644 --- a/pcs/cli/routing/booth.py +++ b/pcs/cli/routing/booth.py @@ -4,10 +4,7 @@ ) from pcs.cli.booth import command from pcs.cli.common.routing import create_router -from pcs.resource import ( - resource_remove, - resource_restart_cmd, -) +from pcs.resource import resource_remove mapping = { "help": lambda lib, argv, modifiers: print(usage.booth(argv)), @@ -30,7 +27,7 @@ # a function to pcs.lib "delete": command.get_remove_from_cluster(resource_remove), # type:ignore "remove": command.get_remove_from_cluster(resource_remove), # type:ignore - "restart": command.get_restart(resource_restart_cmd), # type:ignore + "restart": command.restart, "sync": command.sync, "pull": command.pull, "enable": command.enable, diff --git a/pcs/lib/booth/resource.py b/pcs/lib/booth/resource.py index 83db5b24e..fd6cacd9b 100644 --- a/pcs/lib/booth/resource.py +++ b/pcs/lib/booth/resource.py @@ -1,7 +1,4 @@ -from typing import ( - List, - cast, -) +from typing import cast from lxml.etree import _Element @@ -49,9 +46,9 @@ def remove_from_cluster(booth_element_list): def find_for_config( resources_section: _Element, booth_config_file_path: str -) -> List[_Element]: +) -> list[_Element]: return cast( - List[_Element], + list[_Element], resources_section.xpath( """ .//primitive[ @@ -69,9 +66,9 @@ def find_for_config( def find_bound_ip( resources_section: _Element, booth_config_file_path: str -) -> List[_Element]: +) -> list[_Element]: return cast( - List[_Element], + list[_Element], resources_section.xpath( """ .//group[ diff --git a/pcs/lib/commands/booth.py b/pcs/lib/commands/booth.py index f291a085b..bb9752da9 100644 --- a/pcs/lib/commands/booth.py +++ b/pcs/lib/commands/booth.py @@ -6,6 +6,8 @@ cast, ) +from lxml.etree import _Element + from pcs import settings from pcs.common import ( file_type_codes, @@ -34,6 +36,7 @@ resource, status, ) +from pcs.lib.booth.env import BoothEnv from pcs.lib.cib.resource import ( group, hierarchy, @@ -58,7 +61,10 @@ ) from pcs.lib.interface.config import ParserErrorException from pcs.lib.node import get_existing_nodes_names -from pcs.lib.pacemaker.live import has_cib_xml +from pcs.lib.pacemaker.live import ( + has_cib_xml, + resource_restart, +) from pcs.lib.resource_agent import ( ResourceAgentError, ResourceAgentFacade, @@ -558,30 +564,26 @@ def remove_from_cluster( def restart( env: LibraryEnvironment, - resource_restart, - instance_name=None, - allow_multiple=False, -): + instance_name: Optional[str] = None, + allow_multiple: bool = False, +) -> None: """ Restart group with ip resource and booth resource env -- provides all for communication with externals - function resource_restart -- provisional hack til resources are moved to lib - string instance_name -- booth instance name - bool allow_remove_multiple -- remove all resources if more than one found + instance_name -- booth instance name + allow_multiple -- restart all resources if more than one found """ - # TODO resource_remove is provisional hack til resources are moved to lib - report_processor = env.report_processor booth_env = env.get_booth_env(instance_name) _ensure_live_env(env, booth_env) for booth_element in _find_resource_elements_for_operation( - report_processor, + env.report_processor, get_resources(env.get_cib()), booth_env, allow_multiple, ): - resource_restart([booth_element.attrib["id"]]) + resource_restart(env.cmd_runner(), str(booth_element.attrib["id"])) def ticket_grant( @@ -945,10 +947,10 @@ def get_status(env: LibraryEnvironment, instance_name=None): def _find_resource_elements_for_operation( report_processor: ReportProcessor, - resources_section, - booth_env, - allow_multiple, -): + resources_section: _Element, + booth_env: BoothEnv, + allow_multiple: bool, +) -> list[_Element]: booth_element_list = resource.find_for_config( resources_section, booth_env.config_path, @@ -989,7 +991,7 @@ def _ensure_live_booth_env(booth_env): ) -def _ensure_live_env(env: LibraryEnvironment, booth_env): +def _ensure_live_env(env: LibraryEnvironment, booth_env: BoothEnv): not_live = ( booth_env.ghost_file_codes + diff --git a/pcs_test/tier0/cli/test_booth.py b/pcs_test/tier0/cli/test_booth.py index 1499bf60d..fa2ec6e3c 100644 --- a/pcs_test/tier0/cli/test_booth.py +++ b/pcs_test/tier0/cli/test_booth.py @@ -315,36 +315,24 @@ def setUp(self): self.lib = mock.Mock(spec_set=["booth"]) self.lib.booth = mock.Mock(spec_set=["restart"]) - def test_lib_call_minimal(self): - def resource_restart(something): - return something + def test_args(self): + with self.assertRaises(CmdLineInputError) as cm: + booth_cmd.restart(self.lib, ["something"], dict_to_modifiers({})) + self.assertIsNone(cm.exception.message) + self.lib.booth.restart.assert_not_called() - booth_cmd.get_restart(resource_restart)( - self.lib, [], dict_to_modifiers({}) - ) - # The first arg going to the lib call is a lambda which we cannot get - # in here. So we must check all the other parameters in a bit more - # complicated way. - self.assertEqual(self.lib.booth.restart.call_count, 1) - call = self.lib.booth.restart.call_args - self.assertEqual( - call[1], dict(instance_name=None, allow_multiple=False) + def test_lib_call_minimal(self): + booth_cmd.restart(self.lib, [], dict_to_modifiers({})) + self.lib.booth.restart.assert_called_once_with( + instance_name=None, allow_multiple=False ) def test_lib_call_full(self): - def resource_restart(something): - return something - - booth_cmd.get_restart(resource_restart)( + booth_cmd.restart( self.lib, [], dict_to_modifiers(dict(name="my_booth", force=True)) ) - # The first arg going to the lib call is a lambda which we cannot get - # in here. So we must check all the other parameters in a bit more - # complicated way. - self.assertEqual(self.lib.booth.restart.call_count, 1) - call = self.lib.booth.restart.call_args - self.assertEqual( - call[1], dict(instance_name="my_booth", allow_multiple=True) + self.lib.booth.restart.assert_called_once_with( + instance_name="my_booth", allow_multiple=True ) diff --git a/pcs_test/tier0/lib/commands/test_booth.py b/pcs_test/tier0/lib/commands/test_booth.py index 2957e378b..d152407f8 100644 --- a/pcs_test/tier0/lib/commands/test_booth.py +++ b/pcs_test/tier0/lib/commands/test_booth.py @@ -2216,16 +2216,12 @@ def test_more_booth_resources_forced(self): class Restart(TestCase, FixtureMixin): def setUp(self): self.env_assist, self.config = get_env_tools(self) - # mock pcs.resource.resource_restart function which does all the heavy - # lifting - self.resource_restart = mock.Mock() def test_invalid_instance(self): instance_name = "/tmp/booth/booth" self.env_assist.assert_raise_library_error( lambda: commands.restart( self.env_assist.get_env(), - self.resource_restart, instance_name=instance_name, ), [ @@ -2233,32 +2229,24 @@ def test_invalid_instance(self): ], expected_in_processor=False, ) - self.resource_restart.assert_not_called() def test_success_default_instance(self): self.config.runner.cib.load(resources=self.fixture_cib_booth_group()) - commands.restart(self.env_assist.get_env(), self.resource_restart) - self.resource_restart.assert_has_calls( - [ - mock.call(["booth-booth-service"]), - ] - ) + self.config.runner.pcmk.resource_restart("booth-booth-service") + commands.restart(self.env_assist.get_env()) def test_success_custom_instance(self): instance_name = "my_booth" self.config.runner.cib.load( resources=self.fixture_cib_booth_group(instance_name) ) + self.config.runner.pcmk.resource_restart( + f"booth-{instance_name}-service" + ) commands.restart( self.env_assist.get_env(), - self.resource_restart, instance_name=instance_name, ) - self.resource_restart.assert_has_calls( - [ - mock.call([f"booth-{instance_name}-service"]), - ] - ) def test_not_live(self): self.config.env.set_booth( @@ -2270,9 +2258,7 @@ def test_not_live(self): ) self.config.env.set_cib_data("") self.env_assist.assert_raise_library_error( - lambda: commands.restart( - self.env_assist.get_env(), self.resource_restart - ), + lambda: commands.restart(self.env_assist.get_env()), [ fixture.error( reports.codes.LIVE_ENVIRONMENT_REQUIRED, @@ -2285,14 +2271,11 @@ def test_not_live(self): ], expected_in_processor=False, ) - self.resource_restart.assert_not_called() def test_booth_resource_does_not_exist(self): - (self.config.runner.cib.load()) + self.config.runner.cib.load() self.env_assist.assert_raise_library_error( - lambda: commands.restart( - self.env_assist.get_env(), self.resource_restart - ), + lambda: commands.restart(self.env_assist.get_env()), ) self.env_assist.assert_reports( [ @@ -2302,14 +2285,11 @@ def test_booth_resource_does_not_exist(self): ), ] ) - self.resource_restart.assert_not_called() def test_more_booth_resources(self): self.config.runner.cib.load(resources=self.fixture_cib_more_resources()) self.env_assist.assert_raise_library_error( - lambda: commands.restart( - self.env_assist.get_env(), self.resource_restart - ), + lambda: commands.restart(self.env_assist.get_env()), ) self.env_assist.assert_reports( [ @@ -2320,13 +2300,17 @@ def test_more_booth_resources(self): ), ] ) - self.resource_restart.assert_not_called() def test_more_booth_resources_forced(self): self.config.runner.cib.load(resources=self.fixture_cib_more_resources()) + self.config.runner.pcmk.resource_restart( + "booth1", name="runner.pcmk.restart.1" + ) + self.config.runner.pcmk.resource_restart( + "booth2", name="runner.pcmk.restart.2" + ) commands.restart( self.env_assist.get_env(), - self.resource_restart, allow_multiple=True, ) self.env_assist.assert_reports( @@ -2337,12 +2321,6 @@ def test_more_booth_resources_forced(self): ), ] ) - self.resource_restart.assert_has_calls( - [ - mock.call(["booth1"]), - mock.call(["booth2"]), - ] - ) class TicketGrantRevokeMixin(FixtureMixin): From 895190172aa63e24d4db8b7985bb9efe9bfe504c Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Tue, 2 Jul 2024 16:17:36 +0200 Subject: [PATCH 017/227] add resource restart to APIv2 --- CHANGELOG.md | 3 ++- pcs/daemon/async_tasks/worker/command_mapping.py | 4 ++++ pcsd/capabilities.xml.in | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dc92a13e..80db001c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,9 @@ ## [Unreleased] ### Added -- Support for output formats `json` and `cmd` to `pcs tag config` command +- Support for output formats `json` and `cmd` to `pcs tag config` command ([RHEL-46284]) +- Command `resource.restart` in API v2 [RHEL-46284]: https://issues.redhat.com/browse/RHEL-46284 diff --git a/pcs/daemon/async_tasks/worker/command_mapping.py b/pcs/daemon/async_tasks/worker/command_mapping.py index 6dc99164f..5366f9c5d 100644 --- a/pcs/daemon/async_tasks/worker/command_mapping.py +++ b/pcs/daemon/async_tasks/worker/command_mapping.py @@ -334,6 +334,10 @@ class _Cmd: cmd=resource.move_autoclean, required_permission=p.WRITE, ), + "resource.restart": _Cmd( + cmd=resource.restart, + required_permission=p.WRITE, + ), "resource.unmanage": _Cmd( cmd=resource.unmanage, required_permission=p.WRITE, diff --git a/pcsd/capabilities.xml.in b/pcsd/capabilities.xml.in index 5586aa643..b194ae37d 100644 --- a/pcsd/capabilities.xml.in +++ b/pcsd/capabilities.xml.in @@ -2003,12 +2003,13 @@ pcs commands: resource relocate ( dry-run | run | show | clear ) - + Restart a resource, allow specifying a node for multi-node resources (clones, bundles), allow waiting for the resource to restart. pcs commands: resource restart + API v2: resource.restart From 2870cb1da12e4c0b602ce2727195fa8da46b334f Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Tue, 21 May 2024 14:50:01 +0200 Subject: [PATCH 018/227] fix xpath expression in tests --- pcs_test/tier0/lib/pacemaker/test_live.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pcs_test/tier0/lib/pacemaker/test_live.py b/pcs_test/tier0/lib/pacemaker/test_live.py index 5075e6447..bfd8b0969 100644 --- a/pcs_test/tier0/lib/pacemaker/test_live.py +++ b/pcs_test/tier0/lib/pacemaker/test_live.py @@ -70,7 +70,7 @@ class GetClusterStatusMixin(TestCase): def setUp(self): self.env_assist, self.config = get_env_tools(test_case=self) self._xml_summary = etree_to_str( - etree.parse(rc("crm_mon.minimal.xml")).find("/summary") + etree.parse(rc("crm_mon.minimal.xml")).find("summary") ) def fixture_xml(self, transformed=False): @@ -1354,8 +1354,8 @@ def assert_output(self, real_output): @staticmethod def fixture_status_xml(nodes, resources): doc = etree.parse(rc("crm_mon.minimal.xml")) - doc.find("/summary/nodes_configured").set("number", str(nodes)) - doc.find("/summary/resources_configured").set("number", str(resources)) + doc.find("summary/nodes_configured").set("number", str(nodes)) + doc.find("summary/resources_configured").set("number", str(resources)) return etree_to_str(doc) def test_basic(self): From bf9652e6fdb5bf35392d553ac322fcc3781e08ac Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Mon, 5 Aug 2024 16:51:08 +0200 Subject: [PATCH 019/227] tests: overhaul resource agents mocking --- pcs_test/Makefile.am | 22 +- pcs_test/resources/cib-all.xml | 22 +- pcs_test/resources/cib-large.xml | 504 ++--- pcs_test/resources/cib-tags.xml | 22 +- .../resource_agent_ocf_heartbeat_ipaddr2.xml | 329 +++- .../resource_agent_ocf_pacemaker_dummy.xml | 79 +- .../remote_node/test_node_add_remote.py | 3 + .../resource/test_get_configured_resources.py | 8 +- pcs_test/tier1/cib_resource/test_bundle.py | 10 +- .../tier1/cib_resource/test_clone_unclone.py | 52 +- pcs_test/tier1/cib_resource/test_create.py | 507 ++--- .../tier1/cib_resource/test_enable_disable.py | 18 +- .../tier1/cib_resource/test_group_ungroup.py | 6 +- .../cib_resource/test_manage_unmanage.py | 28 +- .../tier1/cib_resource/test_operation_add.py | 16 +- .../tier1/cib_resource/test_stonith_create.py | 12 +- .../test_stonith_enable_disable.py | 8 +- pcs_test/tier1/legacy/test_constraints.py | 417 ++--- pcs_test/tier1/legacy/test_resource.py | 1629 ++++++++--------- pcs_test/tier1/legacy/test_rule.py | 4 +- pcs_test/tier1/legacy/test_stonith.py | 23 +- pcs_test/tier1/resource/test_config.py | 3 + pcs_test/tier1/test_cluster_pcmk_remote.py | 31 +- pcs_test/tier1/test_status.py | 107 +- pcs_test/tier1/test_tag.py | 18 +- pcs_test/tools/bin_mock/__init__.py | 2 + .../crm_resource.d/list_agents_ocf__heartbeat | 55 +- .../crm_resource.d/list_agents_ocf__pacemaker | 14 +- .../crm_resource.d/list_agents_ocf__pcsmock | 7 + .../pcmk/crm_resource.d/list_ocf_providers | 3 +- ...metadata.xml => lsb__pcsmock_metadata.xml} | 14 +- .../ocf__heartbeat__Dummy_metadata.xml | 50 - .../ocf__heartbeat__Filesystem_metadata.xml | 145 -- .../ocf__heartbeat__IPaddr2_metadata.xml | 286 --- .../ocf__heartbeat__pcsMock_metadata.xml | 1 + .../ocf__pacemaker__Dummy_metadata.xml | 78 - .../ocf__pacemaker__HealthCPU_metadata.xml | 59 - .../ocf__pacemaker__Stateful_metadata.xml | 50 - .../ocf__pacemaker__SystemHealth_metadata.xml | 24 - .../ocf__pacemaker__pcsMock_metadata.xml | 23 + .../ocf__pacemaker__remote_metadata.xml | 66 +- .../ocf__pcsmock__CamelCase_metadata.xml | 21 + .../ocf__pcsmock__action_method_metadata.xml | 41 + ...f__pcsmock__duplicate_monitor_metadata.xml | 23 + .../ocf__pcsmock__minimal_metadata.xml | 21 + .../ocf__pcsmock__params_metadata.xml | 70 + .../ocf__pcsmock__stateful_metadata.xml | 22 + .../ocf__pcsmock__unique_metadata.xml | 31 + .../systemd__pcsmock@a__b_metadata.xml | 1 + .../systemd__pcsmock_metadata.xml | 17 + .../systemd__test@a__b_metadata.xml | 21 - .../tools/bin_mock/pcmk/crm_resource_mock.py | 29 +- pcs_test/tools/fixture_cib.py | 4 +- pcs_test/tools/resources_dto.py | 29 +- typos_known | 1 - 55 files changed, 2400 insertions(+), 2686 deletions(-) mode change 120000 => 100644 pcs_test/resources/resource_agent_ocf_heartbeat_ipaddr2.xml mode change 120000 => 100644 pcs_test/resources/resource_agent_ocf_pacemaker_dummy.xml create mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/list_agents_ocf__pcsmock rename pcs_test/tools/bin_mock/pcmk/crm_resource.d/{lsb__network_metadata.xml => lsb__pcsmock_metadata.xml} (73%) delete mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__heartbeat__Dummy_metadata.xml delete mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__heartbeat__Filesystem_metadata.xml delete mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__heartbeat__IPaddr2_metadata.xml create mode 120000 pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__heartbeat__pcsMock_metadata.xml delete mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__Dummy_metadata.xml delete mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__HealthCPU_metadata.xml delete mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__Stateful_metadata.xml delete mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__SystemHealth_metadata.xml create mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__pcsMock_metadata.xml create mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__CamelCase_metadata.xml create mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__action_method_metadata.xml create mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__duplicate_monitor_metadata.xml create mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__minimal_metadata.xml create mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__params_metadata.xml create mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__stateful_metadata.xml create mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__unique_metadata.xml create mode 120000 pcs_test/tools/bin_mock/pcmk/crm_resource.d/systemd__pcsmock@a__b_metadata.xml create mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/systemd__pcsmock_metadata.xml delete mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/systemd__test@a__b_metadata.xml diff --git a/pcs_test/Makefile.am b/pcs_test/Makefile.am index 414b0ceb9..b062feda4 100644 --- a/pcs_test/Makefile.am +++ b/pcs_test/Makefile.am @@ -404,23 +404,27 @@ EXTRA_DIST = \ tools/bin_mock/__init__.py \ tools/bin_mock/pcmk/crm_resource.d/list_agents_ocf__heartbeat \ tools/bin_mock/pcmk/crm_resource.d/list_agents_ocf__pacemaker \ + tools/bin_mock/pcmk/crm_resource.d/list_agents_ocf__pcsmock \ tools/bin_mock/pcmk/crm_resource.d/list_ocf_providers \ tools/bin_mock/pcmk/crm_resource.d/list_standards \ - tools/bin_mock/pcmk/crm_resource.d/lsb__network_metadata.xml \ - tools/bin_mock/pcmk/crm_resource.d/ocf__heartbeat__Dummy_metadata.xml \ - tools/bin_mock/pcmk/crm_resource.d/ocf__heartbeat__Filesystem_metadata.xml \ - tools/bin_mock/pcmk/crm_resource.d/ocf__heartbeat__IPaddr2_metadata.xml \ - tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__Dummy_metadata.xml \ - tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__HealthCPU_metadata.xml \ + tools/bin_mock/pcmk/crm_resource.d/lsb__pcsmock_metadata.xml \ + tools/bin_mock/pcmk/crm_resource.d/ocf__heartbeat__pcsMock_metadata.xml \ + tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__pcsMock_metadata.xml \ tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__remote_metadata.xml \ - tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__Stateful_metadata.xml \ - tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__SystemHealth_metadata.xml \ + tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__action_method_metadata.xml \ + tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__CamelCase_metadata.xml \ + tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__duplicate_monitor_metadata.xml \ + tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__minimal_metadata.xml \ + tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__params_metadata.xml \ + tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__stateful_metadata.xml \ + tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__unique_metadata.xml \ tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_action_metadata.xml \ tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_method_metadata.xml \ tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_minimal_metadata.xml \ tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_params_metadata.xml \ tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_unfencing_metadata.xml \ - tools/bin_mock/pcmk/crm_resource.d/systemd__test@a__b_metadata.xml \ + tools/bin_mock/pcmk/crm_resource.d/systemd__pcsmock@a__b_metadata.xml \ + tools/bin_mock/pcmk/crm_resource.d/systemd__pcsmock_metadata.xml \ tools/bin_mock/pcmk/crm_resource_mock.py \ tools/bin_mock/pcmk/pacemaker_metadata.d/pacemaker_based.xml \ tools/bin_mock/pcmk/pacemaker_metadata.d/pacemaker_controld.xml \ diff --git a/pcs_test/resources/cib-all.xml b/pcs_test/resources/cib-all.xml index b5298a021..8e5ef68e1 100644 --- a/pcs_test/resources/cib-all.xml +++ b/pcs_test/resources/cib-all.xml @@ -27,13 +27,13 @@ - + - + @@ -61,20 +61,22 @@ - + - + - + - + + + @@ -87,17 +89,17 @@ - + - + - + @@ -108,7 +110,7 @@ - + diff --git a/pcs_test/resources/cib-large.xml b/pcs_test/resources/cib-large.xml index 88d7d6499..545e7377d 100644 --- a/pcs_test/resources/cib-large.xml +++ b/pcs_test/resources/cib-large.xml @@ -7,7 +7,7 @@ - + @@ -18,7 +18,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -40,7 +40,7 @@ - + @@ -51,7 +51,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -73,7 +73,7 @@ - + @@ -84,7 +84,7 @@ - + @@ -95,7 +95,7 @@ - + @@ -106,7 +106,7 @@ - + @@ -117,7 +117,7 @@ - + @@ -128,7 +128,7 @@ - + @@ -139,7 +139,7 @@ - + @@ -150,7 +150,7 @@ - + @@ -161,7 +161,7 @@ - + @@ -172,7 +172,7 @@ - + @@ -183,7 +183,7 @@ - + @@ -194,7 +194,7 @@ - + @@ -205,7 +205,7 @@ - + @@ -216,7 +216,7 @@ - + @@ -227,7 +227,7 @@ - + @@ -238,7 +238,7 @@ - + @@ -249,7 +249,7 @@ - + @@ -260,7 +260,7 @@ - + @@ -271,7 +271,7 @@ - + @@ -282,7 +282,7 @@ - + @@ -293,7 +293,7 @@ - + @@ -304,7 +304,7 @@ - + @@ -315,7 +315,7 @@ - + @@ -326,7 +326,7 @@ - + @@ -337,7 +337,7 @@ - + @@ -348,7 +348,7 @@ - + @@ -359,7 +359,7 @@ - + @@ -370,7 +370,7 @@ - + @@ -381,7 +381,7 @@ - + @@ -392,7 +392,7 @@ - + @@ -403,7 +403,7 @@ - + @@ -414,7 +414,7 @@ - + @@ -425,7 +425,7 @@ - + @@ -436,7 +436,7 @@ - + @@ -447,7 +447,7 @@ - + @@ -458,7 +458,7 @@ - + @@ -469,7 +469,7 @@ - + @@ -480,7 +480,7 @@ - + @@ -491,7 +491,7 @@ - + @@ -502,7 +502,7 @@ - + @@ -513,7 +513,7 @@ - + @@ -524,7 +524,7 @@ - + @@ -535,7 +535,7 @@ - + @@ -546,7 +546,7 @@ - + @@ -557,7 +557,7 @@ - + @@ -568,7 +568,7 @@ - + @@ -579,7 +579,7 @@ - + @@ -590,7 +590,7 @@ - + @@ -601,7 +601,7 @@ - + @@ -612,7 +612,7 @@ - + @@ -623,7 +623,7 @@ - + @@ -634,7 +634,7 @@ - + @@ -645,7 +645,7 @@ - + @@ -656,7 +656,7 @@ - + @@ -667,7 +667,7 @@ - + @@ -678,7 +678,7 @@ - + @@ -689,7 +689,7 @@ - + @@ -700,7 +700,7 @@ - + @@ -711,7 +711,7 @@ - + @@ -722,7 +722,7 @@ - + @@ -733,7 +733,7 @@ - + @@ -744,7 +744,7 @@ - + @@ -755,7 +755,7 @@ - + @@ -766,7 +766,7 @@ - + @@ -777,7 +777,7 @@ - + @@ -788,7 +788,7 @@ - + @@ -799,7 +799,7 @@ - + @@ -810,7 +810,7 @@ - + @@ -821,7 +821,7 @@ - + @@ -832,7 +832,7 @@ - + @@ -843,7 +843,7 @@ - + @@ -854,7 +854,7 @@ - + @@ -865,7 +865,7 @@ - + @@ -876,7 +876,7 @@ - + @@ -887,7 +887,7 @@ - + @@ -898,7 +898,7 @@ - + @@ -909,7 +909,7 @@ - + @@ -920,7 +920,7 @@ - + @@ -931,7 +931,7 @@ - + @@ -942,7 +942,7 @@ - + @@ -953,7 +953,7 @@ - + @@ -964,7 +964,7 @@ - + @@ -975,7 +975,7 @@ - + @@ -986,7 +986,7 @@ - + @@ -997,7 +997,7 @@ - + @@ -1008,7 +1008,7 @@ - + @@ -1019,7 +1019,7 @@ - + @@ -1030,7 +1030,7 @@ - + @@ -1041,7 +1041,7 @@ - + @@ -1052,7 +1052,7 @@ - + @@ -1063,7 +1063,7 @@ - + @@ -1074,7 +1074,7 @@ - + @@ -1085,7 +1085,7 @@ - + @@ -1096,7 +1096,7 @@ - + @@ -1107,7 +1107,7 @@ - + @@ -1118,7 +1118,7 @@ - + @@ -1129,7 +1129,7 @@ - + @@ -1140,7 +1140,7 @@ - + @@ -1151,7 +1151,7 @@ - + @@ -1162,7 +1162,7 @@ - + @@ -1173,7 +1173,7 @@ - + @@ -1184,7 +1184,7 @@ - + @@ -1195,7 +1195,7 @@ - + @@ -1206,7 +1206,7 @@ - + @@ -1217,7 +1217,7 @@ - + @@ -1228,7 +1228,7 @@ - + @@ -1239,7 +1239,7 @@ - + @@ -1250,7 +1250,7 @@ - + @@ -1261,7 +1261,7 @@ - + @@ -1272,7 +1272,7 @@ - + @@ -1283,7 +1283,7 @@ - + @@ -1294,7 +1294,7 @@ - + @@ -1305,7 +1305,7 @@ - + @@ -1316,7 +1316,7 @@ - + @@ -1327,7 +1327,7 @@ - + @@ -1338,7 +1338,7 @@ - + @@ -1349,7 +1349,7 @@ - + @@ -1360,7 +1360,7 @@ - + @@ -1371,7 +1371,7 @@ - + @@ -1382,7 +1382,7 @@ - + @@ -1393,7 +1393,7 @@ - + @@ -1404,7 +1404,7 @@ - + @@ -1415,7 +1415,7 @@ - + @@ -1426,7 +1426,7 @@ - + @@ -1437,7 +1437,7 @@ - + @@ -1448,7 +1448,7 @@ - + @@ -1459,7 +1459,7 @@ - + @@ -1470,7 +1470,7 @@ - + @@ -1481,7 +1481,7 @@ - + @@ -1492,7 +1492,7 @@ - + @@ -1503,7 +1503,7 @@ - + @@ -1514,7 +1514,7 @@ - + @@ -1525,7 +1525,7 @@ - + @@ -1536,7 +1536,7 @@ - + @@ -1547,7 +1547,7 @@ - + @@ -1558,7 +1558,7 @@ - + @@ -1569,7 +1569,7 @@ - + @@ -1580,7 +1580,7 @@ - + @@ -1591,7 +1591,7 @@ - + @@ -1602,7 +1602,7 @@ - + @@ -1613,7 +1613,7 @@ - + @@ -1624,7 +1624,7 @@ - + @@ -1635,7 +1635,7 @@ - + @@ -1646,7 +1646,7 @@ - + @@ -1657,7 +1657,7 @@ - + @@ -1668,7 +1668,7 @@ - + @@ -1679,7 +1679,7 @@ - + @@ -1690,7 +1690,7 @@ - + @@ -1701,7 +1701,7 @@ - + @@ -1712,7 +1712,7 @@ - + @@ -1723,7 +1723,7 @@ - + @@ -1734,7 +1734,7 @@ - + @@ -1745,7 +1745,7 @@ - + @@ -1756,7 +1756,7 @@ - + @@ -1767,7 +1767,7 @@ - + @@ -1778,7 +1778,7 @@ - + @@ -1789,7 +1789,7 @@ - + @@ -1800,7 +1800,7 @@ - + @@ -1811,7 +1811,7 @@ - + @@ -1822,7 +1822,7 @@ - + @@ -1833,7 +1833,7 @@ - + @@ -1844,7 +1844,7 @@ - + @@ -1855,7 +1855,7 @@ - + @@ -1866,7 +1866,7 @@ - + @@ -1877,7 +1877,7 @@ - + @@ -1888,7 +1888,7 @@ - + @@ -1899,7 +1899,7 @@ - + @@ -1910,7 +1910,7 @@ - + @@ -1921,7 +1921,7 @@ - + @@ -1932,7 +1932,7 @@ - + @@ -1943,7 +1943,7 @@ - + @@ -1954,7 +1954,7 @@ - + @@ -1965,7 +1965,7 @@ - + @@ -1976,7 +1976,7 @@ - + @@ -1987,7 +1987,7 @@ - + @@ -1998,7 +1998,7 @@ - + @@ -2009,7 +2009,7 @@ - + @@ -2020,7 +2020,7 @@ - + @@ -2031,7 +2031,7 @@ - + @@ -2042,7 +2042,7 @@ - + @@ -2053,7 +2053,7 @@ - + @@ -2064,7 +2064,7 @@ - + @@ -2075,7 +2075,7 @@ - + @@ -2086,7 +2086,7 @@ - + @@ -2097,7 +2097,7 @@ - + @@ -2108,7 +2108,7 @@ - + @@ -2119,7 +2119,7 @@ - + @@ -2130,7 +2130,7 @@ - + @@ -2141,7 +2141,7 @@ - + @@ -2152,7 +2152,7 @@ - + @@ -2163,7 +2163,7 @@ - + @@ -2174,7 +2174,7 @@ - + @@ -2185,7 +2185,7 @@ - + @@ -2196,7 +2196,7 @@ - + @@ -2207,7 +2207,7 @@ - + @@ -2218,7 +2218,7 @@ - + @@ -2229,7 +2229,7 @@ - + @@ -2240,7 +2240,7 @@ - + @@ -2251,7 +2251,7 @@ - + @@ -2262,7 +2262,7 @@ - + @@ -2273,7 +2273,7 @@ - + @@ -2284,7 +2284,7 @@ - + @@ -2295,7 +2295,7 @@ - + @@ -2306,7 +2306,7 @@ - + @@ -2317,7 +2317,7 @@ - + @@ -2328,7 +2328,7 @@ - + @@ -2339,7 +2339,7 @@ - + @@ -2350,7 +2350,7 @@ - + @@ -2361,7 +2361,7 @@ - + @@ -2372,7 +2372,7 @@ - + @@ -2383,7 +2383,7 @@ - + @@ -2394,7 +2394,7 @@ - + @@ -2405,7 +2405,7 @@ - + @@ -2416,7 +2416,7 @@ - + @@ -2427,7 +2427,7 @@ - + @@ -2438,7 +2438,7 @@ - + @@ -2449,7 +2449,7 @@ - + @@ -2460,7 +2460,7 @@ - + @@ -2471,7 +2471,7 @@ - + @@ -2482,7 +2482,7 @@ - + @@ -2493,7 +2493,7 @@ - + @@ -2504,7 +2504,7 @@ - + @@ -2515,7 +2515,7 @@ - + @@ -2526,7 +2526,7 @@ - + @@ -2537,7 +2537,7 @@ - + @@ -2548,7 +2548,7 @@ - + @@ -2559,7 +2559,7 @@ - + @@ -2570,7 +2570,7 @@ - + @@ -2581,7 +2581,7 @@ - + @@ -2592,7 +2592,7 @@ - + @@ -2603,7 +2603,7 @@ - + @@ -2614,7 +2614,7 @@ - + @@ -2625,7 +2625,7 @@ - + @@ -2636,7 +2636,7 @@ - + @@ -2647,7 +2647,7 @@ - + @@ -2658,7 +2658,7 @@ - + @@ -2669,7 +2669,7 @@ - + @@ -2680,7 +2680,7 @@ - + @@ -2691,7 +2691,7 @@ - + @@ -2702,7 +2702,7 @@ - + @@ -2713,7 +2713,7 @@ - + @@ -2724,7 +2724,7 @@ - + @@ -2735,7 +2735,7 @@ - + @@ -2746,7 +2746,7 @@ - + @@ -2757,7 +2757,7 @@ - + @@ -2768,7 +2768,7 @@ - + diff --git a/pcs_test/resources/cib-tags.xml b/pcs_test/resources/cib-tags.xml index 5b3882833..beb85551f 100644 --- a/pcs_test/resources/cib-tags.xml +++ b/pcs_test/resources/cib-tags.xml @@ -6,17 +6,17 @@ - + - + - + @@ -24,33 +24,33 @@ - + - + - + - + - + - + @@ -92,10 +92,10 @@ - + - + diff --git a/pcs_test/resources/resource_agent_ocf_heartbeat_ipaddr2.xml b/pcs_test/resources/resource_agent_ocf_heartbeat_ipaddr2.xml deleted file mode 120000 index de45337ce..000000000 --- a/pcs_test/resources/resource_agent_ocf_heartbeat_ipaddr2.xml +++ /dev/null @@ -1 +0,0 @@ -../tools/bin_mock/pcmk/crm_resource.d/ocf__heartbeat__IPaddr2_metadata.xml \ No newline at end of file diff --git a/pcs_test/resources/resource_agent_ocf_heartbeat_ipaddr2.xml b/pcs_test/resources/resource_agent_ocf_heartbeat_ipaddr2.xml new file mode 100644 index 000000000..ce29b3414 --- /dev/null +++ b/pcs_test/resources/resource_agent_ocf_heartbeat_ipaddr2.xml @@ -0,0 +1,328 @@ + + + +1.0 + + +This Linux-specific resource manages IP alias IP addresses. +It can add an IP alias, or remove one. +In addition, it can implement Cluster Alias IP functionality +if invoked as a clone resource. + +If used as a clone, "shared address with a trivial, stateless +(autonomous) load-balancing/mutual exclusion on ingress" mode gets +applied (as opposed to "assume resource uniqueness" mode otherwise). +For that, Linux firewall (kernel and userspace) is assumed, and since +recent distributions are ambivalent in plain "iptables" command to +particular back-end resolution, "iptables-legacy" (when present) gets +prioritized so as to avoid incompatibilities (note that respective +ipt_CLUSTERIP firewall extension in use here is, at the same time, +marked deprecated, yet said "legacy" layer can make it workable, +literally, to this day) with "netfilter" one (as in "iptables-nft"). +In that case, you should explicitly set clone-node-max >= 2, +and/or clone-max < number of nodes. In case of node failure, +clone instances need to be re-allocated on surviving nodes. +This would not be possible if there is already an instance +on those nodes, and clone-node-max=1 (which is the default). + +When the specified IP address gets assigned to a respective interface, the +resource agent sends unsolicited ARP (Address Resolution Protocol, IPv4) or NA +(Neighbor Advertisement, IPv6) packets to inform neighboring machines about the +change. This functionality is controlled for both IPv4 and IPv6 by shared +'arp_*' parameters. + + +Manages virtual IPv4 and IPv6 addresses (Linux specific version) + + + + +The IPv4 (dotted quad notation) or IPv6 address (colon hexadecimal notation) +example IPv4 "192.168.1.1". +example IPv6 "2001:db8:DC28:0:0:FC57:D4C8:1FFF". + +IPv4 or IPv6 address + + + + +The base network interface on which the IP address will be brought +online. +If left empty, the script will try and determine this from the +routing table. + +Do NOT specify an alias interface in the form eth0:1 or anything here; +rather, specify the base interface only. +If you want a label, see the iflabel parameter. + +Prerequisite: + +There must be at least one static IP address, which is not managed by +the cluster, assigned to the network interface. +If you can not assign any static IP address on the interface, +modify this kernel parameter: + +sysctl -w net.ipv4.conf.all.promote_secondaries=1 # (or per device) + +Network interface + + + + + +The netmask for the interface in CIDR format +(e.g., 24 and not 255.255.255.0) + +If unspecified, the script will also try to determine this from the +routing table. + +CIDR netmask + + + + + +Broadcast address associated with the IP. It is possible to use the +special symbols '+' and '-' instead of the broadcast address. In this +case, the broadcast address is derived by setting/resetting the host +bits of the interface prefix. + +Broadcast address + + + + + +You can specify an additional label for your IP address here. +This label is appended to your interface name. + +The kernel allows alphanumeric labels up to a maximum length of 15 +characters including the interface name and colon (e.g. eth0:foobar1234) + +A label can be specified in nic parameter but it is deprecated. +If a label is specified in nic name, this parameter has no effect. + +Interface label + + + + + +Table to use to lookup which interface to use for the IP. + +This can be used for policy based routing. See man ip-rule(8). + +Table + + + + + +Enable support for LVS Direct Routing configurations. In case a IP +address is stopped, only move it to the loopback device to allow the +local node to continue to service requests, but no longer advertise it +on the network. + +Notes for IPv6: +It is not necessary to enable this option on IPv6. +Instead, enable 'lvs_ipv6_addrlabel' option for LVS-DR usage on IPv6. + +Enable support for LVS DR + + + + + +Enable adding IPv6 address label so IPv6 traffic originating from +the address's interface does not use this address as the source. +This is necessary for LVS-DR health checks to realservers to work. Without it, +the most recently added IPv6 address (probably the address added by IPaddr2) +will be used as the source address for IPv6 traffic from that interface and +since that address exists on loopback on the realservers, the realserver +response to pings/connections will never leave its loopback. +See RFC3484 for the detail of the source address selection. + +See also 'lvs_ipv6_addrlabel_value' parameter. + +Enable adding IPv6 address label. + + + + + +Specify IPv6 address label value used when 'lvs_ipv6_addrlabel' is enabled. +The value should be an unused label in the policy table +which is shown by 'ip addrlabel list' command. +You would rarely need to change this parameter. + +IPv6 address label value. + + + + + +Set the interface MAC address explicitly. Currently only used in case of +the Cluster IP Alias. Leave empty to chose automatically. + + +Cluster IP MAC address + + + + + +Specify the hashing algorithm used for the Cluster IP functionality. + + +Cluster IP hashing function + + + + + +If true, add the clone ID to the supplied value of IP to create +a unique address to manage + +Create a unique address for cloned instances + + + + + +Specify the interval between unsolicited ARP (IPv4) or NA (IPv6) packets in +milliseconds. + +This parameter is deprecated and used for the backward compatibility only. +It is effective only for the send_arp binary which is built with libnet, +and send_ua for IPv6. It has no effect for other arp_sender. + +ARP/NA packet interval in ms (deprecated) + + + + + +Number of unsolicited ARP (IPv4) or NA (IPv6) packets to send at resource +initialization. + +ARP/NA packet count sent during initialization + + + + + +For IPv4, number of unsolicited ARP packets to send during resource monitoring. +Doing so helps mitigate issues of stuck ARP caches resulting from split-brain +situations. + +ARP packet count sent during monitoring + + + + + +Whether or not to send the ARP (IPv4) or NA (IPv6) packets in the background. +The default is true for IPv4 and false for IPv6. + +ARP/NA from background + + + + + +For IPv4, the program to send ARP packets with on start. Available options are: + - send_arp: default + - ipoibarping: default for infiniband interfaces if ipoibarping is available + - iputils_arping: use arping in iputils package + - libnet_arping: use another variant of arping based on libnet + +ARP sender + + + + + +For IPv4, extra options to pass to the arp_sender program. +Available options are vary depending on which arp_sender is used. + +A typical use case is specifying '-A' for iputils_arping to use +ARP REPLY instead of ARP REQUEST as Gratuitous ARPs. + +Options for ARP sender + + + + + +Flush the routing table on stop. This is for +applications which use the cluster IP address +and which run on the same physical host that the +IP address lives on. The Linux kernel may force that +application to take a shortcut to the local loopback +interface, instead of the interface the address +is really bound to. Under those circumstances, an +application may, somewhat unexpectedly, continue +to use connections for some time even after the +IP address is deconfigured. Set this parameter in +order to immediately disable said shortcut when the +IP address goes away. + +Flush kernel routing table on stop + + + + + +For IPv4, whether or not to run arping for collision detection check. + +Run arping for IPv4 collision detection check + + + + + +For IPv6, do not perform Duplicate Address Detection when adding the address. + +Use nodad flag + + + + + +Use noprefixroute flag (see 'man ip-address'). + +Use noprefixroute flag + + + + + +For IPv6, set the preferred lifetime of the IP address. +This can be used to ensure that the created IP address will not +be used as a source address for routing. +Expects a value as specified in section 5.5.4 of RFC 4862. + +IPv6 preferred lifetime + + + + + +Specifies the network namespace to operate within. +The namespace must already exist, and the interface to be used must be within +the namespace. + +Network namespace to use + + + + + + + + + + + + + diff --git a/pcs_test/resources/resource_agent_ocf_pacemaker_dummy.xml b/pcs_test/resources/resource_agent_ocf_pacemaker_dummy.xml deleted file mode 120000 index d34c46d93..000000000 --- a/pcs_test/resources/resource_agent_ocf_pacemaker_dummy.xml +++ /dev/null @@ -1 +0,0 @@ -../tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__Dummy_metadata.xml \ No newline at end of file diff --git a/pcs_test/resources/resource_agent_ocf_pacemaker_dummy.xml b/pcs_test/resources/resource_agent_ocf_pacemaker_dummy.xml new file mode 100644 index 000000000..5e95b480b --- /dev/null +++ b/pcs_test/resources/resource_agent_ocf_pacemaker_dummy.xml @@ -0,0 +1,78 @@ + + +1.1 + + +This is a dummy OCF resource agent. It does absolutely nothing except keep track +of whether it is running or not, and can be configured so that actions fail or +take a long time. Its purpose is primarily for testing, and to serve as a +template for resource agent writers. + +Example stateless resource agent + + + + +Location to store the resource state in. + +State file + + + + + +Fake password field + +Password + + + + + +Fake attribute that can be changed to cause an agent reload + +Fake attribute that can be changed to cause an agent reload + + + + + +Number of seconds to sleep during operations. This can be used to test how +the cluster reacts to operation timeouts. + +Operation sleep duration in seconds. + + + + + +Start, migrate_from, and reload-agent actions will return failure if running on +the host specified here, but the resource will run successfully anyway (future +monitor calls will find it running). This can be used to test on-fail=ignore. + +Report bogus start failure on specified host + + + + + +If this is set, the environment will be dumped to this file for every call. + +Environment dump file + + + + + + + + + + + + + + + + + diff --git a/pcs_test/tier0/lib/commands/remote_node/test_node_add_remote.py b/pcs_test/tier0/lib/commands/remote_node/test_node_add_remote.py index e5017c9b8..a5c6d54eb 100644 --- a/pcs_test/tier0/lib/commands/remote_node/test_node_add_remote.py +++ b/pcs_test/tier0/lib/commands/remote_node/test_node_add_remote.py @@ -106,6 +106,9 @@ def load_cluster_configs(self, cluster_node_list): + diff --git a/pcs_test/tier0/lib/commands/resource/test_get_configured_resources.py b/pcs_test/tier0/lib/commands/resource/test_get_configured_resources.py index bdffb8955..181af17a0 100644 --- a/pcs_test/tier0/lib/commands/resource/test_get_configured_resources.py +++ b/pcs_test/tier0/lib/commands/resource/test_get_configured_resources.py @@ -1,3 +1,4 @@ +from pprint import pformat from unittest import TestCase from pcs.common import reports @@ -86,8 +87,11 @@ def test_unsupported_bundle_container_type(self): ) def test_success(self): + self.maxDiff = None self.config.runner.cib.load(filename="cib-resources.xml") self.assertEqual( - ALL_RESOURCES, - resource.get_configured_resources(self.env_assist.get_env()), + pformat(ALL_RESOURCES), + pformat( + resource.get_configured_resources(self.env_assist.get_env()) + ), ) diff --git a/pcs_test/tier1/cib_resource/test_bundle.py b/pcs_test/tier1/cib_resource/test_bundle.py index d4f8ffd59..775f80939 100644 --- a/pcs_test/tier1/cib_resource/test_bundle.py +++ b/pcs_test/tier1/cib_resource/test_bundle.py @@ -30,7 +30,7 @@ def setUp(self): self.temp_cib = get_tmp_file("tier1_bundle_create") write_file_to_tmpfile(self.empty_cib, self.temp_cib) self.pcs_runner = PcsRunner(self.temp_cib.name) - self.pcs_runner.mock_settings = get_mock_settings("crm_resource_exec") + self.pcs_runner.mock_settings = get_mock_settings() def tearDown(self): self.temp_cib.close() @@ -808,7 +808,7 @@ def test_resource(self): ).split() ) self.assert_pcs_success( - "resource create A ocf:pacemaker:Dummy bundle B1 --no-default-ops".split() + "resource create A ocf:pcsmock:minimal bundle B1 --no-default-ops".split() ) self.assert_pcs_success( "resource config B1".split(), @@ -817,7 +817,7 @@ def test_resource(self): Bundle: B1 Docker: image=pcs:test Network: control-port=1234 - Resource: A (class=ocf provider=pacemaker type=Dummy) + Resource: A (class=ocf provider=pcsmock type=minimal) Operations: monitor: A-monitor-interval-10s interval=10s timeout=20s @@ -861,7 +861,7 @@ def test_all(self): ] ) self.assert_pcs_success( - "resource create A ocf:pacemaker:Dummy bundle B1 --no-default-ops".split() + "resource create A ocf:pcsmock:minimal bundle B1 --no-default-ops".split() ) self.assert_pcs_success( "resource config B1".split(), @@ -879,7 +879,7 @@ def test_all(self): Meta Attributes: B1-meta_attributes is-managed=false target-role=Stopped - Resource: A (class=ocf provider=pacemaker type=Dummy) + Resource: A (class=ocf provider=pcsmock type=minimal) Operations: monitor: A-monitor-interval-10s interval=10s timeout=20s diff --git a/pcs_test/tier1/cib_resource/test_clone_unclone.py b/pcs_test/tier1/cib_resource/test_clone_unclone.py index 3f991c234..ce22e8706 100644 --- a/pcs_test/tier1/cib_resource/test_clone_unclone.py +++ b/pcs_test/tier1/cib_resource/test_clone_unclone.py @@ -2,6 +2,7 @@ from lxml import etree +from pcs_test.tools.bin_mock import get_mock_settings from pcs_test.tools.cib import get_assert_pcs_effect_mixin from pcs_test.tools.misc import get_test_resource as rc from pcs_test.tools.misc import ( @@ -13,7 +14,7 @@ def _get_primitive_fixture( - res_id, agent_standard="ocf", agent_provider="heartbeat", agent_type="Dummy" + res_id, agent_standard="ocf", agent_provider="pcsmock", agent_type="minimal" ): _provider = "" if agent_provider: @@ -29,7 +30,7 @@ def _get_primitive_fixture( FIXTURE_DUMMY = _get_primitive_fixture("Dummy") -FIXTURE_PRIMITIVE_FOR_CLONE = _get_primitive_fixture("C") +FIXTURE_PRIMITIVE_FOR_CLONE = _get_primitive_fixture("C", agent_type="stateful") FIXTURE_CLONE = f"""{FIXTURE_PRIMITIVE_FOR_CLONE}""" @@ -70,13 +71,13 @@ def _get_primitive_fixture( FIXTURE_CLONED_GROUP = """ - + - + @@ -152,8 +153,9 @@ def fixture_clone(clone_id, primitive_id, promotable=False): parts.append(f"""""") parts.append( f""" - + @@ -224,6 +226,7 @@ def assert_constraint_xml(self, expected_xml): def setUp(self): self.temp_cib = get_tmp_file("tier1_cib_resource_group_ungroup") self.pcs_runner = PcsRunner(self.temp_cib.name) + self.pcs_runner.mock_settings = get_mock_settings() xml_manip = XmlManipulation.from_file(self.empty_cib) xml_manip.append_to_first_tag_name( "resources", @@ -293,6 +296,7 @@ class Clone( def setUp(self): self.temp_cib = get_tmp_file("tier1_cib_resource_clone_unclone_clone") self.pcs_runner = PcsRunner(self.temp_cib.name) + self.pcs_runner.mock_settings = get_mock_settings() self.set_cib_file(FIXTURE_PRIMITIVE_FOR_CLONE) self.stonith_deprecation_warning = ( "Deprecation Warning: Ability of this command to accept stonith " @@ -373,14 +377,14 @@ def test_clone_globally_unique_not_ocf_agent(self): "C", agent_standard="systemd", agent_provider=None, - agent_type="pacemaker", + agent_type="pcsmock", ) ) self.assert_pcs_fail( "resource clone C meta globally-unique=true".split(), ( "Error: Clone option 'globally-unique' is not compatible with " - "'systemd:pacemaker' resource agent of resource 'C'\n" + "'systemd:pcsmock' resource agent of resource 'C'\n" ), ) @@ -393,17 +397,14 @@ def test_clone_promotable_group_some_unsupported(self): "A", agent_standard="systemd", agent_provider=None, - agent_type="pacemaker", + agent_type="pcsmock", ), _get_primitive_fixture( "B", - agent_provider="pacemaker", - agent_type="Stateful", - ), - _get_primitive_fixture( - "C", - agent_provider="pacemaker", + agent_provider="pcsmock", + agent_type="stateful", ), + _get_primitive_fixture("C"), ] ) + "" @@ -412,9 +413,10 @@ def test_clone_promotable_group_some_unsupported(self): "resource clone G meta promotable=true".split(), ( "Error: Clone option 'promotable' is not compatible with " - "'systemd:pacemaker' resource agent of resource 'A' in group " - "'G'\nError: Clone option 'promotable' is not compatible with " - "'ocf:pacemaker:Dummy' resource agent of resource 'C' in group " + "'systemd:pcsmock' resource agent of resource 'A' in group " + "'G'\n" + "Error: Clone option 'promotable' is not compatible with " + "'ocf:pcsmock:minimal' resource agent of resource 'C' in group " "'G', use --force to override\n" ), ) @@ -425,14 +427,14 @@ def test_clone_promotable_not_ocf_agent(self): "C", agent_standard="systemd", agent_provider=None, - agent_type="pacemaker", + agent_type="pcsmock", ) ) self.assert_pcs_fail( "resource clone C meta promotable=true".split(), ( "Error: Clone option 'promotable' is not compatible with " - "'systemd:pacemaker' resource agent of resource 'C'\n" + "'systemd:pcsmock' resource agent of resource 'C'\n" ), ) @@ -450,26 +452,24 @@ def test_promotable_clone_not_ocf_agent(self): "C", agent_standard="systemd", agent_provider=None, - agent_type="pacemaker", + agent_type="pcsmock", ) ) self.assert_pcs_fail( "resource promotable C".split(), ( "Error: Clone option 'promotable' is not compatible with " - "'systemd:pacemaker' resource agent of resource 'C'\n" + "'systemd:pcsmock' resource agent of resource 'C'\n" ), ) def test_promotable_clone_unsupported_agent(self): - self.set_cib_file( - _get_primitive_fixture("C", agent_provider="pacemaker") - ) + self.set_cib_file(_get_primitive_fixture("C")) self.assert_pcs_fail( "resource promotable C".split(), ( "Error: Clone option 'promotable' is not compatible with " - "'ocf:pacemaker:Dummy' resource agent of resource 'C', use " + "'ocf:pcsmock:minimal' resource agent of resource 'C', use " "--force to override\n" ), ) diff --git a/pcs_test/tier1/cib_resource/test_create.py b/pcs_test/tier1/cib_resource/test_create.py index 3405c881d..93b8dbe45 100644 --- a/pcs_test/tier1/cib_resource/test_create.py +++ b/pcs_test/tier1/cib_resource/test_create.py @@ -1,4 +1,3 @@ -import re from unittest import ( TestCase, mock, @@ -31,13 +30,13 @@ class Success(ResourceTest): def setUp(self): super().setUp() - self.pcs_runner.mock_settings = get_mock_settings("crm_resource_exec") + self.pcs_runner.mock_settings = get_mock_settings() def test_base_create(self): self.assert_effect( - "resource create R ocf:heartbeat:Dummy --no-default-ops".split(), + "resource create R ocf:pcsmock:minimal --no-default-ops".split(), """ - + - + - @@ -65,9 +64,9 @@ def test_base_create_with_agent_name_including_systemd_instance(self): def test_base_create_with_default_ops(self): self.assert_effect( - "resource create R ocf:heartbeat:Dummy".split(), + "resource create R ocf:pcsmock:minimal".split(), """ - + + @@ -95,19 +97,17 @@ def test_base_create_with_default_ops(self): def test_create_with_options(self): self.assert_effect( ( - "resource create --no-default-ops R ocf:heartbeat:IPaddr2 " - "ip=192.168.0.99 cidr_netmask=32" + "resource create --no-default-ops R ocf:pcsmock:params " + "mandatory=mandat optional=opti" ).split(), """ - + - - @@ -125,12 +125,12 @@ def test_create_with_trace_options(self): # checks it is possible to set them without --force. self.assert_effect( ( - "resource create --no-default-ops R ocf:heartbeat:Dummy " + "resource create --no-default-ops R ocf:heartbeat:pcsMock " "trace_ra=1 trace_file=/root/trace" ).split(), """ - + - - @@ -179,11 +177,11 @@ def test_create_with_options_and_operations(self): def test_create_disabled(self): self.assert_effect( ( - "resource create R ocf:heartbeat:Dummy --no-default-ops " + "resource create R ocf:pcsmock:minimal --no-default-ops " "--disabled" ).split(), """ - + - - - - + - - @@ -300,16 +296,16 @@ def test_create_with_options_and_meta(self): class SuccessOperations(ResourceTest): def setUp(self): super().setUp() - self.pcs_runner.mock_settings = get_mock_settings("crm_resource_exec") + self.pcs_runner.mock_settings = get_mock_settings() def test_create_with_operations(self): self.assert_effect( ( - "resource create --no-default-ops R ocf:heartbeat:Dummy " + "resource create --no-default-ops R ocf:pcsmock:minimal " "op monitor interval=30s" ).split(), """ - + - + - + - + - + - + + @@ -436,9 +435,9 @@ def test_default_ops_only(self): def test_merging_default_ops_explicitly_specified(self): self.assert_effect( - "resource create R ocf:heartbeat:Dummy op start timeout=200".split(), + "resource create R ocf:pcsmock:minimal op start timeout=200".split(), """ - + + @@ -465,9 +467,9 @@ def test_merging_default_ops_explicitly_specified(self): def test_completing_monitor_operation(self): self.assert_effect( - "resource create --no-default-ops R ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops R ocf:pcsmock:minimal".split(), """ - + - + @@ -494,6 +499,12 @@ def test_adapt_second_op_interval(self): + + @@ -512,11 +523,11 @@ def test_adapt_second_op_interval(self): def test_warn_on_forced_unknown_operation(self): self.assert_effect( ( - "resource create --no-default-ops R ocf:heartbeat:Dummy " + "resource create --no-default-ops R ocf:pcsmock:minimal " "op monitro interval=30s --force" ).split(), """ - + - + @@ -551,11 +562,15 @@ def test_op_id(self): class SuccessNewParser(ResourceTest): + def setUp(self): + super().setUp() + self.pcs_runner.mock_settings = get_mock_settings() + def test_primitive_meta(self): self.assert_effect( - "resource create R ocf:pacemaker:Dummy meta a=b --no-default-ops --future".split(), + "resource create R ocf:pcsmock:minimal meta a=b --no-default-ops --future".split(), """ - + @@ -570,10 +585,10 @@ def test_primitive_meta(self): def test_clone_meta(self): self.assert_effect( - "resource create R ocf:pacemaker:Dummy clone meta a=b --no-default-ops --future".split(), + "resource create R ocf:pcsmock:minimal clone meta a=b --no-default-ops --future".split(), """ - + - + @@ -631,16 +646,20 @@ class SuccessGroup(ResourceTest): "to the future behavior.\n" ) + def setUp(self): + super().setUp() + self.pcs_runner.mock_settings = get_mock_settings() + def test_with_group(self): self.assert_effect( ( - "resource create R ocf:heartbeat:Dummy --no-default-ops " + "resource create R ocf:pcsmock:minimal --no-default-ops " f"{self.GROUP} G {self.FUTURE}" ).split(), """ - - - - - - - - - - - - @@ -850,8 +873,8 @@ def test_clone_places_disabled_correctly(self): name="target-role" value="Stopped" /> - + @@ -893,6 +919,7 @@ def setUp(self): self.lib.resource = self.resource # used for tests where code does not even call lib, so cib is not needed self.pcs_runner = PcsRunner(cib_file=None) + self.pcs_runner.mock_settings = get_mock_settings() @staticmethod def fixture_options( @@ -910,15 +937,14 @@ def fixture_options( @mock.patch("pcs.cli.reports.output.print_to_stderr") def test_alias_for_clone(self, mock_print_to_stderr): - del mock_print_to_stderr resource.resource_create( self.lib, - ["R", "ocf:pacemaker:Stateful", "promotable", "a=b", "c=d"], + ["R", "ocf:pcsmock:stateful", "promotable", "a=b", "c=d"], InputModifiers({}), ) self.resource.create_as_clone.assert_called_once_with( "R", - "ocf:pacemaker:Stateful", + "ocf:pcsmock:stateful", [], {}, {}, @@ -927,11 +953,17 @@ def test_alias_for_clone(self, mock_print_to_stderr): allow_incompatible_clone_meta_attributes=False, **self.fixture_options(), ) + mock_print_to_stderr.assert_called_once_with( + "Deprecation Warning: Configuring promotable meta attributes " + "without specifying the 'meta' keyword after the 'promotable' " + "keyword is deprecated and will be removed in a future release. " + "Specify --future to switch to the future behavior." + ) def test_fail_on_promotable(self): self.assert_pcs_fail( ( - "resource create R ocf:pacemaker:Stateful promotable " + "resource create R ocf:pcsmock:stateful promotable " "promotable=a" ).split(), ( @@ -944,7 +976,7 @@ def test_fail_on_promotable(self): def test_fail_on_promotable_true(self): self.assert_pcs_fail( ( - "resource create R ocf:pacemaker:Stateful promotable " + "resource create R ocf:pcsmock:stateful promotable " "promotable=true" ).split(), ( @@ -957,7 +989,7 @@ def test_fail_on_promotable_true(self): def test_fail_on_promotable_false(self): self.assert_pcs_fail( ( - "resource create R ocf:pacemaker:Stateful promotable " + "resource create R ocf:pcsmock:stateful promotable " "promotable=false" ).split(), ( @@ -969,6 +1001,10 @@ def test_fail_on_promotable_false(self): class Bundle(ResourceTest): + def setUp(self): + super().setUp() + self.pcs_runner.mock_settings = get_mock_settings() + def fixture_primitive(self, name, bundle=None): if bundle: self.assert_pcs_success( @@ -976,14 +1012,14 @@ def fixture_primitive(self, name, bundle=None): "resource", "create", name, - "ocf:heartbeat:Dummy", + "ocf:pcsmock:minimal", "bundle", bundle, ] ) else: self.assert_pcs_success( - ["resource", "create", name, "ocf:heartbeat:Dummy"] + ["resource", "create", name, "ocf:pcsmock:minimal"] ) def fixture_bundle(self, name): @@ -1003,20 +1039,20 @@ def fixture_bundle(self, name): def test_bundle_id_not_specified(self): self.assert_pcs_fail( - "resource create R ocf:heartbeat:Dummy --no-default-ops bundle".split(), + "resource create R ocf:pcsmock:minimal --no-default-ops bundle".split(), "Error: you have to specify exactly one bundle\n", ) def test_bundle_id_is_not_bundle(self): self.fixture_primitive("R1") self.assert_pcs_fail( - "resource create R2 ocf:heartbeat:Dummy bundle R1".split(), + "resource create R2 ocf:pcsmock:minimal bundle R1".split(), "Error: 'R1' is not a bundle\n", ) def test_bundle_id_does_not_exist(self): self.assert_pcs_fail( - "resource create R1 ocf:heartbeat:Dummy bundle B".split(), + "resource create R1 ocf:pcsmock:minimal bundle B".split(), "Error: bundle 'B' does not exist\n", ) @@ -1025,7 +1061,7 @@ def test_primitive_already_in_bundle(self): self.fixture_primitive("R1", bundle="B") self.assert_pcs_fail( ( - "resource create R2 ocf:heartbeat:Dummy --no-default-ops " + "resource create R2 ocf:pcsmock:minimal --no-default-ops " "bundle B" ).split(), ( @@ -1038,7 +1074,7 @@ def test_success(self): self.fixture_bundle("B") self.assert_effect( ( - "resource create R1 ocf:heartbeat:Dummy --no-default-ops " + "resource create R1 ocf:pcsmock:minimal --no-default-ops " "bundle B" ).split(), """ @@ -1046,8 +1082,8 @@ def test_success(self): - @@ -1166,7 +1192,11 @@ def test_warn_when_forcing_noexistent_agent(self): """, - stderr_regexp=output_regexp, + stderr_full=( + "Warning: Agent 'ocf:heartbeat:NoExisting' is not installed or " + "does not provide valid metadata: " + "pcs mock error message: unable to load agent metadata\n" + ), ) def test_fail_on_invalid_resource_agent_name(self): @@ -1225,57 +1255,58 @@ def test_fail_when_provider_appear_with_non_ocf_resource_agent(self): def test_print_info_about_agent_completion(self): self.assert_pcs_success( - "resource create R delay".split(), + "resource create R camelcase".split(), stderr_full=( - "Assumed agent name 'ocf:heartbeat:Delay' (deduced from 'delay')\n" + "Assumed agent name 'ocf:pcsmock:CamelCase' " + "(deduced from 'camelcase')\n" ), ) def test_fail_for_unambiguous_agent(self): self.assert_pcs_fail( - "resource create R Dummy".split(), - "Error: Multiple agents match 'Dummy', please specify full name:" - " 'ocf:heartbeat:Dummy' or 'ocf:pacemaker:Dummy'\n" + "resource create R pcsmock".split(), + "Error: Multiple agents match 'pcsmock', please specify full name:" + " 'ocf:heartbeat:pcsMock' or 'ocf:pacemaker:pcsMock'\n" + ERRORS_HAVE_OCCURRED, ) def test_for_options_not_matching_resource_agent(self): self.assert_pcs_fail( - "resource create R ocf:heartbeat:Dummy a=b c=d".split(), + "resource create R ocf:pcsmock:params a=b mandatory=x c=d".split(), "Error: invalid resource options: 'a', 'c', allowed options are: " - "'fake', 'state', 'trace_file', 'trace_ra', use --force to " - "override\n" + ERRORS_HAVE_OCCURRED, + "'advanced', 'enum', 'mandatory', 'optional', 'unique1', 'unique2'" + ", use --force to override\n" + ERRORS_HAVE_OCCURRED, ) def test_for_missing_options_of_resource_agent(self): self.assert_pcs_fail( - "resource create --no-default-ops R IPaddr2".split(), + "resource create --no-default-ops R params".split(), ( - "Assumed agent name 'ocf:heartbeat:IPaddr2' (deduced from" - " 'IPaddr2')\n" - "Error: required resource option 'ip' is missing," + "Assumed agent name 'ocf:pcsmock:params' (deduced from" + " 'params')\n" + "Error: required resource option 'mandatory' is missing," " use --force to override\n" + ERRORS_HAVE_OCCURRED ), ) def test_fail_on_invalid_resource_id(self): self.assert_pcs_fail( - "resource create #R ocf:heartbeat:Dummy".split(), + "resource create #R ocf:pcsmock:minimal".split(), "Error: invalid resource name '#R'," " '#' is not a valid first character for a resource name\n", ) def test_fail_on_existing_resource_id(self): - self.assert_pcs_success("resource create R ocf:heartbeat:Dummy".split()) + self.assert_pcs_success("resource create R ocf:pcsmock:minimal".split()) self.assert_pcs_fail( - "resource create R ocf:heartbeat:Dummy".split(), + "resource create R ocf:pcsmock:minimal".split(), "Error: 'R' already exists\n", ) def test_fail_on_invalid_operation_id(self): self.assert_pcs_fail( ( - "resource create R ocf:heartbeat:Dummy " + "resource create R ocf:pcsmock:minimal " "op monitor interval=30 id=#O" ).split(), ( @@ -1286,10 +1317,10 @@ def test_fail_on_invalid_operation_id(self): ) def test_fail_on_existing_operation_id(self): - self.assert_pcs_success("resource create R ocf:heartbeat:Dummy".split()) + self.assert_pcs_success("resource create R ocf:pcsmock:minimal".split()) self.assert_pcs_fail( ( - "resource create S ocf:heartbeat:Dummy " + "resource create S ocf:pcsmock:minimal " "op monitor interval=30 id=R" ).split(), "Error: 'R' already exists\n", @@ -1298,7 +1329,7 @@ def test_fail_on_existing_operation_id(self): def test_fail_on_duplicate_operation_id(self): self.assert_pcs_fail( ( - "resource create R ocf:heartbeat:Dummy " + "resource create R ocf:pcsmock:minimal " "op monitor interval=30 id=O op monitor interval=60 id=O" ).split(), "Error: 'O' already exists\n", @@ -1307,7 +1338,7 @@ def test_fail_on_duplicate_operation_id(self): def test_fail_on_resource_id_same_as_operation_id(self): self.assert_pcs_fail( ( - "resource create R ocf:heartbeat:Dummy " + "resource create R ocf:pcsmock:minimal " "op monitor interval=30 id=R" ).split(), "Error: 'R' already exists\n", @@ -1315,19 +1346,19 @@ def test_fail_on_resource_id_same_as_operation_id(self): def test_fail_on_unknown_operation(self): self.assert_pcs_fail( - "resource create R ocf:heartbeat:Dummy op monitro interval=100".split(), + "resource create R ocf:pcsmock:minimal op monitro interval=100".split(), ( "Error: 'monitro' is not a valid operation name value, use" " 'meta-data', 'migrate_from', 'migrate_to', 'monitor'," - " 'reload', 'start', 'stop', 'validate-all', use --force to" - " override\n" + ERRORS_HAVE_OCCURRED + " 'reload', 'reload-agent', 'start', 'stop', 'validate-all', " + "use --force to override\n" + ERRORS_HAVE_OCCURRED ), ) def test_fail_on_ambiguous_value_of_option(self): self.assert_pcs_fail( ( - "resource create R ocf:heartbeat:Dummy " + "resource create R ocf:pcsmock:minimal " "op monitor timeout=10 timeout=20" ).split(), "Error: duplicate option 'timeout' with different values '10' and" @@ -1335,47 +1366,45 @@ def test_fail_on_ambiguous_value_of_option(self): ) def test_unique_err(self): - self.pcs_runner.mock_settings = get_mock_settings("crm_resource_exec") self.assert_pcs_success( - "resource create R1 ocf:pacemaker:Dummy state=1".split() + "resource create R1 ocf:pcsmock:unique state=1".split() ) self.assert_pcs_fail( - "resource create R2 ocf:pacemaker:Dummy state=1".split(), + "resource create R2 ocf:pcsmock:unique state=1".split(), ( "Error: Value '1' of option 'state' is not unique across " - "'ocf:pacemaker:Dummy' resources. Following resources are " + "'ocf:pcsmock:unique' resources. Following resources are " "configured with the same value of the instance attribute: " "'R1', use --force to override\n" + ERRORS_HAVE_OCCURRED ), ) def test_unique_multiple_resources_warn_and_err(self): - self.pcs_runner.mock_settings = get_mock_settings("crm_resource_exec") self.assert_pcs_success( - "resource create R1 ocf:pacemaker:Dummy state=1".split() + "resource create R1 ocf:pcsmock:unique state=1".split() ) self.assert_pcs_success( - "resource create R2 ocf:pacemaker:Dummy state=1 --force".split(), + "resource create R2 ocf:pcsmock:unique state=1 --force".split(), stderr_full=( "Warning: Value '1' of option 'state' is not unique across " - "'ocf:pacemaker:Dummy' resources. Following resources are " + "'ocf:pcsmock:unique' resources. Following resources are " "configured with the same value of the instance attribute: 'R1'\n" ), ) self.assert_pcs_success( - "resource create R3 ocf:pacemaker:Dummy state=1 --force".split(), + "resource create R3 ocf:pcsmock:unique state=1 --force".split(), stderr_full=( "Warning: Value '1' of option 'state' is not unique across " - "'ocf:pacemaker:Dummy' resources. Following resources are " + "'ocf:pcsmock:unique' resources. Following resources are " "configured with the same value of the instance attribute: 'R1', " "'R2'\n" ), ) self.assert_pcs_fail( - "resource create R4 ocf:pacemaker:Dummy state=1".split(), + "resource create R4 ocf:pcsmock:unique state=1".split(), ( "Error: Value '1' of option 'state' is not unique across " - "'ocf:pacemaker:Dummy' resources. Following resources are " + "'ocf:pcsmock:unique' resources. Following resources are " "configured with the same value of the instance attribute: " "'R1', 'R2', 'R3', use --force to override\n" + ERRORS_HAVE_OCCURRED @@ -1384,10 +1413,14 @@ def test_unique_multiple_resources_warn_and_err(self): class FailOrWarnOp(ResourceTest): + def setUp(self): + super().setUp() + self.pcs_runner.mock_settings = get_mock_settings() + def test_fail_empty(self): self.assert_pcs_fail( ( - "resource create --no-default-ops R ocf:heartbeat:Dummy " + "resource create --no-default-ops R ocf:pcsmock:minimal " "op meta is-managed=false" ).split(), "Error: When using 'op' you must specify an operation name and at" @@ -1397,7 +1430,7 @@ def test_fail_empty(self): def test_fail_only_name_without_any_option(self): self.assert_pcs_fail( ( - "resource create --no-default-ops R ocf:heartbeat:Dummy " + "resource create --no-default-ops R ocf:pcsmock:minimal " "op monitor meta is-managed=false" ).split(), "Error: When using 'op' you must specify an operation name and at" @@ -1407,7 +1440,7 @@ def test_fail_only_name_without_any_option(self): def test_fail_duplicit(self): self.assert_pcs_fail( ( - "resource create --no-default-ops R ocf:heartbeat:Dummy " + "resource create --no-default-ops R ocf:pcsmock:minimal " "op monitor interval=1h monitor interval=3600sec " "monitor interval=1min monitor interval=60s" ).split(), @@ -1422,7 +1455,7 @@ def test_fail_duplicit(self): def test_fail_invalid_first_action(self): self.assert_pcs_fail( ( - "resource create --no-default-ops R ocf:heartbeat:Dummy " + "resource create --no-default-ops R ocf:pcsmock:minimal " "op mo=nitor interval=1min" ).split(), "Error: When using 'op' you must specify an operation name after" @@ -1432,7 +1465,7 @@ def test_fail_invalid_first_action(self): def test_fail_invalid_option(self): self.assert_pcs_fail( ( - "resource create --no-default-ops R ocf:heartbeat:Dummy " + "resource create --no-default-ops R ocf:pcsmock:minimal " "op monitor interval=1min moni=tor timeout=80s" ).split(), "Error: invalid resource operation option 'moni', allowed options" @@ -1445,7 +1478,7 @@ def test_fail_invalid_option(self): def test_fail_on_invalid_role(self): self.assert_pcs_fail( ( - "resource create --no-default-ops R ocf:heartbeat:Dummy " + "resource create --no-default-ops R ocf:pcsmock:minimal " "op monitor role=abc" ).split(), ( @@ -1459,7 +1492,7 @@ def test_fail_on_invalid_role(self): def test_force_invalid_role(self): self.assert_pcs_fail( ( - "resource create --no-default-ops R ocf:heartbeat:Dummy " + "resource create --no-default-ops R ocf:pcsmock:minimal " "op monitor role=abc --force" ).split(), ( @@ -1473,7 +1506,7 @@ def test_force_invalid_role(self): def test_fail_on_invalid_on_fail(self): self.assert_pcs_fail_regardless_of_force( ( - "resource create --no-default-ops R ocf:heartbeat:Dummy " + "resource create --no-default-ops R ocf:pcsmock:minimal " "op monitor on-fail=Abc" ).split(), ( @@ -1486,7 +1519,7 @@ def test_fail_on_invalid_on_fail(self): def test_fail_on_invalid_record_pending(self): self.assert_pcs_fail_regardless_of_force( ( - "resource create --no-default-ops R ocf:heartbeat:Dummy " + "resource create --no-default-ops R ocf:pcsmock:minimal " "op monitor record-pending=Abc" ).split(), ( @@ -1499,7 +1532,7 @@ def test_fail_on_invalid_record_pending(self): def test_fail_on_invalid_enabled(self): self.assert_pcs_fail_regardless_of_force( ( - "resource create --no-default-ops R ocf:heartbeat:Dummy " + "resource create --no-default-ops R ocf:pcsmock:minimal " "op monitor enabled=Abc" ).split(), ( @@ -1512,7 +1545,7 @@ def test_fail_on_invalid_enabled(self): def test_fail_on_combination_of_start_delay_and_interval_origin(self): self.assert_pcs_fail_regardless_of_force( ( - "resource create --no-default-ops R ocf:heartbeat:Dummy " + "resource create --no-default-ops R ocf:pcsmock:minimal " "op monitor start-delay=10 interval-origin=20" ).split(), ( @@ -1525,7 +1558,7 @@ def test_fail_on_combination_of_start_delay_and_interval_origin(self): def test_fail_on_invalid_interval(self): self.assert_pcs_fail_regardless_of_force( ( - "resource create --no-default-ops R ocf:heartbeat:Dummy " + "resource create --no-default-ops R ocf:pcsmock:minimal " "op monitor interval=" ).split(), ( @@ -1556,9 +1589,13 @@ class FailOrWarnGroup(ResourceTest): "to the future behavior.\n" ) + def setUp(self): + super().setUp() + self.pcs_runner.mock_settings = get_mock_settings() + def test_fail_when_invalid_group(self): self.assert_pcs_fail( - f"resource create R ocf:heartbeat:Dummy {self.GROUP} 1 {self.FUTURE}".split(), + f"resource create R ocf:pcsmock:minimal {self.GROUP} 1 {self.FUTURE}".split(), ( self.DEPRECATED_GROUP + "Error: invalid group name '1', '1' is not a valid first character" @@ -1569,12 +1606,12 @@ def test_fail_when_invalid_group(self): def test_fail_when_try_use_id_of_another_element(self): self.assert_effect( ( - "resource create R1 ocf:heartbeat:Dummy --no-default-ops " + "resource create R1 ocf:pcsmock:minimal --no-default-ops " "meta a=b" ).split(), """ - @@ -1589,7 +1626,7 @@ def test_fail_when_try_use_id_of_another_element(self): ) self.assert_pcs_fail( ( - "resource create R2 ocf:heartbeat:Dummy " + "resource create R2 ocf:pcsmock:minimal " f"{self.GROUP} R1-meta_attributes {self.FUTURE}" ).split(), ( @@ -1602,7 +1639,7 @@ def test_fail_when_try_use_id_of_another_element(self): def test_fail_when_entered_both_after_and_before(self): self.assert_pcs_fail( ( - "resource create R ocf:heartbeat:Dummy " + "resource create R ocf:pcsmock:minimal " f"{self.GROUP} G {self.AFTER} S1 {self.BEFORE} S2 {self.FUTURE}" ).split(), ( @@ -1615,33 +1652,33 @@ def test_fail_when_entered_both_after_and_before(self): def test_fail_when_after_is_used_without_group(self): self.assert_pcs_fail( - "resource create R ocf:heartbeat:Dummy --after S1".split(), + "resource create R ocf:pcsmock:minimal --after S1".split(), "Error: you cannot use --after without --group\n", ) def test_fail_when_before_is_used_without_group(self): self.assert_pcs_fail( - "resource create R ocf:heartbeat:Dummy --before S1".split(), + "resource create R ocf:pcsmock:minimal --before S1".split(), "Error: you cannot use --before without --group\n", ) def test_fail_when_before_after_conflicts_and_moreover_without_group(self): self.assert_pcs_fail( - "resource create R ocf:heartbeat:Dummy --after S1 --before S2".split(), + "resource create R ocf:pcsmock:minimal --after S1 --before S2".split(), "Error: you cannot use --before without --group\n", ) def test_fail_when_before_does_not_exist(self): self.assert_pcs_success( ( - f"resource create R0 ocf:heartbeat:Dummy {self.GROUP} G1 " + f"resource create R0 ocf:pcsmock:minimal {self.GROUP} G1 " f"{self.FUTURE}" ).split(), stderr_full=self.DEPRECATED_GROUP, ) self.assert_pcs_fail( ( - f"resource create R2 ocf:heartbeat:Dummy {self.GROUP} G1 " + f"resource create R2 ocf:pcsmock:minimal {self.GROUP} G1 " f"{self.BEFORE} R1 {self.FUTURE}" ).split(), ( @@ -1655,7 +1692,7 @@ def test_fail_when_before_does_not_exist(self): def test_fail_when_use_before_with_new_group(self): self.assert_pcs_fail( ( - f"resource create R2 ocf:heartbeat:Dummy {self.GROUP} G1 " + f"resource create R2 ocf:pcsmock:minimal {self.GROUP} G1 " f"{self.BEFORE} R1 {self.FUTURE}" ).split(), ( @@ -1669,14 +1706,14 @@ def test_fail_when_use_before_with_new_group(self): def test_fail_when_after_does_not_exist(self): self.assert_pcs_success( ( - f"resource create R0 ocf:heartbeat:Dummy {self.GROUP} G1 " + f"resource create R0 ocf:pcsmock:minimal {self.GROUP} G1 " f"{self.FUTURE}" ).split(), stderr_full=self.DEPRECATED_GROUP, ) self.assert_pcs_fail( ( - f"resource create R2 ocf:heartbeat:Dummy {self.GROUP} G1 " + f"resource create R2 ocf:pcsmock:minimal {self.GROUP} G1 " f"{self.AFTER} R1 {self.FUTURE}" ).split(), ( @@ -1690,7 +1727,7 @@ def test_fail_when_after_does_not_exist(self): def test_fail_when_use_after_with_new_group(self): self.assert_pcs_fail( ( - f"resource create R2 ocf:heartbeat:Dummy {self.GROUP} G1 " + f"resource create R2 ocf:pcsmock:minimal {self.GROUP} G1 " f"{self.AFTER} R1 {self.FUTURE}" ).split(), ( @@ -1712,7 +1749,7 @@ class FailOrWarnGroupFuture(FailOrWarnGroup): def test_fail_when_entered_both_after_and_before(self): self.assert_pcs_fail( ( - "resource create R ocf:heartbeat:Dummy " + "resource create R ocf:pcsmock:minimal " f"{self.GROUP} G {self.AFTER} S1 {self.BEFORE} S2 {self.FUTURE}" ).split(), ( @@ -1725,24 +1762,28 @@ def test_fail_when_entered_both_after_and_before(self): def test_fail_when_after_is_used_without_group(self): self.assert_pcs_fail( - "resource create R ocf:heartbeat:Dummy after S1".split(), + "resource create R ocf:pcsmock:minimal after S1".split(), "Error: missing value of 'after' option\n", ) def test_fail_when_before_is_used_without_group(self): self.assert_pcs_fail( - "resource create R ocf:heartbeat:Dummy before S1".split(), + "resource create R ocf:pcsmock:minimal before S1".split(), "Error: missing value of 'before' option\n", ) def test_fail_when_before_after_conflicts_and_moreover_without_group(self): self.assert_pcs_fail( - "resource create R ocf:heartbeat:Dummy after S1 before S2".split(), + "resource create R ocf:pcsmock:minimal after S1 before S2".split(), "Error: missing value of 'after' option\n", ) class FailOrWarnPacemakerRemoteOrGuestNode(ResourceTest): + def setUp(self): + super().setUp() + self.pcs_runner.mock_settings = get_mock_settings() + def test_fail_when_on_pacemaker_remote_attempt(self): self.assert_pcs_fail( "resource create R2 ocf:pacemaker:remote".split(), @@ -1809,7 +1850,7 @@ def test_fail_when_on_guest_conflict_with_existing_node(self): self.assert_pcs_fail( ( - "resource create R2 ocf:heartbeat:Dummy " + "resource create R2 ocf:pcsmock:minimal " "meta remote-node=R --force" ).split(), ( @@ -1830,7 +1871,7 @@ def test_fail_when_on_guest_conflict_with_existing_node_host(self): self.assert_pcs_fail( ( - "resource create R2 ocf:heartbeat:Dummy " + "resource create R2 ocf:pcsmock:minimal " "meta remote-node=HOST --force" ).split(), ( @@ -1851,7 +1892,7 @@ def test_fail_when_on_guest_conflict_with_existing_node_host_addr(self): self.assert_pcs_fail( ( - "resource create R2 ocf:heartbeat:Dummy " + "resource create R2 ocf:pcsmock:minimal " "meta remote-node=A remote-addr=HOST --force" ).split(), ( @@ -1872,7 +1913,7 @@ def test_not_fail_when_on_guest_when_conflict_host_with_name(self): self.assert_pcs_success( ( - "resource create R2 ocf:heartbeat:Dummy " + "resource create R2 ocf:pcsmock:minimal " "meta remote-node=HOST remote-addr=R --force" ).split(), stderr_full=( @@ -1883,7 +1924,7 @@ def test_not_fail_when_on_guest_when_conflict_host_with_name(self): def test_fail_when_on_pacemaker_remote_guest_attempt(self): self.assert_pcs_fail( - "resource create R2 ocf:heartbeat:Dummy meta remote-node=HOST".split(), + "resource create R2 ocf:pcsmock:minimal meta remote-node=HOST".split(), ( "Error: this command is not sufficient for creating a guest " "node, use 'pcs cluster node add-guest', use --force to " @@ -1894,7 +1935,7 @@ def test_fail_when_on_pacemaker_remote_guest_attempt(self): def test_warn_when_on_pacemaker_remote_guest_attempt(self): self.assert_pcs_success( ( - "resource create R2 ocf:heartbeat:Dummy " + "resource create R2 ocf:pcsmock:minimal " "meta remote-node=HOST --force" ).split(), stderr_full=( diff --git a/pcs_test/tier1/cib_resource/test_enable_disable.py b/pcs_test/tier1/cib_resource/test_enable_disable.py index 98685e46b..84b4cc6d8 100644 --- a/pcs_test/tier1/cib_resource/test_enable_disable.py +++ b/pcs_test/tier1/cib_resource/test_enable_disable.py @@ -2,6 +2,7 @@ from lxml import etree +from pcs_test.tools.bin_mock import get_mock_settings from pcs_test.tools.cib import get_assert_pcs_effect_mixin from pcs_test.tools.misc import get_test_resource as rc from pcs_test.tools.misc import ( @@ -27,6 +28,7 @@ def setUp(self): self.temp_cib = get_tmp_file("tier1_cib_resource_enable_disable") write_file_to_tmpfile(self.empty_cib, self.temp_cib) self.pcs_runner = PcsRunner(self.temp_cib.name) + self.pcs_runner.mock_settings = get_mock_settings() def tearDown(self): self.temp_cib.close() @@ -36,7 +38,7 @@ def fixture_resource(self, name, disabled=False): "resource", "create", name, - "ocf:heartbeat:Dummy", + "ocf:pcsmock:minimal", "--no-default-ops", ] if disabled: @@ -66,7 +68,7 @@ def test_enable(self): "resource enable TA B".split(), """ - + - + - + - + - + - + - + diff --git a/pcs_test/tier1/cib_resource/test_group_ungroup.py b/pcs_test/tier1/cib_resource/test_group_ungroup.py index 492852600..bcd07d0b7 100644 --- a/pcs_test/tier1/cib_resource/test_group_ungroup.py +++ b/pcs_test/tier1/cib_resource/test_group_ungroup.py @@ -2,6 +2,7 @@ from lxml import etree +from pcs_test.tools.bin_mock import get_mock_settings from pcs_test.tools.cib import get_assert_pcs_effect_mixin from pcs_test.tools.misc import get_test_resource as rc from pcs_test.tools.misc import ( @@ -24,8 +25,8 @@ def fixture_resources_xml(resources_xml_list): def fixture_primitive_xml(primitive_id): return f""" - - + - + {empty_meta_b} - + - + - + - + - + - + - + - + - + - + + # # # - + - + - + - + - + \'' ) - stdout, stderr, retval = pcs( - self.temp_cib.name, - "resource create dummy1 ocf:heartbeat:Dummy".split(), + self.assert_pcs_success( + "resource create dummy1 ocf:pcsmock:minimal".split(), ) - self.assertEqual(stdout, "") - self.assertEqual(stderr, "") - self.assertEqual(retval, 0) # pcs no longer allows creating masters but supports existing ones. In # order to test it, we need to put a master in the CIB without pcs. fixture_to_cib(self.temp_cib.name, fixture_master_xml("stateful1")) - stdout, stderr, retval = pcs( - self.temp_cib.name, - "resource create stateful2 ocf:pacemaker:Stateful --group statefulG".split(), - mock_settings=get_mock_settings("crm_resource_exec"), - ) - self.assertEqual(stdout, "") - ac( - stderr, - DEPRECATED_DASH_DASH_GROUP - + "Warning: changing a monitor operation interval from 10s to 11 to make the operation unique\n", + self.assert_pcs_success( + "resource create stateful2 ocf:pcsmock:stateful --group statefulG".split(), + stderr_full=DEPRECATED_DASH_DASH_GROUP, ) - self.assertEqual(retval, 0) # pcs no longer allows turning resources into masters but supports # existing ones. In order to test it, we need to put a master in the @@ -2459,36 +2373,17 @@ def test_clone_constraint(self): + f' {settings.cibadmin_exec} -R --scope nodes --xml-text \'\'' ) - stdout, stderr, retval = pcs( - self.temp_cib.name, - "resource create dummy1 ocf:heartbeat:Dummy".split(), - ) - self.assertEqual(stdout, "") - self.assertEqual(stderr, "") - self.assertEqual(retval, 0) - - stdout, stderr, retval = pcs( - self.temp_cib.name, - "resource create dummy ocf:heartbeat:Dummy clone".split(), + self.assert_pcs_success( + "resource create dummy1 ocf:pcsmock:minimal".split() ) - self.assertEqual(stdout, "") - self.assertEqual(stderr, "") - self.assertEqual(retval, 0) - - stdout, stderr, retval = pcs( - self.temp_cib.name, - "resource create dummy2 ocf:heartbeat:Dummy --group dummyG".split(), + self.assert_pcs_success( + "resource create dummy ocf:pcsmock:minimal clone".split() ) - self.assertEqual(stdout, "") - self.assertEqual(stderr, DEPRECATED_DASH_DASH_GROUP) - self.assertEqual(retval, 0) - - stdout, stderr, retval = pcs( - self.temp_cib.name, "resource clone dummyG".split() + self.assert_pcs_success( + "resource create dummy2 ocf:pcsmock:minimal --group dummyG".split(), + stderr_full=DEPRECATED_DASH_DASH_GROUP, ) - self.assertEqual(stdout, "") - self.assertEqual(stderr, "") - self.assertEqual(retval, 0) + self.assert_pcs_success("resource clone dummyG".split()) stdout, stderr, retval = pcs( self.temp_cib.name, @@ -2838,50 +2733,22 @@ def test_many_constraints(self): def test_constraint_resource_clone_update(self): self.fixture_resources() - stdout, stderr, retval = pcs( - self.temp_cib.name, + self.assert_pcs_success( "constraint location D1 prefers rh7-1".split(), + stderr_full=LOCATION_NODE_VALIDATION_SKIP_WARNING, ) - self.assertEqual(stderr, LOCATION_NODE_VALIDATION_SKIP_WARNING) - self.assertEqual(stdout, "") - self.assertEqual(retval, 0) - - stdout, stderr, retval = pcs( - self.temp_cib.name, + self.assert_pcs_success( "constraint colocation add D1 with D5".split(), ) - self.assertEqual(stdout, "") - self.assertEqual(stderr, "") - self.assertEqual(retval, 0) - - stdout, stderr, retval = pcs( - self.temp_cib.name, "constraint order D1 then D5".split() - ) - self.assertEqual(stdout, "") - ac( - stderr, - "Adding D1 D5 (kind: Mandatory) (Options: first-action=start then-action=start)\n", - ) - self.assertEqual(retval, 0) - - stdout, stderr, retval = pcs( - self.temp_cib.name, "constraint order D6 then D1".split() - ) - self.assertEqual(stdout, "") - ac( - stderr, - "Adding D6 D1 (kind: Mandatory) (Options: first-action=start then-action=start)\n", + self.assert_pcs_success( + "constraint order D1 then D5".split(), + stderr_full="Adding D1 D5 (kind: Mandatory) (Options: first-action=start then-action=start)\n", ) - self.assertEqual(retval, 0) - - self.assertEqual(retval, 0) - stdout, stderr, retval = pcs( - self.temp_cib.name, "resource clone D1".split() + self.assert_pcs_success( + "constraint order D6 then D1".split(), + stderr_full="Adding D6 D1 (kind: Mandatory) (Options: first-action=start then-action=start)\n", ) - self.assertEqual(stderr, "") - self.assertEqual(stdout, "") - self.assertEqual(retval, 0) - + self.assert_pcs_success("resource clone D1".split()) self.assert_pcs_success( "constraint --full".split(), stdout_full=outdent( @@ -2900,57 +2767,23 @@ def test_constraint_resource_clone_update(self): def test_constraint_group_clone_update(self): self.fixture_resources() - stdout, stderr, retval = pcs( - self.temp_cib.name, "resource group add DG D1".split() - ) - self.assertEqual(stdout, "") - self.assertEqual(stderr, "") - self.assertEqual(retval, 0) - - stdout, stderr, retval = pcs( - self.temp_cib.name, + self.assert_pcs_success("resource group add DG D1".split()) + self.assert_pcs_success( "constraint location DG prefers rh7-1".split(), + stderr_full=LOCATION_NODE_VALIDATION_SKIP_WARNING, ) - self.assertEqual(stderr, LOCATION_NODE_VALIDATION_SKIP_WARNING) - self.assertEqual(stdout, "") - self.assertEqual(retval, 0) - - stdout, stderr, retval = pcs( - self.temp_cib.name, + self.assert_pcs_success( "constraint colocation add DG with D5".split(), ) - self.assertEqual(stdout, "") - self.assertEqual(stderr, "") - self.assertEqual(retval, 0) - - stdout, stderr, retval = pcs( - self.temp_cib.name, "constraint order DG then D5".split() - ) - self.assertEqual(stdout, "") - ac( - stderr, - "Adding DG D5 (kind: Mandatory) (Options: first-action=start then-action=start)\n", - ) - self.assertEqual(retval, 0) - - stdout, stderr, retval = pcs( - self.temp_cib.name, "constraint order D6 then DG".split() - ) - self.assertEqual(stdout, "") - ac( - stderr, - "Adding D6 DG (kind: Mandatory) (Options: first-action=start then-action=start)\n", + self.assert_pcs_success( + "constraint order DG then D5".split(), + stderr_full="Adding DG D5 (kind: Mandatory) (Options: first-action=start then-action=start)\n", ) - self.assertEqual(retval, 0) - - self.assertEqual(retval, 0) - stdout, stderr, retval = pcs( - self.temp_cib.name, "resource clone DG".split() + self.assert_pcs_success( + "constraint order D6 then DG".split(), + stderr_full="Adding D6 DG (kind: Mandatory) (Options: first-action=start then-action=start)\n", ) - self.assertEqual(stdout, "") - self.assertEqual(stderr, "") - self.assertEqual(retval, 0) - + self.assert_pcs_success("resource clone DG".split()) self.assert_pcs_success( "constraint --full".split(), stdout_full=outdent( @@ -2976,14 +2809,12 @@ def test_remote_node_constraints_remove(self): # deleting the remote node resource self.assert_pcs_success( ( - "resource create vm-guest1 ocf:heartbeat:VirtualDomain " - "hypervisor=qemu:///system config=/root/guest1.xml " + "resource create vm-guest1 ocf:pcsmock:minimal " "meta remote-node=guest1 --force" ).split(), - stdout_full="", - stderr_start=( + stderr_full=( "Warning: this command is not sufficient for creating a guest " - "node, use 'pcs cluster node add-guest'\n", + "node, use 'pcs cluster node add-guest'\n" ), ) @@ -3063,14 +2894,12 @@ def test_remote_node_constraints_remove(self): # removing the remote node self.assert_pcs_success( ( - "resource create vm-guest1 ocf:heartbeat:VirtualDomain " - "hypervisor=qemu:///system config=/root/guest1.xml " + "resource create vm-guest1 ocf:pcsmock:minimal " "meta remote-node=guest1 --force" ).split(), - stdout_full="", - stderr_start=( + stderr_full=( "Warning: this command is not sufficient for creating a guest " - "node, use 'pcs cluster node add-guest'\n", + "node, use 'pcs cluster node add-guest'\n" ), ) @@ -3144,14 +2973,12 @@ def test_remote_node_constraints_remove(self): # deleting the remote node resource self.assert_pcs_success( ( - "resource create vm-guest1 ocf:heartbeat:VirtualDomain " - "hypervisor=qemu:///system config=/root/guest1.xml " + "resource create vm-guest1 ocf:pcsmock:minimal " "meta remote-node=guest1 --force" ).split(), - stdout_full="", - stderr_start=( + stderr_full=( "Warning: this command is not sufficient for creating a guest " - "node, use 'pcs cluster node add-guest'\n", + "node, use 'pcs cluster node add-guest'\n" ), ) @@ -4075,8 +3902,9 @@ def setUp(self): self.temp_cib = get_tmp_file("tier1_constraint") write_file_to_tmpfile(self.empty_cib, self.temp_cib) self.pcs_runner = PcsRunner(self.temp_cib.name) - self.assert_pcs_success("resource create A ocf:heartbeat:Dummy".split()) - self.assert_pcs_success("resource create B ocf:heartbeat:Dummy".split()) + self.pcs_runner.mock_settings = get_mock_settings() + self.assert_pcs_success("resource create A ocf:pcsmock:minimal".split()) + self.assert_pcs_success("resource create B ocf:pcsmock:minimal".split()) def tearDown(self): self.temp_cib.close() @@ -4389,13 +4217,14 @@ def setUp(self): self.temp_cib = get_tmp_file("tier1_constraint") write_file_to_tmpfile(self.empty_cib, self.temp_cib) self.pcs_runner = PcsRunner(self.temp_cib.name) + self.pcs_runner.mock_settings = get_mock_settings() def tearDown(self): self.temp_cib.close() def fixture_primitive(self, name): self.assert_pcs_success( - ["resource", "create", name, "ocf:heartbeat:Dummy"] + ["resource", "create", name, "ocf:pcsmock:minimal"] ) @@ -4532,9 +4361,9 @@ class LocationShowWithPattern(ConstraintBaseTest): def fixture(self): self.assert_pcs_success_all( [ - "resource create R1 ocf:heartbeat:Dummy".split(), - "resource create R2 ocf:heartbeat:Dummy".split(), - "resource create R3 ocf:heartbeat:Dummy".split(), + "resource create R1 ocf:pcsmock:minimal".split(), + "resource create R2 ocf:pcsmock:minimal".split(), + "resource create R3 ocf:pcsmock:minimal".split(), "constraint location R1 prefers node1 node2=20".split(), "constraint location R1 avoids node3=30 node4".split(), "constraint location R2 prefers node3 node4=20".split(), @@ -4701,7 +4530,7 @@ def fixture_primitive(self, name, bundle=None): "resource", "create", name, - "ocf:heartbeat:Dummy", + "ocf:pcsmock:minimal", "bundle", bundle, ] @@ -5099,6 +4928,7 @@ def setUp(self): self.temp_cib = get_tmp_file("tier1_constraint_location") write_file_to_tmpfile(self.empty_cib, self.temp_cib) self.pcs_runner = PcsRunner(self.temp_cib.name) + self.pcs_runner.mock_settings = get_mock_settings() self.command = "to-be-overridden" def tearDown(self): @@ -5268,10 +5098,10 @@ class ExpiredConstraints(ConstraintBaseTest): def fixture_group(self): self.assert_pcs_success( - "resource create dummy1 ocf:heartbeat:Dummy".split() + "resource create dummy1 ocf:pcsmock:minimal".split() ) self.assert_pcs_success( - "resource create dummy2 ocf:heartbeat:Dummy".split() + "resource create dummy2 ocf:pcsmock:minimal".split() ) self.assert_pcs_success( "resource group add dummy_group dummy1 dummy2".split() @@ -5279,23 +5109,25 @@ def fixture_group(self): def fixture_primitive(self): self.assert_pcs_success( - "resource create dummy ocf:heartbeat:Dummy".split() + "resource create dummy ocf:pcsmock:minimal".split() ) def fixture_multiple_primitive(self): self.assert_pcs_success( - "resource create D1 ocf:heartbeat:Dummy".split() + "resource create D1 ocf:pcsmock:minimal".split() ) self.assert_pcs_success( - "resource create D2 ocf:heartbeat:Dummy".split() + "resource create D2 ocf:pcsmock:minimal".split() ) self.assert_pcs_success( - "resource create D3 ocf:heartbeat:Dummy".split() + "resource create D3 ocf:pcsmock:minimal".split() ) def test_crm_rule_missing(self): + mock_settings = get_mock_settings() + mock_settings["crm_rule_exec"] = "" self.pcs_runner = PcsRunner( - self.temp_cib.name, mock_settings={"crm_rule_exec": ""} + self.temp_cib.name, mock_settings=mock_settings ) self.fixture_primitive() self.assert_pcs_success( @@ -5847,19 +5679,20 @@ def setUp(self): self.temp_cib = get_tmp_file("tier1_constraint_order_vs_group") write_file_to_tmpfile(self.empty_cib, self.temp_cib) self.pcs_runner = PcsRunner(self.temp_cib.name) + self.pcs_runner.mock_settings = get_mock_settings() self.assert_pcs_success( - "resource create A ocf:heartbeat:Dummy --group grAB".split(), + "resource create A ocf:pcsmock:minimal --group grAB".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) self.assert_pcs_success( - "resource create B ocf:heartbeat:Dummy --group grAB".split(), + "resource create B ocf:pcsmock:minimal --group grAB".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) self.assert_pcs_success( - "resource create C ocf:heartbeat:Dummy --group grC".split(), + "resource create C ocf:pcsmock:minimal --group grC".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) - self.assert_pcs_success("resource create D ocf:heartbeat:Dummy".split()) + self.assert_pcs_success("resource create D ocf:pcsmock:minimal".split()) def tearDown(self): self.temp_cib.close() diff --git a/pcs_test/tier1/legacy/test_resource.py b/pcs_test/tier1/legacy/test_resource.py index ec7bed01e..b0350edec 100644 --- a/pcs_test/tier1/legacy/test_resource.py +++ b/pcs_test/tier1/legacy/test_resource.py @@ -66,46 +66,54 @@ class ResourceDescribe(TestCase, AssertPcsMixin): def setUp(self): self.pcs_runner = PcsRunner(None) - self.pcs_runner.mock_settings = get_mock_settings("crm_resource_exec") + self.pcs_runner.mock_settings = get_mock_settings() @staticmethod def fixture_description(advanced=False): - advanced_params = """\ - trace_ra (advanced use only) - Description: Set to 1 to turn on resource agent tracing (expect large output) The trace output will be saved to trace_file, if set, or by default to $HA_VARRUN/ra_trace//.. e.g. $HA_VARRUN/ra_trace/oracle/db.start.2012-11-27.08:37:08 - Type: integer - Default: 0 - trace_file (advanced use only) - Description: Path to a file to store resource agent tracing log - Type: string - """ + advanced_params = """ + advanced (advanced use only) + Description: This parameter should not be set usually + Type: string""" return dedent( """\ - ocf:pacemaker:HealthCPU - System health CPU usage + ocf:pcsmock:params - Mock agent for pcs tests - agent with various parameters - System health agent that measures the CPU idling and updates the #health-cpu attribute. + This is a mock agent for pcs test - agent with parameters Resource options: - state (unique) - Description: Location to store the resource state in. + mandatory (required) + Description: A generic mandatory string parameter + Type: string + optional + Description: A generic optional string parameter Type: string - Default: /var/run/health-cpu-HealthCPU.state - yellow_limit (unique) - Description: Lower (!) limit of idle percentage to switch the health attribute to yellow. I.e. the #health-cpu will go yellow if the %idle of the CPU falls below 50%. + Default: if not specified + enum + Description: An optional enum parameter + Allowed values: 'value1', 'value2', 'value3' + Default: value1{0} + unique1 (unique group: group-A) + Description: First parameter in a unique group Type: string - Default: 50 - red_limit - Description: Lower (!) limit of idle percentage to switch the health attribute to red. I.e. the #health-cpu will go red if the %idle of the CPU falls below 10%. + unique2 (unique group: group-A) + Description: Second parameter in a unique group Type: string - Default: 10 -{0} + Default operations: start: - interval=0s timeout=10s + interval=0s timeout=20s stop: - interval=0s timeout=10s + interval=0s timeout=20s monitor: - interval=10s start-delay=0s timeout=10s + interval=10s timeout=20s + reload: + interval=0s timeout=20s + reload-agent: + interval=0s timeout=20s + migrate_to: + interval=0s timeout=20s + migrate_from: + interval=0s timeout=20s """.format( advanced_params if advanced else "" ) @@ -113,31 +121,30 @@ def fixture_description(advanced=False): def test_success(self): self.assert_pcs_success( - "resource describe ocf:pacemaker:HealthCPU".split(), + "resource describe ocf:pcsmock:params".split(), self.fixture_description(), ) def test_full(self): self.assert_pcs_success( - "resource describe ocf:pacemaker:HealthCPU --full".split(), + "resource describe ocf:pcsmock:params --full".split(), self.fixture_description(True), ) def test_success_guess_name(self): self.assert_pcs_success( - "resource describe healthcpu".split(), + "resource describe params".split(), stdout_full=self.fixture_description(), stderr_full=( - "Assumed agent name 'ocf:pacemaker:HealthCPU' (deduced from " - "'healthcpu')\n" + "Assumed agent name 'ocf:pcsmock:params' (deduced from 'params')\n" ), ) def test_nonextisting_agent(self): self.assert_pcs_fail( - "resource describe ocf:pacemaker:nonexistent".split(), + "resource describe ocf:pcsmock:nonexistent".split(), ( - "Error: Agent 'ocf:pacemaker:nonexistent' is not installed or does " + "Error: Agent 'ocf:pcsmock:nonexistent' is not installed or does " "not provide valid metadata: " "pcs mock error message: unable to load agent metadata\n" + ERRORS_HAVE_OCCURRED @@ -155,10 +162,10 @@ def test_nonextisting_agent_guess_name(self): def test_more_agents_guess_name(self): self.assert_pcs_fail( - "resource describe dummy".split(), + "resource describe pcsmock".split(), ( - "Error: Multiple agents match 'dummy', please specify full" - " name: 'ocf:heartbeat:Dummy' or 'ocf:pacemaker:Dummy'\n" + "Error: Multiple agents match 'pcsmock', please specify full" + " name: 'ocf:heartbeat:pcsMock' or 'ocf:pacemaker:pcsMock'\n" ), ) @@ -175,140 +182,111 @@ def test_too_many_params(self): ) def test_pcsd_interface(self): + self.maxDiff = None stdout, stderr, returncode = self.pcs_runner.run( - "resource get_resource_agent_info ocf:pacemaker:Dummy".split() + "resource get_resource_agent_info ocf:pcsmock:params".split() ) self.assertEqual(stderr, "") self.assertEqual(returncode, 0) self.assertEqual( json.loads(stdout), { - "name": "ocf:pacemaker:Dummy", + "name": "ocf:pcsmock:params", "standard": "ocf", - "provider": "pacemaker", - "type": "Dummy", - "shortdesc": "Example stateless resource agent", - "longdesc": "This is a dummy OCF resource agent. It does absolutely nothing except keep track\nof whether it is running or not, and can be configured so that actions fail or\ntake a long time. Its purpose is primarily for testing, and to serve as a\ntemplate for resource agent writers.", + "provider": "pcsmock", + "type": "params", + "shortdesc": "Mock agent for pcs tests - agent with various parameters", + "longdesc": "This is a mock agent for pcs test - agent with parameters", "parameters": [ { - "name": "state", - "shortdesc": "State file", - "longdesc": "Location to store the resource state in.", - "type": "string", - "default": "/var/run/Dummy-Dummy.state", - "enum_values": None, - "required": False, "advanced": False, + "default": None, "deprecated": False, "deprecated_by": [], "deprecated_desc": None, - "unique_group": "state", + "enum_values": None, + "longdesc": "A generic mandatory string parameter", + "name": "mandatory", "reloadable": False, - }, - { - "name": "passwd", - "shortdesc": "Password", - "longdesc": "Fake password field", + "required": True, + "shortdesc": "mandatory string parameter", "type": "string", - "default": "", - "enum_values": None, - "required": False, - "advanced": False, - "deprecated": False, - "deprecated_by": [], - "deprecated_desc": None, "unique_group": None, - "reloadable": True, }, { - "name": "fake", - "shortdesc": "Fake attribute that can be changed to cause an agent reload", - "longdesc": "Fake attribute that can be changed to cause an agent reload", - "type": "string", - "default": "dummy", - "enum_values": None, - "required": False, "advanced": False, + "default": "if not specified", "deprecated": False, "deprecated_by": [], "deprecated_desc": None, - "unique_group": None, - "reloadable": True, - }, - { - "name": "op_sleep", - "shortdesc": "Operation sleep duration in seconds.", - "longdesc": "Number of seconds to sleep during operations. This can be used to test how\nthe cluster reacts to operation timeouts.", - "type": "string", - "default": "0", "enum_values": None, + "longdesc": "A generic optional string parameter", + "name": "optional", + "reloadable": False, "required": False, - "advanced": False, - "deprecated": False, - "deprecated_by": [], - "deprecated_desc": None, + "shortdesc": "optional string parameter", + "type": "string", "unique_group": None, - "reloadable": True, }, { - "name": "fail_start_on", - "shortdesc": "Report bogus start failure on specified host", - "longdesc": "Start, migrate_from, and reload-agent actions will return failure if running on\nthe host specified here, but the resource will run successfully anyway (future\nmonitor calls will find it running). This can be used to test on-fail=ignore.", - "type": "string", - "default": "", - "enum_values": None, - "required": False, "advanced": False, + "default": "value1", "deprecated": False, "deprecated_by": [], "deprecated_desc": None, + "enum_values": ["value1", "value2", "value3"], + "longdesc": "An optional enum parameter", + "name": "enum", + "reloadable": False, + "required": False, + "shortdesc": "optional enum parameter", + "type": "select", "unique_group": None, - "reloadable": True, }, { - "name": "envfile", - "shortdesc": "Environment dump file", - "longdesc": "If this is set, the environment will be dumped to this file for every call.", - "type": "string", - "default": "", - "enum_values": None, - "required": False, - "advanced": False, + "advanced": True, + "default": None, "deprecated": False, "deprecated_by": [], "deprecated_desc": None, + "enum_values": None, + "longdesc": "This parameter should not be set usually", + "name": "advanced", + "reloadable": False, + "required": False, + "shortdesc": "advanced parameter", + "type": "string", "unique_group": None, - "reloadable": True, }, { - "name": "trace_ra", - "shortdesc": "Set to 1 to turn on resource agent tracing (expect large output)", - "longdesc": "Set to 1 to turn on resource agent tracing (expect large output) The trace output will be saved to trace_file, if set, or by default to $HA_VARRUN/ra_trace//.. e.g. $HA_VARRUN/ra_trace/oracle/db.start.2012-11-27.08:37:08", - "type": "integer", - "default": "0", - "enum_values": None, - "required": False, - "advanced": True, + "advanced": False, + "default": None, "deprecated": False, "deprecated_by": [], "deprecated_desc": None, - "unique_group": None, + "enum_values": None, + "longdesc": "First parameter in a unique group", + "name": "unique1", "reloadable": False, + "required": False, + "shortdesc": "unique param 1", + "type": "string", + "unique_group": "group-A", }, { - "name": "trace_file", - "shortdesc": "Path to a file to store resource agent tracing log", - "longdesc": "Path to a file to store resource agent tracing log", - "type": "string", - "default": "", - "enum_values": None, - "required": False, - "advanced": True, + "advanced": False, + "default": None, "deprecated": False, "deprecated_by": [], "deprecated_desc": None, - "unique_group": None, + "enum_values": None, + "longdesc": "Second parameter in a unique group", + "name": "unique2", "reloadable": False, + "required": False, + "shortdesc": "unique param 2", + "type": "string", + "unique_group": "group-A", }, ], "actions": [ @@ -483,38 +461,32 @@ class ResourceTestCibFixture(CachedCibFixture): def _setup_cib(self): self.assert_pcs_success_ignore_output( ( - "resource create --no-default-ops ClusterIP ocf:heartbeat:IPaddr2" - " cidr_netmask=32 ip=192.168.0.99 op monitor interval=30s --force" + "resource create --no-default-ops ClusterIP ocf:pcsmock:minimal" ).split() ) self.assert_pcs_success_ignore_output( ( - "resource create --no-default-ops ClusterIP2 ocf:heartbeat:IPaddr2" - " cidr_netmask=32 ip=192.168.0.92 op monitor interval=30s --force" + "resource create --no-default-ops ClusterIP2 ocf:pcsmock:minimal" ).split() ) self.assert_pcs_success_ignore_output( ( - "resource create --no-default-ops ClusterIP3 ocf:heartbeat:IPaddr2" - " cidr_netmask=32 ip=192.168.0.93 op monitor interval=30s --force" + "resource create --no-default-ops ClusterIP3 ocf:pcsmock:minimal" ).split() ) self.assert_pcs_success_ignore_output( ( - "resource create --no-default-ops ClusterIP4 ocf:heartbeat:IPaddr2" - " cidr_netmask=32 ip=192.168.0.94 op monitor interval=30s --force" + "resource create --no-default-ops ClusterIP4 ocf:pcsmock:minimal" ).split() ) self.assert_pcs_success_ignore_output( ( - "resource create --no-default-ops ClusterIP5 ocf:heartbeat:IPaddr2" - " cidr_netmask=32 ip=192.168.0.95 op monitor interval=30s --force" + "resource create --no-default-ops ClusterIP5 ocf:pcsmock:minimal" ).split() ) self.assert_pcs_success_ignore_output( ( - "resource create --no-default-ops ClusterIP6 ocf:heartbeat:IPaddr2" - " cidr_netmask=32 ip=192.168.0.96 op monitor interval=30s --force" + "resource create --no-default-ops ClusterIP6 ocf:pcsmock:minimal" ).split() ) self.assert_pcs_success( @@ -543,7 +515,7 @@ def setUp(self): write_file_to_tmpfile(empty_cib, self.temp_cib) write_file_to_tmpfile(large_cib, self.temp_large_cib) self.pcs_runner = PcsRunner(self.temp_cib.name) - self.pcs_runner.mock_settings = get_mock_settings("crm_resource_exec") + self.pcs_runner.mock_settings = get_mock_settings() def tearDown(self): self.temp_cib.close() @@ -555,42 +527,34 @@ def setup_cluster_a(self): def test_case_insensitive(self): self.assert_pcs_fail( - "resource create --no-default-ops D0 dummy".split(), + "resource create --no-default-ops D0 pcsmock".split(), ( - "Error: Multiple agents match 'dummy', please specify full name: " - "'ocf:heartbeat:Dummy' or 'ocf:pacemaker:Dummy'\n" + "Error: Multiple agents match 'pcsmock', please specify full name: " + "'ocf:heartbeat:pcsMock' or 'ocf:pacemaker:pcsMock'\n" + ERRORS_HAVE_OCCURRED ), ) self.assert_pcs_success( - "resource create --no-default-ops D1 systemhealth".split(), - stderr_full=( - "Assumed agent name 'ocf:pacemaker:SystemHealth'" - " (deduced from 'systemhealth')\n" - ), - ) - - self.assert_pcs_success( - "resource create --no-default-ops D2 SYSTEMHEALTH".split(), + "resource create --no-default-ops D1 camelcase".split(), stderr_full=( - "Assumed agent name 'ocf:pacemaker:SystemHealth'" - " (deduced from 'SYSTEMHEALTH')\n" + "Assumed agent name 'ocf:pcsmock:CamelCase'" + " (deduced from 'camelcase')\n" ), ) self.assert_pcs_success( - "resource create --no-default-ops D3 ipaddr2 ip=1.1.1.1".split(), + "resource create --no-default-ops D2 CAMELCASE".split(), stderr_full=( - "Assumed agent name 'ocf:heartbeat:IPaddr2'" - " (deduced from 'ipaddr2')\n" + "Assumed agent name 'ocf:pcsmock:CamelCase'" + " (deduced from 'CAMELCASE')\n" ), ) self.assert_pcs_fail( - "resource create --no-default-ops D4 ipaddr3".split(), + "resource create --no-default-ops D4 camel_case".split(), ( - "Error: Unable to find agent 'ipaddr3', try specifying its full name\n" + "Error: Unable to find agent 'camel_case', try specifying its full name\n" + ERRORS_HAVE_OCCURRED ), ) @@ -600,14 +564,15 @@ def test_empty(self): def test_add_resources_large_cib(self): self.pcs_runner = PcsRunner(self.temp_large_cib.name) + self.pcs_runner.mock_settings = get_mock_settings() self.assert_pcs_success( - "resource create dummy0 ocf:heartbeat:Dummy --no-default-ops".split(), + "resource create dummy0 ocf:pcsmock:minimal --no-default-ops".split(), ) self.assert_pcs_success( "resource config dummy0".split(), dedent( """\ - Resource: dummy0 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy0 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy0-monitor-interval-10s interval=10s timeout=20s @@ -619,10 +584,7 @@ def _test_delete_remove_resources(self, command): assert command in {"delete", "remove"} self.assert_pcs_success( - ( - "resource create --no-default-ops ClusterIP ocf:heartbeat:IPaddr2" - " cidr_netmask=32 ip=192.168.0.99 op monitor interval=30s" - ).split() + "resource create --no-default-ops ClusterIP ocf:pcsmock:minimal".split() ) self.assert_pcs_success( @@ -668,18 +630,18 @@ def test_remove_resources(self): def test_resource_show(self): self.assert_pcs_success( ( - "resource create --no-default-ops ClusterIP ocf:heartbeat:IPaddr2" - " cidr_netmask=32 ip=192.168.0.99 op monitor interval=30s" + "resource create --no-default-ops ClusterIP ocf:pcsmock:params" + " mandatory=mandat optional=opti op monitor interval=30s" ).split() ) self.assert_pcs_success( "resource config ClusterIP".split(), dedent( """\ - Resource: ClusterIP (class=ocf provider=heartbeat type=IPaddr2) + Resource: ClusterIP (class=ocf provider=pcsmock type=params) Attributes: ClusterIP-instance_attributes - cidr_netmask=32 - ip=192.168.0.99 + mandatory=mandat + optional=opti Operations: monitor: ClusterIP-monitor-interval-30s interval=30s @@ -691,8 +653,8 @@ def test_add_operation(self): # see also BundleMiscCommands self.assert_pcs_success( ( - "resource create --no-default-ops ClusterIP ocf:heartbeat:IPaddr2" - " cidr_netmask=32 ip=192.168.0.99 op monitor interval=30s" + "resource create --no-default-ops ClusterIP ocf:pcsmock:minimal" + " op monitor interval=30s" ).split() ) @@ -745,10 +707,7 @@ def test_add_operation(self): "resource config ClusterIP".split(), dedent( """\ - Resource: ClusterIP (class=ocf provider=heartbeat type=IPaddr2) - Attributes: ClusterIP-instance_attributes - cidr_netmask=32 - ip=192.168.0.99 + Resource: ClusterIP (class=ocf provider=pcsmock type=minimal) Operations: monitor: ClusterIP-monitor-interval-30s interval=30s @@ -759,14 +718,18 @@ def test_add_operation(self): ) self.assert_pcs_success( - "resource create --no-default-ops OPTest ocf:heartbeat:Dummy op monitor interval=30s OCF_CHECK_LEVEL=1 op monitor interval=25s OCF_CHECK_LEVEL=1 enabled=0".split(), + ( + "resource create --no-default-ops OPTest ocf:pcsmock:minimal " + "op monitor interval=30s OCF_CHECK_LEVEL=1 " + "op monitor interval=25s OCF_CHECK_LEVEL=1 enabled=0" + ).split(), ) self.assert_pcs_success( "resource config OPTest".split(), dedent( """\ - Resource: OPTest (class=ocf provider=heartbeat type=Dummy) + Resource: OPTest (class=ocf provider=pcsmock type=minimal) Operations: monitor: OPTest-monitor-interval-30s interval=30s OCF_CHECK_LEVEL=1 @@ -777,7 +740,12 @@ def test_add_operation(self): ) self.assert_pcs_success( - "resource create --no-default-ops OPTest2 ocf:heartbeat:Dummy op monitor interval=30s OCF_CHECK_LEVEL=1 op monitor interval=25s OCF_CHECK_LEVEL=2 op start timeout=30s".split(), + ( + "resource create --no-default-ops OPTest2 ocf:pcsmock:minimal " + "op monitor interval=30s OCF_CHECK_LEVEL=1 " + "op monitor interval=25s OCF_CHECK_LEVEL=2 " + "op start timeout=30s" + ).split(), ) self.assert_pcs_fail( @@ -804,7 +772,7 @@ def test_add_operation(self): "resource config OPTest2".split(), dedent( """\ - Resource: OPTest2 (class=ocf provider=heartbeat type=Dummy) + Resource: OPTest2 (class=ocf provider=pcsmock type=minimal) Operations: monitor: OPTest2-monitor-interval-30s interval=30s OCF_CHECK_LEVEL=1 @@ -819,14 +787,17 @@ def test_add_operation(self): ) self.assert_pcs_success( - "resource create --no-default-ops OPTest3 ocf:heartbeat:Dummy op monitor OCF_CHECK_LEVEL=1".split(), + ( + "resource create --no-default-ops OPTest3 ocf:pcsmock:minimal " + "op monitor OCF_CHECK_LEVEL=1" + ).split(), ) self.assert_pcs_success( "resource config OPTest3".split(), dedent( """\ - Resource: OPTest3 (class=ocf provider=heartbeat type=Dummy) + Resource: OPTest3 (class=ocf provider=pcsmock type=minimal) Operations: monitor: OPTest3-monitor-interval-60s interval=60s OCF_CHECK_LEVEL=1 @@ -835,7 +806,10 @@ def test_add_operation(self): ) self.assert_pcs_success( - "resource create --no-default-ops OPTest4 ocf:heartbeat:Dummy op monitor interval=30s".split(), + ( + "resource create --no-default-ops OPTest4 ocf:pcsmock:minimal " + "op monitor interval=30s" + ).split(), ) self.assert_pcs_success( @@ -846,7 +820,7 @@ def test_add_operation(self): "resource config OPTest4".split(), dedent( """\ - Resource: OPTest4 (class=ocf provider=heartbeat type=Dummy) + Resource: OPTest4 (class=ocf provider=pcsmock type=minimal) Operations: monitor: OPTest4-monitor-interval-60s interval=60s OCF_CHECK_LEVEL=1 @@ -855,7 +829,7 @@ def test_add_operation(self): ) self.assert_pcs_success( - "resource create --no-default-ops OPTest5 ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops OPTest5 ocf:pcsmock:minimal".split(), ) self.assert_pcs_success( @@ -866,7 +840,7 @@ def test_add_operation(self): "resource config OPTest5".split(), dedent( """\ - Resource: OPTest5 (class=ocf provider=heartbeat type=Dummy) + Resource: OPTest5 (class=ocf provider=pcsmock type=minimal) Operations: monitor: OPTest5-monitor-interval-60s interval=60s OCF_CHECK_LEVEL=1 @@ -875,7 +849,7 @@ def test_add_operation(self): ) self.assert_pcs_success( - "resource create --no-default-ops OPTest6 ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops OPTest6 ocf:pcsmock:minimal".split(), ) self.assert_pcs_success( @@ -886,7 +860,7 @@ def test_add_operation(self): "resource config OPTest6".split(), dedent( """\ - Resource: OPTest6 (class=ocf provider=heartbeat type=Dummy) + Resource: OPTest6 (class=ocf provider=pcsmock type=minimal) Operations: monitor: OPTest6-monitor-interval-10s interval=10s timeout=20s @@ -897,7 +871,7 @@ def test_add_operation(self): ) self.assert_pcs_success( - "resource create --no-default-ops OPTest7 ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops OPTest7 ocf:pcsmock:minimal".split(), ) self.assert_pcs_success( @@ -920,7 +894,7 @@ def test_add_operation(self): "resource config OPTest7".split(), dedent( """\ - Resource: OPTest7 (class=ocf provider=heartbeat type=Dummy) + Resource: OPTest7 (class=ocf provider=pcsmock type=minimal) Operations: monitor: OPTest7-monitor-interval-60s interval=60s OCF_CHECK_LEVEL=1 @@ -939,7 +913,7 @@ def test_add_operation(self): ) self.assert_pcs_success( - "resource create --no-default-ops OCFTest1 ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops OCFTest1 ocf:pcsmock:minimal".split(), ) self.assert_pcs_fail( @@ -962,7 +936,7 @@ def test_add_operation(self): "resource config OCFTest1".split(), dedent( """\ - Resource: OCFTest1 (class=ocf provider=heartbeat type=Dummy) + Resource: OCFTest1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: OCFTest1-monitor-interval-10s interval=10s timeout=20s @@ -982,7 +956,7 @@ def test_add_operation(self): "resource config OCFTest1".split(), dedent( """\ - Resource: OCFTest1 (class=ocf provider=heartbeat type=Dummy) + Resource: OCFTest1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: OCFTest1-monitor-interval-61s interval=61s OCF_CHECK_LEVEL=5 @@ -1002,7 +976,7 @@ def test_add_operation(self): "resource config OCFTest1".split(), dedent( """\ - Resource: OCFTest1 (class=ocf provider=heartbeat type=Dummy) + Resource: OCFTest1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: OCFTest1-monitor-interval-60s interval=60s OCF_CHECK_LEVEL=4 @@ -1022,7 +996,7 @@ def test_add_operation(self): "resource config OCFTest1".split(), dedent( """\ - Resource: OCFTest1 (class=ocf provider=heartbeat type=Dummy) + Resource: OCFTest1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: OCFTest1-monitor-interval-35s interval=35s OCF_CHECK_LEVEL=4 @@ -1035,11 +1009,7 @@ def test_add_operation(self): ) self.assert_pcs_success( - "resource create --no-default-ops state ocf:pacemaker:Stateful".split(), - stderr_full=( - "Warning: changing a monitor operation interval from 10s to 11 to" - " make the operation unique\n" - ), + "resource create --no-default-ops state ocf:pcsmock:stateful".split(), ) self.assert_pcs_fail( @@ -1066,12 +1036,12 @@ def test_add_operation(self): "resource config state".split(), dedent( f"""\ - Resource: state (class=ocf provider=pacemaker type=Stateful) + Resource: state (class=ocf provider=pcsmock type=stateful) Operations: monitor: state-monitor-interval-10s interval=10s timeout=20s role={const.PCMK_ROLE_PROMOTED_PRIMARY} - monitor: state-monitor-interval-11 - interval=11 timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} + monitor: state-monitor-interval-11s + interval=11s timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} monitor: state-monitor-interval-15 interval=15 role={const.PCMK_ROLE_PROMOTED_PRIMARY} """ @@ -1082,7 +1052,7 @@ def test_add_operation(self): def test_add_operation_onfail_demote_upgrade_cib(self): write_file_to_tmpfile(rc("cib-empty-3.3.xml"), self.temp_cib) self.assert_pcs_success( - "resource create --no-default-ops R ocf:pacemaker:Dummy".split() + "resource create --no-default-ops R ocf:pcsmock:minimal".split() ) self.assert_pcs_success( "resource op add R start on-fail=demote".split(), @@ -1093,7 +1063,7 @@ def test_add_operation_onfail_demote_upgrade_cib(self): def test_update_add_operation_onfail_demote_upgrade_cib(self): write_file_to_tmpfile(rc("cib-empty-3.3.xml"), self.temp_cib) self.assert_pcs_success( - "resource create --no-default-ops R ocf:pacemaker:Dummy".split() + "resource create --no-default-ops R ocf:pcsmock:minimal".split() ) self.assert_pcs_success( "resource update R op start on-fail=demote".split(), @@ -1105,8 +1075,8 @@ def _test_delete_remove_operation(self, command): self.assert_pcs_success( ( - "resource create --no-default-ops ClusterIP ocf:heartbeat:IPaddr2" - " cidr_netmask=32 ip=192.168.0.99 op monitor interval=30s" + "resource create --no-default-ops ClusterIP ocf:pcsmock:minimal" + " op monitor interval=30s" ).split() ) @@ -1143,10 +1113,7 @@ def _test_delete_remove_operation(self, command): "resource config ClusterIP".split(), dedent( """\ - Resource: ClusterIP (class=ocf provider=heartbeat type=IPaddr2) - Attributes: ClusterIP-instance_attributes - cidr_netmask=32 - ip=192.168.0.99 + Resource: ClusterIP (class=ocf provider=pcsmock type=minimal) Operations: monitor: ClusterIP-monitor-interval-31s interval=31s @@ -1162,10 +1129,7 @@ def _test_delete_remove_operation(self, command): "resource config ClusterIP".split(), dedent( """\ - Resource: ClusterIP (class=ocf provider=heartbeat type=IPaddr2) - Attributes: ClusterIP-instance_attributes - cidr_netmask=32 - ip=192.168.0.99 + Resource: ClusterIP (class=ocf provider=pcsmock type=minimal) """ ), ) @@ -1194,10 +1158,7 @@ def _test_delete_remove_operation(self, command): "resource config ClusterIP".split(), dedent( """\ - Resource: ClusterIP (class=ocf provider=heartbeat type=IPaddr2) - Attributes: ClusterIP-instance_attributes - cidr_netmask=32 - ip=192.168.0.99 + Resource: ClusterIP (class=ocf provider=pcsmock type=minimal) Operations: stop: ClusterIP-stop-interval-0s interval=0s timeout=34s @@ -1228,18 +1189,17 @@ def test_remove_operation(self): def test_update_operation(self): self.assert_pcs_success( ( - "resource create --no-default-ops ClusterIP ocf:heartbeat:IPaddr2" - " cidr_netmask=32 ip=192.168.0.99 op monitor interval=30s" + "resource create --no-default-ops ClusterIP ocf:pcsmock:params" + " mandatory=value op monitor interval=30s" ).split() ) self.assert_pcs_success( "resource config ClusterIP".split(), dedent( """\ - Resource: ClusterIP (class=ocf provider=heartbeat type=IPaddr2) + Resource: ClusterIP (class=ocf provider=pcsmock type=params) Attributes: ClusterIP-instance_attributes - cidr_netmask=32 - ip=192.168.0.99 + mandatory=value Operations: monitor: ClusterIP-monitor-interval-30s interval=30s @@ -1254,10 +1214,9 @@ def test_update_operation(self): "resource config ClusterIP".split(), dedent( """\ - Resource: ClusterIP (class=ocf provider=heartbeat type=IPaddr2) + Resource: ClusterIP (class=ocf provider=pcsmock type=params) Attributes: ClusterIP-instance_attributes - cidr_netmask=32 - ip=192.168.0.99 + mandatory=value Operations: monitor: ClusterIP-monitor-interval-32s interval=32s @@ -1267,10 +1226,9 @@ def test_update_operation(self): show_clusterip = dedent( """\ - Resource: ClusterIP (class=ocf provider=heartbeat type=IPaddr2) + Resource: ClusterIP (class=ocf provider=pcsmock type=params) Attributes: ClusterIP-instance_attributes - cidr_netmask=32 - ip=192.168.0.99 + mandatory=value Operations: monitor: ClusterIP-monitor-interval-33s interval=33s @@ -1341,10 +1299,9 @@ def test_update_operation(self): "resource config ClusterIP".split(), dedent( """\ - Resource: ClusterIP (class=ocf provider=heartbeat type=IPaddr2) + Resource: ClusterIP (class=ocf provider=pcsmock type=params) Attributes: ClusterIP-instance_attributes - cidr_netmask=32 - ip=192.168.0.99 + mandatory=value Operations: monitor: abcd interval=60s @@ -1358,13 +1315,13 @@ def test_update_operation(self): # - the first one is updated # - operation duplicity detection test self.assert_pcs_success( - "resource create A ocf:heartbeat:Dummy op monitor interval=10 op monitor interval=20".split() + "resource create A ocf:pcsmock:minimal op monitor interval=10 op monitor interval=20".split() ) self.assert_pcs_success( "resource config A".split(), dedent( """\ - Resource: A (class=ocf provider=heartbeat type=Dummy) + Resource: A (class=ocf provider=pcsmock type=minimal) Operations: migrate_from: A-migrate_from-interval-0s interval=0s timeout=20s @@ -1376,6 +1333,8 @@ def test_update_operation(self): interval=20 reload: A-reload-interval-0s interval=0s timeout=20s + reload-agent: A-reload-agent-interval-0s + interval=0s timeout=20s start: A-start-interval-0s interval=0s timeout=20s stop: A-stop-interval-0s @@ -1400,7 +1359,7 @@ def test_update_operation(self): "resource config A".split(), dedent( """\ - Resource: A (class=ocf provider=heartbeat type=Dummy) + Resource: A (class=ocf provider=pcsmock type=minimal) Operations: migrate_from: A-migrate_from-interval-0s interval=0s timeout=20s @@ -1412,6 +1371,8 @@ def test_update_operation(self): interval=20 reload: A-reload-interval-0s interval=0s timeout=20s + reload-agent: A-reload-agent-interval-0s + interval=0s timeout=20s start: A-start-interval-0s interval=0s timeout=20s stop: A-stop-interval-0s @@ -1421,7 +1382,7 @@ def test_update_operation(self): ) self.assert_pcs_success( - "resource create B ocf:heartbeat:Dummy --no-default-ops".split(), + "resource create B ocf:pcsmock:minimal --no-default-ops".split(), ) self.assert_pcs_success( @@ -1430,7 +1391,7 @@ def test_update_operation(self): self.assert_pcs_success( "resource config B".split(), - "Resource: B (class=ocf provider=heartbeat type=Dummy)\n", + "Resource: B (class=ocf provider=pcsmock type=minimal)\n", ) self.assert_pcs_success( @@ -1441,7 +1402,7 @@ def test_update_operation(self): "resource config B".split(), dedent( """\ - Resource: B (class=ocf provider=heartbeat type=Dummy) + Resource: B (class=ocf provider=pcsmock type=minimal) Operations: monitor: B-monitor-interval-60s interval=60s @@ -1457,7 +1418,7 @@ def test_update_operation(self): "resource config B".split(), dedent( """\ - Resource: B (class=ocf provider=heartbeat type=Dummy) + Resource: B (class=ocf provider=pcsmock type=minimal) Operations: monitor: B-monitor-interval-30 interval=30 @@ -1473,7 +1434,7 @@ def test_update_operation(self): "resource config B".split(), dedent( """\ - Resource: B (class=ocf provider=heartbeat type=Dummy) + Resource: B (class=ocf provider=pcsmock type=minimal) Operations: monitor: B-monitor-interval-30 interval=30 @@ -1491,7 +1452,7 @@ def test_update_operation(self): "resource config B".split(), dedent( """\ - Resource: B (class=ocf provider=heartbeat type=Dummy) + Resource: B (class=ocf provider=pcsmock type=minimal) Operations: monitor: B-monitor-interval-30 interval=30 @@ -1509,7 +1470,7 @@ def test_update_operation(self): "resource config B".split(), dedent( """\ - Resource: B (class=ocf provider=heartbeat type=Dummy) + Resource: B (class=ocf provider=pcsmock type=minimal) Operations: monitor: B-monitor-interval-33 interval=33 @@ -1527,7 +1488,7 @@ def test_update_operation(self): "resource config B".split(), dedent( f"""\ - Resource: B (class=ocf provider=heartbeat type=Dummy) + Resource: B (class=ocf provider=pcsmock type=minimal) Operations: monitor: B-monitor-interval-33 interval=33 @@ -1547,7 +1508,7 @@ def test_update_operation(self): "resource config B".split(), dedent( f"""\ - Resource: B (class=ocf provider=heartbeat type=Dummy) + Resource: B (class=ocf provider=pcsmock type=minimal) Operations: monitor: B-monitor-interval-33 interval=33 @@ -1561,15 +1522,15 @@ def test_update_operation(self): def test_group_delete_test(self): self.assert_pcs_success( - "resource create --no-default-ops A1 ocf:heartbeat:Dummy --group AGroup".split(), + "resource create --no-default-ops A1 ocf:pcsmock:minimal --group AGroup".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) self.assert_pcs_success( - "resource create --no-default-ops A2 ocf:heartbeat:Dummy --group AGroup".split(), + "resource create --no-default-ops A2 ocf:pcsmock:minimal --group AGroup".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) self.assert_pcs_success( - "resource create --no-default-ops A3 ocf:heartbeat:Dummy --group AGroup".split(), + "resource create --no-default-ops A3 ocf:pcsmock:minimal --group AGroup".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) @@ -1584,9 +1545,9 @@ def test_group_delete_test(self): outdent( """\ * Resource Group: AGroup: - * A1\t(ocf:heartbeat:Dummy):\t Stopped - * A2\t(ocf:heartbeat:Dummy):\t Stopped - * A3\t(ocf:heartbeat:Dummy):\t Stopped + * A1\t(ocf:pcsmock:minimal):\t Stopped + * A2\t(ocf:pcsmock:minimal):\t Stopped + * A3\t(ocf:pcsmock:minimal):\t Stopped """ ), ) @@ -1595,9 +1556,9 @@ def test_group_delete_test(self): stdout, """\ * Resource Group: AGroup: - * A1\t(ocf::heartbeat:Dummy):\tStopped - * A2\t(ocf::heartbeat:Dummy):\tStopped - * A3\t(ocf::heartbeat:Dummy):\tStopped + * A1\t(ocf::pcsmock:minimal):\tStopped + * A2\t(ocf::pcsmock:minimal):\tStopped + * A3\t(ocf::pcsmock:minimal):\tStopped """, ) else: @@ -1605,9 +1566,9 @@ def test_group_delete_test(self): stdout, """\ Resource Group: AGroup - A1\t(ocf::heartbeat:Dummy):\tStopped - A2\t(ocf::heartbeat:Dummy):\tStopped - A3\t(ocf::heartbeat:Dummy):\tStopped + A1\t(ocf::pcsmock:minimal):\tStopped + A2\t(ocf::pcsmock:minimal):\tStopped + A3\t(ocf::pcsmock:minimal):\tStopped """, ) @@ -1652,19 +1613,19 @@ def test_group_ungroup(self): ) self.assert_pcs_success( - "resource create --no-default-ops A1 ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops A1 ocf:pcsmock:minimal".split(), ) self.assert_pcs_success( - "resource create --no-default-ops A2 ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops A2 ocf:pcsmock:minimal".split(), ) self.assert_pcs_success( - "resource create --no-default-ops A3 ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops A3 ocf:pcsmock:minimal".split(), ) self.assert_pcs_success( - "resource create --no-default-ops A4 ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops A4 ocf:pcsmock:minimal".split(), ) self.assert_pcs_success( - "resource create --no-default-ops A5 ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops A5 ocf:pcsmock:minimal".split(), ) self.assert_pcs_success( @@ -1676,23 +1637,23 @@ def test_group_ungroup(self): dedent( """\ Group: AGroup - Resource: A1 (class=ocf provider=heartbeat type=Dummy) + Resource: A1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: A1-monitor-interval-10s interval=10s timeout=20s - Resource: A2 (class=ocf provider=heartbeat type=Dummy) + Resource: A2 (class=ocf provider=pcsmock type=minimal) Operations: monitor: A2-monitor-interval-10s interval=10s timeout=20s - Resource: A3 (class=ocf provider=heartbeat type=Dummy) + Resource: A3 (class=ocf provider=pcsmock type=minimal) Operations: monitor: A3-monitor-interval-10s interval=10s timeout=20s - Resource: A4 (class=ocf provider=heartbeat type=Dummy) + Resource: A4 (class=ocf provider=pcsmock type=minimal) Operations: monitor: A4-monitor-interval-10s interval=10s timeout=20s - Resource: A5 (class=ocf provider=heartbeat type=Dummy) + Resource: A5 (class=ocf provider=pcsmock type=minimal) Operations: monitor: A5-monitor-interval-10s interval=10s timeout=20s @@ -1702,6 +1663,7 @@ def test_group_ungroup(self): def test_group_large_resource_remove(self): self.pcs_runner = PcsRunner(self.temp_large_cib.name) + self.pcs_runner.mock_settings = get_mock_settings() self.assert_pcs_success( "resource group add dummies dummylarge".split(), ) @@ -1721,37 +1683,37 @@ def test_group_order(self): # and tests overhaul. However, this is the only test where "resource # group list" is called. Due to that this test was not deleted. self.assert_pcs_success( - "resource create --no-default-ops A ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops A ocf:pcsmock:minimal".split(), ) self.assert_pcs_success( - "resource create --no-default-ops B ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops B ocf:pcsmock:minimal".split(), ) self.assert_pcs_success( - "resource create --no-default-ops C ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops C ocf:pcsmock:minimal".split(), ) self.assert_pcs_success( - "resource create --no-default-ops D ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops D ocf:pcsmock:minimal".split(), ) self.assert_pcs_success( - "resource create --no-default-ops E ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops E ocf:pcsmock:minimal".split(), ) self.assert_pcs_success( - "resource create --no-default-ops F ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops F ocf:pcsmock:minimal".split(), ) self.assert_pcs_success( - "resource create --no-default-ops G ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops G ocf:pcsmock:minimal".split(), ) self.assert_pcs_success( - "resource create --no-default-ops H ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops H ocf:pcsmock:minimal".split(), ) self.assert_pcs_success( - "resource create --no-default-ops I ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops I ocf:pcsmock:minimal".split(), ) self.assert_pcs_success( - "resource create --no-default-ops J ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops J ocf:pcsmock:minimal".split(), ) self.assert_pcs_success( - "resource create --no-default-ops K ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops K ocf:pcsmock:minimal".split(), ) self.assert_pcs_success( @@ -1766,18 +1728,18 @@ def test_group_order(self): stdout, outdent( """\ - * F\t(ocf:heartbeat:Dummy):\t Stopped - * G\t(ocf:heartbeat:Dummy):\t Stopped - * H\t(ocf:heartbeat:Dummy):\t Stopped + * F\t(ocf:pcsmock:minimal):\t Stopped + * G\t(ocf:pcsmock:minimal):\t Stopped + * H\t(ocf:pcsmock:minimal):\t Stopped * Resource Group: RGA: - * A\t(ocf:heartbeat:Dummy):\t Stopped - * B\t(ocf:heartbeat:Dummy):\t Stopped - * C\t(ocf:heartbeat:Dummy):\t Stopped - * E\t(ocf:heartbeat:Dummy):\t Stopped - * D\t(ocf:heartbeat:Dummy):\t Stopped - * K\t(ocf:heartbeat:Dummy):\t Stopped - * J\t(ocf:heartbeat:Dummy):\t Stopped - * I\t(ocf:heartbeat:Dummy):\t Stopped + * A\t(ocf:pcsmock:minimal):\t Stopped + * B\t(ocf:pcsmock:minimal):\t Stopped + * C\t(ocf:pcsmock:minimal):\t Stopped + * E\t(ocf:pcsmock:minimal):\t Stopped + * D\t(ocf:pcsmock:minimal):\t Stopped + * K\t(ocf:pcsmock:minimal):\t Stopped + * J\t(ocf:pcsmock:minimal):\t Stopped + * I\t(ocf:pcsmock:minimal):\t Stopped """ ), ) @@ -1785,36 +1747,36 @@ def test_group_order(self): assert_pcs_status( stdout, """\ - * F\t(ocf::heartbeat:Dummy):\tStopped - * G\t(ocf::heartbeat:Dummy):\tStopped - * H\t(ocf::heartbeat:Dummy):\tStopped + * F\t(ocf::pcsmock:minimal):\tStopped + * G\t(ocf::pcsmock:minimal):\tStopped + * H\t(ocf::pcsmock:minimal):\tStopped * Resource Group: RGA: - * A\t(ocf::heartbeat:Dummy):\tStopped - * B\t(ocf::heartbeat:Dummy):\tStopped - * C\t(ocf::heartbeat:Dummy):\tStopped - * E\t(ocf::heartbeat:Dummy):\tStopped - * D\t(ocf::heartbeat:Dummy):\tStopped - * K\t(ocf::heartbeat:Dummy):\tStopped - * J\t(ocf::heartbeat:Dummy):\tStopped - * I\t(ocf::heartbeat:Dummy):\tStopped + * A\t(ocf::pcsmock:minimal):\tStopped + * B\t(ocf::pcsmock:minimal):\tStopped + * C\t(ocf::pcsmock:minimal):\tStopped + * E\t(ocf::pcsmock:minimal):\tStopped + * D\t(ocf::pcsmock:minimal):\tStopped + * K\t(ocf::pcsmock:minimal):\tStopped + * J\t(ocf::pcsmock:minimal):\tStopped + * I\t(ocf::pcsmock:minimal):\tStopped """, ) else: self.assertEqual( stdout, """\ - F\t(ocf::heartbeat:Dummy):\tStopped - G\t(ocf::heartbeat:Dummy):\tStopped - H\t(ocf::heartbeat:Dummy):\tStopped + F\t(ocf::pcsmock:minimal):\tStopped + G\t(ocf::pcsmock:minimal):\tStopped + H\t(ocf::pcsmock:minimal):\tStopped Resource Group: RGA - A\t(ocf::heartbeat:Dummy):\tStopped - B\t(ocf::heartbeat:Dummy):\tStopped - C\t(ocf::heartbeat:Dummy):\tStopped - E\t(ocf::heartbeat:Dummy):\tStopped - D\t(ocf::heartbeat:Dummy):\tStopped - K\t(ocf::heartbeat:Dummy):\tStopped - J\t(ocf::heartbeat:Dummy):\tStopped - I\t(ocf::heartbeat:Dummy):\tStopped + A\t(ocf::pcsmock:minimal):\tStopped + B\t(ocf::pcsmock:minimal):\tStopped + C\t(ocf::pcsmock:minimal):\tStopped + E\t(ocf::pcsmock:minimal):\tStopped + D\t(ocf::pcsmock:minimal):\tStopped + K\t(ocf::pcsmock:minimal):\tStopped + J\t(ocf::pcsmock:minimal):\tStopped + I\t(ocf::pcsmock:minimal):\tStopped """, ) @@ -1839,61 +1801,43 @@ def test_cluster_config(self): Pacemaker Nodes: Resources: - Resource: ClusterIP6 (class=ocf provider=heartbeat type=IPaddr2) - Attributes: ClusterIP6-instance_attributes - cidr_netmask=32 - ip=192.168.0.96 + Resource: ClusterIP6 (class=ocf provider=pcsmock type=minimal) Operations: - monitor: ClusterIP6-monitor-interval-30s - interval=30s + monitor: ClusterIP6-monitor-interval-10s + interval=10s timeout=20s Group: TestGroup1 - Resource: ClusterIP (class=ocf provider=heartbeat type=IPaddr2) - Attributes: ClusterIP-instance_attributes - cidr_netmask=32 - ip=192.168.0.99 + Resource: ClusterIP (class=ocf provider=pcsmock type=minimal) Operations: - monitor: ClusterIP-monitor-interval-30s - interval=30s + monitor: ClusterIP-monitor-interval-10s + interval=10s timeout=20s Group: TestGroup2 - Resource: ClusterIP2 (class=ocf provider=heartbeat type=IPaddr2) - Attributes: ClusterIP2-instance_attributes - cidr_netmask=32 - ip=192.168.0.92 + Resource: ClusterIP2 (class=ocf provider=pcsmock type=minimal) Operations: - monitor: ClusterIP2-monitor-interval-30s - interval=30s - Resource: ClusterIP3 (class=ocf provider=heartbeat type=IPaddr2) - Attributes: ClusterIP3-instance_attributes - cidr_netmask=32 - ip=192.168.0.93 + monitor: ClusterIP2-monitor-interval-10s + interval=10s timeout=20s + Resource: ClusterIP3 (class=ocf provider=pcsmock type=minimal) Operations: - monitor: ClusterIP3-monitor-interval-30s - interval=30s + monitor: ClusterIP3-monitor-interval-10s + interval=10s timeout=20s Clone: ClusterIP4-clone - Resource: ClusterIP4 (class=ocf provider=heartbeat type=IPaddr2) - Attributes: ClusterIP4-instance_attributes - cidr_netmask=32 - ip=192.168.0.94 + Resource: ClusterIP4 (class=ocf provider=pcsmock type=minimal) Operations: - monitor: ClusterIP4-monitor-interval-30s - interval=30s + monitor: ClusterIP4-monitor-interval-10s + interval=10s timeout=20s Clone: Master Meta Attributes: promotable=true - Resource: ClusterIP5 (class=ocf provider=heartbeat type=IPaddr2) - Attributes: ClusterIP5-instance_attributes - cidr_netmask=32 - ip=192.168.0.95 + Resource: ClusterIP5 (class=ocf provider=pcsmock type=minimal) Operations: - monitor: ClusterIP5-monitor-interval-30s - interval=30s + monitor: ClusterIP5-monitor-interval-10s + interval=10s timeout=20s """ ), ) def test_clone_remove(self): self.assert_pcs_success( - "resource create --no-default-ops D1 ocf:heartbeat:Dummy clone".split(), + "resource create --no-default-ops D1 ocf:pcsmock:minimal clone".split(), ) self.assert_pcs_success( @@ -1911,7 +1855,7 @@ def test_clone_remove(self): dedent( """\ Clone: D1-clone - Resource: D1 (class=ocf provider=heartbeat type=Dummy) + Resource: D1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: D1-monitor-interval-10s interval=10s timeout=20s @@ -1935,7 +1879,7 @@ def test_clone_remove(self): ) self.assert_pcs_success( - "resource create d99 ocf:heartbeat:Dummy clone globally-unique=true".split(), + "resource create d99 ocf:pcsmock:minimal clone globally-unique=true".split(), stderr_full=( "Deprecation Warning: Configuring clone meta attributes without " "specifying the 'meta' keyword after the 'clone' keyword is " @@ -1951,6 +1895,7 @@ def test_clone_remove(self): def test_clone_remove_large(self): self.pcs_runner = PcsRunner(self.temp_large_cib.name) + self.pcs_runner.mock_settings = get_mock_settings() self.assert_pcs_success("resource clone dummylarge".split()) self.assert_pcs_success( "resource delete dummylarge".split(), @@ -1959,6 +1904,7 @@ def test_clone_remove_large(self): def test_clone_group_large_resource_remove(self): self.pcs_runner = PcsRunner(self.temp_large_cib.name) + self.pcs_runner.mock_settings = get_mock_settings() self.assert_pcs_success( "resource group add dummies dummylarge".split(), ) @@ -1999,7 +1945,7 @@ def test_master_slave_remove(self): ) self.assert_pcs_success( - "resource create --no-default-ops ClusterIP5 ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops ClusterIP5 ocf:pcsmock:minimal".split(), ) self.assert_pcs_success( @@ -2024,10 +1970,7 @@ def test_master_slave_remove(self): ) self.assert_pcs_success( - ( - "resource create --no-default-ops ClusterIP5 ocf:heartbeat:IPaddr2" - " cidr_netmask=32 ip=192.168.0.95 op monitor interval=30s" - ).split() + "resource create --no-default-ops ClusterIP5 ocf:pcsmock:minimal".split() ) self.assert_pcs_success( @@ -2053,51 +1996,33 @@ def test_master_slave_remove(self): Pacemaker Nodes: Resources: - Resource: ClusterIP6 (class=ocf provider=heartbeat type=IPaddr2) - Attributes: ClusterIP6-instance_attributes - cidr_netmask=32 - ip=192.168.0.96 + Resource: ClusterIP6 (class=ocf provider=pcsmock type=minimal) Operations: - monitor: ClusterIP6-monitor-interval-30s - interval=30s - Resource: ClusterIP5 (class=ocf provider=heartbeat type=IPaddr2) - Attributes: ClusterIP5-instance_attributes - cidr_netmask=32 - ip=192.168.0.95 + monitor: ClusterIP6-monitor-interval-10s + interval=10s timeout=20s + Resource: ClusterIP5 (class=ocf provider=pcsmock type=minimal) Operations: - monitor: ClusterIP5-monitor-interval-30s - interval=30s + monitor: ClusterIP5-monitor-interval-10s + interval=10s timeout=20s Group: TestGroup1 - Resource: ClusterIP (class=ocf provider=heartbeat type=IPaddr2) - Attributes: ClusterIP-instance_attributes - cidr_netmask=32 - ip=192.168.0.99 + Resource: ClusterIP (class=ocf provider=pcsmock type=minimal) Operations: - monitor: ClusterIP-monitor-interval-30s - interval=30s + monitor: ClusterIP-monitor-interval-10s + interval=10s timeout=20s Group: TestGroup2 - Resource: ClusterIP2 (class=ocf provider=heartbeat type=IPaddr2) - Attributes: ClusterIP2-instance_attributes - cidr_netmask=32 - ip=192.168.0.92 + Resource: ClusterIP2 (class=ocf provider=pcsmock type=minimal) Operations: - monitor: ClusterIP2-monitor-interval-30s - interval=30s - Resource: ClusterIP3 (class=ocf provider=heartbeat type=IPaddr2) - Attributes: ClusterIP3-instance_attributes - cidr_netmask=32 - ip=192.168.0.93 + monitor: ClusterIP2-monitor-interval-10s + interval=10s timeout=20s + Resource: ClusterIP3 (class=ocf provider=pcsmock type=minimal) Operations: - monitor: ClusterIP3-monitor-interval-30s - interval=30s + monitor: ClusterIP3-monitor-interval-10s + interval=10s timeout=20s Clone: ClusterIP4-clone - Resource: ClusterIP4 (class=ocf provider=heartbeat type=IPaddr2) - Attributes: ClusterIP4-instance_attributes - cidr_netmask=32 - ip=192.168.0.94 + Resource: ClusterIP4 (class=ocf provider=pcsmock type=minimal) Operations: - monitor: ClusterIP4-monitor-interval-30s - interval=30s + monitor: ClusterIP4-monitor-interval-10s + interval=10s timeout=20s Location Constraints: resource 'ClusterIP5' prefers node 'rh7-1' with score INFINITY (id: location-ClusterIP5-rh7-1-INFINITY) @@ -2113,6 +2038,7 @@ def test_master_slave_remove(self): wrap_element_by_master(self.temp_large_cib, "dummylarge") self.pcs_runner = PcsRunner(self.temp_large_cib.name) + self.pcs_runner.mock_settings = get_mock_settings() self.assert_pcs_success( "resource delete dummylarge".split(), stderr_full="Deleting Resource - dummylarge\n", @@ -2120,6 +2046,7 @@ def test_master_slave_remove(self): def test_master_slave_group_large_resource_remove(self): self.pcs_runner = PcsRunner(self.temp_large_cib.name) + self.pcs_runner.mock_settings = get_mock_settings() self.assert_pcs_success( "resource group add dummies dummylarge".split(), ) @@ -2140,10 +2067,10 @@ def test_master_slave_group_large_resource_remove(self): def test_ms_group(self): self.assert_pcs_success( - "resource create --no-default-ops D0 ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops D0 ocf:pcsmock:minimal".split(), ) self.assert_pcs_success( - "resource create --no-default-ops D1 ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops D1 ocf:pcsmock:minimal".split(), ) self.assert_pcs_success("resource group add Group D0 D1".split()) @@ -2160,11 +2087,11 @@ def test_ms_group(self): Meta Attributes: promotable=true Group: Group - Resource: D0 (class=ocf provider=heartbeat type=Dummy) + Resource: D0 (class=ocf provider=pcsmock type=minimal) Operations: monitor: D0-monitor-interval-10s interval=10s timeout=20s - Resource: D1 (class=ocf provider=heartbeat type=Dummy) + Resource: D1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: D1-monitor-interval-10s interval=10s timeout=20s @@ -2183,10 +2110,10 @@ def test_ms_group(self): def test_unclone(self): # see also BundleClone self.assert_pcs_success( - "resource create --no-default-ops dummy1 ocf:heartbeat:Dummy".split() + "resource create --no-default-ops dummy1 ocf:pcsmock:minimal".split() ) self.assert_pcs_success( - "resource create --no-default-ops dummy2 ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops dummy2 ocf:pcsmock:minimal".split(), ) self.assert_pcs_success("resource group add gr dummy1".split()) @@ -2204,11 +2131,11 @@ def test_unclone(self): """\ Clone: gr-clone Group: gr - Resource: dummy1 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy1-monitor-interval-10s interval=10s timeout=20s - Resource: dummy2 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy2 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy2-monitor-interval-10s interval=10s timeout=20s @@ -2222,11 +2149,11 @@ def test_unclone(self): dedent( """\ Group: gr - Resource: dummy1 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy1-monitor-interval-10s interval=10s timeout=20s - Resource: dummy2 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy2 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy2-monitor-interval-10s interval=10s timeout=20s @@ -2242,11 +2169,11 @@ def test_unclone(self): """\ Clone: gr-clone Group: gr - Resource: dummy1 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy1-monitor-interval-10s interval=10s timeout=20s - Resource: dummy2 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy2 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy2-monitor-interval-10s interval=10s timeout=20s @@ -2260,11 +2187,11 @@ def test_unclone(self): dedent( """\ Group: gr - Resource: dummy1 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy1-monitor-interval-10s interval=10s timeout=20s - Resource: dummy2 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy2 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy2-monitor-interval-10s interval=10s timeout=20s @@ -2280,11 +2207,11 @@ def test_unclone(self): """\ Clone: gr-clone Group: gr - Resource: dummy1 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy1-monitor-interval-10s interval=10s timeout=20s - Resource: dummy2 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy2 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy2-monitor-interval-10s interval=10s timeout=20s @@ -2297,13 +2224,13 @@ def test_unclone(self): "resource config".split(), dedent( """\ - Resource: dummy1 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy1-monitor-interval-10s interval=10s timeout=20s Clone: gr-clone Group: gr - Resource: dummy2 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy2 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy2-monitor-interval-10s interval=10s timeout=20s @@ -2316,11 +2243,11 @@ def test_unclone(self): "resource config".split(), dedent( """\ - Resource: dummy1 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy1-monitor-interval-10s interval=10s timeout=20s - Resource: dummy2 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy2 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy2-monitor-interval-10s interval=10s timeout=20s @@ -2331,18 +2258,10 @@ def test_unclone(self): def test_unclone_master(self): # see also BundleClone self.assert_pcs_success( - "resource create --no-default-ops dummy1 ocf:pacemaker:Stateful".split(), - stderr_full=( - "Warning: changing a monitor operation interval from 10s to 11 " - "to make the operation unique\n" - ), + "resource create --no-default-ops dummy1 ocf:pcsmock:stateful".split(), ) self.assert_pcs_success( - "resource create --no-default-ops dummy2 ocf:pacemaker:Stateful".split(), - stderr_full=( - "Warning: changing a monitor operation interval from 10s to 11 " - "to make the operation unique\n" - ), + "resource create --no-default-ops dummy2 ocf:pcsmock:stateful".split(), ) # try to unclone a non-cloned resource @@ -2368,21 +2287,21 @@ def test_unclone_master(self): dedent( f"""\ Group: gr - Resource: dummy1 (class=ocf provider=pacemaker type=Stateful) + Resource: dummy1 (class=ocf provider=pcsmock type=stateful) Operations: monitor: dummy1-monitor-interval-10s interval=10s timeout=20s role={const.PCMK_ROLE_PROMOTED_PRIMARY} - monitor: dummy1-monitor-interval-11 - interval=11 timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} + monitor: dummy1-monitor-interval-11s + interval=11s timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} Clone: dummy2-master Meta Attributes: promotable=true - Resource: dummy2 (class=ocf provider=pacemaker type=Stateful) + Resource: dummy2 (class=ocf provider=pcsmock type=stateful) Operations: monitor: dummy2-monitor-interval-10s interval=10s timeout=20s role={const.PCMK_ROLE_PROMOTED_PRIMARY} - monitor: dummy2-monitor-interval-11 - interval=11 timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} + monitor: dummy2-monitor-interval-11s + interval=11s timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} """ ), ) @@ -2392,19 +2311,19 @@ def test_unclone_master(self): "resource config".split(), dedent( f"""\ - Resource: dummy2 (class=ocf provider=pacemaker type=Stateful) + Resource: dummy2 (class=ocf provider=pcsmock type=stateful) Operations: monitor: dummy2-monitor-interval-10s interval=10s timeout=20s role={const.PCMK_ROLE_PROMOTED_PRIMARY} - monitor: dummy2-monitor-interval-11 - interval=11 timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} + monitor: dummy2-monitor-interval-11s + interval=11s timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} Group: gr - Resource: dummy1 (class=ocf provider=pacemaker type=Stateful) + Resource: dummy1 (class=ocf provider=pcsmock type=stateful) Operations: monitor: dummy1-monitor-interval-10s interval=10s timeout=20s role={const.PCMK_ROLE_PROMOTED_PRIMARY} - monitor: dummy1-monitor-interval-11 - interval=11 timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} + monitor: dummy1-monitor-interval-11s + interval=11s timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} """ ), ) @@ -2423,18 +2342,18 @@ def test_unclone_master(self): Meta Attributes: promotable=true Group: gr - Resource: dummy1 (class=ocf provider=pacemaker type=Stateful) + Resource: dummy1 (class=ocf provider=pcsmock type=stateful) Operations: monitor: dummy1-monitor-interval-10s interval=10s timeout=20s role={const.PCMK_ROLE_PROMOTED_PRIMARY} - monitor: dummy1-monitor-interval-11 - interval=11 timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} - Resource: dummy2 (class=ocf provider=pacemaker type=Stateful) + monitor: dummy1-monitor-interval-11s + interval=11s timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} + Resource: dummy2 (class=ocf provider=pcsmock type=stateful) Operations: monitor: dummy2-monitor-interval-10s interval=10s timeout=20s role={const.PCMK_ROLE_PROMOTED_PRIMARY} - monitor: dummy2-monitor-interval-11 - interval=11 timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} + monitor: dummy2-monitor-interval-11s + interval=11s timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} """ ), ) @@ -2445,18 +2364,18 @@ def test_unclone_master(self): dedent( f"""\ Group: gr - Resource: dummy1 (class=ocf provider=pacemaker type=Stateful) + Resource: dummy1 (class=ocf provider=pcsmock type=stateful) Operations: monitor: dummy1-monitor-interval-10s interval=10s timeout=20s role={const.PCMK_ROLE_PROMOTED_PRIMARY} - monitor: dummy1-monitor-interval-11 - interval=11 timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} - Resource: dummy2 (class=ocf provider=pacemaker type=Stateful) + monitor: dummy1-monitor-interval-11s + interval=11s timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} + Resource: dummy2 (class=ocf provider=pcsmock type=stateful) Operations: monitor: dummy2-monitor-interval-10s interval=10s timeout=20s role={const.PCMK_ROLE_PROMOTED_PRIMARY} - monitor: dummy2-monitor-interval-11 - interval=11 timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} + monitor: dummy2-monitor-interval-11s + interval=11s timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} """ ), ) @@ -2474,18 +2393,18 @@ def test_unclone_master(self): Meta Attributes: promotable=true Group: gr - Resource: dummy1 (class=ocf provider=pacemaker type=Stateful) + Resource: dummy1 (class=ocf provider=pcsmock type=stateful) Operations: monitor: dummy1-monitor-interval-10s interval=10s timeout=20s role={const.PCMK_ROLE_PROMOTED_PRIMARY} - monitor: dummy1-monitor-interval-11 - interval=11 timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} - Resource: dummy2 (class=ocf provider=pacemaker type=Stateful) + monitor: dummy1-monitor-interval-11s + interval=11s timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} + Resource: dummy2 (class=ocf provider=pcsmock type=stateful) Operations: monitor: dummy2-monitor-interval-10s interval=10s timeout=20s role={const.PCMK_ROLE_PROMOTED_PRIMARY} - monitor: dummy2-monitor-interval-11 - interval=11 timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} + monitor: dummy2-monitor-interval-11s + interval=11s timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} """ ), ) @@ -2496,18 +2415,18 @@ def test_unclone_master(self): dedent( f"""\ Group: gr - Resource: dummy1 (class=ocf provider=pacemaker type=Stateful) + Resource: dummy1 (class=ocf provider=pcsmock type=stateful) Operations: monitor: dummy1-monitor-interval-10s interval=10s timeout=20s role={const.PCMK_ROLE_PROMOTED_PRIMARY} - monitor: dummy1-monitor-interval-11 - interval=11 timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} - Resource: dummy2 (class=ocf provider=pacemaker type=Stateful) + monitor: dummy1-monitor-interval-11s + interval=11s timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} + Resource: dummy2 (class=ocf provider=pcsmock type=stateful) Operations: monitor: dummy2-monitor-interval-10s interval=10s timeout=20s role={const.PCMK_ROLE_PROMOTED_PRIMARY} - monitor: dummy2-monitor-interval-11 - interval=11 timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} + monitor: dummy2-monitor-interval-11s + interval=11s timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} """ ), ) @@ -2522,22 +2441,22 @@ def test_unclone_master(self): "resource config".split(), dedent( f"""\ - Resource: dummy2 (class=ocf provider=pacemaker type=Stateful) + Resource: dummy2 (class=ocf provider=pcsmock type=stateful) Operations: monitor: dummy2-monitor-interval-10s interval=10s timeout=20s role={const.PCMK_ROLE_PROMOTED_PRIMARY} - monitor: dummy2-monitor-interval-11 - interval=11 timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} + monitor: dummy2-monitor-interval-11s + interval=11s timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} Clone: gr-master Meta Attributes: promotable=true Group: gr - Resource: dummy1 (class=ocf provider=pacemaker type=Stateful) + Resource: dummy1 (class=ocf provider=pcsmock type=stateful) Operations: monitor: dummy1-monitor-interval-10s interval=10s timeout=20s role={const.PCMK_ROLE_PROMOTED_PRIMARY} - monitor: dummy1-monitor-interval-11 - interval=11 timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} + monitor: dummy1-monitor-interval-11s + interval=11s timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} """ ), ) @@ -2547,18 +2466,18 @@ def test_unclone_master(self): "resource config".split(), dedent( f"""\ - Resource: dummy2 (class=ocf provider=pacemaker type=Stateful) + Resource: dummy2 (class=ocf provider=pcsmock type=stateful) Operations: monitor: dummy2-monitor-interval-10s interval=10s timeout=20s role={const.PCMK_ROLE_PROMOTED_PRIMARY} - monitor: dummy2-monitor-interval-11 - interval=11 timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} - Resource: dummy1 (class=ocf provider=pacemaker type=Stateful) + monitor: dummy2-monitor-interval-11s + interval=11s timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} + Resource: dummy1 (class=ocf provider=pcsmock type=stateful) Operations: monitor: dummy1-monitor-interval-10s interval=10s timeout=20s role={const.PCMK_ROLE_PROMOTED_PRIMARY} - monitor: dummy1-monitor-interval-11 - interval=11 timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} + monitor: dummy1-monitor-interval-11s + interval=11s timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} """ ), ) @@ -2577,18 +2496,18 @@ def test_unclone_master(self): Meta Attributes: promotable=true Group: gr - Resource: dummy1 (class=ocf provider=pacemaker type=Stateful) + Resource: dummy1 (class=ocf provider=pcsmock type=stateful) Operations: monitor: dummy1-monitor-interval-10s interval=10s timeout=20s role={const.PCMK_ROLE_PROMOTED_PRIMARY} - monitor: dummy1-monitor-interval-11 - interval=11 timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} - Resource: dummy2 (class=ocf provider=pacemaker type=Stateful) + monitor: dummy1-monitor-interval-11s + interval=11s timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} + Resource: dummy2 (class=ocf provider=pcsmock type=stateful) Operations: monitor: dummy2-monitor-interval-10s interval=10s timeout=20s role={const.PCMK_ROLE_PROMOTED_PRIMARY} - monitor: dummy2-monitor-interval-11 - interval=11 timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} + monitor: dummy2-monitor-interval-11s + interval=11s timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} """ ), ) @@ -2599,33 +2518,33 @@ def test_unclone_master(self): "resource config".split(), dedent( f"""\ - Resource: dummy2 (class=ocf provider=pacemaker type=Stateful) + Resource: dummy2 (class=ocf provider=pcsmock type=stateful) Operations: monitor: dummy2-monitor-interval-10s interval=10s timeout=20s role={const.PCMK_ROLE_PROMOTED_PRIMARY} - monitor: dummy2-monitor-interval-11 - interval=11 timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} + monitor: dummy2-monitor-interval-11s + interval=11s timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} Clone: gr-master Meta Attributes: promotable=true Group: gr - Resource: dummy1 (class=ocf provider=pacemaker type=Stateful) + Resource: dummy1 (class=ocf provider=pcsmock type=stateful) Operations: monitor: dummy1-monitor-interval-10s interval=10s timeout=20s role={const.PCMK_ROLE_PROMOTED_PRIMARY} - monitor: dummy1-monitor-interval-11 - interval=11 timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} + monitor: dummy1-monitor-interval-11s + interval=11s timeout=20s role={const.PCMK_ROLE_UNPROMOTED_PRIMARY} """ ), ) def test_clone_group_member(self): self.assert_pcs_success( - "resource create --no-default-ops D0 ocf:heartbeat:Dummy --group AG".split(), + "resource create --no-default-ops D0 ocf:pcsmock:minimal --group AG".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) self.assert_pcs_success( - "resource create --no-default-ops D1 ocf:heartbeat:Dummy --group AG".split(), + "resource create --no-default-ops D1 ocf:pcsmock:minimal --group AG".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) @@ -2635,12 +2554,12 @@ def test_clone_group_member(self): dedent( """\ Group: AG - Resource: D1 (class=ocf provider=heartbeat type=Dummy) + Resource: D1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: D1-monitor-interval-10s interval=10s timeout=20s Clone: D0-clone - Resource: D0 (class=ocf provider=heartbeat type=Dummy) + Resource: D0 (class=ocf provider=pcsmock type=minimal) Operations: monitor: D0-monitor-interval-10s interval=10s timeout=20s @@ -2654,12 +2573,12 @@ def test_clone_group_member(self): dedent( """\ Clone: D0-clone - Resource: D0 (class=ocf provider=heartbeat type=Dummy) + Resource: D0 (class=ocf provider=pcsmock type=minimal) Operations: monitor: D0-monitor-interval-10s interval=10s timeout=20s Clone: D1-clone - Resource: D1 (class=ocf provider=heartbeat type=Dummy) + Resource: D1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: D1-monitor-interval-10s interval=10s timeout=20s @@ -2669,11 +2588,11 @@ def test_clone_group_member(self): def test_promotable_group_member(self): self.assert_pcs_success( - "resource create --no-default-ops D0 ocf:heartbeat:Dummy --group AG".split(), + "resource create --no-default-ops D0 ocf:pcsmock:stateful --group AG".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) self.assert_pcs_success( - "resource create --no-default-ops D1 ocf:heartbeat:Dummy --group AG".split(), + "resource create --no-default-ops D1 ocf:pcsmock:stateful --group AG".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) @@ -2683,17 +2602,21 @@ def test_promotable_group_member(self): dedent( """\ Group: AG - Resource: D1 (class=ocf provider=heartbeat type=Dummy) + Resource: D1 (class=ocf provider=pcsmock type=stateful) Operations: monitor: D1-monitor-interval-10s - interval=10s timeout=20s + interval=10s timeout=20s role=Promoted + monitor: D1-monitor-interval-11s + interval=11s timeout=20s role=Unpromoted Clone: D0-clone Meta Attributes: D0-clone-meta_attributes promotable=true - Resource: D0 (class=ocf provider=heartbeat type=Dummy) + Resource: D0 (class=ocf provider=pcsmock type=stateful) Operations: monitor: D0-monitor-interval-10s - interval=10s timeout=20s + interval=10s timeout=20s role=Promoted + monitor: D0-monitor-interval-11s + interval=11s timeout=20s role=Unpromoted """ ), ) @@ -2706,17 +2629,21 @@ def test_promotable_group_member(self): Clone: D0-clone Meta Attributes: D0-clone-meta_attributes promotable=true - Resource: D0 (class=ocf provider=heartbeat type=Dummy) + Resource: D0 (class=ocf provider=pcsmock type=stateful) Operations: monitor: D0-monitor-interval-10s - interval=10s timeout=20s + interval=10s timeout=20s role=Promoted + monitor: D0-monitor-interval-11s + interval=11s timeout=20s role=Unpromoted Clone: D1-clone Meta Attributes: D1-clone-meta_attributes promotable=true - Resource: D1 (class=ocf provider=heartbeat type=Dummy) + Resource: D1 (class=ocf provider=pcsmock type=stateful) Operations: monitor: D1-monitor-interval-10s - interval=10s timeout=20s + interval=10s timeout=20s role=Promoted + monitor: D1-monitor-interval-11s + interval=11s timeout=20s role=Unpromoted """ ), ) @@ -2724,16 +2651,16 @@ def test_promotable_group_member(self): def test_clone_master(self): # see also BundleClone self.assert_pcs_success( - "resource create --no-default-ops D0 ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops D0 ocf:pcsmock:stateful".split(), ) self.assert_pcs_success( - "resource create --no-default-ops D1 ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops D1 ocf:pcsmock:stateful".split(), ) self.assert_pcs_success( - "resource create --no-default-ops D2 ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops D2 ocf:pcsmock:stateful".split(), ) self.assert_pcs_success( - "resource create --no-default-ops D3 ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops D3 ocf:pcsmock:stateful".split(), ) self.assert_pcs_success("resource clone D0".split()) @@ -2761,31 +2688,39 @@ def test_clone_master(self): dedent( """\ Clone: D0-clone - Resource: D0 (class=ocf provider=heartbeat type=Dummy) + Resource: D0 (class=ocf provider=pcsmock type=stateful) Operations: monitor: D0-monitor-interval-10s - interval=10s timeout=20s + interval=10s timeout=20s role=Promoted + monitor: D0-monitor-interval-11s + interval=11s timeout=20s role=Unpromoted Clone: D3-clone Meta Attributes: D3-clone-meta_attributes promotable=true - Resource: D3 (class=ocf provider=heartbeat type=Dummy) + Resource: D3 (class=ocf provider=pcsmock type=stateful) Operations: monitor: D3-monitor-interval-10s - interval=10s timeout=20s + interval=10s timeout=20s role=Promoted + monitor: D3-monitor-interval-11s + interval=11s timeout=20s role=Unpromoted Clone: D1-master-custom Meta Attributes: promotable=true - Resource: D1 (class=ocf provider=heartbeat type=Dummy) + Resource: D1 (class=ocf provider=pcsmock type=stateful) Operations: monitor: D1-monitor-interval-10s - interval=10s timeout=20s + interval=10s timeout=20s role=Promoted + monitor: D1-monitor-interval-11s + interval=11s timeout=20s role=Unpromoted Clone: D2-master Meta Attributes: promotable=true - Resource: D2 (class=ocf provider=heartbeat type=Dummy) + Resource: D2 (class=ocf provider=pcsmock type=stateful) Operations: monitor: D2-monitor-interval-10s - interval=10s timeout=20s + interval=10s timeout=20s role=Promoted + monitor: D2-monitor-interval-11s + interval=11s timeout=20s role=Unpromoted """ ), ) @@ -2800,51 +2735,59 @@ def test_clone_master(self): ) self.assert_pcs_success( - "resource create --no-default-ops D0 ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops D0 ocf:pcsmock:stateful".split(), ) self.assert_pcs_success( - "resource create --no-default-ops D2 ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops D2 ocf:pcsmock:stateful".split(), ) self.assert_pcs_success( "resource config".split(), dedent( """\ - Resource: D0 (class=ocf provider=heartbeat type=Dummy) + Resource: D0 (class=ocf provider=pcsmock type=stateful) Operations: monitor: D0-monitor-interval-10s - interval=10s timeout=20s - Resource: D2 (class=ocf provider=heartbeat type=Dummy) + interval=10s timeout=20s role=Promoted + monitor: D0-monitor-interval-11s + interval=11s timeout=20s role=Unpromoted + Resource: D2 (class=ocf provider=pcsmock type=stateful) Operations: monitor: D2-monitor-interval-10s - interval=10s timeout=20s + interval=10s timeout=20s role=Promoted + monitor: D2-monitor-interval-11s + interval=11s timeout=20s role=Unpromoted Clone: D3-clone Meta Attributes: D3-clone-meta_attributes promotable=true - Resource: D3 (class=ocf provider=heartbeat type=Dummy) + Resource: D3 (class=ocf provider=pcsmock type=stateful) Operations: monitor: D3-monitor-interval-10s - interval=10s timeout=20s + interval=10s timeout=20s role=Promoted + monitor: D3-monitor-interval-11s + interval=11s timeout=20s role=Unpromoted Clone: D1-master-custom Meta Attributes: promotable=true - Resource: D1 (class=ocf provider=heartbeat type=Dummy) + Resource: D1 (class=ocf provider=pcsmock type=stateful) Operations: monitor: D1-monitor-interval-10s - interval=10s timeout=20s + interval=10s timeout=20s role=Promoted + monitor: D1-monitor-interval-11s + interval=11s timeout=20s role=Unpromoted """ ), ) def test_lsb_resource(self): self.assert_pcs_fail( - "resource create --no-default-ops D2 lsb:network foo=bar".split(), + "resource create --no-default-ops D2 lsb:pcsmock foo=bar".split(), ( "Error: invalid resource option 'foo', there are no options" " allowed, use --force to override\n" + ERRORS_HAVE_OCCURRED ), ) self.assert_pcs_success( - "resource create --no-default-ops D2 lsb:network foo=bar --force".split(), + "resource create --no-default-ops D2 lsb:pcsmock foo=bar --force".split(), stderr_full=( "Warning: invalid resource option 'foo', there are no options" " allowed\n" @@ -2854,7 +2797,7 @@ def test_lsb_resource(self): "resource config".split(), dedent( """\ - Resource: D2 (class=lsb type=network) + Resource: D2 (class=lsb type=pcsmock) Attributes: D2-instance_attributes foo=bar Operations: @@ -2882,7 +2825,7 @@ def test_lsb_resource(self): "resource config".split(), dedent( """\ - Resource: D2 (class=lsb type=network) + Resource: D2 (class=lsb type=pcsmock) Attributes: D2-instance_attributes bar=baz foo=bar @@ -2899,15 +2842,15 @@ def test_lsb_resource(self): ) def test_debug_start_clone_group(self): self.assert_pcs_success( - "resource create D0 ocf:heartbeat:Dummy --group DGroup".split(), + "resource create D0 ocf:pcsmock:minimal --group DGroup".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) self.assert_pcs_success( - "resource create D1 ocf:heartbeat:Dummy --group DGroup".split(), + "resource create D1 ocf:pcsmock:minimal --group DGroup".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) self.assert_pcs_success( - "resource create D2 ocf:heartbeat:Dummy clone".split(), + "resource create D2 ocf:pcsmock:minimal clone".split(), ) # pcs no longer allows creating masters but supports existing ones. In @@ -2929,7 +2872,7 @@ def test_debug_start_clone_group(self): def test_group_clone_creation(self): self.assert_pcs_success( - "resource create --no-default-ops D1 ocf:heartbeat:Dummy --group DGroup".split(), + "resource create --no-default-ops D1 ocf:pcsmock:minimal --group DGroup".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) @@ -2945,7 +2888,7 @@ def test_group_clone_creation(self): """\ Clone: DGroup-clone Group: DGroup - Resource: D1 (class=ocf provider=heartbeat type=Dummy) + Resource: D1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: D1-monitor-interval-10s interval=10s timeout=20s @@ -2960,7 +2903,7 @@ def test_group_clone_creation(self): def test_group_promotable_creation(self): self.assert_pcs_success( - "resource create --no-default-ops D1 ocf:heartbeat:Dummy --group DGroup".split(), + "resource create --no-default-ops D1 ocf:pcsmock:stateful --group DGroup".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) @@ -2978,10 +2921,12 @@ def test_group_promotable_creation(self): Meta Attributes: DGroup-clone-meta_attributes promotable=true Group: DGroup - Resource: D1 (class=ocf provider=heartbeat type=Dummy) + Resource: D1 (class=ocf provider=pcsmock type=stateful) Operations: monitor: D1-monitor-interval-10s - interval=10s timeout=20s + interval=10s timeout=20s role=Promoted + monitor: D1-monitor-interval-11s + interval=11s timeout=20s role=Unpromoted """ ), ) @@ -2993,10 +2938,6 @@ def test_group_promotable_creation(self): @skip_unless_crm_rule() def test_group_remove_with_constraints1(self): - # The mock executable for crm_resource does not support the - # `move-with-constraint` command, and so the real executable is used. - self.pcs_runner.mock_settings = {} - # Load nodes into cib so move will work self.temp_cib.seek(0) xml = etree.fromstring(self.temp_cib.read()) @@ -3006,11 +2947,11 @@ def test_group_remove_with_constraints1(self): write_data_to_tmpfile(etree.tounicode(xml), self.temp_cib) self.assert_pcs_success( - "resource create --no-default-ops D1 ocf:heartbeat:Dummy --group DGroup".split(), + "resource create --no-default-ops D1 ocf:pcsmock:minimal --group DGroup".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) self.assert_pcs_success( - "resource create --no-default-ops D2 ocf:heartbeat:Dummy --group DGroup".split(), + "resource create --no-default-ops D2 ocf:pcsmock:minimal --group DGroup".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) @@ -3025,8 +2966,8 @@ def test_group_remove_with_constraints1(self): outdent( """\ * Resource Group: DGroup: - * D1\t(ocf:heartbeat:Dummy):\t Stopped - * D2\t(ocf:heartbeat:Dummy):\t Stopped + * D1\t(ocf:pcsmock:minimal):\t Stopped + * D2\t(ocf:pcsmock:minimal):\t Stopped """ ), ) @@ -3035,8 +2976,8 @@ def test_group_remove_with_constraints1(self): stdout, """\ * Resource Group: DGroup: - * D1\t(ocf::heartbeat:Dummy):\tStopped - * D2\t(ocf::heartbeat:Dummy):\tStopped + * D1\t(ocf::pcsmock:minimal):\tStopped + * D2\t(ocf::pcsmock:minimal):\tStopped """, ) else: @@ -3044,11 +2985,14 @@ def test_group_remove_with_constraints1(self): stdout, """\ Resource Group: DGroup - D1\t(ocf::heartbeat:Dummy):\tStopped - D2\t(ocf::heartbeat:Dummy):\tStopped + D1\t(ocf::pcsmock:minimal):\tStopped + D2\t(ocf::pcsmock:minimal):\tStopped """, ) + # The mock executable for crm_resource does not support the + # `move-with-constraint` command, and so the real executable is used. + self.pcs_runner.mock_settings = {} self.assert_pcs_success( "resource move-with-constraint DGroup rh7-1".split(), stderr_full=( @@ -3057,6 +3001,7 @@ def test_group_remove_with_constraints1(self): "\n" ), ) + self.pcs_runner.mock_settings = get_mock_settings() self.assert_pcs_success( ["constraint"], outdent( @@ -3087,27 +3032,28 @@ def test_group_remove_with_constraints1(self): def test_resource_clone_creation(self): self.pcs_runner = PcsRunner(self.temp_large_cib.name) + self.pcs_runner.mock_settings = get_mock_settings() # resource "dummy1" is already in "temp_large_cib self.assert_pcs_success("resource clone dummy1".split()) def test_resource_clone_id(self): self.assert_pcs_success( - "resource create --no-default-ops dummy-clone ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops dummy-clone ocf:pcsmock:minimal".split(), ) self.assert_pcs_success( - "resource create --no-default-ops dummy ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops dummy ocf:pcsmock:minimal".split(), ) self.assert_pcs_success("resource clone dummy".split()) self.assert_pcs_success( "resource config".split(), dedent( """\ - Resource: dummy-clone (class=ocf provider=heartbeat type=Dummy) + Resource: dummy-clone (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy-clone-monitor-interval-10s interval=10s timeout=20s Clone: dummy-clone-1 - Resource: dummy (class=ocf provider=heartbeat type=Dummy) + Resource: dummy (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy-monitor-interval-10s interval=10s timeout=20s @@ -3120,18 +3066,18 @@ def test_resource_clone_id(self): stderr_full="Deleting Resource - dummy\n", ) self.assert_pcs_success( - "resource create --no-default-ops dummy ocf:heartbeat:Dummy clone".split(), + "resource create --no-default-ops dummy ocf:pcsmock:minimal clone".split(), ) self.assert_pcs_success( "resource config".split(), dedent( """\ - Resource: dummy-clone (class=ocf provider=heartbeat type=Dummy) + Resource: dummy-clone (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy-clone-monitor-interval-10s interval=10s timeout=20s Clone: dummy-clone-1 - Resource: dummy (class=ocf provider=heartbeat type=Dummy) + Resource: dummy (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy-monitor-interval-10s interval=10s timeout=20s @@ -3141,27 +3087,31 @@ def test_resource_clone_id(self): def test_resource_promotable_id(self): self.assert_pcs_success( - "resource create --no-default-ops dummy-clone ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops dummy-clone ocf:pcsmock:stateful".split(), ) self.assert_pcs_success( - "resource create --no-default-ops dummy ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops dummy ocf:pcsmock:stateful".split(), ) self.assert_pcs_success("resource promotable dummy".split()) self.assert_pcs_success( "resource config".split(), dedent( """\ - Resource: dummy-clone (class=ocf provider=heartbeat type=Dummy) + Resource: dummy-clone (class=ocf provider=pcsmock type=stateful) Operations: monitor: dummy-clone-monitor-interval-10s - interval=10s timeout=20s + interval=10s timeout=20s role=Promoted + monitor: dummy-clone-monitor-interval-11s + interval=11s timeout=20s role=Unpromoted Clone: dummy-clone-1 Meta Attributes: dummy-clone-1-meta_attributes promotable=true - Resource: dummy (class=ocf provider=heartbeat type=Dummy) + Resource: dummy (class=ocf provider=pcsmock type=stateful) Operations: monitor: dummy-monitor-interval-10s - interval=10s timeout=20s + interval=10s timeout=20s role=Promoted + monitor: dummy-monitor-interval-11s + interval=11s timeout=20s role=Unpromoted """ ), ) @@ -3171,37 +3121,41 @@ def test_resource_promotable_id(self): stderr_full="Deleting Resource - dummy\n", ) self.assert_pcs_success( - "resource create --no-default-ops dummy ocf:heartbeat:Dummy promotable".split(), + "resource create --no-default-ops dummy ocf:pcsmock:stateful promotable".split(), ) self.assert_pcs_success( "resource config".split(), dedent( """\ - Resource: dummy-clone (class=ocf provider=heartbeat type=Dummy) + Resource: dummy-clone (class=ocf provider=pcsmock type=stateful) Operations: monitor: dummy-clone-monitor-interval-10s - interval=10s timeout=20s + interval=10s timeout=20s role=Promoted + monitor: dummy-clone-monitor-interval-11s + interval=11s timeout=20s role=Unpromoted Clone: dummy-clone-1 Meta Attributes: dummy-clone-1-meta_attributes promotable=true - Resource: dummy (class=ocf provider=heartbeat type=Dummy) + Resource: dummy (class=ocf provider=pcsmock type=stateful) Operations: monitor: dummy-monitor-interval-10s - interval=10s timeout=20s + interval=10s timeout=20s role=Promoted + monitor: dummy-monitor-interval-11s + interval=11s timeout=20s role=Unpromoted """ ), ) def test_resource_clone_update(self): self.assert_pcs_success( - "resource create --no-default-ops D1 ocf:heartbeat:Dummy clone".split(), + "resource create --no-default-ops D1 ocf:pcsmock:minimal clone".split(), ) self.assert_pcs_success( "resource config".split(), dedent( """\ Clone: D1-clone - Resource: D1 (class=ocf provider=heartbeat type=Dummy) + Resource: D1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: D1-monitor-interval-10s interval=10s timeout=20s @@ -3217,7 +3171,7 @@ def test_resource_clone_update(self): Clone: D1-clone Meta Attributes: D1-clone-meta_attributes foo=bar - Resource: D1 (class=ocf provider=heartbeat type=Dummy) + Resource: D1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: D1-monitor-interval-10s interval=10s timeout=20s @@ -3234,7 +3188,7 @@ def test_resource_clone_update(self): Meta Attributes: D1-clone-meta_attributes bar=baz foo=bar - Resource: D1 (class=ocf provider=heartbeat type=Dummy) + Resource: D1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: D1-monitor-interval-10s interval=10s timeout=20s @@ -3250,7 +3204,7 @@ def test_resource_clone_update(self): Clone: D1-clone Meta Attributes: D1-clone-meta_attributes bar=baz - Resource: D1 (class=ocf provider=heartbeat type=Dummy) + Resource: D1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: D1-monitor-interval-10s interval=10s timeout=20s @@ -3260,11 +3214,11 @@ def test_resource_clone_update(self): def test_group_remove_with_constraints2(self): self.assert_pcs_success( - "resource create --no-default-ops A ocf:heartbeat:Dummy --group AG".split(), + "resource create --no-default-ops A ocf:pcsmock:minimal --group AG".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) self.assert_pcs_success( - "resource create --no-default-ops B ocf:heartbeat:Dummy --group AG".split(), + "resource create --no-default-ops B ocf:pcsmock:minimal --group AG".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) self.assert_pcs_success( @@ -3281,11 +3235,11 @@ def test_group_remove_with_constraints2(self): "resource config".split(), dedent( """\ - Resource: A (class=ocf provider=heartbeat type=Dummy) + Resource: A (class=ocf provider=pcsmock type=minimal) Operations: monitor: A-monitor-interval-10s interval=10s timeout=20s - Resource: B (class=ocf provider=heartbeat type=Dummy) + Resource: B (class=ocf provider=pcsmock type=minimal) Operations: monitor: B-monitor-interval-10s interval=10s timeout=20s @@ -3294,11 +3248,11 @@ def test_group_remove_with_constraints2(self): ) self.assert_pcs_success( - "resource create --no-default-ops A1 ocf:heartbeat:Dummy --group AA".split(), + "resource create --no-default-ops A1 ocf:pcsmock:minimal --group AA".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) self.assert_pcs_success( - "resource create --no-default-ops A2 ocf:heartbeat:Dummy --group AA".split(), + "resource create --no-default-ops A2 ocf:pcsmock:minimal --group AA".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) # pcs no longer allows turning resources into masters but supports @@ -3328,15 +3282,15 @@ def test_group_remove_with_constraints2(self): def test_mastered_group(self): self.assert_pcs_success( - "resource create --no-default-ops A ocf:heartbeat:Dummy --group AG".split(), + "resource create --no-default-ops A ocf:pcsmock:minimal --group AG".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) self.assert_pcs_success( - "resource create --no-default-ops B ocf:heartbeat:Dummy --group AG".split(), + "resource create --no-default-ops B ocf:pcsmock:minimal --group AG".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) self.assert_pcs_success( - "resource create --no-default-ops C ocf:heartbeat:Dummy --group AG".split(), + "resource create --no-default-ops C ocf:pcsmock:minimal --group AG".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) # pcs no longer allows turning resources into masters but supports @@ -3345,15 +3299,15 @@ def test_mastered_group(self): wrap_element_by_master(self.temp_cib, "AG", master_id="AGMaster") self.assert_pcs_fail( - "resource create --no-default-ops A ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops A ocf:pcsmock:minimal".split(), "Error: 'A' already exists\n", ) self.assert_pcs_fail( - "resource create --no-default-ops AG ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops AG ocf:pcsmock:minimal".split(), "Error: 'AG' already exists\n", ) self.assert_pcs_fail( - "resource create --no-default-ops AGMaster ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops AGMaster ocf:pcsmock:minimal".split(), "Error: 'AGMaster' already exists\n", ) @@ -3378,7 +3332,7 @@ def test_mastered_group(self): Clone: AGMaster Meta Attributes: promotable=true - Resource: A (class=ocf provider=heartbeat type=Dummy) + Resource: A (class=ocf provider=pcsmock type=minimal) Operations: monitor: A-monitor-interval-10s interval=10s timeout=20s @@ -3388,11 +3342,11 @@ def test_mastered_group(self): def test_cloned_group(self): self.assert_pcs_success( - "resource create --no-default-ops D1 ocf:heartbeat:Dummy --group DG".split(), + "resource create --no-default-ops D1 ocf:pcsmock:minimal --group DG".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) self.assert_pcs_success( - "resource create --no-default-ops D2 ocf:heartbeat:Dummy --group DG".split(), + "resource create --no-default-ops D2 ocf:pcsmock:minimal --group DG".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) self.assert_pcs_success("resource clone DG".split()) @@ -3402,11 +3356,11 @@ def test_cloned_group(self): """\ Clone: DG-clone Group: DG - Resource: D1 (class=ocf provider=heartbeat type=Dummy) + Resource: D1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: D1-monitor-interval-10s interval=10s timeout=20s - Resource: D2 (class=ocf provider=heartbeat type=Dummy) + Resource: D2 (class=ocf provider=pcsmock type=minimal) Operations: monitor: D2-monitor-interval-10s interval=10s timeout=20s @@ -3415,30 +3369,30 @@ def test_cloned_group(self): ) self.assert_pcs_fail( - "resource create --no-default-ops D1 ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops D1 ocf:pcsmock:minimal".split(), "Error: 'D1' already exists\n", ) self.assert_pcs_fail( - "resource create --no-default-ops DG ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops DG ocf:pcsmock:minimal".split(), "Error: 'DG' already exists\n", ) self.assert_pcs_fail( - "resource create --no-default-ops DG-clone ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops DG-clone ocf:pcsmock:minimal".split(), "Error: 'DG-clone' already exists\n", ) def test_op_option(self): self.assert_pcs_success( - "resource create --no-default-ops B ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops B ocf:pcsmock:minimal".split(), ) self.assert_pcs_fail( - "resource update B ocf:heartbeat:Dummy op monitor interval=30s blah=blah".split(), + "resource update B ocf:pcsmock:minimal op monitor interval=30s blah=blah".split(), "Error: blah is not a valid op option (use --force to override)\n", ) self.assert_pcs_success( - "resource create --no-default-ops C ocf:heartbeat:Dummy".split(), + "resource create --no-default-ops C ocf:pcsmock:minimal".split(), ) self.assert_pcs_fail( @@ -3457,11 +3411,11 @@ def test_op_option(self): "resource config".split(), dedent( """\ - Resource: B (class=ocf provider=heartbeat type=Dummy) + Resource: B (class=ocf provider=pcsmock type=minimal) Operations: monitor: B-monitor-interval-10s interval=10s timeout=20s - Resource: C (class=ocf provider=heartbeat type=Dummy) + Resource: C (class=ocf provider=pcsmock type=minimal) Operations: monitor: C-monitor-interval-10s interval=10s timeout=20s @@ -3484,13 +3438,13 @@ def test_op_option(self): "resource config".split(), dedent( f"""\ - Resource: B (class=ocf provider=heartbeat type=Dummy) + Resource: B (class=ocf provider=pcsmock type=minimal) Operations: monitor: B-monitor-interval-30s interval=30s monitor: B-monitor-interval-31s interval=31s role={const.PCMK_ROLE_PROMOTED_PRIMARY} - Resource: C (class=ocf provider=heartbeat type=Dummy) + Resource: C (class=ocf provider=pcsmock type=minimal) Operations: monitor: C-monitor-interval-10s interval=10s timeout=20s @@ -3524,19 +3478,19 @@ def test_clone_bad_resources(self): def test_group_ms_and_clone(self): self.assert_pcs_fail( - "resource create --no-default-ops D3 ocf:heartbeat:Dummy promotable --group xxx clone".split(), + "resource create --no-default-ops D3 ocf:pcsmock:minimal promotable --group xxx clone".split(), DEPRECATED_DASH_DASH_GROUP + "Error: you can specify only one of clone, promotable, bundle or --group\n", ) self.assert_pcs_fail( - "resource create --no-default-ops D4 ocf:heartbeat:Dummy promotable --group xxx".split(), + "resource create --no-default-ops D4 ocf:pcsmock:minimal promotable --group xxx".split(), DEPRECATED_DASH_DASH_GROUP + "Error: you can specify only one of clone, promotable, bundle or --group\n", ) def test_resource_clone_group(self): self.assert_pcs_success( - "resource create --no-default-ops dummy0 ocf:heartbeat:Dummy --group group".split(), + "resource create --no-default-ops dummy0 ocf:pcsmock:minimal --group group".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) self.assert_pcs_success("resource clone group".split()) @@ -3547,92 +3501,47 @@ def test_resource_clone_group(self): def test_resource_missing_values(self): self.assert_pcs_success( - "resource create --no-default-ops myip IPaddr2 --force".split(), - stderr_full=( - "Assumed agent name 'ocf:heartbeat:IPaddr2' (deduced from 'IPaddr2')\n" - "Warning: required resource option 'ip' is missing\n" - ), - ) - self.assert_pcs_success( - "resource create --no-default-ops myip2 IPaddr2 ip=3.3.3.3".split(), - stderr_full=( - "Assumed agent name 'ocf:heartbeat:IPaddr2' (deduced from 'IPaddr2')\n" - ), - ) - self.assert_pcs_success( - "resource create --no-default-ops myfs Filesystem --force".split(), + "resource create --no-default-ops myip params --force".split(), stderr_full=( - "Assumed agent name 'ocf:heartbeat:Filesystem' (deduced from 'Filesystem')\n" - "Warning: required resource options 'device', 'directory', 'fstype' are missing\n" + "Assumed agent name 'ocf:pcsmock:params' (deduced from 'params')\n" + "Warning: required resource option 'mandatory' is missing\n" ), ) self.assert_pcs_success( - ( - "resource create --no-default-ops myfs2 Filesystem device=x" - " directory=y --force" - ).split(), + "resource create --no-default-ops myip2 params mandatory=value".split(), stderr_full=( - "Assumed agent name 'ocf:heartbeat:Filesystem' (deduced from 'Filesystem')\n" - "Warning: required resource option 'fstype' is missing\n" - ), - ) - self.assert_pcs_success( - ( - "resource create --no-default-ops myfs3 Filesystem device=x" - " directory=y fstype=z" - ).split(), - stderr_full=( - "Assumed agent name 'ocf:heartbeat:Filesystem' (deduced from 'Filesystem')\n" + "Assumed agent name 'ocf:pcsmock:params' (deduced from 'params')\n" ), ) self.assert_pcs_success( "resource config".split(), dedent( """\ - Resource: myip (class=ocf provider=heartbeat type=IPaddr2) + Resource: myip (class=ocf provider=pcsmock type=params) Operations: monitor: myip-monitor-interval-10s interval=10s timeout=20s - Resource: myip2 (class=ocf provider=heartbeat type=IPaddr2) + Resource: myip2 (class=ocf provider=pcsmock type=params) Attributes: myip2-instance_attributes - ip=3.3.3.3 + mandatory=value Operations: monitor: myip2-monitor-interval-10s interval=10s timeout=20s - Resource: myfs (class=ocf provider=heartbeat type=Filesystem) - Operations: - monitor: myfs-monitor-interval-20s - interval=20s timeout=40s - Resource: myfs2 (class=ocf provider=heartbeat type=Filesystem) - Attributes: myfs2-instance_attributes - device=x - directory=y - Operations: - monitor: myfs2-monitor-interval-20s - interval=20s timeout=40s - Resource: myfs3 (class=ocf provider=heartbeat type=Filesystem) - Attributes: myfs3-instance_attributes - device=x - directory=y - fstype=z - Operations: - monitor: myfs3-monitor-interval-20s - interval=20s timeout=40s """ ), ) def test_cloned_mastered_group(self): self.assert_pcs_success( - "resource create dummy1 ocf:heartbeat:Dummy --no-default-ops --group dummies".split(), + "resource create dummy1 ocf:pcsmock:minimal --no-default-ops --group dummies".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) self.assert_pcs_success( - "resource create dummy2 ocf:heartbeat:Dummy --no-default-ops --group dummies".split(), + "resource create dummy2 ocf:pcsmock:minimal --no-default-ops --group dummies".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) self.assert_pcs_success( - "resource create dummy3 ocf:heartbeat:Dummy --no-default-ops --group dummies".split(), + "resource create dummy3 ocf:pcsmock:minimal --no-default-ops --group dummies".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) self.assert_pcs_success("resource clone dummies".split()) @@ -3642,15 +3551,15 @@ def test_cloned_mastered_group(self): """\ Clone: dummies-clone Group: dummies - Resource: dummy1 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy1-monitor-interval-10s interval=10s timeout=20s - Resource: dummy2 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy2 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy2-monitor-interval-10s interval=10s timeout=20s - Resource: dummy3 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy3 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy3-monitor-interval-10s interval=10s timeout=20s @@ -3670,9 +3579,9 @@ def test_cloned_mastered_group(self): outdent( """\ * Resource Group: dummies: - * dummy1\t(ocf:heartbeat:Dummy):\t Stopped - * dummy2\t(ocf:heartbeat:Dummy):\t Stopped - * dummy3\t(ocf:heartbeat:Dummy):\t Stopped + * dummy1\t(ocf:pcsmock:minimal):\t Stopped + * dummy2\t(ocf:pcsmock:minimal):\t Stopped + * dummy3\t(ocf:pcsmock:minimal):\t Stopped """ ), ) @@ -3682,9 +3591,9 @@ def test_cloned_mastered_group(self): outdent( """\ * Resource Group: dummies: - * dummy1\t(ocf::heartbeat:Dummy):\tStopped - * dummy2\t(ocf::heartbeat:Dummy):\tStopped - * dummy3\t(ocf::heartbeat:Dummy):\tStopped + * dummy1\t(ocf::pcsmock:minimal):\tStopped + * dummy2\t(ocf::pcsmock:minimal):\tStopped + * dummy3\t(ocf::pcsmock:minimal):\tStopped """ ), ) @@ -3694,9 +3603,9 @@ def test_cloned_mastered_group(self): outdent( """\ Resource Group: dummies - dummy1\t(ocf::heartbeat:Dummy):\tStopped - dummy2\t(ocf::heartbeat:Dummy):\tStopped - dummy3\t(ocf::heartbeat:Dummy):\tStopped + dummy1\t(ocf::pcsmock:minimal):\tStopped + dummy2\t(ocf::pcsmock:minimal):\tStopped + dummy3\t(ocf::pcsmock:minimal):\tStopped """ ), ) @@ -3708,15 +3617,15 @@ def test_cloned_mastered_group(self): """\ Clone: dummies-clone Group: dummies - Resource: dummy1 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy1-monitor-interval-10s interval=10s timeout=20s - Resource: dummy2 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy2 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy2-monitor-interval-10s interval=10s timeout=20s - Resource: dummy3 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy3 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy3-monitor-interval-10s interval=10s timeout=20s @@ -3741,15 +3650,15 @@ def test_cloned_mastered_group(self): ) self.assert_pcs_success( - "resource create dummy1 ocf:heartbeat:Dummy --no-default-ops --group dummies".split(), + "resource create dummy1 ocf:pcsmock:minimal --no-default-ops --group dummies".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) self.assert_pcs_success( - "resource create dummy2 ocf:heartbeat:Dummy --no-default-ops --group dummies".split(), + "resource create dummy2 ocf:pcsmock:minimal --no-default-ops --group dummies".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) self.assert_pcs_success( - "resource create dummy3 ocf:heartbeat:Dummy --no-default-ops --group dummies".split(), + "resource create dummy3 ocf:pcsmock:minimal --no-default-ops --group dummies".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) # pcs no longer allows turning resources into masters but supports @@ -3765,15 +3674,15 @@ def test_cloned_mastered_group(self): Meta Attributes: promotable=true Group: dummies - Resource: dummy1 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy1-monitor-interval-10s interval=10s timeout=20s - Resource: dummy2 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy2 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy2-monitor-interval-10s interval=10s timeout=20s - Resource: dummy3 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy3 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy3-monitor-interval-10s interval=10s timeout=20s @@ -3793,9 +3702,9 @@ def test_cloned_mastered_group(self): outdent( """\ * Resource Group: dummies: - * dummy1\t(ocf:heartbeat:Dummy):\t Stopped - * dummy2\t(ocf:heartbeat:Dummy):\t Stopped - * dummy3\t(ocf:heartbeat:Dummy):\t Stopped + * dummy1\t(ocf:pcsmock:minimal):\t Stopped + * dummy2\t(ocf:pcsmock:minimal):\t Stopped + * dummy3\t(ocf:pcsmock:minimal):\t Stopped """ ), ) @@ -3805,9 +3714,9 @@ def test_cloned_mastered_group(self): outdent( """\ * Resource Group: dummies: - * dummy1\t(ocf::heartbeat:Dummy):\tStopped - * dummy2\t(ocf::heartbeat:Dummy):\tStopped - * dummy3\t(ocf::heartbeat:Dummy):\tStopped + * dummy1\t(ocf::pcsmock:minimal):\tStopped + * dummy2\t(ocf::pcsmock:minimal):\tStopped + * dummy3\t(ocf::pcsmock:minimal):\tStopped """ ), ) @@ -3817,9 +3726,9 @@ def test_cloned_mastered_group(self): outdent( """\ Resource Group: dummies - dummy1\t(ocf::heartbeat:Dummy):\tStopped - dummy2\t(ocf::heartbeat:Dummy):\tStopped - dummy3\t(ocf::heartbeat:Dummy):\tStopped + dummy1\t(ocf::pcsmock:minimal):\tStopped + dummy2\t(ocf::pcsmock:minimal):\tStopped + dummy3\t(ocf::pcsmock:minimal):\tStopped """ ), ) @@ -3836,15 +3745,15 @@ def test_cloned_mastered_group(self): Meta Attributes: promotable=true Group: dummies - Resource: dummy1 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy1-monitor-interval-10s interval=10s timeout=20s - Resource: dummy2 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy2 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy2-monitor-interval-10s interval=10s timeout=20s - Resource: dummy3 (class=ocf provider=heartbeat type=Dummy) + Resource: dummy3 (class=ocf provider=pcsmock type=minimal) Operations: monitor: dummy3-monitor-interval-10s interval=10s timeout=20s @@ -3871,56 +3780,56 @@ def test_cloned_mastered_group(self): def test_relocate_stickiness(self): # pylint: disable=too-many-statements self.assert_pcs_success( - "resource create D1 ocf:pacemaker:Dummy --no-default-ops".split() + "resource create D1 ocf:pcsmock:minimal --no-default-ops".split() ) self.assert_pcs_success( - "resource create DG1 ocf:pacemaker:Dummy --no-default-ops --group GR".split(), + "resource create DG1 ocf:pcsmock:minimal --no-default-ops --group GR".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) self.assert_pcs_success( - "resource create DG2 ocf:pacemaker:Dummy --no-default-ops --group GR".split(), + "resource create DG2 ocf:pcsmock:minimal --no-default-ops --group GR".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) self.assert_pcs_success( - "resource create DC ocf:pacemaker:Dummy --no-default-ops clone".split() + "resource create DC ocf:pcsmock:minimal --no-default-ops clone".split() ) self.assert_pcs_success( - "resource create DGC1 ocf:pacemaker:Dummy --no-default-ops --group GRC".split(), + "resource create DGC1 ocf:pcsmock:minimal --no-default-ops --group GRC".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) self.assert_pcs_success( - "resource create DGC2 ocf:pacemaker:Dummy --no-default-ops --group GRC".split(), + "resource create DGC2 ocf:pcsmock:minimal --no-default-ops --group GRC".split(), stderr_full=DEPRECATED_DASH_DASH_GROUP, ) self.assert_pcs_success("resource clone GRC".split()) status = dedent( """\ - Resource: D1 (class=ocf provider=pacemaker type=Dummy) + Resource: D1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: D1-monitor-interval-10s interval=10s timeout=20s Group: GR - Resource: DG1 (class=ocf provider=pacemaker type=Dummy) + Resource: DG1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: DG1-monitor-interval-10s interval=10s timeout=20s - Resource: DG2 (class=ocf provider=pacemaker type=Dummy) + Resource: DG2 (class=ocf provider=pcsmock type=minimal) Operations: monitor: DG2-monitor-interval-10s interval=10s timeout=20s Clone: DC-clone - Resource: DC (class=ocf provider=pacemaker type=Dummy) + Resource: DC (class=ocf provider=pcsmock type=minimal) Operations: monitor: DC-monitor-interval-10s interval=10s timeout=20s Clone: GRC-clone Group: GRC - Resource: DGC1 (class=ocf provider=pacemaker type=Dummy) + Resource: DGC1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: DGC1-monitor-interval-10s interval=10s timeout=20s - Resource: DGC2 (class=ocf provider=pacemaker type=Dummy) + Resource: DGC2 (class=ocf provider=pcsmock type=minimal) Operations: monitor: DGC2-monitor-interval-10s interval=10s timeout=20s @@ -3961,7 +3870,7 @@ def test_relocate_stickiness(self): "resource config".split(), dedent( """\ - Resource: D1 (class=ocf provider=pacemaker type=Dummy) + Resource: D1 (class=ocf provider=pcsmock type=minimal) Meta Attributes: D1-meta_attributes resource-stickiness=0 Operations: @@ -3970,13 +3879,13 @@ def test_relocate_stickiness(self): Group: GR Meta Attributes: GR-meta_attributes resource-stickiness=0 - Resource: DG1 (class=ocf provider=pacemaker type=Dummy) + Resource: DG1 (class=ocf provider=pcsmock type=minimal) Meta Attributes: DG1-meta_attributes resource-stickiness=0 Operations: monitor: DG1-monitor-interval-10s interval=10s timeout=20s - Resource: DG2 (class=ocf provider=pacemaker type=Dummy) + Resource: DG2 (class=ocf provider=pcsmock type=minimal) Meta Attributes: DG2-meta_attributes resource-stickiness=0 Operations: @@ -3985,7 +3894,7 @@ def test_relocate_stickiness(self): Clone: DC-clone Meta Attributes: DC-clone-meta_attributes resource-stickiness=0 - Resource: DC (class=ocf provider=pacemaker type=Dummy) + Resource: DC (class=ocf provider=pcsmock type=minimal) Meta Attributes: DC-meta_attributes resource-stickiness=0 Operations: @@ -3997,13 +3906,13 @@ def test_relocate_stickiness(self): Group: GRC Meta Attributes: GRC-meta_attributes resource-stickiness=0 - Resource: DGC1 (class=ocf provider=pacemaker type=Dummy) + Resource: DGC1 (class=ocf provider=pcsmock type=minimal) Meta Attributes: DGC1-meta_attributes resource-stickiness=0 Operations: monitor: DGC1-monitor-interval-10s interval=10s timeout=20s - Resource: DGC2 (class=ocf provider=pacemaker type=Dummy) + Resource: DGC2 (class=ocf provider=pcsmock type=minimal) Meta Attributes: DGC2-meta_attributes resource-stickiness=0 Operations: @@ -4028,25 +3937,25 @@ def test_relocate_stickiness(self): "resource config".split(), dedent( """\ - Resource: D1 (class=ocf provider=pacemaker type=Dummy) + Resource: D1 (class=ocf provider=pcsmock type=minimal) Meta Attributes: D1-meta_attributes resource-stickiness=0 Operations: monitor: D1-monitor-interval-10s interval=10s timeout=20s Group: GR - Resource: DG1 (class=ocf provider=pacemaker type=Dummy) + Resource: DG1 (class=ocf provider=pcsmock type=minimal) Meta Attributes: DG1-meta_attributes resource-stickiness=0 Operations: monitor: DG1-monitor-interval-10s interval=10s timeout=20s - Resource: DG2 (class=ocf provider=pacemaker type=Dummy) + Resource: DG2 (class=ocf provider=pcsmock type=minimal) Operations: monitor: DG2-monitor-interval-10s interval=10s timeout=20s Clone: DC-clone - Resource: DC (class=ocf provider=pacemaker type=Dummy) + Resource: DC (class=ocf provider=pcsmock type=minimal) Meta Attributes: DC-meta_attributes resource-stickiness=0 Operations: @@ -4054,13 +3963,13 @@ def test_relocate_stickiness(self): interval=10s timeout=20s Clone: GRC-clone Group: GRC - Resource: DGC1 (class=ocf provider=pacemaker type=Dummy) + Resource: DGC1 (class=ocf provider=pcsmock type=minimal) Meta Attributes: DGC1-meta_attributes resource-stickiness=0 Operations: monitor: DGC1-monitor-interval-10s interval=10s timeout=20s - Resource: DGC2 (class=ocf provider=pacemaker type=Dummy) + Resource: DGC2 (class=ocf provider=pcsmock type=minimal) Operations: monitor: DGC2-monitor-interval-10s interval=10s timeout=20s @@ -4083,21 +3992,21 @@ def test_relocate_stickiness(self): "resource config".split(), dedent( """\ - Resource: D1 (class=ocf provider=pacemaker type=Dummy) + Resource: D1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: D1-monitor-interval-10s interval=10s timeout=20s Group: GR - Resource: DG1 (class=ocf provider=pacemaker type=Dummy) + Resource: DG1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: DG1-monitor-interval-10s interval=10s timeout=20s - Resource: DG2 (class=ocf provider=pacemaker type=Dummy) + Resource: DG2 (class=ocf provider=pcsmock type=minimal) Operations: monitor: DG2-monitor-interval-10s interval=10s timeout=20s Clone: DC-clone - Resource: DC (class=ocf provider=pacemaker type=Dummy) + Resource: DC (class=ocf provider=pcsmock type=minimal) Operations: monitor: DC-monitor-interval-10s interval=10s timeout=20s @@ -4107,13 +4016,13 @@ def test_relocate_stickiness(self): Group: GRC Meta Attributes: GRC-meta_attributes resource-stickiness=0 - Resource: DGC1 (class=ocf provider=pacemaker type=Dummy) + Resource: DGC1 (class=ocf provider=pcsmock type=minimal) Meta Attributes: DGC1-meta_attributes resource-stickiness=0 Operations: monitor: DGC1-monitor-interval-10s interval=10s timeout=20s - Resource: DGC2 (class=ocf provider=pacemaker type=Dummy) + Resource: DGC2 (class=ocf provider=pcsmock type=minimal) Meta Attributes: DGC2-meta_attributes resource-stickiness=0 Operations: @@ -4138,20 +4047,20 @@ def test_relocate_stickiness(self): "resource config".split(), dedent( """\ - Resource: D1 (class=ocf provider=pacemaker type=Dummy) + Resource: D1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: D1-monitor-interval-10s interval=10s timeout=20s Group: GR Meta Attributes: GR-meta_attributes resource-stickiness=0 - Resource: DG1 (class=ocf provider=pacemaker type=Dummy) + Resource: DG1 (class=ocf provider=pcsmock type=minimal) Meta Attributes: DG1-meta_attributes resource-stickiness=0 Operations: monitor: DG1-monitor-interval-10s interval=10s timeout=20s - Resource: DG2 (class=ocf provider=pacemaker type=Dummy) + Resource: DG2 (class=ocf provider=pcsmock type=minimal) Meta Attributes: DG2-meta_attributes resource-stickiness=0 Operations: @@ -4160,7 +4069,7 @@ def test_relocate_stickiness(self): Clone: DC-clone Meta Attributes: DC-clone-meta_attributes resource-stickiness=0 - Resource: DC (class=ocf provider=pacemaker type=Dummy) + Resource: DC (class=ocf provider=pcsmock type=minimal) Meta Attributes: DC-meta_attributes resource-stickiness=0 Operations: @@ -4168,11 +4077,11 @@ def test_relocate_stickiness(self): interval=10s timeout=20s Clone: GRC-clone Group: GRC - Resource: DGC1 (class=ocf provider=pacemaker type=Dummy) + Resource: DGC1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: DGC1-monitor-interval-10s interval=10s timeout=20s - Resource: DGC2 (class=ocf provider=pacemaker type=Dummy) + Resource: DGC2 (class=ocf provider=pcsmock type=minimal) Operations: monitor: DGC2-monitor-interval-10s interval=10s timeout=20s @@ -4197,7 +4106,7 @@ def setUp(self): write_file_to_tmpfile(empty_cib, self.temp_cib) write_file_to_tmpfile(large_cib, self.temp_large_cib) self.pcs_runner = PcsRunner(self.temp_cib.name) - self.pcs_runner.mock_settings = get_mock_settings("crm_resource_exec") + self.pcs_runner.mock_settings = get_mock_settings() self.command = "to-be-overridden" def tearDown(self): @@ -4206,7 +4115,7 @@ def tearDown(self): fixture_xml_1_monitor = """ - + - + @@ -4227,7 +4136,7 @@ def tearDown(self): def fixture_resource(self): self.assert_effect( - "resource create --no-default-ops R ocf:pacemaker:Dummy".split(), + "resource create --no-default-ops R ocf:pcsmock:minimal".split(), self.fixture_xml_1_monitor, ) @@ -4236,8 +4145,8 @@ def fixture_monitor_20(self): "resource op add R monitor interval=20s timeout=20s --force".split(), """ - - - - + - + - + + - + - + @@ -4659,13 +4570,13 @@ def fixture_xml_resource_with_meta(): def fixture_resource(self): self.assert_effect( - "resource create --no-default-ops R ocf:pacemaker:Dummy".split(), + "resource create --no-default-ops R ocf:pcsmock:minimal".split(), self.fixture_xml_resource_no_meta(), ) def fixture_resource_meta(self): self.assert_effect( - "resource create --no-default-ops R ocf:pacemaker:Dummy meta a=b".split(), + "resource create --no-default-ops R ocf:pcsmock:minimal meta a=b".split(), self.fixture_xml_resource_with_meta(), ) @@ -4673,28 +4584,20 @@ def test_meta_attrs(self): # see also BundleMiscCommands self.assert_pcs_success( ( - "resource create --no-default-ops --force D0 ocf:heartbeat:Dummy" - " test=testA test2=test2a op monitor interval=30 meta" + "resource create --no-default-ops D0 ocf:pcsmock:params" + " mandatory=test1a optional=test2a op monitor interval=30 meta" " test5=test5a test6=test6a" ).split(), - stderr_full=( - "Warning: invalid resource options: 'test', 'test2', allowed" - " options are: 'fake', 'state', 'trace_file', 'trace_ra'\n" - ), ) self.assert_pcs_success( ( - "resource create --no-default-ops --force D1 ocf:heartbeat:Dummy" - " test=testA test2=test2a op monitor interval=30" + "resource create --no-default-ops D1 ocf:pcsmock:params" + " mandatory=test1a optional=test2a op monitor interval=30" ).split(), - stderr_full=( - "Warning: invalid resource options: 'test', 'test2', allowed" - " options are: 'fake', 'state', 'trace_file', 'trace_ra'\n" - ), ) self.assert_pcs_success( ( - "resource update --force D0 test=testC test2=test2a op monitor " + "resource update D0 mandatory=test1b optional=test2a op monitor " "interval=35 meta test7=test7a test6=" ).split() ) @@ -4707,10 +4610,10 @@ def test_meta_attrs(self): "resource config".split(), dedent( """\ - Resource: D0 (class=ocf provider=heartbeat type=Dummy) + Resource: D0 (class=ocf provider=pcsmock type=params) Attributes: D0-instance_attributes - test=testC - test2=test2a + mandatory=test1b + optional=test2a Meta Attributes: D0-meta_attributes test5=test5a test7=test7a @@ -4721,10 +4624,10 @@ def test_meta_attrs(self): Meta Attributes: TestRG-meta_attributes testrgmeta=mymeta testrgmeta2=mymeta2 - Resource: D1 (class=ocf provider=heartbeat type=Dummy) + Resource: D1 (class=ocf provider=pcsmock type=params) Attributes: D1-instance_attributes - test=testA - test2=test2a + mandatory=test1a + optional=test2a Meta Attributes: D1-meta_attributes d1meta=superd1meta Operations: @@ -4766,7 +4669,7 @@ def test_resource_update_dont_create_meta_on_removal(self): def fixture_not_ocf_clone(): return """ - + - - + + - + - @@ -4933,13 +4836,14 @@ def fixture_xml_resource_with_attrs(): def fixture_resource(self): self.assert_effect( - "resource create --no-default-ops R ocf:pacemaker:Dummy".split(), + "resource create --no-default-ops R ocf:pcsmock:params --force".split(), self.fixture_xml_resource_no_attrs(), + stderr_full="Warning: required resource option 'mandatory' is missing\n", ) def fixture_resource_attrs(self): self.assert_effect( - "resource create --no-default-ops R ocf:pacemaker:Dummy fake=F".split(), + "resource create --no-default-ops R ocf:pcsmock:params mandatory=F".split(), self.fixture_xml_resource_with_attrs(), ) @@ -4952,27 +4856,30 @@ def test_usage(self): def test_bad_instance_variables(self): self.assert_pcs_fail( ( - "resource create --no-default-ops D0 ocf:heartbeat:Dummy" + "resource create --no-default-ops D0 ocf:pcsmock:params" " test=testC test2=test2a test4=test4A op monitor interval=35" " meta test7=test7a test6=" ).split(), ( - "Error: invalid resource options: 'test', 'test2', 'test4'," - " allowed options are: 'fake', 'state', 'trace_file', " - "'trace_ra', use --force to override\n" + ERRORS_HAVE_OCCURRED + "Error: invalid resource options: 'test', 'test2', 'test4', " + "allowed options are: 'advanced', 'enum', 'mandatory', " + "'optional', 'unique1', 'unique2', use --force to override\n" + "Error: required resource option 'mandatory' is missing, " + "use --force to override\n" + ERRORS_HAVE_OCCURRED ), ) self.assert_pcs_success( ( - "resource create --no-default-ops --force D0 ocf:heartbeat:Dummy" + "resource create --no-default-ops --force D0 ocf:pcsmock:params" " test=testC test2=test2a test4=test4A op monitor interval=35" " meta test7=test7a test6=" ).split(), stderr_full=( "Warning: invalid resource options: 'test', 'test2', 'test4'," - " allowed options are: 'fake', 'state', 'trace_file', " - "'trace_ra'\n" + " allowed options are: 'advanced', 'enum', 'mandatory', " + "'optional', 'unique1', 'unique2'\n" + "Warning: required resource option 'mandatory' is missing\n" ), ) @@ -4980,8 +4887,8 @@ def test_bad_instance_variables(self): "resource update D0 test=testA test2=testB test3=testD".split(), ( "Error: invalid resource option 'test3', allowed options" - " are: 'fake', 'state', 'trace_file', 'trace_ra', use --force " - "to override\n" + " are: 'advanced', 'enum', 'mandatory', 'optional', 'unique1', " + "'unique2', use --force to override\n" ), ) @@ -4989,8 +4896,8 @@ def test_bad_instance_variables(self): "resource update D0 test=testB test2=testC test3=testD --force".split(), stderr_full=( "Warning: invalid resource option 'test3'," - " allowed options are: 'fake', 'state', 'trace_file', " - "'trace_ra'\n" + " allowed options are: 'advanced', 'enum', 'mandatory', " + "'optional', 'unique1', 'unique2'\n" ), ) @@ -4998,7 +4905,7 @@ def test_bad_instance_variables(self): "resource config D0".split(), dedent( """\ - Resource: D0 (class=ocf provider=heartbeat type=Dummy) + Resource: D0 (class=ocf provider=pcsmock type=params) Attributes: D0-instance_attributes test=testB test2=testC @@ -5015,7 +4922,7 @@ def test_bad_instance_variables(self): ) def test_nonexisting_agent(self): - agent = "ocf:pacemaker:nonexistent" + agent = "ocf:pcsmock:nonexistent" message = ( f"Agent '{agent}' is not installed or does " "not provide valid metadata: " @@ -5038,19 +4945,19 @@ def test_nonexisting_agent(self): def test_update_existing(self): xml = """ - - - + - - @@ -5059,29 +4966,31 @@ def test_update_existing(self): """ self.assert_effect( ( - "resource create --no-default-ops ClusterIP ocf:heartbeat:IPaddr2" - " cidr_netmask=32 ip=192.168.0.99 op monitor interval=30s" + "resource create --no-default-ops Dummy ocf:pcsmock:params" + " mandatory=manda optional=opti1 op monitor interval=30s" ).split(), - xml.format(ip="192.168.0.99"), + xml.format(optional="opti1"), ) self.assert_effect( - "resource update ClusterIP ip=192.168.0.100".split(), - xml.format(ip="192.168.0.100"), + "resource update Dummy optional=opti2".split(), + xml.format(optional="opti2"), ) def test_keep_empty_nvset(self): self.fixture_resource_attrs() self.assert_effect( - "resource update R fake=".split(), + "resource update R mandatory= --force".split(), self.fixture_xml_resource_empty_attrs(), + stderr_full="Warning: required resource option 'mandatory' is missing\n", ) def test_dont_create_nvset_on_removal(self): self.fixture_resource() self.assert_effect( - "resource update R fake=".split(), + "resource update R mandatory= --force".split(), self.fixture_xml_resource_no_attrs(), + stderr_full="Warning: required resource option 'mandatory' is missing\n", ) def test_agent_self_validation_failure(self): @@ -5091,11 +5000,12 @@ def test_agent_self_validation_failure(self): "resource", "update", "R", - "fake=is_invalid=True", + "mandatory=is_invalid=True", "--agent-validation", ], - stderr_start=( - "Error: Validation result from agent (use --force to override):" + stderr_full=( + "Error: Validation result from agent (use --force to override):\n" + " pcsmock validation failure\n" ), ) @@ -5103,7 +5013,7 @@ def test_agent_self_validation_failure(self): def fixture_not_ocf_clone(): return """ - + - - - - + @@ -159,7 +162,7 @@ def test_fail_when_server_already_used(self): def test_fail_when_server_already_used_as_guest(self): self.pcs_runner.corosync_conf_opt = None self.assert_pcs_success( - "resource create G ocf:heartbeat:Dummy --no-default-ops".split(), + "resource create G ocf:pcsmock:minimal --no-default-ops".split(), ) self.pcs_runner.corosync_conf_opt = self.corosync_conf self.assert_pcs_success( @@ -179,9 +182,9 @@ class NodeAddGuest(RemoteTest): def create_resource(self): self.pcs_runner.corosync_conf_opt = None self.assert_effect( - "resource create G ocf:heartbeat:Dummy --no-default-ops".split(), + "resource create G ocf:pcsmock:minimal --no-default-ops".split(), """ - + - + - + - - diff --git a/pcs_test/tier1/test_status.py b/pcs_test/tier1/test_status.py index f6780c60e..e1482c11b 100644 --- a/pcs_test/tier1/test_status.py +++ b/pcs_test/tier1/test_status.py @@ -27,9 +27,7 @@ def setUp(self): self.temp_cib = get_tmp_file("tier0_statust_stonith_warning") write_file_to_tmpfile(self.empty_cib, self.temp_cib) self.pcs_runner = PcsRunner(self.temp_cib.name) - self.pcs_runner.mock_settings = get_mock_settings( - "crm_resource_exec", "stonith_admin_exec" - ) + self.pcs_runner.mock_settings = get_mock_settings() def tearDown(self): self.temp_cib.close() @@ -54,14 +52,9 @@ def fixture_stonith_cycle(self): def fixture_resource(self): self.assert_pcs_success( ( - "resource create dummy ocf:pacemaker:Dummy action=reboot " - "method=cycle --force" + "resource create dummy ocf:pcsmock:action_method action=reboot " + "method=cycle" ).split(), - stderr_full=( - "Warning: invalid resource options: 'action', 'method', allowed " - "options are: 'envfile', 'fail_start_on', 'fake', 'op_sleep', " - "'passwd', 'state', 'trace_file', 'trace_ra'\n" - ), ) def test_warning_stonith_action(self): @@ -320,9 +313,9 @@ def test_more_no_node_option(self): def test_resource_id(self): if is_pacemaker_21_without_20_compatibility(): - stdout_full = " * x1 (ocf:pacemaker:Dummy): Started rh-1\n" + stdout_full = " * x1 (ocf:pcsmock:minimal): Started rh-1\n" else: - stdout_full = " * x1 (ocf::pacemaker:Dummy): Started rh-1\n" + stdout_full = " * x1 (ocf::pcsmock:minimal): Started rh-1\n" self.assert_pcs_success( self.command + ["x1"], stdout_full=stdout_full, @@ -342,9 +335,9 @@ def test_resource_id_with_node_hide_inactive(self): def test_resource_id_with_node_started(self): if is_pacemaker_21_without_20_compatibility(): - stdout_full = " * x1 (ocf:pacemaker:Dummy): Started rh-1\n" + stdout_full = " * x1 (ocf:pcsmock:minimal): Started rh-1\n" else: - stdout_full = " * x1 (ocf::pacemaker:Dummy): Started rh-1\n" + stdout_full = " * x1 (ocf::pcsmock:minimal): Started rh-1\n" self.assert_pcs_success( self.command + ["x1", "node=rh-1"], stdout_full=stdout_full, @@ -352,9 +345,9 @@ def test_resource_id_with_node_started(self): def test_resource_id_with_node_stopped(self): if is_pacemaker_21_without_20_compatibility(): - stdout_full = " * x2 (ocf:pacemaker:Dummy): Stopped\n" + stdout_full = " * x2 (ocf:pcsmock:minimal): Stopped\n" else: - stdout_full = " * x2 (ocf::pacemaker:Dummy): Stopped\n" + stdout_full = " * x2 (ocf::pcsmock:minimal): Stopped\n" self.assert_pcs_success( self.command + ["x2", "node=rh-1"], stdout_full=stdout_full, @@ -368,9 +361,9 @@ def test_resource_id_with_node_without_status(self): def test_resource_id_with_node_changed_arg_order(self): if is_pacemaker_21_without_20_compatibility(): - stdout_full = " * x1 (ocf:pacemaker:Dummy): Started rh-1\n" + stdout_full = " * x1 (ocf:pcsmock:minimal): Started rh-1\n" else: - stdout_full = " * x1 (ocf::pacemaker:Dummy): Started rh-1\n" + stdout_full = " * x1 (ocf::pcsmock:minimal): Started rh-1\n" self.assert_pcs_success( self.command + ["node=rh-1", "x1"], stdout_full=stdout_full, @@ -379,7 +372,7 @@ def test_resource_id_with_node_changed_arg_order(self): def test_stonith_id(self): self.assert_pcs_success( self.command + ["fence-rh-1"], - stdout_full=" * fence-rh-1 (stonith:fence_xvm): Started rh-1\n", + stdout_full=" * fence-rh-1 (stonith:fence_pcsmock_minimal): Started rh-1\n", ) def test_stonith_id_hide_inactive(self): @@ -397,13 +390,13 @@ def test_stonith_id_with_node_hide_inactive(self): def test_stonith_id_with_node_started(self): self.assert_pcs_success( self.command + ["fence-rh-1", "node=rh-1"], - stdout_full=" * fence-rh-1 (stonith:fence_xvm): Started rh-1\n", + stdout_full=" * fence-rh-1 (stonith:fence_pcsmock_minimal): Started rh-1\n", ) def test_stonith_id_with_node_stopped(self): self.assert_pcs_success( self.command + ["fence-rh-2", "node=rh-2"], - stdout_full=" * fence-rh-2 (stonith:fence_xvm): Stopped\n", + stdout_full=" * fence-rh-2 (stonith:fence_pcsmock_minimal): Stopped\n", ) def test_stonith_id_with_node_without_status(self): @@ -416,19 +409,19 @@ def test_tag_id(self): if is_pacemaker_21_without_20_compatibility(): stdout_full = outdent( """\ - * fence-rh-1 (stonith:fence_xvm): Started rh-1 - * fence-rh-2 (stonith:fence_xvm): Stopped - * x3 (ocf:pacemaker:Dummy): Stopped - * y1 (ocf:pacemaker:Dummy): Stopped + * fence-rh-1 (stonith:fence_pcsmock_minimal): Started rh-1 + * fence-rh-2 (stonith:fence_pcsmock_minimal): Stopped + * x3 (ocf:pcsmock:minimal): Stopped + * y1 (ocf:pcsmock:minimal): Stopped """ ) else: stdout_full = outdent( """\ - * fence-rh-1 (stonith:fence_xvm): Started rh-1 - * fence-rh-2 (stonith:fence_xvm): Stopped - * x3 (ocf::pacemaker:Dummy): Stopped - * y1 (ocf::pacemaker:Dummy): Stopped + * fence-rh-1 (stonith:fence_pcsmock_minimal): Started rh-1 + * fence-rh-2 (stonith:fence_pcsmock_minimal): Stopped + * x3 (ocf::pcsmock:minimal): Stopped + * y1 (ocf::pcsmock:minimal): Stopped """ ) self.assert_pcs_success( @@ -442,7 +435,7 @@ def test_tag_id_hide_inactive(self): + ["tag-mixed-stonith-devices-and-resources", "--hide-inactive"], stdout_full=outdent( """\ - * fence-rh-1 (stonith:fence_xvm): Started rh-1 + * fence-rh-1 (stonith:fence_pcsmock_minimal): Started rh-1 """ ), ) @@ -451,17 +444,17 @@ def test_tag_id_with_node(self): if is_pacemaker_21_without_20_compatibility(): stdout_full = outdent( """\ - * fence-rh-2 (stonith:fence_xvm): Stopped - * x3 (ocf:pacemaker:Dummy): Stopped - * y1 (ocf:pacemaker:Dummy): Stopped + * fence-rh-2 (stonith:fence_pcsmock_minimal): Stopped + * x3 (ocf:pcsmock:minimal): Stopped + * y1 (ocf:pcsmock:minimal): Stopped """ ) else: stdout_full = outdent( """\ - * fence-rh-2 (stonith:fence_xvm): Stopped - * x3 (ocf::pacemaker:Dummy): Stopped - * y1 (ocf::pacemaker:Dummy): Stopped + * fence-rh-2 (stonith:fence_pcsmock_minimal): Stopped + * x3 (ocf::pcsmock:minimal): Stopped + * y1 (ocf::pcsmock:minimal): Stopped """ ) self.assert_pcs_success( @@ -480,7 +473,7 @@ def test_tag_id_with_node_hide_inactive(self): ], stdout_full=outdent( """\ - * fence-rh-1 (stonith:fence_xvm): Started rh-1 + * fence-rh-1 (stonith:fence_pcsmock_minimal): Started rh-1 """ ), ) @@ -562,9 +555,9 @@ class StonithStatus(ResourceStonithStatusBase, TestCase): no_resources_msg = "NO stonith devices configured\n" all_resources_output = outdent( """\ - * fence-rh-1 (stonith:fence_xvm): Started rh-1 - * fence-rh-2 (stonith:fence_xvm): Stopped - * fence-kdump (stonith:fence_kdump): Stopped + * fence-rh-1 (stonith:fence_pcsmock_minimal): Started rh-1 + * fence-rh-2 (stonith:fence_pcsmock_minimal): Stopped + * fence-kdump (stonith:fence_pcsmock_minimal): Stopped Fencing Levels: Target: rh-1 @@ -577,7 +570,7 @@ class StonithStatus(ResourceStonithStatusBase, TestCase): ) active_resources_output = outdent( """\ - * fence-rh-1 (stonith:fence_xvm): Started rh-1 + * fence-rh-1 (stonith:fence_pcsmock_minimal): Started rh-1 Fencing Levels: Target: rh-1 @@ -590,14 +583,14 @@ class StonithStatus(ResourceStonithStatusBase, TestCase): ) active_resources_output_node = outdent( """\ - * fence-rh-1 (stonith:fence_xvm): Started rh-1 + * fence-rh-1 (stonith:fence_pcsmock_minimal): Started rh-1 """ ) node_output = outdent( """\ - * fence-rh-1 (stonith:fence_xvm): Started rh-1 - * fence-rh-2 (stonith:fence_xvm): Stopped - * fence-kdump (stonith:fence_kdump): Stopped + * fence-rh-1 (stonith:fence_pcsmock_minimal): Started rh-1 + * fence-rh-2 (stonith:fence_pcsmock_minimal): Stopped + * fence-kdump (stonith:fence_pcsmock_minimal): Stopped """ ) @@ -607,34 +600,34 @@ def fixture_resources_status_output(nodes="rh-1 rh-2", inactive=True): if is_pacemaker_21_without_20_compatibility(): return outdent( """\ - * x1 (ocf:pacemaker:Dummy): Started rh-1 + * x1 (ocf:pcsmock:minimal): Started rh-1 """ ) return outdent( """\ - * x1 (ocf::pacemaker:Dummy): Started rh-1 + * x1 (ocf::pcsmock:minimal): Started rh-1 """ ) if is_pacemaker_21_without_20_compatibility(): return outdent( f"""\ - * not-in-tags (ocf:pacemaker:Dummy): Stopped - * x1 (ocf:pacemaker:Dummy): Started rh-1 - * x2 (ocf:pacemaker:Dummy): Stopped - * x3 (ocf:pacemaker:Dummy): Stopped - * y1 (ocf:pacemaker:Dummy): Stopped + * not-in-tags (ocf:pcsmock:minimal): Stopped + * x1 (ocf:pcsmock:minimal): Started rh-1 + * x2 (ocf:pcsmock:minimal): Stopped + * x3 (ocf:pcsmock:minimal): Stopped + * y1 (ocf:pcsmock:minimal): Stopped * Clone Set: y2-clone [y2]: * Stopped: [ {nodes} ] """ ) return outdent( f"""\ - * not-in-tags (ocf::pacemaker:Dummy): Stopped - * x1 (ocf::pacemaker:Dummy): Started rh-1 - * x2 (ocf::pacemaker:Dummy): Stopped - * x3 (ocf::pacemaker:Dummy): Stopped - * y1 (ocf::pacemaker:Dummy): Stopped + * not-in-tags (ocf::pcsmock:minimal): Stopped + * x1 (ocf::pcsmock:minimal): Started rh-1 + * x2 (ocf::pcsmock:minimal): Stopped + * x3 (ocf::pcsmock:minimal): Stopped + * y1 (ocf::pcsmock:minimal): Stopped * Clone Set: y2-clone [y2]: * Stopped: [ {nodes} ] """ diff --git a/pcs_test/tier1/test_tag.py b/pcs_test/tier1/test_tag.py index 7a461ab17..96d62d7fa 100644 --- a/pcs_test/tier1/test_tag.py +++ b/pcs_test/tier1/test_tag.py @@ -419,28 +419,28 @@ class PcsConfigTagsTest(TestTagMixin, TestCase): expected_resources = outdent( """ Resources: - Resource: not-in-tags (class=ocf provider=pacemaker type=Dummy) + Resource: not-in-tags (class=ocf provider=pcsmock type=minimal) Operations: monitor: not-in-tags-monitor-interval-10s interval=10s timeout=20s - Resource: x1 (class=ocf provider=pacemaker type=Dummy) + Resource: x1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: x1-monitor-interval-10s interval=10s timeout=20s - Resource: x2 (class=ocf provider=pacemaker type=Dummy) + Resource: x2 (class=ocf provider=pcsmock type=minimal) Operations: monitor: x2-monitor-interval-10s interval=10s timeout=20s - Resource: x3 (class=ocf provider=pacemaker type=Dummy) + Resource: x3 (class=ocf provider=pcsmock type=minimal) Operations: monitor: x3-monitor-interval-10s interval=10s timeout=20s - Resource: y1 (class=ocf provider=pacemaker type=Dummy) + Resource: y1 (class=ocf provider=pcsmock type=minimal) Operations: monitor: y1-monitor-interval-10s interval=10s timeout=20s Clone: y2-clone - Resource: y2 (class=ocf provider=pacemaker type=Dummy) + Resource: y2 (class=ocf provider=pcsmock type=minimal) Operations: monitor: y2-monitor-interval-10s interval=10s timeout=20s @@ -449,15 +449,15 @@ class PcsConfigTagsTest(TestTagMixin, TestCase): expected_stonith_devices = outdent( """ Stonith Devices: - Resource: fence-rh-1 (class=stonith type=fence_xvm) + Resource: fence-rh-1 (class=stonith type=fence_pcsmock_minimal) Operations: monitor: fence-rh-1-monitor-interval-60s interval=60s - Resource: fence-rh-2 (class=stonith type=fence_xvm) + Resource: fence-rh-2 (class=stonith type=fence_pcsmock_minimal) Operations: monitor: fence-rh-2-monitor-interval-60s interval=60s - Resource: fence-kdump (class=stonith type=fence_kdump) + Resource: fence-kdump (class=stonith type=fence_pcsmock_minimal) Attributes: fence-kdump-instance_attributes pcmk_host_list="rh-1 rh-2" Operations: diff --git a/pcs_test/tools/bin_mock/__init__.py b/pcs_test/tools/bin_mock/__init__.py index 9bef8e64f..0a9b5c1cd 100644 --- a/pcs_test/tools/bin_mock/__init__.py +++ b/pcs_test/tools/bin_mock/__init__.py @@ -20,6 +20,8 @@ def get_mock_settings(*required_settings): + if not required_settings: + required_settings = MOCK_SETTINGS.keys() return { key: value for key, value in MOCK_SETTINGS.items() diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/list_agents_ocf__heartbeat b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/list_agents_ocf__heartbeat index 61f28bc15..186d3a817 100644 --- a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/list_agents_ocf__heartbeat +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/list_agents_ocf__heartbeat @@ -1,54 +1 @@ -CTDB -Delay -Dummy -Filesystem -IPaddr -IPaddr2 -IPsrcaddr -LVM-activate -MailTo -NodeUtilization -Route -SendArp -Squid -VirtualDomain -Xinetd -aliyun-vpc-move-ip -apache -aws-vpc-move-ip -awseip -awsvip -azure-lb -conntrackd -db2 -dhcpd -docker -ethmonitor -exportfs -galera -garbd -iSCSILogicalUnit -iSCSITarget -iface-vlan -lvmlockd -mysql -nagios -named -nfsnotify -nfsserver -nginx -oraasm -oracle -oralsnr -pgsql -podman -portblock -postfix -rabbitmq-cluster -redis -rsyncd -slapd -sybaseASE -symlink -tomcat -vdo-vol +pcsMock diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/list_agents_ocf__pacemaker b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/list_agents_ocf__pacemaker index 730c4f64a..47ddaaa95 100644 --- a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/list_agents_ocf__pacemaker +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/list_agents_ocf__pacemaker @@ -1,14 +1,2 @@ -ClusterMon -Dummy -HealthCPU -HealthSMART -Stateful -SysInfo -SystemHealth -attribute -booth-site -controld -ifspeed -ping -pingd +pcsMock remote diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/list_agents_ocf__pcsmock b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/list_agents_ocf__pcsmock new file mode 100644 index 000000000..17972817c --- /dev/null +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/list_agents_ocf__pcsmock @@ -0,0 +1,7 @@ +action_method +CamelCase +duplicate_monitor +minimal +params +stateful +unique diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/list_ocf_providers b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/list_ocf_providers index d2ad008ef..4b1933a7c 100644 --- a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/list_ocf_providers +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/list_ocf_providers @@ -1,4 +1,3 @@ -booth heartbeat -openstack pacemaker +pcsmock diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/lsb__network_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/lsb__pcsmock_metadata.xml similarity index 73% rename from pcs_test/tools/bin_mock/pcmk/crm_resource.d/lsb__network_metadata.xml rename to pcs_test/tools/bin_mock/pcmk/crm_resource.d/lsb__pcsmock_metadata.xml index 8652f537b..9a93b36f1 100644 --- a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/lsb__network_metadata.xml +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/lsb__pcsmock_metadata.xml @@ -1,12 +1,11 @@ - + 1.0 - Bring up/down networking +This is a mock agent for pcs test - lsb agent - Bring up/down networking - + pcs test mock lsb agent @@ -19,15 +18,12 @@ - $network - + pcsmock - iptables ip6tables NetworkManager-wait-online NetworkManager $network-pre - + - diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__heartbeat__Dummy_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__heartbeat__Dummy_metadata.xml deleted file mode 100644 index 5fe999ef2..000000000 --- a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__heartbeat__Dummy_metadata.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - -1.0 - - -This is a Dummy Resource Agent. It does absolutely nothing except -keep track of whether its running or not. -Its purpose in life is for testing and to serve as a template for RA writers. - -NB: Please pay attention to the timeouts specified in the actions -section below. They should be meaningful for the kind of resource -the agent manages. They should be the minimum advised timeouts, -but they shouldn't/cannot cover _all_ possible resource -instances. So, try to be neither overly generous nor too stingy, -but moderate. The minimum timeouts should never be below 10 seconds. - -Example stateless resource agent - - - - -Location to store the resource state in. - -State file - - - - - -Fake attribute that can be changed to cause a reload - -Fake attribute that can be changed to cause a reload - - - - - - - - - - - - - - - - - diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__heartbeat__Filesystem_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__heartbeat__Filesystem_metadata.xml deleted file mode 100644 index 847a03a2c..000000000 --- a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__heartbeat__Filesystem_metadata.xml +++ /dev/null @@ -1,145 +0,0 @@ - - - -1.1 - - -Resource script for Filesystem. It manages a Filesystem on a -shared storage medium. - -The standard monitor operation of depth 0 (also known as probe) -checks if the filesystem is mounted. If you want deeper tests, -set OCF_CHECK_LEVEL to one of the following values: - -10: read first 16 blocks of the device (raw read) - -This doesn't exercise the filesystem at all, but the device on -which the filesystem lives. This is noop for non-block devices -such as NFS, SMBFS, or bind mounts. - -20: test if a status file can be written and read - -The status file must be writable by root. This is not always the -case with an NFS mount, as NFS exports usually have the -"root_squash" option set. In such a setup, you must either use -read-only monitoring (depth=10), export with "no_root_squash" on -your NFS server, or grant world write permissions on the -directory where the status file is to be placed. - -Manages filesystem mounts - - - - -The name of block device for the filesystem, or -U, -L options for mount, or NFS mount specification. - -block device - - - - - -The mount point for the filesystem. - -mount point - - - - - -The type of filesystem to be mounted. - -filesystem type - - - - - -Any extra options to be given as -o options to mount. - -For bind mounts, add "bind" here and set fstype to "none". -We will do the right thing for options such as "bind,ro". - -options - - - - - -The prefix to be used for a status file for resource monitoring -with depth 20. If you don't specify this parameter, all status -files will be created in a separate directory. - -status file prefix - - - - - -Specify how to decide whether to run fsck or not. - -"auto" : decide to run fsck depending on the fstype(default) -"force" : always run fsck regardless of the fstype -"no" : do not run fsck ever. - -run_fsck - - - - - -Normally, we expect no users of the filesystem and the stop -operation to finish quickly. If you cannot control the filesystem -users easily and want to prevent the stop action from failing, -then set this parameter to "no" and add an appropriate timeout -for the stop operation. - -fast stop - - - - - -The use of a clone setup for local filesystems is forbidden -by default. For special setups like glusterfs, cloning a mount -of a local device with a filesystem like ext4 or xfs independently -on several nodes is a valid use case. - -Only set this to "true" if you know what you are doing! - -allow running as a clone, regardless of filesystem type - - - - - -This option allows specifying how to handle processes that are -currently accessing the mount directory. - -"true" : Default value, kill processes accessing mount point -"safe" : Kill processes accessing mount point using methods that - avoid functions that could potentially block during process - detection -"false" : Do not kill any processes. - -The 'safe' option uses shell logic to walk the /procs/ directory -for pids using the mount point while the default option uses the -fuser cli tool. fuser is known to perform operations that can potentially -block if unresponsive nfs mounts are in use on the system. - -Kill processes before unmount - - - - - - - - - - - - - - - diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__heartbeat__IPaddr2_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__heartbeat__IPaddr2_metadata.xml deleted file mode 100644 index 91ab65c62..000000000 --- a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__heartbeat__IPaddr2_metadata.xml +++ /dev/null @@ -1,286 +0,0 @@ - - - -1.0 - - -This Linux-specific resource manages IP alias IP addresses. -It can add an IP alias, or remove one. -In addition, it can implement Cluster Alias IP functionality -if invoked as a clone resource. - -If used as a clone, you should explicitly set clone-node-max >= 2, -and/or clone-max < number of nodes. In case of node failure, -clone instances need to be re-allocated on surviving nodes. -This would not be possible if there is already an instance on those nodes, -and clone-node-max=1 (which is the default). - - -Manages virtual IPv4 and IPv6 addresses (Linux specific version) - - - - -The IPv4 (dotted quad notation) or IPv6 address (colon hexadecimal notation) -example IPv4 "192.168.1.1". -example IPv6 "2001:db8:DC28:0:0:FC57:D4C8:1FFF". - -IPv4 or IPv6 address - - - - -The base network interface on which the IP address will be brought -online. -If left empty, the script will try and determine this from the -routing table. - -Do NOT specify an alias interface in the form eth0:1 or anything here; -rather, specify the base interface only. -If you want a label, see the iflabel parameter. - -Prerequisite: - -There must be at least one static IP address, which is not managed by -the cluster, assigned to the network interface. -If you can not assign any static IP address on the interface, -modify this kernel parameter: - -sysctl -w net.ipv4.conf.all.promote_secondaries=1 # (or per device) - -Network interface - - - - - -The netmask for the interface in CIDR format -(e.g., 24 and not 255.255.255.0) - -If unspecified, the script will also try to determine this from the -routing table. - -CIDR netmask - - - - - -Broadcast address associated with the IP. It is possible to use the -special symbols '+' and '-' instead of the broadcast address. In this -case, the broadcast address is derived by setting/resetting the host -bits of the interface prefix. - -Broadcast address - - - - - -You can specify an additional label for your IP address here. -This label is appended to your interface name. - -The kernel allows alphanumeric labels up to a maximum length of 15 -characters including the interface name and colon (e.g. eth0:foobar1234) - -A label can be specified in nic parameter but it is deprecated. -If a label is specified in nic name, this parameter has no effect. - -Interface label - - - - - -Enable support for LVS Direct Routing configurations. In case a IP -address is stopped, only move it to the loopback device to allow the -local node to continue to service requests, but no longer advertise it -on the network. - -Notes for IPv6: -It is not necessary to enable this option on IPv6. -Instead, enable 'lvs_ipv6_addrlabel' option for LVS-DR usage on IPv6. - -Enable support for LVS DR - - - - - -Enable adding IPv6 address label so IPv6 traffic originating from -the address's interface does not use this address as the source. -This is necessary for LVS-DR health checks to realservers to work. Without it, -the most recently added IPv6 address (probably the address added by IPaddr2) -will be used as the source address for IPv6 traffic from that interface and -since that address exists on loopback on the realservers, the realserver -response to pings/connections will never leave its loopback. -See RFC3484 for the detail of the source address selection. - -See also 'lvs_ipv6_addrlabel_value' parameter. - -Enable adding IPv6 address label. - - - - - -Specify IPv6 address label value used when 'lvs_ipv6_addrlabel' is enabled. -The value should be an unused label in the policy table -which is shown by 'ip addrlabel list' command. -You would rarely need to change this parameter. - -IPv6 address label value. - - - - - -Set the interface MAC address explicitly. Currently only used in case of -the Cluster IP Alias. Leave empty to chose automatically. - - -Cluster IP MAC address - - - - - -Specify the hashing algorithm used for the Cluster IP functionality. - - -Cluster IP hashing function - - - - - -If true, add the clone ID to the supplied value of IP to create -a unique address to manage - -Create a unique address for cloned instances - - - - - -Specify the interval between unsolicited ARP packets in milliseconds. - -This parameter is deprecated and used for the backward compatibility only. -It is effective only for the send_arp binary which is built with libnet, -and send_ua for IPv6. It has no effect for other arp_sender. - -ARP packet interval in ms (deprecated) - - - - - -Number of unsolicited ARP packets to send at resource initialization. - -ARP packet count sent during initialization - - - - - -Number of unsolicited ARP packets to send during resource monitoring. Doing -so helps mitigate issues of stuck ARP caches resulting from split-brain -situations. - -ARP packet count sent during monitoring - - - - - -Whether or not to send the ARP packets in the background. - -ARP from background - - - - - -The program to send ARP packets with on start. Available options are: - - send_arp: default - - ipoibarping: default for infiniband interfaces if ipoibarping is available - - iputils_arping: use arping in iputils package - - libnet_arping: use another variant of arping based on libnet - -ARP sender - - - - - -Extra options to pass to the arp_sender program. -Available options are vary depending on which arp_sender is used. - -A typical use case is specifying '-A' for iputils_arping to use -ARP REPLY instead of ARP REQUEST as Gratuitous ARPs. - -Options for ARP sender - - - - - -Flush the routing table on stop. This is for -applications which use the cluster IP address -and which run on the same physical host that the -IP address lives on. The Linux kernel may force that -application to take a shortcut to the local loopback -interface, instead of the interface the address -is really bound to. Under those circumstances, an -application may, somewhat unexpectedly, continue -to use connections for some time even after the -IP address is deconfigured. Set this parameter in -order to immediately disable said shortcut when the -IP address goes away. - -Flush kernel routing table on stop - - - - - -Whether or not to run arping for IPv4 collision detection check. - -Run arping for IPv4 collision detection check - - - - - -For IPv6, set the preferred lifetime of the IP address. -This can be used to ensure that the created IP address will not -be used as a source address for routing. -Expects a value as specified in section 5.5.4 of RFC 4862. - -IPv6 preferred lifetime - - - - - -Set number of retries to find interface in monitor-action. - -ONLY INCREASE IF THE AGENT HAS ISSUES FINDING YOUR NIC DURING THE -MONITOR-ACTION. A HIGHER SETTING MAY LEAD TO DELAYS IN DETECTING -A FAILURE. - -Number of retries to find interface in monitor-action - - - - - - - - - - - - - - diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__heartbeat__pcsMock_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__heartbeat__pcsMock_metadata.xml new file mode 120000 index 000000000..6278b9335 --- /dev/null +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__heartbeat__pcsMock_metadata.xml @@ -0,0 +1 @@ +ocf__pacemaker__pcsMock_metadata.xml \ No newline at end of file diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__Dummy_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__Dummy_metadata.xml deleted file mode 100644 index c4d74476d..000000000 --- a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__Dummy_metadata.xml +++ /dev/null @@ -1,78 +0,0 @@ - - -1.1 - - -This is a dummy OCF resource agent. It does absolutely nothing except keep track -of whether it is running or not, and can be configured so that actions fail or -take a long time. Its purpose is primarily for testing, and to serve as a -template for resource agent writers. - -Example stateless resource agent - - - - -Location to store the resource state in. - -State file - - - - - -Fake password field - -Password - - - - - -Fake attribute that can be changed to cause an agent reload - -Fake attribute that can be changed to cause an agent reload - - - - - -Number of seconds to sleep during operations. This can be used to test how -the cluster reacts to operation timeouts. - -Operation sleep duration in seconds. - - - - - -Start, migrate_from, and reload-agent actions will return failure if running on -the host specified here, but the resource will run successfully anyway (future -monitor calls will find it running). This can be used to test on-fail=ignore. - -Report bogus start failure on specified host - - - - - -If this is set, the environment will be dumped to this file for every call. - -Environment dump file - - - - - - - - - - - - - - - - - diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__HealthCPU_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__HealthCPU_metadata.xml deleted file mode 100644 index a3b00b026..000000000 --- a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__HealthCPU_metadata.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - -1.0 - - -System health agent that measures the CPU idling and updates the #health-cpu attribute. - -System health CPU usage - - - - -Location to store the resource state in. - -State file - - - - - -Lower (!) limit of idle percentage to switch the health attribute to yellow. I.e. -the #health-cpu will go yellow if the %idle of the CPU falls below 50%. - -Lower limit for yellow health attribute - - - - - - -Lower (!) limit of idle percentage to switch the health attribute to red. I.e. -the #health-cpu will go red if the %idle of the CPU falls below 10%. - -Lower limit for red health attribute - - - - - - - - - - - - - - diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__Stateful_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__Stateful_metadata.xml deleted file mode 100644 index d35d66c8d..000000000 --- a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__Stateful_metadata.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - -1.0 - - -This is an example resource agent that implements two states - -Example stateful resource agent - - - - - -Location to store the resource state in - -State file - - - - - -If this is set, the environment will be dumped to this file for every call. - -Environment dump file - - - - - -The notify action will sleep for this many seconds before returning, -to simulate a long-running notify. - -Notify delay in seconds - - - - - - - - - - - - - - - - diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__SystemHealth_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__SystemHealth_metadata.xml deleted file mode 100644 index 3aa758f40..000000000 --- a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__SystemHealth_metadata.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - -1.0 - - -This is a SystemHealth Resource Agent. It is used to monitor -the health of a system via IPMI. - -SystemHealth resource agent - - - - - - - - - - - - - - diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__pcsMock_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__pcsMock_metadata.xml new file mode 100644 index 000000000..941a2fb90 --- /dev/null +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__pcsMock_metadata.xml @@ -0,0 +1,23 @@ + + +1.1 + + + This is a mock agent for pcs test - minimal agent. It is placed in a real + provider to cover those test cases where we need a real provider. It is also + useful for test cases where two providers ship an agent with the same name. + +Mock agent for pcs tests - minimal agent + + + + + + + + + + + + + diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__remote_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__remote_metadata.xml index b7d2bca3e..b6cf4a60f 100644 --- a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__remote_metadata.xml +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pacemaker__remote_metadata.xml @@ -1,43 +1,41 @@ - - -1.0 -remote resource agent + + 1.1 + Pacemaker Remote connection - - - Server location to connect to. This can be an ip address or hostname. - - Server location - + + + Server location to connect to (IP address or resolvable host name) + + Remote hostname + - - - tcp port to connect to. - - tcp port - + + + TCP port at which to contact Pacemaker Remote executor + + Remote port + - - - Interval in seconds at which Pacemaker will attempt to reconnect to a - remote node after an active connection to the remote node has been - severed. When this value is nonzero, Pacemaker will retry the connection - indefinitely, at the specified interval. As with any time-based actions, - this is not guaranteed to be checked more frequently than the value of - the cluster-recheck-interval cluster option. - - reconnect interval - + + + If this is a positive time interval, the cluster will attempt to + reconnect to a remote node after an active connection has been + lost at this interval. Otherwise, the cluster will attempt to + reconnect immediately (after any fencing needed). + + reconnect interval + - - - - - - - + + + + + + + + diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__CamelCase_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__CamelCase_metadata.xml new file mode 100644 index 000000000..3611fda75 --- /dev/null +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__CamelCase_metadata.xml @@ -0,0 +1,21 @@ + + +1.1 + + +This is a mock agent for pcs test - minimal agent with CamelCase name + +Mock agent for pcs tests - minimal agent + + + + + + + + + + + + + diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__action_method_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__action_method_metadata.xml new file mode 100644 index 000000000..88715f067 --- /dev/null +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__action_method_metadata.xml @@ -0,0 +1,41 @@ + + +1.1 + + +This is a mock agent for pcs test - agent with stonith parameters action and method + +Mock agent for pcs tests - stonithlike agent + + + + + Fencing action (null, off, on, [reboot], status, list, list-status, monitor, validate-all, metadata) + + Fencing action + + + + + Method to fence - cycle or onoff + + Method to fence + + + + + + + + + + + + + + + + + diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__duplicate_monitor_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__duplicate_monitor_metadata.xml new file mode 100644 index 000000000..3a01814b6 --- /dev/null +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__duplicate_monitor_metadata.xml @@ -0,0 +1,23 @@ + + +1.1 + + + This is a mock agent for pcs test - promotable agent with both monitor + operations having the same interval + +Mock agent for pcs tests - promotable agent with duplicate monitors + + + + + + + + + + + + + + diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__minimal_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__minimal_metadata.xml new file mode 100644 index 000000000..013ac61ce --- /dev/null +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__minimal_metadata.xml @@ -0,0 +1,21 @@ + + +1.1 + + +This is a mock agent for pcs test - minimal agent + +Mock agent for pcs tests - minimal agent + + + + + + + + + + + + + diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__params_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__params_metadata.xml new file mode 100644 index 000000000..32cb028a9 --- /dev/null +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__params_metadata.xml @@ -0,0 +1,70 @@ + + +1.1 + + +This is a mock agent for pcs test - agent with parameters + +Mock agent for pcs tests - agent with various parameters + + + + + A generic mandatory string parameter + + mandatory string parameter + + + + + A generic optional string parameter + + optional string parameter + + + + + An optional enum parameter + + optional enum parameter + + + + + + This parameter should not be set usually + + advanced parameter + + + + + First parameter in a unique group + + unique param 1 + + + + + Second parameter in a unique group + + unique param 2 + + + + + + + + + + + + + + + + diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__stateful_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__stateful_metadata.xml new file mode 100644 index 000000000..e3e2ac366 --- /dev/null +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__stateful_metadata.xml @@ -0,0 +1,22 @@ + + +1.1 + + +This is a mock agent for pcs test - minimal promotable agent + +Mock agent for pcs tests - promotable agent + + + + + + + + + + + + + + diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__unique_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__unique_metadata.xml new file mode 100644 index 000000000..7eb8d3ceb --- /dev/null +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/ocf__pcsmock__unique_metadata.xml @@ -0,0 +1,31 @@ + + +1.1 + + +This is a mock agent for pcs test - agent with unique-group parameters + +Mock agent for pcs tests - unique parameters + + + + + Location to store the resource state in. + + State file + + + + + + + + + + + + + + + + diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/systemd__pcsmock@a__b_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/systemd__pcsmock@a__b_metadata.xml new file mode 120000 index 000000000..2aaf126ef --- /dev/null +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/systemd__pcsmock@a__b_metadata.xml @@ -0,0 +1 @@ +systemd__pcsmock_metadata.xml \ No newline at end of file diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/systemd__pcsmock_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/systemd__pcsmock_metadata.xml new file mode 100644 index 000000000..d4039cbc2 --- /dev/null +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/systemd__pcsmock_metadata.xml @@ -0,0 +1,17 @@ + + + 1.1 + + This is a mock agent for pcs test - systemd agent + + systemd unit file for pcsmock + + + + + + + + + + diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/systemd__test@a__b_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/systemd__test@a__b_metadata.xml deleted file mode 100644 index ca284171a..000000000 --- a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/systemd__test@a__b_metadata.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - 1.0 - - test@a:b.service - - systemd unit file for test@a:b - - - - - - - - - - - - - diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource_mock.py b/pcs_test/tools/bin_mock/pcmk/crm_resource_mock.py index 58cf25e66..768098452 100644 --- a/pcs_test/tools/bin_mock/pcmk/crm_resource_mock.py +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource_mock.py @@ -42,21 +42,24 @@ def main(): raise AssertionError() arg = argv[0] known_agents = ( - "lsb:network", - "ocf:heartbeat:Dummy", - "ocf:heartbeat:IPaddr2", - "ocf:heartbeat:Filesystem", - "ocf:pacemaker:Dummy", - "ocf:pacemaker:HealthCPU", + "lsb:pcsmock", + "ocf:heartbeat:pcsMock", + "ocf:pacemaker:pcsMock", "ocf:pacemaker:remote", - "ocf:pacemaker:Stateful", - "ocf:pacemaker:SystemHealth", + "ocf:pcsmock:action_method", + "ocf:pcsmock:CamelCase", + "ocf:pcsmock:duplicate_monitor", + "ocf:pcsmock:minimal", + "ocf:pcsmock:params", + "ocf:pcsmock:stateful", + "ocf:pcsmock:unique", "stonith:fence_pcsmock_action", "stonith:fence_pcsmock_method", "stonith:fence_pcsmock_minimal", "stonith:fence_pcsmock_params", "stonith:fence_pcsmock_unfencing", - "systemd:test@a:b", + "systemd:pcsmock", + "systemd:pcsmock@a:b", ) # known_agents_map = {item.lower()} if arg in known_agents: @@ -76,7 +79,7 @@ def main(): if len(argv) != 1: raise AssertionError() arg = argv[0] - known_providers = ("ocf:heartbeat", "ocf:pacemaker", "stonith") + known_providers = ("ocf:heartbeat", "ocf:pacemaker", "ocf:pcsmock") if arg in known_providers: write_local_file_to_stdout( "list_agents_{}".format(arg.replace(":", "__")) @@ -87,7 +90,11 @@ def main(): if get_arg_values(argv, "--output-as")[0] != "xml": raise AssertionError() provider = get_arg_values(argv, "--provider") - is_invalid = "fake=is_invalid=True" in argv + is_invalid = False + for arg in argv: + if "=" in arg and arg.split("=", 1)[1] == "is_invalid=True": + is_invalid = True + break output = "" if is_invalid: output = """pcsmock validation failure""" diff --git a/pcs_test/tools/fixture_cib.py b/pcs_test/tools/fixture_cib.py index 711f769c4..8f11b0aaa 100644 --- a/pcs_test/tools/fixture_cib.py +++ b/pcs_test/tools/fixture_cib.py @@ -8,6 +8,7 @@ from pcs.lib.external import CommandRunner from pcs_test.tools.assertions import AssertPcsMixin +from pcs_test.tools.bin_mock import get_mock_settings from pcs_test.tools.custom_mock import MockLibraryReportProcessor from pcs_test.tools.misc import ( get_test_resource, @@ -33,6 +34,7 @@ def set_up(self): os.makedirs(fixture_dir, exist_ok=True) self._cache_path = os.path.join(fixture_dir, self._cache_name) self._pcs_runner = PcsRunner(self._cache_path) + self._pcs_runner.mock_settings = get_mock_settings() with ( open(self._empty_cib_path, "r") as template_file, @@ -139,7 +141,7 @@ def fixture_master_xml(name, all_ops=True, meta_dict=None): meta_xml = "\n".join(meta_lines) master = f""" - + `dumb` ./pcs_test/tier1/legacy/test_utils.py: `ue` -> `use`, `due` ./pcs_test/tier1/legacy/test_utils.py: `ue` -> `use`, `due` -./pcs_test/tools/bin_mock/pcmk/crm_resource.d/list_agents_ocf__heartbeat: `exportfs` -> `exports` From fb430f0b4ed2db3d5bee89f1e402de06566c56fa Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Tue, 20 Aug 2024 11:48:05 +0200 Subject: [PATCH 020/227] test: fix tier1.legacy.test_cluster.ClusterUpgradeTest Make the test verify that the version has been increased rather than looking for a specific version which may change with new pcmk release. --- pcs_test/tier1/legacy/test_cluster.py | 36 +++++++++++++++++++++------ 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/pcs_test/tier1/legacy/test_cluster.py b/pcs_test/tier1/legacy/test_cluster.py index 03064000a..ebb9b6cb1 100644 --- a/pcs_test/tier1/legacy/test_cluster.py +++ b/pcs_test/tier1/legacy/test_cluster.py @@ -1,8 +1,10 @@ import os +import re from functools import partial from unittest import TestCase from pcs_test.tools.assertions import AssertPcsMixin +from pcs_test.tools.misc import compare_version from pcs_test.tools.misc import get_test_resource as rc from pcs_test.tools.misc import ( get_tmp_dir, @@ -16,6 +18,7 @@ PcsRunner, pcs, ) +from pcs_test.tools.xml import str_to_etree class UidGidTest(TestCase): @@ -278,10 +281,23 @@ def tearDown(self): self.temp_cib.close() def test_cluster_upgrade(self): + def extract_version(string_value): + match = re.match( + r"^pacemaker-(?P\d+)\.(?P\d+)(\.(?P\d+))?$", + string_value, + ) + return ( + int(match.group("major")), + int(match.group("minor")), + int(match.group("rev")) if match.group("rev") else 0, + ) + self.temp_cib.seek(0) - data = self.temp_cib.read() - assert data.find("pacemaker-1.2") != -1 - assert data.find("pacemaker-2.") == -1 + cib = str_to_etree(self.temp_cib.read()) + validate_with = cib.getroottree().getroot().get("validate-with") + self.assertEqual(validate_with, "pacemaker-1.2") + version_before = extract_version(validate_with) + self.assertEqual(version_before, (1, 2, 0)) stdout, stderr, retval = pcs( self.temp_cib.name, "cluster cib-upgrade".split() @@ -293,10 +309,10 @@ def test_cluster_upgrade(self): self.assertEqual(retval, 0) self.temp_cib.seek(0) - data = self.temp_cib.read() - assert data.find("pacemaker-1.2") == -1 - assert data.find("pacemaker-2.") == -1 - assert data.find("pacemaker-3.") != -1 + cib = str_to_etree(self.temp_cib.read()) + validate_with = cib.getroottree().getroot().get("validate-with") + version_after = extract_version(validate_with) + self.assertEqual(compare_version(version_before, version_after), -1) stdout, stderr, retval = pcs( self.temp_cib.name, "cluster cib-upgrade".split() @@ -307,6 +323,12 @@ def test_cluster_upgrade(self): ) self.assertEqual(retval, 0) + self.temp_cib.seek(0) + cib = str_to_etree(self.temp_cib.read()) + validate_with = cib.getroottree().getroot().get("validate-with") + version_after2 = extract_version(validate_with) + self.assertEqual(compare_version(version_after, version_after2), 0) + @skip_unless_root() class ClusterStartStop(TestCase, AssertPcsMixin): From 20613b8d89735dc5a1f916979bd5c6cfd2c8ca37 Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Thu, 22 Aug 2024 09:54:41 +0200 Subject: [PATCH 021/227] do not return error when using instances quantifier on clone with one instance --- CHANGELOG.md | 5 + pcs/common/resource_status.py | 89 ++++++++++------- pcs_test/tier0/common/test_resource_status.py | 96 +++++++++++++++++++ 3 files changed, 156 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80db001c3..175b85a22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,12 @@ ([RHEL-46284]) - Command `resource.restart` in API v2 +### Fixed +- Do not end with error when using the instances quantifier in `pcs status + query resource is-state` command ([RHEL-55441]) + [RHEL-46284]: https://issues.redhat.com/browse/RHEL-46284 +[RHEL-55441]: https://issues.redhat.com/browse/RHEL-55441 ## [0.11.8] - 2024-07-09 diff --git a/pcs/common/resource_status.py b/pcs/common/resource_status.py index d0b297dbb..fea91a903 100644 --- a/pcs/common/resource_status.py +++ b/pcs/common/resource_status.py @@ -422,43 +422,62 @@ def _get_instances_for_state_check( return GroupInstances(cast(list[GroupStatusDto], instance_list)) - def _validate_members_quantifier( - self, - resource: CheckedResourceType, - members_quantifier: Optional[MoreChildrenQuantifierType], - ) -> None: - if members_quantifier is None: - return - - if isinstance(resource, GroupInstances): - return + def can_have_multiple_members( + self, resource_id: str, instance_id: Optional[str] = None + ) -> bool: + """ + Check if the resource with the given id can have multiple inner members. - if isinstance(resource, CloneStatusDto): - member_id_list = self.get_members(resource.resource_id, None) - if any( + resource_id -- id of the resource + instance_id -- id describing unique instance of cloned or bundled + resource + """ + resource_type = self.get_type(resource_id, instance_id) + return resource_type == ResourceType.GROUP or ( + resource_type == ResourceType.CLONE + and any( self.get_type(member_id, None) == ResourceType.GROUP - for member_id in member_id_list - ): - return + for member_id in self.get_members(resource_id, instance_id) + ) + ) - raise MembersQuantifierUnsupportedException() + def can_have_multiple_instances( + self, resource_id: str, instance_id: Optional[str] = None + ) -> bool: + """ + Check if the resource with the given id can have multiple instances. - def _validate_instance_quantifier( + resource_id -- id of the resource + instance_id -- id describing unique instance of cloned or bundled + resource + """ + resource_type = self.get_type(resource_id, instance_id) + return instance_id is None and ( + resource_type in (ResourceType.CLONE, ResourceType.BUNDLE) + or self.get_parent_clone_id(resource_id, None) is not None + or ( + resource_type == ResourceType.PRIMITIVE + and self.get_parent_bundle_id(resource_id, None) is not None + ) + ) + + def _validate_quantifiers( self, - resource: CheckedResourceType, + resource_id: str, + instance_id: Optional[str], + members_quantifier: Optional[MoreChildrenQuantifierType], instances_quantifier: Optional[MoreChildrenQuantifierType], ) -> None: - # pylint: disable=no-self-use - if instances_quantifier is None: - return - - if isinstance(resource, (BundleStatusDto, CloneStatusDto)): - return - - if len(resource.instances) > 1: - return - - raise InstancesQuantifierUnsupportedException() + if ( + members_quantifier is not None + and not self.can_have_multiple_members(resource_id, instance_id) + ): + raise MembersQuantifierUnsupportedException() + if ( + instances_quantifier is not None + and not self.can_have_multiple_instances(resource_id, instance_id) + ): + raise InstancesQuantifierUnsupportedException() def is_state( self, @@ -496,8 +515,9 @@ def is_state( """ resource = self._get_instances_for_state_check(resource_id, instance_id) - self._validate_members_quantifier(resource, members_quantifier) - self._validate_instance_quantifier(resource, instances_quantifier) + self._validate_quantifiers( + resource_id, instance_id, members_quantifier, instances_quantifier + ) if not isinstance(state.value, list): checked_state = [state.value] @@ -554,8 +574,9 @@ def is_state_exact_value( """ resource = self._get_instances_for_state_check(resource_id, instance_id) - self._validate_members_quantifier(resource, members_quantifier) - self._validate_instance_quantifier(resource, instances_quantifier) + self._validate_quantifiers( + resource_id, instance_id, members_quantifier, instances_quantifier + ) if not isinstance(state.value, list): checked_state = [state.value] diff --git a/pcs_test/tier0/common/test_resource_status.py b/pcs_test/tier0/common/test_resource_status.py index 6b969d397..c38a72e66 100644 --- a/pcs_test/tier0/common/test_resource_status.py +++ b/pcs_test/tier0/common/test_resource_status.py @@ -1042,6 +1042,84 @@ def test_nonexistent(self): self.assertEqual(cm.exception.instance_id, None) +class TestFacadeCanHaveMultipleMembers(TestCase): + def test_nonexistent(self): + facade = ResourcesStatusFacade([]) + + with self.assertRaises(ResourceNonExistentException) as cm: + facade.can_have_multiple_members("nonexistent", None) + self.assertEqual(cm.exception.resource_id, "nonexistent") + self.assertEqual(cm.exception.instance_id, None) + + def test_all(self): + facade = fixture_facade() + resource_id_list = [ + ("stonith", False), + ("primitive", False), + ("group", True), + ("group-primitive_1", False), + ("clone", False), + ("clone-primitive", False), + ("cloned_group", True), + ("cloned_group-group", True), + ("cloned_group-group-primitive_1", False), + ("bundle", False), + ("bundle-member", False), + ] + for resource_id, result in resource_id_list: + with self.subTest(value=f"{resource_id}"): + self.assertEqual( + facade.can_have_multiple_members(resource_id, None), result + ) + + +class TestFacadeCanHaveMultipleInstances(TestCase): + def setUp(self): + self.facade = fixture_facade() + + def test_nonexistent(self): + with self.assertRaises(ResourceNonExistentException) as cm: + self.facade.can_have_multiple_instances("nonexistent", None) + self.assertEqual(cm.exception.resource_id, "nonexistent") + self.assertEqual(cm.exception.instance_id, None) + + def test_all(self): + resource_id_list = [ + ("stonith", False), + ("primitive", False), + ("group", False), + ("group-primitive_1", False), + ("clone", True), + ("clone-primitive", True), + ("cloned_group", True), + ("cloned_group-group", True), + ("cloned_group-group-primitive_1", True), + ("bundle", True), + ("bundle-member", True), + ] + for resource_id, result in resource_id_list: + with self.subTest(value=f"{resource_id}"): + self.assertEqual( + self.facade.can_have_multiple_instances(resource_id, None), + result, + ) + + def test_instance_id(self): + self.assertTrue( + self.facade.can_have_multiple_instances("clone_unique", None) + ) + self.assertTrue( + self.facade.can_have_multiple_instances( + "clone_unique-primitive", None + ) + ) + self.assertFalse( + self.facade.can_have_multiple_instances( + "clone_unique-primitive", "0" + ) + ) + + class TestFacadeIsState(TestCase): # pylint: disable=too-many-public-methods def test_nonexistent(self): @@ -2124,6 +2202,24 @@ def test_clone_instance(self): facade.is_state("primitive", None, ResourceState.MANAGED) ) + def test_clone_one_instance_quantifier(self): + facade = ResourcesStatusFacade( + [ + fixture_clone_dto( + "clone", + instances=[fixture_primitive_dto("primitive", None)], + ) + ] + ) + self.assertTrue( + facade.is_state( + "primitive", + None, + ResourceState.STARTED, + instances_quantifier=MoreChildrenQuantifierType.ALL, + ) + ) + def test_clone_instance_orphans(self): facade = ResourcesStatusFacade( [ From 1184ca0e60c7473c8b3409f11df85721df7dc08d Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Mon, 9 Sep 2024 15:30:24 +0200 Subject: [PATCH 022/227] add cluster.get_corosync_conf_struct to APIv2 --- CHANGELOG.md | 1 + pcs/common/types.py | 2 +- pcs/daemon/async_tasks/worker/command_mapping.py | 4 ++++ pcsd/capabilities.xml.in | 3 ++- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 175b85a22..e32697191 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Support for output formats `json` and `cmd` to `pcs tag config` command ([RHEL-46284]) - Command `resource.restart` in API v2 +- Add lib command `cluster.get_corosync_conf_struct` to API v2 ### Fixed - Do not end with error when using the instances quantifier in `pcs status diff --git a/pcs/common/types.py b/pcs/common/types.py index d97dd9ab1..41f14ff32 100644 --- a/pcs/common/types.py +++ b/pcs/common/types.py @@ -95,7 +95,7 @@ def from_str(cls, transport: str) -> "CorosyncTransportType": raise UnknownCorosyncTransportTypeException(transport) from None -class CorosyncNodeAddressType(Enum): +class CorosyncNodeAddressType(str, Enum): IPV4 = "IPv4" IPV6 = "IPv6" FQDN = "FQDN" diff --git a/pcs/daemon/async_tasks/worker/command_mapping.py b/pcs/daemon/async_tasks/worker/command_mapping.py index 5366f9c5d..ce465ad86 100644 --- a/pcs/daemon/async_tasks/worker/command_mapping.py +++ b/pcs/daemon/async_tasks/worker/command_mapping.py @@ -116,6 +116,10 @@ class _Cmd: cmd=cluster.generate_cluster_uuid, required_permission=p.SUPERUSER, ), + "cluster.get_corosync_conf_struct": _Cmd( + cmd=cluster.get_corosync_conf_struct, + required_permission=p.READ, + ), "cluster.node_clear": _Cmd( cmd=cluster.node_clear, required_permission=p.WRITE, diff --git a/pcsd/capabilities.xml.in b/pcsd/capabilities.xml.in index b194ae37d..e57e65bdb 100644 --- a/pcsd/capabilities.xml.in +++ b/pcsd/capabilities.xml.in @@ -249,11 +249,12 @@ daemon urls: get_corosync_conf - + Provide the local corosync.conf in a structured format. pcs commands: cluster config show + API v2: cluster.get_corosync_conf_struct From ace4460acd794c72bf3d306e101b122184d68a46 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Mon, 9 Sep 2024 16:51:21 +0200 Subject: [PATCH 023/227] add resource.get_configured_resources to APIv2 --- CHANGELOG.md | 3 ++- pcs/daemon/async_tasks/worker/command_mapping.py | 4 ++++ pcsd/capabilities.xml.in | 7 +++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e32697191..10a3881c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ - Support for output formats `json` and `cmd` to `pcs tag config` command ([RHEL-46284]) - Command `resource.restart` in API v2 -- Add lib command `cluster.get_corosync_conf_struct` to API v2 +- Add lib commands `cluster.get_corosync_conf_struct` and + `resource.get_configured_resources` to API v2 ### Fixed - Do not end with error when using the instances quantifier in `pcs status diff --git a/pcs/daemon/async_tasks/worker/command_mapping.py b/pcs/daemon/async_tasks/worker/command_mapping.py index ce465ad86..f81eb1ac1 100644 --- a/pcs/daemon/async_tasks/worker/command_mapping.py +++ b/pcs/daemon/async_tasks/worker/command_mapping.py @@ -322,6 +322,10 @@ class _Cmd: cmd=resource.enable, required_permission=p.WRITE, ), + "resource.get_configured_resources": _Cmd( + cmd=resource.get_configured_resources, + required_permission=p.READ, + ), "resource.group_add": _Cmd( cmd=resource.group_add, required_permission=p.WRITE, diff --git a/pcsd/capabilities.xml.in b/pcsd/capabilities.xml.in index e57e65bdb..111ef77aa 100644 --- a/pcsd/capabilities.xml.in +++ b/pcsd/capabilities.xml.in @@ -1961,6 +1961,13 @@ stonith config --output-format=text|json|cmd + + + Get configured pacemaker resources. + + API v2: resource.get_configured_resources + + Forget history of resources and redetect their current state. Optionally From 7d19a050c0401cf688db2408adafbaadb0d42418 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Tue, 17 Sep 2024 16:04:05 +0200 Subject: [PATCH 024/227] replace deprecated function find_unique_id --- pcs/lib/cib/alert.py | 269 ++++++++---- pcs/lib/commands/alert.py | 83 ++-- pcs/lib/pacemaker/values.py | 19 + pcs_test/tier0/lib/cib/test_alert.py | 511 +++++++++------------- pcs_test/tier0/lib/commands/test_alert.py | 170 +++++-- pcs_test/tier1/legacy/test_alert.py | 13 +- 6 files changed, 601 insertions(+), 464 deletions(-) diff --git a/pcs/lib/cib/alert.py b/pcs/lib/cib/alert.py index 40607b5e1..f4cac0ec9 100644 --- a/pcs/lib/cib/alert.py +++ b/pcs/lib/cib/alert.py @@ -1,19 +1,21 @@ from functools import partial +from typing import ( + Optional, + cast, +) from lxml import etree +from lxml.etree import _Element from pcs.common import reports -from pcs.common.reports import ReportProcessor -from pcs.common.reports.item import ReportItem from pcs.lib.cib.nvpair import get_nvset from pcs.lib.cib.tools import ( - check_new_id_applicable, + IdProvider, + create_subelement_id, find_element_by_tag_and_id, - find_unique_id, get_alerts, - validate_id_does_not_exist, ) -from pcs.lib.errors import LibraryError +from pcs.lib.pacemaker.values import validate_id_reports from pcs.lib.xml_tools import get_sub_element TAG_ALERT = "alert" @@ -23,7 +25,9 @@ find_recipient = partial(find_element_by_tag_and_id, TAG_RECIPIENT) -def _update_optional_attribute(element, attribute, value): +def _update_optional_attribute( + element: _Element, attribute: str, value: Optional[str] +) -> None: """ Update optional attribute of element. Remove existing element if value is empty. @@ -40,65 +44,95 @@ def _update_optional_attribute(element, attribute, value): del element.attrib[attribute] -def ensure_recipient_value_is_unique( - reporter: ReportProcessor, - alert, - recipient_value, - recipient_id="", - allow_duplicity=False, -): +def _validate_recipient_value_is_unique( + alert_el: _Element, + recipient_value: str, + recipient_id: Optional[str] = None, + allow_duplicity: bool = False, +) -> reports.ReportItemList: """ - Ensures that recipient_value is unique in alert. + Validate that the recipient_value is unique in the specified alert - reporter -- report processor - alert -- alert + alert_el -- alert recipient_value -- recipient value - recipient_id -- recipient id of to which value belongs to - allow_duplicity -- if True only warning will be shown if value already - exists + recipient_id -- id of the recipient to which the value belongs + allow_duplicity -- if True, report a warning if the value already exists """ - recipient_list = alert.xpath( + report_list: reports.ReportItemList = [] + recipient_list = alert_el.xpath( "./recipient[@value=$value and @id!=$id]", value=recipient_value, - id=recipient_id, + id=recipient_id or "", ) if recipient_list: - reporter.report( - ReportItem( + report_list.append( + reports.ReportItem( severity=reports.item.get_severity( reports.codes.FORCE, allow_duplicity, ), message=reports.messages.CibAlertRecipientAlreadyExists( - alert.get("id", None), + str(alert_el.attrib["id"]), recipient_value, ), ) ) - if reporter.has_errors: - raise LibraryError() + return report_list + +def validate_create_alert( + id_provider: IdProvider, + path: str, + alert_id: Optional[str] = None, +) -> reports.ReportItemList: + """ + validate new alert creation -def create_alert(tree, alert_id, path, description=""): + id_provider -- elements' ids generator + path -- path to script + alert_id -- id of new alert or None + """ + report_list: reports.ReportItemList = [] + if not path: + report_list.append( + reports.ReportItem.error( + reports.messages.RequiredOptionsAreMissing(["path"]) + ) + ) + if alert_id: + report_list.extend( + validate_id_reports(alert_id, description="alert-id") + ) + report_list.extend(id_provider.book_ids(alert_id)) + return report_list + + +def create_alert( + tree: _Element, + id_provider: IdProvider, + path: str, + alert_id: Optional[str] = None, + description: Optional[str] = None, +) -> _Element: """ 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 + id_provider -- elements' ids generator path -- path to script + alert_id -- id of new alert, it will be generated if it is None description -- description """ - if alert_id: - check_new_id_applicable(tree, "alert-id", alert_id) - else: - alert_id = find_unique_id(tree, "alert") + if not alert_id: + alert_id = id_provider.allocate_id(TAG_ALERT) - alert = etree.SubElement(get_alerts(tree), "alert", id=alert_id, path=path) + alert_el = etree.SubElement( + get_alerts(tree), TAG_ALERT, id=alert_id, path=path + ) if description: - alert.set("description", description) + alert_el.set("description", description) - return alert + return alert_el def update_alert(tree, alert_id, path, description=None): @@ -131,79 +165,130 @@ def remove_alert(tree, alert_id): alert.getparent().remove(alert) -def add_recipient( - reporter: ReportProcessor, - tree, - alert_id, - recipient_value, - recipient_id=None, - description="", - allow_same_value=False, -): +def validate_add_recipient( + id_provider: IdProvider, + alert_el: _Element, + recipient_value: str, + recipient_id: Optional[str] = None, + allow_same_value: bool = False, +) -> reports.ReportItemList: """ - Add recipient to alert with specified id. Returns added recipient element. - Raises LibraryError if alert with specified recipient_id doesn't exist. - Raises LibraryError if recipient already exists. + Validate adding a recipient to the specified alert - reporter -- report processor - tree -- cib etree node - alert_id -- id of alert which should be parent of new recipient - recipient_value -- value of recipient - recipient_id -- id of new recipient, if None it will be generated - description -- description of recipient - allow_same_value -- if True unique recipient value is not required + id_provider -- elements' ids generator + alert_el -- alert which should be the parent of the new recipient + recipient_value -- value of the new recipient + recipient_id -- id of the new recipient or None + allow_same_value -- if True, unique recipient value is not required """ - if recipient_id is None: - recipient_id = find_unique_id(tree, f"{alert_id}-recipient") - else: - validate_id_does_not_exist(tree, recipient_id) + report_list: reports.ReportItemList = [] - alert = find_alert(get_alerts(tree), alert_id) - ensure_recipient_value_is_unique( - reporter, alert, recipient_value, allow_duplicity=allow_same_value - ) - recipient = etree.SubElement( - alert, "recipient", id=recipient_id, value=recipient_value + if not recipient_value: + report_list.append( + reports.ReportItem.error( + reports.messages.RequiredOptionsAreMissing(["value"]) + ) + ) + + if recipient_id: + report_list.extend( + validate_id_reports(recipient_id, description="recipient-id") + ) + report_list.extend(id_provider.book_ids(recipient_id)) + + report_list.extend( + _validate_recipient_value_is_unique( + alert_el, + recipient_value, + recipient_id, + allow_duplicity=allow_same_value, + ) ) + return report_list + + +def add_recipient( + id_provider: IdProvider, + alert_el: _Element, + recipient_value: str, + recipient_id: Optional[str] = None, + description: Optional[str] = None, +) -> _Element: + """ + Add new recipient to the specified alert, return added recipient element + + id_provider -- elements' ids generator + alert_el -- alert which should be the parent of the new recipient + recipient_value -- value of the new recipient + recipient_id -- id of the new recipient or None + description -- description of the new recipient + """ + if not recipient_id: + recipient_id = create_subelement_id( + alert_el, TAG_RECIPIENT, id_provider + ) + recipient_el = etree.SubElement( + alert_el, TAG_RECIPIENT, id=recipient_id, value=recipient_value + ) if description: - recipient.attrib["description"] = description + recipient_el.attrib["description"] = description + return recipient_el + + +def validate_update_recipient( + recipient_el: _Element, + recipient_value: Optional[str] = None, + allow_same_value: bool = False, +) -> reports.ReportItemList: + """ + validate updating specified recipient - return recipient + recipient_el -- the recipient to be updated + recipient_value -- new recipient value, stay unchanged if None + allow_same_value -- if True, unique recipient value is not required + """ + report_list: reports.ReportItemList = [] + + if recipient_value is not None: + if not recipient_value: + report_list.append( + reports.ReportItem.error( + reports.messages.CibAlertRecipientValueInvalid( + recipient_value + ) + ) + ) + else: + report_list.extend( + _validate_recipient_value_is_unique( + cast(_Element, recipient_el.getparent()), + recipient_value, + str(recipient_el.attrib["id"]), + allow_duplicity=allow_same_value, + ) + ) + + return report_list def update_recipient( - reporter: ReportProcessor, - tree, - recipient_id, - recipient_value=None, - description=None, - allow_same_value=False, -): + recipient_el: _Element, + recipient_value: Optional[str] = None, + description: Optional[str] = None, +) -> _Element: """ Update specified recipient. Returns updated recipient element. - Raises LibraryError if recipient doesn't exist. - reporter -- report processor - tree -- cib etree node - recipient_id -- id of recipient to be updated - recipient_value -- recipient value, stay unchanged if None + recipient_el -- the recipient to be updated + recipient_value -- new recipient value, stay unchanged if None description -- description, if empty it will be removed, stay unchanged if None - allow_same_value -- if True unique recipient value is not required """ - recipient = find_recipient(get_alerts(tree), recipient_id) if recipient_value is not None: - ensure_recipient_value_is_unique( - reporter, - recipient.getparent(), - recipient_value, - recipient_id=recipient_id, - allow_duplicity=allow_same_value, - ) - recipient.set("value", recipient_value) - _update_optional_attribute(recipient, "description", description) - return recipient + recipient_el.set("value", recipient_value) + _update_optional_attribute(recipient_el, "description", description) + return recipient_el def remove_recipient(tree, recipient_id): diff --git a/pcs/lib/commands/alert.py b/pcs/lib/commands/alert.py index 13e172ba6..a8d9cce97 100644 --- a/pcs/lib/commands/alert.py +++ b/pcs/lib/commands/alert.py @@ -1,12 +1,13 @@ -from pcs.common import reports from pcs.common.reports import ReportItemList -from pcs.common.reports.item import ReportItem from pcs.lib.cib import alert from pcs.lib.cib.nvpair import ( arrange_first_instance_attributes, arrange_first_meta_attributes, ) -from pcs.lib.cib.tools import IdProvider +from pcs.lib.cib.tools import ( + IdProvider, + get_alerts, +) from pcs.lib.env import LibraryEnvironment from pcs.lib.errors import LibraryError @@ -30,16 +31,16 @@ def create_alert( meta_attribute_dict -- dictionary of meta attributes description -- alert description description """ - if not path: - raise LibraryError( - ReportItem.error( - reports.messages.RequiredOptionsAreMissing(["path"]) - ) - ) - cib = lib_env.get_cib() id_provider = IdProvider(cib) - alert_el = alert.create_alert(cib, alert_id, path, description) + + lib_env.report_processor.report_list( + alert.validate_create_alert(id_provider, path, alert_id) + ) + if lib_env.report_processor.has_errors: + raise LibraryError() + + alert_el = alert.create_alert(cib, id_provider, path, alert_id, description) arrange_first_instance_attributes( alert_el, instance_attribute_dict, id_provider ) @@ -121,28 +122,35 @@ def add_recipient( description -- recipient description allow_same_value -- if True unique recipient value is not required """ - if not recipient_value: - raise LibraryError( - ReportItem.error( - reports.messages.RequiredOptionsAreMissing(["value"]) - ) - ) - cib = lib_env.get_cib() id_provider = IdProvider(cib) - recipient = alert.add_recipient( - lib_env.report_processor, - cib, - alert_id, + alert_el = alert.find_alert(get_alerts(cib), alert_id) + + lib_env.report_processor.report_list( + alert.validate_add_recipient( + id_provider, + alert_el, + recipient_value, + recipient_id, + allow_same_value=allow_same_value, + ) + ) + if lib_env.report_processor.has_errors: + raise LibraryError() + + recipient_el = alert.add_recipient( + id_provider, + alert_el, recipient_value, - recipient_id=recipient_id, + recipient_id, description=description, - allow_same_value=allow_same_value, ) arrange_first_instance_attributes( - recipient, instance_attribute_dict, id_provider + recipient_el, instance_attribute_dict, id_provider + ) + arrange_first_meta_attributes( + recipient_el, meta_attribute_dict, id_provider ) - arrange_first_meta_attributes(recipient, meta_attribute_dict, id_provider) lib_env.push_cib() @@ -169,21 +177,24 @@ def update_recipient( deleted, if None old value will stay unchanged allow_same_value -- if True unique recipient value is not required """ - if not recipient_value and recipient_value is not None: - raise LibraryError( - ReportItem.error( - reports.messages.CibAlertRecipientValueInvalid(recipient_value) - ) - ) cib = lib_env.get_cib() id_provider = IdProvider(cib) + recipient_el = alert.find_recipient(get_alerts(cib), recipient_id) + + lib_env.report_processor.report_list( + alert.validate_update_recipient( + recipient_el, + recipient_value, + allow_same_value=allow_same_value, + ) + ) + if lib_env.report_processor.has_errors: + raise LibraryError() + recipient = alert.update_recipient( - lib_env.report_processor, - cib, - recipient_id, + recipient_el, recipient_value=recipient_value, description=description, - allow_same_value=allow_same_value, ) arrange_first_instance_attributes( recipient, instance_attribute_dict, id_provider diff --git a/pcs/lib/pacemaker/values.py b/pcs/lib/pacemaker/values.py index 22e074a47..77ad1e18c 100644 --- a/pcs/lib/pacemaker/values.py +++ b/pcs/lib/pacemaker/values.py @@ -89,6 +89,24 @@ def get_valid_timeout_seconds( return wait_timeout +def validate_id_reports( + id_candidate: str, + description: Optional[str] = None, +) -> reports.ReportItemList: + """ + Validate a pacemaker id, return ReportItemList + + id_candidate id's value + description id's role description (default "id") + """ + # This is a temporary improvement of validate_id function. It turns it into + # a function which returns a ReportItemList. When validate_id is removed, + # this function can be removed as well. + report_list: ReportItemList = [] + validate_id(id_candidate, description, report_list) + return report_list + + def validate_id( id_candidate: str, description: Optional[str] = None, @@ -143,6 +161,7 @@ def validate_id( def sanitize_id(id_candidate: str, replacement: str = "") -> str: + # TODO move this to pcs.lib.cib.tools? if not id_candidate: return id_candidate return "".join( diff --git a/pcs_test/tier0/lib/cib/test_alert.py b/pcs_test/tier0/lib/cib/test_alert.py index 1e7c68348..393363894 100644 --- a/pcs_test/tier0/lib/cib/test_alert.py +++ b/pcs_test/tier0/lib/cib/test_alert.py @@ -5,13 +5,14 @@ from pcs.common.reports import ReportItemSeverity as severities from pcs.common.reports import codes as report_codes from pcs.lib.cib import alert +from pcs.lib.cib.tools import IdProvider +from pcs_test.tools import fixture from pcs_test.tools.assertions import ( assert_raise_library_error, assert_report_item_list_equal, assert_xml_equal, ) -from pcs_test.tools.custom_mock import MockLibraryReportProcessor class UpdateOptionalAttributeTest(TestCase): @@ -32,75 +33,127 @@ def test_remove(self): self.assertTrue(element.get("attr") is None) -class EnsureRecipientValueIsUniqueTest(TestCase): +class ValidateRecipientValueIsUniqueTest(TestCase): + # pylint: disable=protected-access def setUp(self): - self.mock_reporter = MockLibraryReportProcessor() self.alert = etree.Element("alert", id="alert-1") self.recipient = etree.SubElement( self.alert, "recipient", id="rec-1", value="value1" ) def test_is_unique_no_duplicity_allowed(self): - alert.ensure_recipient_value_is_unique( - self.mock_reporter, self.alert, "value2" + report_list = alert._validate_recipient_value_is_unique( + self.alert, "value2" ) - self.assertEqual(0, len(self.mock_reporter.report_item_list)) + assert_report_item_list_equal(report_list, []) def test_same_recipient_no_duplicity_allowed(self): - alert.ensure_recipient_value_is_unique( - self.mock_reporter, self.alert, "value1", recipient_id="rec-1" + report_list = alert._validate_recipient_value_is_unique( + self.alert, "value1", recipient_id="rec-1" ) - self.assertEqual(0, len(self.mock_reporter.report_item_list)) + assert_report_item_list_equal(report_list, []) def test_same_recipient_duplicity_allowed(self): - alert.ensure_recipient_value_is_unique( - self.mock_reporter, + report_list = alert._validate_recipient_value_is_unique( self.alert, "value1", recipient_id="rec-1", allow_duplicity=True, ) - self.assertEqual(0, len(self.mock_reporter.report_item_list)) + assert_report_item_list_equal(report_list, []) def test_not_unique_no_duplicity_allowed(self): - report_item = ( - severities.ERROR, - report_codes.CIB_ALERT_RECIPIENT_ALREADY_EXISTS, - {"alert": "alert-1", "recipient": "value1"}, - report_codes.FORCE, - ) - assert_raise_library_error( - lambda: alert.ensure_recipient_value_is_unique( - self.mock_reporter, self.alert, "value1" - ) + report_list = alert._validate_recipient_value_is_unique( + self.alert, "value1" ) assert_report_item_list_equal( - self.mock_reporter.report_item_list, [report_item] + report_list, + [ + fixture.error( + report_codes.CIB_ALERT_RECIPIENT_ALREADY_EXISTS, + force_code=report_codes.FORCE, + alert="alert-1", + recipient="value1", + ) + ], ) def test_is_unique_duplicity_allowed(self): - alert.ensure_recipient_value_is_unique( - self.mock_reporter, self.alert, "value2", allow_duplicity=True + report_list = alert._validate_recipient_value_is_unique( + self.alert, "value2", allow_duplicity=True ) - self.assertEqual(0, len(self.mock_reporter.report_item_list)) + assert_report_item_list_equal(report_list, []) def test_not_unique_duplicity_allowed(self): - alert.ensure_recipient_value_is_unique( - self.mock_reporter, self.alert, "value1", allow_duplicity=True + report_list = alert._validate_recipient_value_is_unique( + self.alert, "value1", allow_duplicity=True ) assert_report_item_list_equal( - self.mock_reporter.report_item_list, + report_list, [ - ( - severities.WARNING, + fixture.warn( report_codes.CIB_ALERT_RECIPIENT_ALREADY_EXISTS, - {"alert": "alert-1", "recipient": "value1"}, + alert="alert-1", + recipient="value1", + ) + ], + ) + + +class ValidateCreateAlert(TestCase): + def setUp(self): + self.tree = etree.XML( + """ + + + + + + + + """ + ) + self.id_provider = IdProvider(self.tree) + + def test_empty_path(self): + assert_report_item_list_equal( + alert.validate_create_alert(self.id_provider, "", "alert2"), + [ + fixture.error( + report_codes.REQUIRED_OPTIONS_ARE_MISSING, + option_names=["path"], + option_type=None, + ) + ], + ) + + def test_invalid_id(self): + assert_report_item_list_equal( + alert.validate_create_alert(self.id_provider, "/path", "1alert"), + [ + fixture.error( + report_codes.INVALID_ID_BAD_CHAR, + id="1alert", + id_description="alert-id", + invalid_character="1", + is_first_char=True, ) ], ) + def test_id_exists(self): + assert_report_item_list_equal( + alert.validate_create_alert(self.id_provider, "/path", "alert"), + [ + fixture.error( + report_codes.ID_ALREADY_EXISTS, + id="alert", + ), + ], + ) + -class CreateAlertTest(TestCase): +class CreateAlert(TestCase): def setUp(self): self.tree = etree.XML( """ @@ -113,6 +166,7 @@ def setUp(self): """ ) + self.id_provider = IdProvider(self.tree) def test_no_alerts(self): # pylint: disable=no-self-use @@ -123,10 +177,11 @@ def test_no_alerts(self): """ ) + id_provider = IdProvider(tree) assert_xml_equal( '', etree.tostring( - alert.create_alert(tree, "my-alert", "/test/path") + alert.create_alert(tree, id_provider, "/test/path", "my-alert") ).decode(), ) assert_xml_equal( @@ -146,7 +201,9 @@ def test_alerts_exists(self): assert_xml_equal( '', etree.tostring( - alert.create_alert(self.tree, "my-alert", "/test/path") + alert.create_alert( + self.tree, self.id_provider, "/test/path", "my-alert" + ) ).decode(), ) assert_xml_equal( @@ -168,7 +225,11 @@ def test_alerts_exists_with_description(self): '', etree.tostring( alert.create_alert( - self.tree, "my-alert", "/test/path", "nothing" + self.tree, + self.id_provider, + "/test/path", + "my-alert", + "nothing", ) ).decode(), ) @@ -190,32 +251,11 @@ def test_alerts_exists_with_description(self): 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_BAD_CHAR, - { - "id": "1alert", - "id_description": "alert-id", - "invalid_character": "1", - "is_first_char": True, - }, - ), - ) - - 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") + alert.create_alert(self.tree, self.id_provider, "/test/path") ).decode(), ) assert_xml_equal( @@ -394,9 +434,8 @@ def test_not_existing_id(self): ) -class AddRecipientTest(TestCase): +class ValidateAddRecipientTest(TestCase): def setUp(self): - self.mock_reporter = MockLibraryReportProcessor() self.tree = etree.XML( """ @@ -410,43 +449,72 @@ def setUp(self): """ ) + self.id_provider = IdProvider(self.tree) + self.alert_el = self.tree.xpath(".//alert[@id='alert']")[0] - def test_with_id(self): - assert_xml_equal( - '', - etree.tostring( - alert.add_recipient( - self.mock_reporter, - self.tree, - "alert", - "value1", - "my-recipient", + def test_id_exists(self): + report_list = alert.validate_add_recipient( + self.id_provider, + self.alert_el, + "value1", + "alert-recipient", + ) + assert_report_item_list_equal( + report_list, + [ + fixture.error( + report_codes.ID_ALREADY_EXISTS, + id="alert-recipient", + ), + ], + ) + + def test_duplicity_of_value_not_allowed(self): + report_list = alert.validate_add_recipient( + self.id_provider, + self.alert_el, + "test_val", + ) + assert_report_item_list_equal( + report_list, + [ + fixture.error( + report_codes.CIB_ALERT_RECIPIENT_ALREADY_EXISTS, + force_code=report_codes.FORCE, + alert="alert", + recipient="test_val", ) - ).decode(), + ], ) - assert_xml_equal( + + +class AddRecipientTest(TestCase): + def setUp(self): + self.tree = etree.XML( """ - - """, - etree.tostring(self.tree).decode(), + """ ) - self.assertEqual([], self.mock_reporter.report_item_list) + self.id_provider = IdProvider(self.tree) + self.alert_el = self.tree.xpath(".//alert[@id='alert']")[0] - def test_without_id(self): + def test_with_id(self): assert_xml_equal( - '', + '', etree.tostring( alert.add_recipient( - self.mock_reporter, self.tree, "alert", "value1" + self.id_provider, + self.alert_el, + "value1", + "my-recipient", ) ).decode(), ) @@ -457,7 +525,7 @@ def test_without_id(self): - + @@ -465,51 +533,15 @@ def test_without_id(self): """, etree.tostring(self.tree).decode(), ) - self.assertEqual([], self.mock_reporter.report_item_list) - - def test_id_exists(self): - assert_raise_library_error( - lambda: alert.add_recipient( - self.mock_reporter, - self.tree, - "alert", - "value1", - "alert-recipient", - ), - ( - severities.ERROR, - report_codes.ID_ALREADY_EXISTS, - {"id": "alert-recipient"}, - ), - ) - self.assertEqual([], self.mock_reporter.report_item_list) - def test_duplicity_of_value_not_allowed(self): - report_item = ( - severities.ERROR, - report_codes.CIB_ALERT_RECIPIENT_ALREADY_EXISTS, - {"alert": "alert", "recipient": "test_val"}, - report_codes.FORCE, - ) - assert_raise_library_error( - lambda: alert.add_recipient( - self.mock_reporter, self.tree, "alert", "test_val" - ) - ) - assert_report_item_list_equal( - self.mock_reporter.report_item_list, [report_item] - ) - - def test_duplicity_of_value_allowed(self): + def test_without_id(self): assert_xml_equal( - '', + '', etree.tostring( alert.add_recipient( - self.mock_reporter, - self.tree, - "alert", - "test_val", - allow_same_value=True, + self.id_provider, + self.alert_el, + "value1", ) ).decode(), ) @@ -520,7 +552,7 @@ def test_duplicity_of_value_allowed(self): - + @@ -528,34 +560,6 @@ def test_duplicity_of_value_allowed(self): """, etree.tostring(self.tree).decode(), ) - assert_report_item_list_equal( - self.mock_reporter.report_item_list, - [ - ( - severities.WARNING, - report_codes.CIB_ALERT_RECIPIENT_ALREADY_EXISTS, - {"alert": "alert", "recipient": "test_val"}, - ) - ], - ) - - def test_alert_not_exist(self): - assert_raise_library_error( - lambda: alert.add_recipient( - self.mock_reporter, self.tree, "alert1", "test_val" - ), - ( - severities.ERROR, - report_codes.ID_NOT_FOUND, - { - "id": "alert1", - "expected_types": ["alert"], - "context_type": "alerts", - "context_id": "", - }, - None, - ), - ) def test_with_description(self): assert_xml_equal( @@ -568,9 +572,8 @@ def test_with_description(self): """, etree.tostring( alert.add_recipient( - self.mock_reporter, - self.tree, - "alert", + self.id_provider, + self.alert_el, "value1", description="desc", ) @@ -595,12 +598,10 @@ def test_with_description(self): """, etree.tostring(self.tree).decode(), ) - self.assertEqual([], self.mock_reporter.report_item_list) -class UpdateRecipientTest(TestCase): +class ValideteUpdateRecipientTest(TestCase): def setUp(self): - self.mock_reporter = MockLibraryReportProcessor() self.tree = etree.XML( """ @@ -619,89 +620,55 @@ def setUp(self): """ ) + self.recipient_el = self.tree.xpath( + ".//recipient[@id='alert-recipient']" + )[0] - def test_update_value(self): - assert_xml_equal( - """ - - """, - etree.tostring( - alert.update_recipient( - self.mock_reporter, - self.tree, - "alert-recipient", - recipient_value="new_val", - ) - ).decode(), - ) - assert_xml_equal( - """ - - - - - - - - - - - """, - etree.tostring(self.tree).decode(), + def test_update_same_value_no_duplicity_allowed(self): + report_list = alert.validate_update_recipient( + self.recipient_el, + recipient_value="test_val", ) - self.assertEqual([], self.mock_reporter.report_item_list) + assert_report_item_list_equal(report_list, []) - def test_update_same_value_no_duplicity_allowed(self): - assert_xml_equal( - '', - etree.tostring( - alert.update_recipient( - self.mock_reporter, - self.tree, - "alert-recipient", - recipient_value="test_val", - ) - ).decode(), + def test_duplicity_of_value_not_allowed(self): + report_list = alert.validate_update_recipient( + self.recipient_el, + "value1", ) - assert_xml_equal( - """ - - - - - - - - - - - """, - etree.tostring(self.tree).decode(), + assert_report_item_list_equal( + report_list, + [ + fixture.error( + report_codes.CIB_ALERT_RECIPIENT_ALREADY_EXISTS, + force_code=report_codes.FORCE, + alert="alert", + recipient="value1", + ) + ], ) - self.assertEqual([], self.mock_reporter.report_item_list) - def test_update_same_value_duplicity_allowed(self): - assert_xml_equal( - '', - etree.tostring( - alert.update_recipient( - self.mock_reporter, - self.tree, - "alert-recipient", - recipient_value="test_val", - allow_same_value=True, + def test_duplicity_of_value_allowed(self): + report_list = alert.validate_update_recipient( + self.recipient_el, + "value1", + allow_same_value=True, + ) + assert_report_item_list_equal( + report_list, + [ + fixture.warn( + report_codes.CIB_ALERT_RECIPIENT_ALREADY_EXISTS, + alert="alert", + recipient="value1", ) - ).decode(), + ], ) - assert_xml_equal( + + +class UpdateRecipientTest(TestCase): + def setUp(self): + self.tree = etree.XML( """ @@ -717,39 +684,24 @@ def test_update_same_value_duplicity_allowed(self): - """, - etree.tostring(self.tree).decode(), - ) - self.assertEqual([], self.mock_reporter.report_item_list) - - def test_duplicity_of_value_not_allowed(self): - report_item = ( - severities.ERROR, - report_codes.CIB_ALERT_RECIPIENT_ALREADY_EXISTS, - {"alert": "alert", "recipient": "value1"}, - report_codes.FORCE, - ) - assert_raise_library_error( - lambda: alert.update_recipient( - self.mock_reporter, self.tree, "alert-recipient", "value1" - ) - ) - assert_report_item_list_equal( - self.mock_reporter.report_item_list, [report_item] + """ ) + self.recipient_el = self.tree.xpath( + ".//recipient[@id='alert-recipient']" + )[0] + self.recipient1_el = self.tree.xpath( + ".//recipient[@id='alert-recipient-1']" + )[0] - def test_duplicity_of_value_allowed(self): + def test_update_value(self): assert_xml_equal( """ - + """, etree.tostring( alert.update_recipient( - self.mock_reporter, - self.tree, - "alert-recipient", - recipient_value="value1", - allow_same_value=True, + self.recipient_el, + recipient_value="new_val", ) ).decode(), ) @@ -759,7 +711,7 @@ def test_duplicity_of_value_allowed(self): - + + + + + """ + } + ) + cmd_alert.add_recipient( + self.env_assist.get_env(), + "alert", + "value1", + {}, + {}, + allow_same_value=True, + ) + self.env_assist.assert_reports( + [ + fixture.warn( + report_codes.CIB_ALERT_RECIPIENT_ALREADY_EXISTS, + alert="alert", + recipient="value1", + ) + ] + ) + def test_without_id(self): self.config.env.push_cib( replace={ @@ -455,7 +509,6 @@ def setUp(self): ) def test_empty_value(self): - self.config.remove("runner.cib.load") self.env_assist.assert_raise_library_error( lambda: cmd_alert.update_recipient( self.env_assist.get_env(), @@ -464,11 +517,12 @@ def test_empty_value(self): {}, recipient_value="", ), + ) + self.env_assist.assert_reports( [ - ( - Severities.ERROR, + fixture.error( report_codes.CIB_ALERT_RECIPIENT_VALUE_INVALID, - {"recipient": ""}, + recipient="", ) ], ) @@ -479,20 +533,72 @@ def test_recipient_not_found(self): self.env_assist.get_env(), "recipient", {}, {} ), [ - ( - Severities.ERROR, + fixture.error( report_codes.ID_NOT_FOUND, - { - "id": "recipient", - "expected_types": ["recipient"], - "context_id": "", - "context_type": "alerts", - }, - None, + id="recipient", + expected_types=["recipient"], + context_id="", + context_type="alerts", ) ], ) + def test_update_duplicity_allowed(self): + self.config.env.push_cib( + replace={ + './/alert[@id="alert"]': """ + + + + + + + + + + + + + """, + } + ) + cmd_alert.update_recipient( + self.env_assist.get_env(), + "alert-recipient-1", + {}, + {}, + recipient_value="value1", + description=None, + allow_same_value=True, + ) + self.env_assist.assert_reports( + [ + fixture.warn( + report_codes.CIB_ALERT_RECIPIENT_ALREADY_EXISTS, + alert="alert", + recipient="value1", + ) + ] + ) + def test_update_all(self): self.config.env.push_cib( replace={ diff --git a/pcs_test/tier1/legacy/test_alert.py b/pcs_test/tier1/legacy/test_alert.py index e830ce734..825d03cf7 100644 --- a/pcs_test/tier1/legacy/test_alert.py +++ b/pcs_test/tier1/legacy/test_alert.py @@ -87,7 +87,7 @@ def test_already_exists(self): self.assert_pcs_success("alert create id=alert1 path=test".split()) self.assert_pcs_fail( "alert create id=alert1 path=test".split(), - "Error: 'alert1' already exists\n", + "Error: 'alert1' already exists\n" + ERRORS_HAVE_OCCURRED, ) self.assert_pcs_success( "alert config".split(), @@ -100,7 +100,7 @@ def test_already_exists(self): def test_path_is_required(self): self.assert_pcs_fail( "alert create id=alert1".split(), - "Error: required option 'path' is missing\n", + "Error: required option 'path' is missing\n" + ERRORS_HAVE_OCCURRED, ) @@ -272,11 +272,11 @@ def test_already_exists(self): ) self.assert_pcs_fail( "alert recipient add alert value=value id=rec".split(), - "Error: 'rec' already exists\n", + "Error: 'rec' already exists\n" + ERRORS_HAVE_OCCURRED, ) self.assert_pcs_fail( "alert recipient add alert value=value id=alert".split(), - "Error: 'alert' already exists\n", + "Error: 'alert' already exists\n" + ERRORS_HAVE_OCCURRED, ) def test_same_value(self): @@ -319,7 +319,8 @@ def test_no_value(self): self.assert_pcs_success("alert create path=test".split()) self.assert_pcs_fail( "alert recipient add alert id=rec".split(), - "Error: required option 'value' is missing\n", + "Error: required option 'value' is missing\n" + + ERRORS_HAVE_OCCURRED, ) @@ -456,7 +457,7 @@ def test_empty_value(self): ) self.assert_pcs_fail( "alert recipient update rec value=".split(), - "Error: Recipient value '' is not valid.\n", + "Error: Recipient value '' is not valid.\n" + ERRORS_HAVE_OCCURRED, ) From c5c7b0f956844f81812baede77883cacc074ced5 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Wed, 2 Oct 2024 18:25:12 +0200 Subject: [PATCH 025/227] fix warning in `pcs status` * do not display a warning when fence_sbd and method="cycle" --- CHANGELOG.md | 3 + pcs/lib/cib/resource/stonith.py | 3 +- pcs_test/Makefile.am | 1 + .../tier0/lib/cib/resource/test_stonith.py | 8 +- pcs_test/tier0/lib/commands/test_status.py | 5 + pcs_test/tier1/test_status.py | 7 +- .../stonith__fence_sbd_metadata.xml | 186 ++++++++++++++++++ .../tools/bin_mock/pcmk/crm_resource_mock.py | 1 + 8 files changed, 210 insertions(+), 4 deletions(-) create mode 100644 pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_sbd_metadata.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 10a3881c2..4e3b3d076 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,11 @@ ### Fixed - Do not end with error when using the instances quantifier in `pcs status query resource is-state` command ([RHEL-55441]) +- Do not display a warning in `pcs status` when a fence\_sbd stonith device has + its `method` option set to `cycle` ([RHEL-46286]) [RHEL-46284]: https://issues.redhat.com/browse/RHEL-46284 +[RHEL-46286]: https://issues.redhat.com/browse/RHEL-46286 [RHEL-55441]: https://issues.redhat.com/browse/RHEL-55441 diff --git a/pcs/lib/cib/resource/stonith.py b/pcs/lib/cib/resource/stonith.py index 3f17c460d..2b8621f6f 100644 --- a/pcs/lib/cib/resource/stonith.py +++ b/pcs/lib/cib/resource/stonith.py @@ -85,7 +85,8 @@ def get_misconfigured_resources( if nvpair.get("name") == "action" and nvpair.get("value"): stonith_with_action.append(stonith) if ( - nvpair.get("name") == "method" + stonith.get("type") != "fence_sbd" + and nvpair.get("name") == "method" and nvpair.get("value") == "cycle" ): stonith_with_method_cycle.append(stonith) diff --git a/pcs_test/Makefile.am b/pcs_test/Makefile.am index b062feda4..e6ebaf544 100644 --- a/pcs_test/Makefile.am +++ b/pcs_test/Makefile.am @@ -423,6 +423,7 @@ EXTRA_DIST = \ tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_minimal_metadata.xml \ tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_params_metadata.xml \ tools/bin_mock/pcmk/crm_resource.d/stonith__fence_pcsmock_unfencing_metadata.xml \ + tools/bin_mock/pcmk/crm_resource.d/stonith__fence_sbd_metadata.xml \ tools/bin_mock/pcmk/crm_resource.d/systemd__pcsmock@a__b_metadata.xml \ tools/bin_mock/pcmk/crm_resource.d/systemd__pcsmock_metadata.xml \ tools/bin_mock/pcmk/crm_resource_mock.py \ diff --git a/pcs_test/tier0/lib/cib/resource/test_stonith.py b/pcs_test/tier0/lib/cib/resource/test_stonith.py index 39af3bfb7..1722c854a 100644 --- a/pcs_test/tier0/lib/cib/resource/test_stonith.py +++ b/pcs_test/tier0/lib/cib/resource/test_stonith.py @@ -139,6 +139,11 @@ def test_issues(self): + + + + + """ ) @@ -146,10 +151,11 @@ def test_issues(self): stonith2 = resources.find("primitive[@id='S2']") stonith3 = resources.find("primitive[@id='S3']") stonith4 = resources.find("primitive[@id='S4']") + stonith5 = resources.find("primitive[@id='S5']") self.assertEqual( stonith.get_misconfigured_resources(resources), ( - [stonith1, stonith2, stonith3, stonith4], + [stonith1, stonith2, stonith3, stonith4, stonith5], [stonith2, stonith4], [stonith3, stonith4], ), diff --git a/pcs_test/tier0/lib/commands/test_status.py b/pcs_test/tier0/lib/commands/test_status.py index c0b736c13..8781507d1 100644 --- a/pcs_test/tier0/lib/commands/test_status.py +++ b/pcs_test/tier0/lib/commands/test_status.py @@ -746,6 +746,11 @@ def test_stonith_warnings_regarding_devices_configuration(self): + + + + + """ ) diff --git a/pcs_test/tier1/test_status.py b/pcs_test/tier1/test_status.py index e1482c11b..491f36eb9 100644 --- a/pcs_test/tier1/test_status.py +++ b/pcs_test/tier1/test_status.py @@ -45,8 +45,11 @@ def fixture_stonith_action(self): ) def fixture_stonith_cycle(self): - self.assert_pcs_success( - "stonith create Sc fence_pcsmock_method method=cycle".split() + self.assert_pcs_success_all( + [ + "stonith create Sc fence_pcsmock_method method=cycle".split(), + "stonith create Ssbd fence_sbd devices=device1 method=cycle".split(), + ] ) def fixture_resource(self): diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_sbd_metadata.xml b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_sbd_metadata.xml new file mode 100644 index 000000000..f43168b98 --- /dev/null +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource.d/stonith__fence_sbd_metadata.xml @@ -0,0 +1,186 @@ + + + fence_sbd is an I/O Fencing agent which can be used in environments where sbd can be used (shared storage). + + + + + + + + Fencing action + + + + + + + SBD Device + + + + + + + + Method to fence + + + + + + + Physical plug number on device, UUID or identification of machine + + + + + + + Physical plug number on device, UUID or identification of machine + + + + + + + Disable logging to stderr. Does not affect --verbose or --debug-file or logging to syslog. + + + + + + + Verbose mode. Multiple -v flags can be stacked on the command line (e.g., -vvv) to increase verbosity. + + + + + + + Level of debugging detail in output. Defaults to the number of --verbose flags specified on the command line, or to 1 if verbose=1 in a stonith device configuration (i.e., on stdin). + + + + + + + Write debug information to given file + + + + + + + Write debug information to given file + + + + + + + Display version information and exit + + + + + + + Display help and exit + + + + + + + Separator for plug parameter when specifying more than 1 plug + + + + + + + Separator for CSV created by 'list' operation + + + + + + + Wait X seconds before fencing is started + + + + + + + Disable timeout (true/false) (default: true when run from Pacemaker 2.0+) + + + + + + + Wait X seconds for cmd prompt after login + + + + + + + Test X seconds for status change after ON/OFF + + + + + + + Wait X seconds after issuing ON/OFF + + + + + + + Path to SBD binary + + + + + + + Wait X seconds for cmd prompt after issuing command + + + + + + + Sleep X seconds between status calls during a STONITH action + + + + + + + Count of attempts to retry power on + + + + + + + + + + + + + + + + + + diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource_mock.py b/pcs_test/tools/bin_mock/pcmk/crm_resource_mock.py index 768098452..875595961 100644 --- a/pcs_test/tools/bin_mock/pcmk/crm_resource_mock.py +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource_mock.py @@ -58,6 +58,7 @@ def main(): "stonith:fence_pcsmock_minimal", "stonith:fence_pcsmock_params", "stonith:fence_pcsmock_unfencing", + "stonith:fence_sbd", "systemd:pcsmock", "systemd:pcsmock@a:b", ) From b1c93e8b05e5056f5605fcc87d3e4888ac959eea Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Mon, 7 Oct 2024 14:51:50 +0200 Subject: [PATCH 026/227] do not display expired constraints in constraint listing --- CHANGELOG.md | 3 ++ pcs/cli/constraint/output/__init__.py | 1 + pcs/cli/constraint/output/all.py | 4 +-- pcs/constraint.py | 5 +-- pcs_test/Makefile.am | 1 + .../tier0/cli/constraint/output/test_all.py | 32 +++++++++++++++++++ 6 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 pcs_test/tier0/cli/constraint/output/test_all.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e3b3d076..1bea7e74f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,9 +14,12 @@ query resource is-state` command ([RHEL-55441]) - Do not display a warning in `pcs status` when a fence\_sbd stonith device has its `method` option set to `cycle` ([RHEL-46286]) +- Do not display expired constraints in `pcs constraint location config + resources` unless `--all` is specified ([RHEL-46293]) [RHEL-46284]: https://issues.redhat.com/browse/RHEL-46284 [RHEL-46286]: https://issues.redhat.com/browse/RHEL-46286 +[RHEL-46293]: https://issues.redhat.com/browse/RHEL-46293 [RHEL-55441]: https://issues.redhat.com/browse/RHEL-55441 diff --git a/pcs/cli/constraint/output/__init__.py b/pcs/cli/constraint/output/__init__.py index 92bbc4823..a68d54706 100644 --- a/pcs/cli/constraint/output/__init__.py +++ b/pcs/cli/constraint/output/__init__.py @@ -8,5 +8,6 @@ CibConstraintLocationAnyDto, constraints_to_cmd, constraints_to_text, + filter_constraints_by_rule_expired_status, print_config, ) diff --git a/pcs/cli/constraint/output/all.py b/pcs/cli/constraint/output/all.py index 51023cea9..3f8256783 100644 --- a/pcs/cli/constraint/output/all.py +++ b/pcs/cli/constraint/output/all.py @@ -113,7 +113,7 @@ def _filter_out_expired_base( ] -def _filter_constraints( +def filter_constraints_by_rule_expired_status( constraints_dto: CibConstraintsDto, include_expired: bool ) -> CibConstraintsDto: return CibConstraintsDto( @@ -139,7 +139,7 @@ def _filter_constraints( def print_config( constraints_dto: CibConstraintsDto, modifiers: InputModifiers ) -> None: - constraints_dto = _filter_constraints( + constraints_dto = filter_constraints_by_rule_expired_status( constraints_dto, include_expired=modifiers.is_specified("--all"), ) diff --git a/pcs/constraint.py b/pcs/constraint.py index 3e9e27e4f..c22675ba5 100644 --- a/pcs/constraint.py +++ b/pcs/constraint.py @@ -23,6 +23,7 @@ from pcs.cli.constraint.location import command as location_command from pcs.cli.constraint.output import ( CibConstraintLocationAnyDto, + filter_constraints_by_rule_expired_status, location, print_config, ) @@ -795,9 +796,9 @@ def location_config_cmd( "with grouping and filtering by nodes or resources" ) - constraints_dto = cast( - CibConstraintsDto, + constraints_dto = filter_constraints_by_rule_expired_status( lib.constraint.get_config(evaluate_rules=True), + modifiers.is_specified("--all"), ) constraints_dto = CibConstraintsDto( diff --git a/pcs_test/Makefile.am b/pcs_test/Makefile.am index e6ebaf544..f50f0faf8 100644 --- a/pcs_test/Makefile.am +++ b/pcs_test/Makefile.am @@ -83,6 +83,7 @@ EXTRA_DIST = \ tier0/cli/constraint/__init__.py \ tier0/cli/constraint/location/__init__.py \ tier0/cli/constraint/location/test_command.py \ + tier0/cli/constraint/output/test_all.py \ tier0/cli/constraint/rule/__init__.py \ tier0/cli/constraint/rule/test_command.py \ tier0/cli/constraint/test_command.py \ diff --git a/pcs_test/tier0/cli/constraint/output/test_all.py b/pcs_test/tier0/cli/constraint/output/test_all.py new file mode 100644 index 000000000..98782685e --- /dev/null +++ b/pcs_test/tier0/cli/constraint/output/test_all.py @@ -0,0 +1,32 @@ +from unittest import TestCase + +from pcs.cli.constraint.output import filter_constraints_by_rule_expired_status + +from pcs_test.tools.constraints_dto import get_all_constraints +from pcs_test.tools.custom_mock import RuleInEffectEvalMock + + +class FilterConstraintsByRuleExpiredStatus(TestCase): + def setUp(self): + self.constraint_dtos_with_expired = get_all_constraints( + RuleInEffectEvalMock({}), include_expired=True + ) + self.constraint_dtos_without_expired = get_all_constraints( + RuleInEffectEvalMock({}), include_expired=False + ) + + def test_include_expired(self): + self.assertEqual( + filter_constraints_by_rule_expired_status( + self.constraint_dtos_with_expired, include_expired=True + ), + self.constraint_dtos_with_expired, + ) + + def test_do_not_include_expired(self): + self.assertEqual( + filter_constraints_by_rule_expired_status( + self.constraint_dtos_with_expired, include_expired=False + ), + self.constraint_dtos_without_expired, + ) From 8bd2b966f185ffc1932ec1a94f76c87f0d21cef6 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Wed, 16 Oct 2024 10:51:11 +0200 Subject: [PATCH 027/227] deduplicate code in ExpiredConstraints test The idea is to make it clearly visible that * test cases use the same fixtures * pcs commands produce the same output --- pcs_test/tier1/legacy/test_constraints.py | 327 +++++----------------- 1 file changed, 68 insertions(+), 259 deletions(-) diff --git a/pcs_test/tier1/legacy/test_constraints.py b/pcs_test/tier1/legacy/test_constraints.py index 3af9a5dca..4d73888c1 100644 --- a/pcs_test/tier1/legacy/test_constraints.py +++ b/pcs_test/tier1/legacy/test_constraints.py @@ -5112,17 +5112,6 @@ def fixture_primitive(self): "resource create dummy ocf:pcsmock:minimal".split() ) - def fixture_multiple_primitive(self): - self.assert_pcs_success( - "resource create D1 ocf:pcsmock:minimal".split() - ) - self.assert_pcs_success( - "resource create D2 ocf:pcsmock:minimal".split() - ) - self.assert_pcs_success( - "resource create D3 ocf:pcsmock:minimal".split() - ) - def test_crm_rule_missing(self): mock_settings = get_mock_settings() mock_settings["crm_rule_exec"] = "" @@ -5150,7 +5139,7 @@ def test_crm_rule_missing(self): stderr_full=f"Warning: {CRM_RULE_MISSING_MSG}\n", ) - def test_in_effect_primitive_plain(self): + def assert_in_effect_primitive(self, flag_all, flag_full): self.fixture_primitive() self.assert_pcs_success( ( @@ -5159,80 +5148,33 @@ def test_in_effect_primitive_plain(self): ).split() ) self.assert_pcs_success( - ["constraint"], + ( + "constraint" + f"{' --full' if flag_full else ''}" + f"{' --all' if flag_all else ''}" + ).split(), outdent( - """\ + f"""\ Location Constraints: - resource 'dummy' + resource 'dummy'{' (id: location-dummy)' if flag_full else ''} Rules: - Rule: score=INFINITY - Expression: date gt 2019-01-01 + Rule: score=INFINITY{' (id: test-rule)' if flag_full else ''} + Expression: date gt 2019-01-01{' (id: test-rule-expr)' if flag_full else ''} """ ), ) + def test_in_effect_primitive_plain(self): + self.assert_in_effect_primitive(flag_full=False, flag_all=False) + def test_in_effect_primitive_full(self): - self.fixture_primitive() - self.assert_pcs_success( - ( - "constraint location dummy rule id=test-rule score=INFINITY " - "date gt 2019-01-01" - ).split() - ) - self.assert_pcs_success( - "constraint --full".split(), - outdent( - """\ - Location Constraints: - resource 'dummy' (id: location-dummy) - Rules: - Rule: score=INFINITY (id: test-rule) - Expression: date gt 2019-01-01 (id: test-rule-expr) - """ - ), - ) + self.assert_in_effect_primitive(flag_full=True, flag_all=False) def test_in_effect_primitive_all(self): - self.fixture_primitive() - self.assert_pcs_success( - ( - "constraint location dummy rule id=test-rule score=INFINITY " - "date gt 2019-01-01" - ).split() - ) - self.assert_pcs_success( - "constraint --all".split(), - outdent( - """\ - Location Constraints: - resource 'dummy' - Rules: - Rule: score=INFINITY - Expression: date gt 2019-01-01 - """ - ), - ) + self.assert_in_effect_primitive(flag_full=False, flag_all=True) def test_in_effect_primitive_full_all(self): - self.fixture_primitive() - self.assert_pcs_success( - ( - "constraint location dummy rule id=test-rule score=INFINITY " - "date gt 2019-01-01" - ).split() - ) - self.assert_pcs_success( - "constraint --full --all".split(), - outdent( - """\ - Location Constraints: - resource 'dummy' (id: location-dummy) - Rules: - Rule: score=INFINITY (id: test-rule) - Expression: date gt 2019-01-01 (id: test-rule-expr) - """ - ), - ) + self.assert_in_effect_primitive(flag_full=True, flag_all=True) def test_in_effect_group_plain(self): self.fixture_group() @@ -5255,7 +5197,7 @@ def test_in_effect_group_plain(self): ), ) - def test_expired_primitive_plain(self): + def fixture_expired_primitive_plain(self): self.fixture_primitive() self.assert_pcs_success( ( @@ -5263,26 +5205,17 @@ def test_expired_primitive_plain(self): "date lt 2019-01-01" ).split() ) + + def test_expired_primitive_plain(self): + self.fixture_expired_primitive_plain() self.assert_pcs_success(["constraint"]) def test_expired_primitive_full(self): - self.fixture_primitive() - self.assert_pcs_success( - ( - "constraint location dummy rule id=test-rule score=INFINITY " - "date lt 2019-01-01" - ).split() - ) + self.fixture_expired_primitive_plain() self.assert_pcs_success("constraint --full".split()) def test_expired_primitive_all(self): - self.fixture_primitive() - self.assert_pcs_success( - ( - "constraint location dummy rule id=test-rule score=INFINITY " - "date lt 2019-01-01" - ).split() - ) + self.fixture_expired_primitive_plain() self.assert_pcs_success( "constraint --all".split(), outdent( @@ -5297,13 +5230,7 @@ def test_expired_primitive_all(self): ) def test_expired_primitive_full_all(self): - self.fixture_primitive() - self.assert_pcs_success( - ( - "constraint location dummy rule id=test-rule score=INFINITY " - "date lt 2019-01-01" - ).split() - ) + self.fixture_expired_primitive_plain() self.assert_pcs_success( "constraint --full --all".split(), outdent( @@ -5327,7 +5254,7 @@ def test_expired_group_plain(self): ) self.assert_pcs_success(["constraint"]) - def test_indeterminate_primitive_plain(self): + def assert_indeterminate_primitive(self, flag_full, flag_all): self.fixture_primitive() self.assert_pcs_success( ( @@ -5336,84 +5263,34 @@ def test_indeterminate_primitive_plain(self): ).split() ) self.assert_pcs_success( - ["constraint"], + ( + "constraint" + f"{' --full' if flag_full else ''}" + f"{' --all' if flag_all else ''}" + ).split(), outdent( - """\ + f"""\ Location Constraints: - resource 'dummy' + resource 'dummy'{' (id: location-dummy)' if flag_full else ''} Rules: - Rule: boolean-op=or score=INFINITY - Expression: date eq 2019-01-01 - Expression: date eq 2019-03-01 + Rule: boolean-op=or score=INFINITY{' (id: test-rule)' if flag_full else ''} + Expression: date eq 2019-01-01{' (id: test-rule-expr)' if flag_full else ''} + Expression: date eq 2019-03-01{' (id: test-rule-expr-1)' if flag_full else ''} """ ), ) + def test_indeterminate_primitive_plain(self): + self.assert_indeterminate_primitive(flag_full=False, flag_all=False) + def test_indeterminate_primitive_full(self): - self.fixture_primitive() - self.assert_pcs_success( - ( - "constraint location dummy rule id=test-rule score=INFINITY " - "date eq 2019-01-01 or date eq 2019-03-01" - ).split() - ) - self.assert_pcs_success( - "constraint --full".split(), - outdent( - """\ - Location Constraints: - resource 'dummy' (id: location-dummy) - Rules: - Rule: boolean-op=or score=INFINITY (id: test-rule) - Expression: date eq 2019-01-01 (id: test-rule-expr) - Expression: date eq 2019-03-01 (id: test-rule-expr-1) - """ - ), - ) + self.assert_indeterminate_primitive(flag_full=True, flag_all=False) def test_indeterminate_primitive_all(self): - self.fixture_primitive() - self.assert_pcs_success( - ( - "constraint location dummy rule id=test-rule score=INFINITY " - "date eq 2019-01-01 or date eq 2019-03-01" - ).split() - ) - self.assert_pcs_success( - "constraint --all".split(), - outdent( - """\ - Location Constraints: - resource 'dummy' - Rules: - Rule: boolean-op=or score=INFINITY - Expression: date eq 2019-01-01 - Expression: date eq 2019-03-01 - """ - ), - ) + self.assert_indeterminate_primitive(flag_full=False, flag_all=True) def test_indeterminate_primitive_full_all(self): - self.fixture_primitive() - self.assert_pcs_success( - ( - "constraint location dummy rule id=test-rule score=INFINITY " - "date eq 2019-01-01 or date eq 2019-03-01" - ).split() - ) - self.assert_pcs_success( - "constraint --full --all".split(), - outdent( - """\ - Location Constraints: - resource 'dummy' (id: location-dummy) - Rules: - Rule: boolean-op=or score=INFINITY (id: test-rule) - Expression: date eq 2019-01-01 (id: test-rule-expr) - Expression: date eq 2019-03-01 (id: test-rule-expr-1) - """ - ), - ) + self.assert_indeterminate_primitive(flag_full=True, flag_all=True) def test_indeterminate_group_plain(self): self.fixture_group() @@ -5437,81 +5314,40 @@ def test_indeterminate_group_plain(self): ), ) - def test_not_yet_in_effect_primitive_plain(self): + def assert_not_yet_in_effect_primitive(self, flag_full, flag_all): self.fixture_primitive() self.assert_pcs_success( "constraint location dummy rule id=test-rule score=INFINITY date gt".split() + [self._tomorrow] ) self.assert_pcs_success( - ["constraint"], + ( + "constraint" + f"{' --full' if flag_full else ''}" + f"{' --all' if flag_all else ''}" + ).split(), outdent( f"""\ Location Constraints: - resource 'dummy' + resource 'dummy'{' (id: location-dummy)' if flag_full else ''} Rules: - Rule (not yet in effect): score=INFINITY - Expression: date gt {self._tomorrow} + Rule (not yet in effect): score=INFINITY{' (id: test-rule)' if flag_full else ''} + Expression: date gt {self._tomorrow}{' (id: test-rule-expr)' if flag_full else ''} """ ), ) + def test_not_yet_in_effect_primitive_plain(self): + self.assert_not_yet_in_effect_primitive(flag_full=False, flag_all=False) + def test_not_yet_in_effect_primitive_full(self): - self.fixture_primitive() - self.assert_pcs_success( - "constraint location dummy rule id=test-rule score=INFINITY date gt".split() - + [self._tomorrow] - ) - self.assert_pcs_success( - "constraint --full".split(), - outdent( - f"""\ - Location Constraints: - resource 'dummy' (id: location-dummy) - Rules: - Rule (not yet in effect): score=INFINITY (id: test-rule) - Expression: date gt {self._tomorrow} (id: test-rule-expr) - """ - ), - ) + self.assert_not_yet_in_effect_primitive(flag_full=True, flag_all=False) def test_not_yet_in_effect_primitive_all(self): - self.fixture_primitive() - self.assert_pcs_success( - "constraint location dummy rule id=test-rule score=INFINITY date gt".split() - + [self._tomorrow] - ) - self.assert_pcs_success( - "constraint --all".split(), - outdent( - f"""\ - Location Constraints: - resource 'dummy' - Rules: - Rule (not yet in effect): score=INFINITY - Expression: date gt {self._tomorrow} - """ - ), - ) + self.assert_not_yet_in_effect_primitive(flag_full=False, flag_all=True) def test_not_yet_in_effect_primitive_full_all(self): - self.fixture_primitive() - self.assert_pcs_success( - "constraint location dummy rule id=test-rule score=INFINITY date gt".split() - + [self._tomorrow] - ) - self.assert_pcs_success( - "constraint --full --all".split(), - outdent( - f"""\ - Location Constraints: - resource 'dummy' (id: location-dummy) - Rules: - Rule (not yet in effect): score=INFINITY (id: test-rule) - Expression: date gt {self._tomorrow} (id: test-rule-expr) - """ - ), - ) + self.assert_not_yet_in_effect_primitive(flag_full=True, flag_all=True) def test_not_yet_in_effect_group_plain(self): self.fixture_group() @@ -5532,8 +5368,16 @@ def test_not_yet_in_effect_group_plain(self): ), ) - def test_complex_primitive_plain(self): - self.fixture_multiple_primitive() + def fixture_complex_primitive(self): + self.assert_pcs_success( + "resource create D1 ocf:pcsmock:minimal".split() + ) + self.assert_pcs_success( + "resource create D2 ocf:pcsmock:minimal".split() + ) + self.assert_pcs_success( + "resource create D3 ocf:pcsmock:minimal".split() + ) self.assert_pcs_success( ( "constraint location D1 rule id=test-rule-D1-1 score=INFINITY " @@ -5571,6 +5415,8 @@ def test_complex_primitive_plain(self): ).split() ) + def test_complex_primitive_plain(self): + self.fixture_complex_primitive() self.assert_pcs_success( ["constraint"], outdent( @@ -5599,44 +5445,7 @@ def test_complex_primitive_plain(self): ) def test_complex_primitive_all(self): - self.fixture_multiple_primitive() - self.assert_pcs_success( - ( - "constraint location D1 rule id=test-rule-D1 score=INFINITY " - "not_defined pingd" - ).split() - ) - self.assert_pcs_success( - ( - "constraint location D1 rule id=test-rule-D1-2 score=INFINITY " - "( date eq 2019-01-01 or date eq 2019-01-30 ) and #uname eq node1" - ).split() - ) - self.assert_pcs_success( - ( - "constraint location D2 rule id=test-constr-D2 score=INFINITY " - "date in_range 2019-01-01 to 2019-02-01" - ).split() - ) - self.assert_pcs_success( - ( - "constraint rule add location-D2 id=test-duration score=INFINITY " - "date in_range 2019-03-01 to duration weeks=2" - ).split() - ) - self.assert_pcs_success( - ( - "constraint location D3 rule id=test-rule-D3-0 score=INFINITY " - "date in_range 2019-03-01 to duration weeks=2" - ).split() - ) - self.assert_pcs_success( - ( - "constraint rule add location-D3 id=test-defined score=INFINITY " - "not_defined pingd" - ).split() - ) - + self.fixture_complex_primitive() self.assert_pcs_success( "constraint --all".split(), outdent( From a8ea35b11b34afa7592ca85294a8808ab593ec4b Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Thu, 17 Oct 2024 17:04:37 +0200 Subject: [PATCH 028/227] fix `pcs dr status` * add command status.full_cluster_status_plaintext to the API_V1_MAP * fix ClusterStatusLegacyHandler --- pcs/daemon/app/api_v1.py | 3 ++- pcs_test/smoke.sh.in | 14 +++++++------- pcsd/capabilities.xml.in | 7 +++++++ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/pcs/daemon/app/api_v1.py b/pcs/daemon/app/api_v1.py index 8e8b8804f..c8aaf2880 100644 --- a/pcs/daemon/app/api_v1.py +++ b/pcs/daemon/app/api_v1.py @@ -101,6 +101,7 @@ "sbd-enable-sbd/v1": "sbd.enable_sbd", "scsi-unfence-node/v2": "scsi.unfence_node", "scsi-unfence-node-mpath/v1": "scsi.unfence_node_mpath", + "status-full-cluster-status-plaintext/v1": "status.full_cluster_status_plaintext", # deprecated, use resource-agent-get-agent-metadata/v1 instead "stonith-agent-describe-agent/v1": "stonith_agent.describe_agent", # deprecated, use resource-agent-get-agents-list/v1 instead @@ -301,7 +302,7 @@ async def get(self) -> None: class ClusterStatusLegacyHandler(LegacyApiV1Handler): @staticmethod def _get_cmd() -> str: - return "status.full_cluster_status_plaintext" + return "status-full-cluster-status-plaintext/v1" class ClusterAddNodesLegacyHandler(LegacyApiV1Handler): diff --git a/pcs_test/smoke.sh.in b/pcs_test/smoke.sh.in index a4b3ac719..bfbb427e6 100755 --- a/pcs_test/smoke.sh.in +++ b/pcs_test/smoke.sh.in @@ -23,12 +23,6 @@ output_file=$(mktemp) token_file=$(mktemp) cookie_file=$(mktemp) -# Sanity check of API V0 -token=$(python3 -c "import json; print(json.load(open('@LOCALSTATEDIR@/lib/pcsd/known-hosts'))['known_hosts']['localhost']['token']);") -curl -kb "token=${token}" https://localhost:2224/remote/cluster_status_plaintext -d 'data_json={}' > "${output_file}" -cat "${output_file}"; echo "" -python3 -c "import json; import sys; json.load(open('${output_file}'))['status'] == 'exception' and (sys.exit(1))"; - dd if=/dev/urandom bs=32 count=1 status=none | base64 > "${token_file}" custom_localhost_node_name="custom-node-name" @@ -71,6 +65,12 @@ curl --insecure --cookie ${cookie_file} --header "X-Requested-With: XMLHttpReque cat "${output_file}"; echo "" [ "$(cat ${output_file})" = "Update Successful" ] +# Sanity check of API V0 +token=$(python3 -c "import json; print(json.load(open('@LOCALSTATEDIR@/lib/pcsd/known-hosts'))['known_hosts']['localhost']['token']);") +curl -kb "token=${token}" https://localhost:2224/remote/cluster_status_plaintext -d 'data_json={}' > "${output_file}" +cat "${output_file}"; echo "" +python3 -c "import json; import sys; json.load(open('${output_file}'))['status'] != 'success' and (sys.exit(1))"; + # Sanity check of API V1 curl -kb "token=${token}" https://localhost:2224/api/v1/resource-agent-get-agents-list/v1 --data '{}' > "${output_file}" cat "${output_file}"; echo "" @@ -98,5 +98,5 @@ rm "${output_file}" rm "${cookie_file}" rm "${pcsd_settings_conf_path}" pcs cluster destroy --force -userdel -r testuser +userdel -rf testuser exit 0 diff --git a/pcsd/capabilities.xml.in b/pcsd/capabilities.xml.in index 111ef77aa..c1696226a 100644 --- a/pcsd/capabilities.xml.in +++ b/pcsd/capabilities.xml.in @@ -2681,6 +2681,13 @@ daemon urls: pacemaker_node_status + + + Display status of the remote site cluster. + + daemon urls: /api/v1/status-full-cluster-status-plaintext/v1 + + Query status of resources. From ec70951cd14bff49319caa88d2c6a6b7e2499107 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Fri, 18 Oct 2024 12:52:09 +0200 Subject: [PATCH 029/227] fix `pcs cluster node add-outside` * fix ClusterAddNodesLegacyHandler --- pcs/common/reports/messages.py | 17 +++++++---------- pcs/daemon/app/api_v1.py | 2 +- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/pcs/common/reports/messages.py b/pcs/common/reports/messages.py index 9da13a9bd..10c19a00b 100644 --- a/pcs/common/reports/messages.py +++ b/pcs/common/reports/messages.py @@ -260,7 +260,7 @@ def _stonith_watchdog_timeout_reason_to_str( }.get(reason, reason) -@dataclass(frozen=True, init=False) +@dataclass(frozen=True) class LegacyCommonMessage(ReportItemMessage): """ This class is used for legacy report transport protocol from @@ -268,22 +268,19 @@ class LegacyCommonMessage(ReportItemMessage): should be replaced with transporting DTOs of reports in the future. """ - def __init__( - self, code: types.MessageCode, info: Mapping[str, Any], message: str - ) -> None: - self.__code = code - self.info = info - self._message = message + legacy_code: types.MessageCode + legacy_info: Mapping[str, Any] + legacy_message: str @property def message(self) -> str: - return self._message + return self.legacy_message def to_dto(self) -> ReportItemMessageDto: return ReportItemMessageDto( - code=self.__code, + code=self.legacy_code, message=self.message, - payload=dict(self.info), + payload=dict(self.legacy_info), ) diff --git a/pcs/daemon/app/api_v1.py b/pcs/daemon/app/api_v1.py index c8aaf2880..5babad1da 100644 --- a/pcs/daemon/app/api_v1.py +++ b/pcs/daemon/app/api_v1.py @@ -308,7 +308,7 @@ def _get_cmd() -> str: class ClusterAddNodesLegacyHandler(LegacyApiV1Handler): @staticmethod def _get_cmd() -> str: - return "cluster.add_nodes" + return "cluster-add-nodes/v1" def get_routes(scheduler: Scheduler, auth_provider: AuthProvider) -> RoutesType: From 3e3861b1282171bc79ed10b6629f9af9bc959e1e Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Tue, 22 Oct 2024 12:44:31 +0200 Subject: [PATCH 030/227] update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bea7e74f..89720716c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ - Command `resource.restart` in API v2 - Add lib commands `cluster.get_corosync_conf_struct` and `resource.get_configured_resources` to API v2 +- Add lib command `status.full_cluster_status_plaintext` to API v1 + ([RHEL-61738]) ### Fixed - Do not end with error when using the instances quantifier in `pcs status @@ -16,11 +18,14 @@ its `method` option set to `cycle` ([RHEL-46286]) - Do not display expired constraints in `pcs constraint location config resources` unless `--all` is specified ([RHEL-46293]) +- Displaying status of local and remote cluster sites in `pcs dr status` + command. ([RHEL-61738]) [RHEL-46284]: https://issues.redhat.com/browse/RHEL-46284 [RHEL-46286]: https://issues.redhat.com/browse/RHEL-46286 [RHEL-46293]: https://issues.redhat.com/browse/RHEL-46293 [RHEL-55441]: https://issues.redhat.com/browse/RHEL-55441 +[RHEL-61738]: https://issues.redhat.com/browse/RHEL-61738 ## [0.11.8] - 2024-07-09 From f84c0dd9e1382d7adcee4cdd53b8e7c093d410ee Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Thu, 22 Aug 2024 09:54:54 +0200 Subject: [PATCH 031/227] overhaul resource delete --- mypy.ini | 8 + pcs/Makefile.am | 1 + pcs/cli/reports/messages.py | 13 +- pcs/common/pacemaker/resource/list.py | 20 + pcs/common/reports/codes.py | 6 + pcs/common/reports/messages.py | 103 +- pcs/lib/cib/const.py | 8 + pcs/lib/cib/constraint/common.py | 8 + pcs/lib/cib/fencing_topology.py | 88 +- pcs/lib/cib/remove_elements.py | 513 +++++ pcs/lib/cib/tools.py | 4 +- pcs/lib/commands/cib.py | 247 ++- pcs/resource.py | 45 +- pcs/stonith.py | 43 +- pcs_test/Makefile.am | 6 + pcs_test/tier0/cli/reports/test_messages.py | 11 + pcs_test/tier0/cli/resource/test_remove.py | 86 + pcs_test/tier0/cli/test_stonith.py | 78 + .../common/pacemaker/resource/__init__.py | 0 .../tier0/common/pacemaker/resource/list.py | 60 + .../tier0/common/reports/test_messages.py | 111 ++ .../tier0/lib/cib/test_fencing_topology.py | 214 +++ .../tier0/lib/cib/test_remove_elements.py | 1674 +++++++++++++++++ pcs_test/tier0/lib/cib/test_tools.py | 106 +- pcs_test/tier0/lib/commands/test_cib.py | 741 +++++--- pcs_test/tier0/lib/commands/test_status.py | 2 +- pcs_test/tier1/legacy/test_constraints.py | 152 +- pcs_test/tier1/legacy/test_resource.py | 870 +-------- pcs_test/tier1/legacy/test_stonith.py | 314 +--- pcs_test/tier1/resource/test_remove.py | 292 +++ pcs_test/tier1/stonith/test_remove.py | 260 +++ pcs_test/tier1/test_tag.py | 57 - pcs_test/tools/resources_dto.py | 3 + 33 files changed, 4373 insertions(+), 1771 deletions(-) create mode 100644 pcs/lib/cib/remove_elements.py create mode 100644 pcs_test/tier0/cli/resource/test_remove.py create mode 100644 pcs_test/tier0/common/pacemaker/resource/__init__.py create mode 100644 pcs_test/tier0/common/pacemaker/resource/list.py create mode 100644 pcs_test/tier0/lib/cib/test_remove_elements.py create mode 100644 pcs_test/tier1/resource/test_remove.py create mode 100644 pcs_test/tier1/stonith/test_remove.py diff --git a/mypy.ini b/mypy.ini index 988afbec5..94ef949ec 100644 --- a/mypy.ini +++ b/mypy.ini @@ -87,6 +87,10 @@ disallow_untyped_calls = True [mypy-pcs.lib.cib.nvpair_multi] disallow_untyped_defs = True +[mypy-pcs.lib.cib.remove_elements] +disallow_untyped_defs = True +ignore_errors = False + [mypy-pcs.lib.cib.resource.clone] disallow_untyped_defs = True disallow_untyped_calls = True @@ -121,6 +125,10 @@ disallow_untyped_calls = True [mypy-pcs.lib.cib.tag] disallow_untyped_defs = True +[mypy-pcs.lib.commands.cib] +disallow_untyped_defs = True +ignore_errors = False + [mypy-pcs.lib.commands.cib_options] disallow_untyped_defs = True diff --git a/pcs/Makefile.am b/pcs/Makefile.am index c910bfd4d..c8bfbf0ab 100644 --- a/pcs/Makefile.am +++ b/pcs/Makefile.am @@ -262,6 +262,7 @@ EXTRA_DIST = \ lib/cib/node.py \ lib/cib/nvpair_multi.py \ lib/cib/nvpair.py \ + lib/cib/remove_elements.py \ lib/cib/resource/agent.py \ lib/cib/resource/bundle.py \ lib/cib/resource/clone.py \ diff --git a/pcs/cli/reports/messages.py b/pcs/cli/reports/messages.py index e4efc2949..56e76bec4 100644 --- a/pcs/cli/reports/messages.py +++ b/pcs/cli/reports/messages.py @@ -266,15 +266,20 @@ def message(self) -> str: ) +class UseCommandNodeRemoveRemote(CliReportMessageCustom): + _obj: messages.UseCommandNodeRemoveRemote + + @property + def message(self) -> str: + return self._obj.message + ", use 'pcs cluster node remove-remote'" + + class UseCommandNodeRemoveGuest(CliReportMessageCustom): _obj: messages.UseCommandNodeRemoveGuest @property def message(self) -> str: - return ( - "this command is not sufficient for removing a guest node, use" - " 'pcs cluster node remove-guest'" - ) + return self._obj.message + ", use 'pcs cluster node remove-guest'" class UseCommandNodeAddGuest(CliReportMessageCustom): diff --git a/pcs/common/pacemaker/resource/list.py b/pcs/common/pacemaker/resource/list.py index 5b45bddb3..86c8aed33 100644 --- a/pcs/common/pacemaker/resource/list.py +++ b/pcs/common/pacemaker/resource/list.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from itertools import chain from typing import Sequence from pcs.common.interface.dto import DataTransferObject @@ -15,3 +16,22 @@ class CibResourcesDto(DataTransferObject): clones: Sequence[CibResourceCloneDto] groups: Sequence[CibResourceGroupDto] bundles: Sequence[CibResourceBundleDto] + + +def get_all_resources_ids(resources_dto: CibResourcesDto) -> set[str]: + return set( + chain( + (primitive.id for primitive in resources_dto.primitives), + (group.id for group in resources_dto.groups), + (clone.id for clone in resources_dto.clones), + (bundle.id for bundle in resources_dto.bundles), + ) + ) + + +def get_stonith_resources_ids(resources_dto: CibResourcesDto) -> set[str]: + return set( + primitive.id + for primitive in resources_dto.primitives + if primitive.agent_name.standard == "stonith" + ) diff --git a/pcs/common/reports/codes.py b/pcs/common/reports/codes.py index e6eb150a4..1089958ed 100644 --- a/pcs/common/reports/codes.py +++ b/pcs/common/reports/codes.py @@ -88,6 +88,10 @@ CANNOT_BAN_RESOURCE_STOPPED_NO_NODE_SPECIFIED = M( "CANNOT_BAN_RESOURCE_STOPPED_NO_NODE_SPECIFIED" ) +STOPPING_RESOURCES_BEFORE_DELETING = M("STOPPING_RESOURCES_BEFORE_DELETING") +CANNOT_STOP_RESOURCES_BEFORE_DELETING = M( + "CANNOT_STOP_RESOURCES_BEFORE_DELETING" +) CANNOT_CREATE_DEFAULT_CLUSTER_PROPERTY_SET = M( "CANNOT_CREATE_DEFAULT_CLUSTER_PROPERTY_SET" ) @@ -139,6 +143,7 @@ CIB_NVSET_AMBIGUOUS_PROVIDE_NVSET_ID = M("CIB_NVSET_AMBIGUOUS_PROVIDE_NVSET_ID") CIB_LOAD_ERROR_SCOPE_MISSING = M("CIB_LOAD_ERROR_SCOPE_MISSING") CIB_PUSH_ERROR = M("CIB_PUSH_ERROR") +CIB_REMOVE_REFERENCES = M("CIB_REMOVE_REFERENCES") CIB_REMOVE_DEPENDANT_ELEMENTS = M("CIB_REMOVE_DEPENDANT_ELEMENTS") CIB_SAVE_TMP_ERROR = M("CIB_SAVE_TMP_ERROR") CIB_SIMULATE_ERROR = M("CIB_SIMULATE_ERROR") @@ -609,6 +614,7 @@ ) USE_COMMAND_NODE_ADD_REMOTE = M("USE_COMMAND_NODE_ADD_REMOTE") USE_COMMAND_NODE_ADD_GUEST = M("USE_COMMAND_NODE_ADD_GUEST") +USE_COMMAND_NODE_REMOVE_REMOTE = M("USE_COMMAND_NODE_REMOVE_REMOTE") USE_COMMAND_NODE_REMOVE_GUEST = M("USE_COMMAND_NODE_REMOVE_GUEST") USING_DEFAULT_ADDRESS_FOR_HOST = M("USING_DEFAULT_ADDRESS_FOR_HOST") USING_DEFAULT_WATCHDOG = M("USING_DEFAULT_WATCHDOG") diff --git a/pcs/common/reports/messages.py b/pcs/common/reports/messages.py index 10c19a00b..4d3ee69e6 100644 --- a/pcs/common/reports/messages.py +++ b/pcs/common/reports/messages.py @@ -137,11 +137,14 @@ def _key_numeric(item: str) -> Tuple[int, str]: "acl_permission": "ACL permission", "acl_role": "ACL role", "acl_target": "ACL user", + "fencing-level": "fencing level", # Pacemaker-2.0 deprecated masters. Masters are now called promotable # clones. We treat masters as clones. Do not report we were doing something # with a master, say we were doing it with a clone instead. "master": "clone", "primitive": "resource", + "resource_set": "resource set", + "rsc_colocation": "colocation constraint", "rsc_location": "location constraint", } _type_articles = { @@ -5156,6 +5159,47 @@ def _format_line(tag: str, ids: list[str]) -> str: return f"Removing dependant {element_pl}:\n{info_lines}" +@dataclass(frozen=True) +class CibRemoveReferences(ReportItemMessage): + """ + Information about removal of references from cib elements due to + dependencies. + """ + + id_tag_map: Mapping[str, str] + removing_references_from: Mapping[str, StringIterable] + + _code = codes.CIB_REMOVE_REFERENCES + + @property + def message(self) -> str: + id_tag_map = defaultdict(lambda: "element", self.id_tag_map) + + def _format_line(tag: str, ids: list[str]) -> str: + tag_desc = format_plural(ids, _type_to_string(tag)).capitalize() + id_list = format_list(ids) + return f" {tag_desc}: {id_list}" + + def _format_one_element(element_id: str, ids: StringIterable) -> str: + tag_ids_map = defaultdict(list) + for _id in ids: + tag_ids_map[id_tag_map[_id]].append(_id) + info_lines = "\n".join( + sorted( + [_format_line(tag, ids) for tag, ids in tag_ids_map.items()] + ) + ) + tag_desc = _type_to_string(id_tag_map[element_id]).capitalize() + return f" {tag_desc} '{element_id}' from:\n{info_lines}" + + lines = "\n".join( + _format_one_element(key, self.removing_references_from[key]) + for key in sorted(self.removing_references_from) + ) + + return f"Removing references:\n{lines}" + + @dataclass(frozen=True) class UseCommandNodeAddRemote(ReportItemMessage): """ @@ -5182,17 +5226,36 @@ def message(self) -> str: return "this command is not sufficient for creating a guest node" +@dataclass(frozen=True) +class UseCommandNodeRemoveRemote(ReportItemMessage): + """ + Advise the user for more appropriate command. + """ + + resource_id: Optional[str] = None + _code = codes.USE_COMMAND_NODE_REMOVE_REMOTE + + @property + def message(self) -> str: + return "this command is not sufficient for removing a remote node{id}".format( + id=format_optional(self.resource_id, template=": '{}'") + ) + + @dataclass(frozen=True) class UseCommandNodeRemoveGuest(ReportItemMessage): """ Advise the user for more appropriate command. """ + resource_id: Optional[str] = None _code = codes.USE_COMMAND_NODE_REMOVE_GUEST @property def message(self) -> str: - return "this command is not sufficient for removing a guest node" + return "this command is not sufficient for removing a guest node{id}".format( + id=format_optional(self.resource_id, template=": '{}") + ) @dataclass(frozen=True) @@ -6265,6 +6328,44 @@ def message(self) -> str: return "You must specify a node when moving/banning a stopped resource" +@dataclass(frozen=True) +class StoppingResourcesBeforeDeleting(ReportItemMessage): + """ + Resources are going to be stopped before deletion + + resource_id_list -- ids of resources that are going to be stopped + """ + + resource_id_list: list[str] + _code = codes.STOPPING_RESOURCES_BEFORE_DELETING + + @property + def message(self) -> str: + return "Stopping {resource} {resource_list} before deleting".format( + resource=format_plural(self.resource_id_list, "resource"), + resource_list=format_list(self.resource_id_list), + ) + + +@dataclass(frozen=True) +class CannotStopResourcesBeforeDeleting(ReportItemMessage): + """ + Cannot stop a resource that is being removed + + resource_id_list -- ids of resources that cannot be stopped + """ + + resource_id_list: list[str] + _code = codes.CANNOT_STOP_RESOURCES_BEFORE_DELETING + + @property + def message(self) -> str: + return "Cannot stop {resource} {resource_list} before deleting".format( + resource=format_plural(self.resource_id_list, "resource"), + resource_list=format_list(self.resource_id_list), + ) + + @dataclass(frozen=True) class ResourceBanPcmkError(ReportItemMessage): """ diff --git a/pcs/lib/cib/const.py b/pcs/lib/cib/const.py index 1149a931a..1ea5678e8 100644 --- a/pcs/lib/cib/const.py +++ b/pcs/lib/cib/const.py @@ -1,16 +1,24 @@ from typing import Final +TAG_ACL_GROUP: Final = "acl_group" +TAG_ACL_PERMISSION: Final = "acl_permission" +TAG_ACL_ROLE: Final = "acl_role" +TAG_ACL_TARGET: Final = "acl_target" TAG_CONSTRAINT_COLOCATION: Final = "rsc_colocation" TAG_CONSTRAINT_LOCATION: Final = "rsc_location" TAG_CONSTRAINT_ORDER: Final = "rsc_order" TAG_CONSTRAINT_TICKET: Final = "rsc_ticket" TAG_CRM_CONFIG: Final = "crm_config" +TAG_FENCING_LEVEL: Final = "fencing-level" TAG_OBJREF: Final = "obj_ref" TAG_RESOURCE_BUNDLE: Final = "bundle" TAG_RESOURCE_CLONE: Final = "clone" TAG_RESOURCE_GROUP: Final = "group" TAG_RESOURCE_MASTER: Final = "master" TAG_RESOURCE_PRIMITIVE: Final = "primitive" +TAG_RESOURCE_REF: Final = "resource_ref" +TAG_RESOURCE_SET: Final = "resource_set" +TAG_ROLE: Final = "role" TAG_RULE: Final = "rule" TAG_TAG: Final = "tag" diff --git a/pcs/lib/cib/constraint/common.py b/pcs/lib/cib/constraint/common.py index 3f95da236..126a3fe56 100644 --- a/pcs/lib/cib/constraint/common.py +++ b/pcs/lib/cib/constraint/common.py @@ -7,6 +7,7 @@ TAG_LIST_CONSTRAINABLE, TAG_LIST_CONSTRAINT, TAG_LIST_RESOURCE_MULTIINSTANCE, + TAG_RESOURCE_SET, ) from pcs.lib.xml_tools import find_parent @@ -15,6 +16,13 @@ def is_constraint(element: _Element) -> bool: return element.tag in TAG_LIST_CONSTRAINT +def is_set_constraint(element: _Element) -> bool: + return ( + is_constraint(element) + and element.find(f"./{TAG_RESOURCE_SET}") is not None + ) + + def validate_constrainable_elements( element_list: Iterable[_Element], in_multiinstance_allowed: bool = False ) -> reports.ReportItemList: diff --git a/pcs/lib/cib/fencing_topology.py b/pcs/lib/cib/fencing_topology.py index 2434723bc..bb628d006 100644 --- a/pcs/lib/cib/fencing_topology.py +++ b/pcs/lib/cib/fencing_topology.py @@ -1,4 +1,5 @@ from typing import ( + Final, Optional, Sequence, Set, @@ -23,8 +24,13 @@ from pcs.common.reports.item import ReportItem from pcs.common.types import StringCollection from pcs.common.validate import is_integer +from pcs.lib.cib.const import TAG_FENCING_LEVEL from pcs.lib.cib.resource.stonith import is_stonith_resource -from pcs.lib.cib.tools import find_unique_id +from pcs.lib.cib.tools import ( + find_unique_id, + get_element_by_id, + get_root, +) from pcs.lib.errors import LibraryError from pcs.lib.pacemaker.state import _Element as StateElement from pcs.lib.pacemaker.values import ( @@ -32,6 +38,8 @@ validate_id, ) +_DEVICES_ATTRIBUTE: Final = "devices" + def add_level( reporter: ReportProcessor, @@ -162,12 +170,37 @@ def remove_levels_by_params( return report_list -def remove_device_from_all_levels(topology_el, device_id): +def remove_device_from_all_levels_dont_remove_elements( + topology_el: _Element, device_id: str +) -> list[_Element]: + """ + Remove specified stonith device from all fencing levels. Do not remove + fencing-level elements with empty devices attribute. Instead, return a list + of all the elements from which the device was removed. + + topology_el -- etree element with levels to remove the device from + device_id -- stonith device to remove + """ + elements = [] + for level_el in topology_el.findall(TAG_FENCING_LEVEL): + old_devices = level_el.get(_DEVICES_ATTRIBUTE, "") + _remove_device_from_level(level_el, device_id) + new_devices = level_el.get(_DEVICES_ATTRIBUTE, "") + + if old_devices != new_devices: + elements.append(level_el) + + return elements + + +def remove_device_from_all_levels( + topology_el: _Element, device_id: str +) -> None: """ Remove specified stonith device from all fencing levels. - etree topology_el -- etree element with levels to remove the device from - string device_id -- stonith device to remove + topology_el -- etree element with levels to remove the device from + device_id -- stonith device to remove """ # Do not ever remove a fencing-topology element, even if it is empty. There # may be ACLs set in pacemaker which allow "write" for fencing-level @@ -176,16 +209,43 @@ def remove_device_from_all_levels(topology_el, device_id): # the whole change to be rejected by pacemaker with a "permission denied" # message. # https://bugzilla.redhat.com/show_bug.cgi?id=1642514 - for level_el in topology_el.findall("fencing-level"): - new_devices = [ - dev - for dev in level_el.get("devices").split(",") - if dev != device_id - ] - if new_devices: - level_el.set("devices", ",".join(new_devices)) - else: - level_el.getparent().remove(level_el) + for level_el in remove_device_from_all_levels_dont_remove_elements( + topology_el, device_id + ): + _remove_fencing_level_if_empty(level_el) + + +def remove_device_from_one_level( + topology_el: _Element, level_id: str, device_id: str +) -> None: + """ + Remove specified stonith device from specified fencing level. + + topology_el -- etree element with levels to remove the device from + level_id -- level element from which the device is removed + device_id -- stonith device to remove + """ + level_el = get_element_by_id(get_root(topology_el), level_id) + _remove_device_from_level(level_el, device_id) + _remove_fencing_level_if_empty(level_el) + + +def _remove_device_from_level(level_el: _Element, device_id: str) -> None: + new_devices = [ + dev + for dev in level_el.get(_DEVICES_ATTRIBUTE, "").split(",") + if dev != device_id + ] + level_el.set(_DEVICES_ATTRIBUTE, ",".join(new_devices)) + + +def _remove_fencing_level_if_empty(level_el: _Element) -> None: + if level_el.get(_DEVICES_ATTRIBUTE, "") != "": + return + + parent = level_el.getparent() + if parent is not None: + parent.remove(level_el) def export(topology_el): diff --git a/pcs/lib/cib/remove_elements.py b/pcs/lib/cib/remove_elements.py new file mode 100644 index 000000000..6e0856b44 --- /dev/null +++ b/pcs/lib/cib/remove_elements.py @@ -0,0 +1,513 @@ +from collections import defaultdict +from dataclasses import dataclass +from itertools import chain +from typing import ( + Iterable, + Mapping, + cast, +) + +from lxml import etree +from lxml.etree import _Element + +from pcs.common import reports +from pcs.common.resource_status import ( + MoreChildrenQuantifierType, + ResourcesStatusFacade, + ResourceState, +) +from pcs.common.types import ( + StringCollection, + StringSequence, +) +from pcs.lib.cib import ( + const, + sections, +) +from pcs.lib.cib.constraint.common import ( + is_constraint, + is_set_constraint, +) +from pcs.lib.cib.constraint.location import ( + is_location_constraint, + is_location_rule, +) +from pcs.lib.cib.fencing_topology import ( + remove_device_from_all_levels_dont_remove_elements, + remove_device_from_one_level, +) +from pcs.lib.cib.resource.clone import is_any_clone +from pcs.lib.cib.resource.common import ( + disable, + get_inner_resources, + is_resource, +) +from pcs.lib.cib.resource.group import is_group +from pcs.lib.cib.tag import is_tag +from pcs.lib.cib.tools import ( + ElementNotFound, + IdProvider, + find_elements_referencing_id, + get_element_by_id, + get_elements_by_ids, + get_fencing_topology, + remove_element_by_id, + remove_one_element, +) +from pcs.lib.pacemaker.live import parse_cib_xml +from pcs.lib.pacemaker.state import ( + ensure_resource_state, + is_resource_managed, +) +from pcs.lib.pacemaker.status import ( + ClusterStatusParser, + ClusterStatusParsingError, + cluster_status_parsing_error_to_report, +) +from pcs.lib.xml_tools import get_root + + +@dataclass(frozen=True) +class DependantElements: + id_tag_map: dict[str, str] + + def to_reports(self) -> reports.ReportItemList: + if not self.id_tag_map: + return [] + + return [ + reports.ReportItem.info( + reports.messages.CibRemoveDependantElements(self.id_tag_map) + ) + ] + + +@dataclass(frozen=True) +class ElementReferences: + reference_map: dict[str, set[str]] + id_tag_map: dict[str, str] + + def to_reports(self) -> reports.ReportItemList: + if not self.reference_map: + return [] + + return [ + reports.ReportItem.info( + reports.messages.CibRemoveReferences( + self.id_tag_map, self.reference_map + ) + ) + ] + + +@dataclass(frozen=True) +class UnsupportedElements: + id_tag_map: Mapping[str, str] + supported_element_types: StringCollection + + +class ElementsToRemove: + """ + Find ids of all elements that should be removed. This function is aware + of relations and references between elements and will also return ids of + elements that are somehow referencing elements with specified ids. + + cib -- the whole cib + ids -- ids of configuration elements to remove + """ + + def __init__(self, cib: _Element, ids: StringCollection): + wip_cib = parse_cib_xml(etree.tostring(cib).decode()) + + initial_ids = set(ids) + elements_to_process, missing_ids = get_elements_by_ids( + wip_cib, initial_ids + ) + + supported_elements, unsupported_elements = _validate_element_types( + elements_to_process + ) + + element_ids_to_remove, removing_references_from = ( + _get_dependencies_to_remove(supported_elements) + ) + + self._ids_to_remove = element_ids_to_remove + self._dependant_element_ids = self._ids_to_remove - initial_ids + self._missing_ids = set(missing_ids) + self._unsupported_ids = set( + str(el.attrib["id"]) for el in unsupported_elements + ) + + all_ids = set( + chain( + self._ids_to_remove, + self._unsupported_ids, + *removing_references_from.values(), + ) + ) + self._id_tag_map = { + str(el.attrib["id"]): el.tag + for el in get_elements_by_ids(cib, all_ids)[0] + } + self._element_references = removing_references_from + self._resource_ids_to_disable = set( + str(el.attrib["id"]) + for el in get_elements_by_ids(cib, element_ids_to_remove)[0] + if is_resource(el) + ) + + @property + def resources_to_disable(self) -> set[str]: + return set(self._resource_ids_to_disable) + + @property + def ids_to_remove(self) -> set[str]: + return set(self._ids_to_remove) + + @property + def dependant_elements(self) -> DependantElements: + return DependantElements( + { + element_id: self._id_tag_map[element_id] + for element_id in self._dependant_element_ids + } + ) + + @property + def element_references(self) -> ElementReferences: + return ElementReferences( + dict(self._element_references), + { + element_id: self._id_tag_map[element_id] + for element_id in chain( + self._element_references, + *self._element_references.values(), + ) + }, + ) + + @property + def missing_ids(self) -> set[str]: + return set(self._missing_ids) + + @property + def unsupported_elements(self) -> UnsupportedElements: + return UnsupportedElements( + id_tag_map={ + element_id: self._id_tag_map[element_id] + for element_id in self._unsupported_ids + }, + supported_element_types=["constraint", "location rule", "resource"], + ) + + +def stop_resources( + cib: _Element, state: _Element, elements: ElementsToRemove +) -> reports.ReportItemList: + """ + Stop all resources that are going to be removed. + + cib -- the whole cib + state -- state of the cluster + elements -- elements planned to be removed + """ + resources_to_disable = sorted(elements.resources_to_disable) + report_list = _warn_resource_unmanaged(state, resources_to_disable) + resources, _ = get_elements_by_ids(cib, resources_to_disable) + provider = IdProvider(cib) + for el in resources: + disable(el, provider) + return report_list + + +def ensure_resources_stopped( + state: _Element, elements: ElementsToRemove +) -> reports.ReportItemList: + """ + Ensure that all resources that should be stopped are stopped. + + state -- state of the cluster + elements -- elements planned to be removed + """ + resources_to_disable = sorted(elements.resources_to_disable) + not_stopped_ids = [] + report_list: reports.ReportItemList = [] + try: + parser = ClusterStatusParser(state) + try: + status_dto = parser.status_xml_to_dto() + except ClusterStatusParsingError as e: + report_list.append(cluster_status_parsing_error_to_report(e)) + return report_list + report_list.extend(parser.get_warnings()) + + status = ResourcesStatusFacade.from_resources_status_dto(status_dto) + not_stopped_ids = [ + resource_id + for resource_id in resources_to_disable + if not status.is_state( + resource_id, + None, + ResourceState.STOPPED, + instances_quantifier=( + MoreChildrenQuantifierType.ALL + if status.can_have_multiple_instances(resource_id) + else None + ), + ) + ] + except NotImplementedError: + # TODO remove when issue with bundles in status is fixed + not_stopped_ids = [ + resource_id + for resource_id in resources_to_disable + if ensure_resource_state(False, state, resource_id).severity.level + == reports.item.ReportItemSeverity.ERROR + ] + + if not_stopped_ids: + report_list.append( + reports.ReportItem.error( + reports.messages.CannotStopResourcesBeforeDeleting( + not_stopped_ids + ), + force_code=reports.codes.FORCE, + ) + ) + + return report_list + + +def remove_specified_elements( + cib: _Element, elements: ElementsToRemove +) -> None: + """ + Remove all elements that need to be removed. + + state -- state of the cluster + elements -- elements planned to be removed + """ + for element_id in elements.ids_to_remove: + remove_element_by_id(cib, element_id) + + element_references = elements.element_references + for ( + referenced_id, + referenced_in_ids, + ) in element_references.reference_map.items(): + for element_id in referenced_in_ids: + _remove_element_reference( + cib, + referenced_id, + element_id, + element_references.id_tag_map[element_id], + ) + + +def _validate_element_types( + elements: Iterable[_Element], +) -> tuple[list[_Element], list[_Element]]: + supported_elements = [] + unsupported_elements = [] + + for el in elements: + if is_constraint(el) or is_location_rule(el) or is_resource(el): + supported_elements.append(el) + else: + unsupported_elements.append(el) + + return supported_elements, unsupported_elements + + +_REFERENCE_TAG_XPATH_MAP = { + const.TAG_RESOURCE_SET: f"./{const.TAG_RESOURCE_REF}[@id=$referenced_id]", + const.TAG_TAG: f"./{const.TAG_OBJREF}[@id=$referenced_id]", +} + + +def _remove_element_reference( + cib: _Element, + element_id: str, + referenced_in_id: str, + referenced_in_tag: str, +) -> None: + # If element has id, then it was already removed using its id and does not + # need to be removed using this reference mapping. Therefore, we need to + # only remove elements that do not have id here, such as obj_ref and + # resource_ref. + + if referenced_in_tag == const.TAG_FENCING_LEVEL: + if not sections.exists(cib, sections.FENCING_TOPOLOGY): + return + + remove_device_from_one_level( + get_fencing_topology(cib), referenced_in_id, element_id + ) + return + + if referenced_in_tag not in _REFERENCE_TAG_XPATH_MAP: + return + try: + element = get_element_by_id(cib, referenced_in_id) + except ElementNotFound: + return + for el in cast( + list[_Element], + element.xpath( + _REFERENCE_TAG_XPATH_MAP[referenced_in_tag], + referenced_id=element_id, + ), + ): + remove_one_element(el) + + +def _warn_resource_unmanaged( + state: _Element, resource_ids: StringSequence +) -> reports.ReportItemList: + report_list: reports.ReportItemList = [] + try: + parser = ClusterStatusParser(state) + try: + status_dto = parser.status_xml_to_dto() + except ClusterStatusParsingError as e: + report_list.append(cluster_status_parsing_error_to_report(e)) + return report_list + report_list.extend(parser.get_warnings()) + + status = ResourcesStatusFacade.from_resources_status_dto(status_dto) + report_list.extend( + reports.ReportItem.warning( + reports.messages.ResourceIsUnmanaged(resource_id) + ) + for resource_id in resource_ids + if status.is_state( + resource_id, + None, + ResourceState.UNMANAGED, + ) + ) + except NotImplementedError: + # TODO remove when issue with bundles in status is fixed + report_list.extend( + reports.ReportItem.warning( + reports.messages.ResourceIsUnmanaged(resource_id) + ) + for resource_id in resource_ids + if not is_resource_managed(state, resource_id) + ) + + return report_list + + +def _get_dependencies_to_remove( + elements: Iterable[_Element], +) -> tuple[set[str], dict[str, set[str]]]: + """ + Get ids of all elements that need to be removed (including specified + elements) together with specified elements based on their relations. + Also return mapping for elements whose references are going to be + deleted from their respective parent elements, without deleting the + parent itself. + + WARNING: this is a destructive operation for elements and their etree. + + elements -- iterable of elements that are planned to be removed + """ + elements_to_process = list(elements) + element_ids_to_remove: set[str] = set() + removing_references_from: dict[str, set[str]] = defaultdict(set) + + while elements_to_process: + el = elements_to_process.pop(0) + element_id = str(el.attrib["id"]) + + if el.tag not in ( + const.TAG_OBJREF, + const.TAG_RESOURCE_REF, + const.TAG_ROLE, + ): + if element_id in element_ids_to_remove: + continue + element_ids_to_remove.add(element_id) + elements_to_process.extend(_get_element_references(el)) + elements_to_process.extend(_get_inner_references(el)) + + for level_el in remove_device_from_all_levels_dont_remove_elements( + get_fencing_topology(get_root(el)), element_id + ): + removing_references_from[element_id].add( + str(level_el.attrib["id"]) + ) + if level_el.get("devices", "") == "": + elements_to_process.append(level_el) + + parent_el = el.getparent() + if parent_el is not None: + if _is_empty_after_inner_el_removal(parent_el): + elements_to_process.append(parent_el) + parent_el.remove(el) + + parent_id = parent_el.get("id") + if parent_id is not None: + removing_references_from[element_id].add(parent_id) + + for key in list(removing_references_from): + removing_references_from[key].difference_update(element_ids_to_remove) + if not removing_references_from[key]: + del removing_references_from[key] + + return element_ids_to_remove, removing_references_from + + +def _get_element_references(element: _Element) -> Iterable[_Element]: + """ + Return all CIB elements that are referencing specified element + + element -- references to this element will be + """ + return find_elements_referencing_id(element, str(element.attrib["id"])) + + +def _get_inner_references(element: _Element) -> Iterable[_Element]: + """ + Get all inner elements with attribute id, which means that they might be + referenced in IDREF. Elements with attribute id and type IDREF are also + returned. + """ + # return cast(Iterable[_Element], element.xpath("./*[@id]")) + if is_resource(element): + try: + # we are removing elements from the tree, therefore assertions of + # this function can fail + return get_inner_resources(element) + except IndexError: + return [] + # if element.tag == "alert": + # return element.findall("recipient") + # if is_set_constraint(element): + # return element.findall("resource_set") + # if element.tag == "acl_role": + # return element.findall("acl_permission") + return [] + + +def _is_last_element(parent_element: _Element, child_tag: str) -> bool: + return len(parent_element.findall(f"./{child_tag}")) == 1 + + +def _is_empty_after_inner_el_removal(parent_el: _Element) -> bool: + # pylint: disable=too-many-return-statements + if is_any_clone(parent_el): + return True + if is_group(parent_el): + return len(get_inner_resources(parent_el)) == 1 + if is_tag(parent_el): + return _is_last_element(parent_el, const.TAG_OBJREF) + if parent_el.tag == const.TAG_RESOURCE_SET: + return _is_last_element(parent_el, const.TAG_RESOURCE_REF) + if is_set_constraint(parent_el): + return _is_last_element(parent_el, const.TAG_RESOURCE_SET) + if is_location_constraint(parent_el): + return _is_last_element(parent_el, const.TAG_RULE) + return False diff --git a/pcs/lib/cib/tools.py b/pcs/lib/cib/tools.py index 2c1227378..ce341cf3f 100644 --- a/pcs/lib/cib/tools.py +++ b/pcs/lib/cib/tools.py @@ -629,8 +629,8 @@ def remove_element_by_id(cib: _Element, element_id: str) -> None: """ Remove element with specified id from cib element. """ - for ref_el in _find_elements_without_id_referencing_id(cib, element_id): - remove_one_element(ref_el) + # raise LibraryError error if configuration section is not in cib + _ = get_configuration(cib) try: remove_one_element(get_element_by_id(cib, element_id)) diff --git a/pcs/lib/commands/cib.py b/pcs/lib/commands/cib.py index cba5b863a..25a7b7b66 100644 --- a/pcs/lib/commands/cib.py +++ b/pcs/lib/commands/cib.py @@ -1,33 +1,22 @@ -from typing import ( - Collection, - Iterable, -) +from typing import Collection -from lxml import etree from lxml.etree import _Element from pcs.common import reports from pcs.common.types import StringCollection -from pcs.lib.cib.const import TAG_OBJREF -from pcs.lib.cib.constraint.common import is_constraint -from pcs.lib.cib.constraint.location import ( - is_location_constraint, - is_location_rule, +from pcs.lib.cib.remove_elements import ( + ElementsToRemove, + ensure_resources_stopped, + remove_specified_elements, + stop_resources, ) -from pcs.lib.cib.constraint.resource_set import is_set_constraint -from pcs.lib.cib.resource.bundle import is_bundle -from pcs.lib.cib.resource.clone import is_any_clone -from pcs.lib.cib.resource.common import get_inner_resources -from pcs.lib.cib.resource.group import is_group -from pcs.lib.cib.tag import is_tag -from pcs.lib.cib.tools import ( - find_elements_referencing_id, - get_elements_by_ids, - remove_element_by_id, +from pcs.lib.cib.resource.guest_node import is_guest_node +from pcs.lib.cib.resource.remote_node import ( + get_node_name_from_resource as get_node_name_from_remote_resource, ) +from pcs.lib.cib.tools import get_elements_by_ids from pcs.lib.env import LibraryEnvironment from pcs.lib.errors import LibraryError -from pcs.lib.pacemaker.live import parse_cib_xml def remove_elements( @@ -43,140 +32,120 @@ def remove_elements( ids -- ids of configuration elements to remove force_flags -- list of flags codes """ - del force_flags - id_set = set(ids) cib = env.get_cib() - wip_cib = parse_cib_xml(etree.tostring(cib).decode()) report_processor = env.report_processor - elements_to_process, not_found_ids = get_elements_by_ids(wip_cib, id_set) - - for non_existing_id in not_found_ids: - report_processor.report( - reports.ReportItem.error( - reports.messages.IdNotFound( - non_existing_id, ["configuration element"] - ) - ) - ) + elements_to_remove = ElementsToRemove(cib, ids) - for element in elements_to_process: - # TODO: add support for other CIB elements - if not (is_constraint(element) or is_location_rule(element)): - report_processor.report( - reports.ReportItem.error( - reports.messages.IdBelongsToUnexpectedType( - str(element.get("id")), - ["constraint", "location rule"], - element.tag, - ) - ) - ) + if report_processor.report_list( + _validate_elements_to_remove(elements_to_remove) + ).has_errors: + raise LibraryError() - if report_processor.has_errors: + if report_processor.report_list( + _ensure_not_guest_remote(cib, ids) + ).has_errors: raise LibraryError() - element_ids_to_remove = _get_dependencies_to_remove(elements_to_process) - dependant_elements, _ = get_elements_by_ids( - cib, element_ids_to_remove - id_set + report_processor.report_list( + elements_to_remove.dependant_elements.to_reports() + ) + report_processor.report_list( + elements_to_remove.element_references.to_reports() ) - if dependant_elements: - report_processor.report( - reports.ReportItem.info( - reports.messages.CibRemoveDependantElements( - { - str(element.attrib["id"]): element.tag - for element in dependant_elements - } - ) - ) - ) - for element_id in element_ids_to_remove: - remove_element_by_id(cib, element_id) + if env.is_cib_live and reports.codes.FORCE not in force_flags: + cib = _stop_resources_wait(env, cib, elements_to_remove) + remove_specified_elements(cib, elements_to_remove) env.push_cib() -def _get_dependencies_to_remove(elements: list[_Element]) -> set[str]: +def _stop_resources_wait( + env: LibraryEnvironment, cib: _Element, elements_to_remove: ElementsToRemove +) -> _Element: """ - Get ids of all elements that need to be removed (including specified - elements) together with specified elements based on their relations. + Stop all resources that are going to be removed. Push cib, wait for the + cluster to settle down, and check if all resources were properly stopped. + If not, report errors. Return cib with the applied changes. - WARNING: this is a destructive operation for elements and their etree. - - elements -- list of elements that are planned to be removed - """ - elements_to_process = list(elements) - element_ids_to_remove: set[str] = set() - - while elements_to_process: - el = elements_to_process.pop(0) - element_id = str(el.attrib["id"]) - if el.tag not in ("obj_ref", "resource_ref", "role"): - if element_id in element_ids_to_remove: - continue - element_ids_to_remove.add(element_id) - elements_to_process.extend(_get_element_references(el)) - elements_to_process.extend(_get_inner_references(el)) - parent_el = el.getparent() - if parent_el is not None: - if _is_empty_after_inner_el_removal(parent_el): - elements_to_process.append(parent_el) - parent_el.remove(el) - - return element_ids_to_remove - - -def _get_element_references(element: _Element) -> Iterable[_Element]: + cib -- whole cib + elements -- elements planned to be removed """ - Return all CIB elements that are referencing specified element + resources_to_disable = elements_to_remove.resources_to_disable + if not resources_to_disable: + return cib + env.report_processor.report( + reports.ReportItem.info( + reports.messages.StoppingResourcesBeforeDeleting( + sorted(resources_to_disable) + ) + ) + ) - element -- references to this element will be - """ - return find_elements_referencing_id(element, str(element.attrib["id"])) + if env.report_processor.report_list( + stop_resources(cib, env.get_cluster_state(), elements_to_remove) + ).has_errors: + raise LibraryError() + env.push_cib() + + env.wait_for_idle() + if env.report_processor.report_list( + ensure_resources_stopped(env.get_cluster_state(), elements_to_remove) + ).has_errors: + raise LibraryError() + return env.get_cib() -def _get_inner_references(element: _Element) -> Iterable[_Element]: - """ - Get all inner elements with attribute id, which means that they might be - refernenced in IDREF. Elements with attribute id and type IDREF are also - returned. - - Note: - Only removing of constraint or location rule elements is supported. - Theirs inner elements cannot be referenced or referencing is not - supported. - """ - # pylint: disable=unused-argument - # return cast(Iterable[_Element], element.xpath("./*[@id]")) - # if is_resource(element): - # return get_inner_resources(element) - # if element.tag == "alert": - # return element.findall("recipient") - # if is_set_constraint(element): - # return element.findall("resource_set") - # if element.tag == "acl_role": - # return element.findall("acl_permission") - return [] - - -def _is_last_element(parent_element: _Element, child_tag: str) -> bool: - return len(parent_element.findall(f"./{child_tag}")) == 1 - - -def _is_empty_after_inner_el_removal(parent_el: _Element) -> bool: - # pylint: disable=too-many-return-statements - if is_bundle(parent_el) or is_any_clone(parent_el): - return True - if is_group(parent_el): - return len(get_inner_resources(parent_el)) == 1 - if is_tag(parent_el): - return _is_last_element(parent_el, TAG_OBJREF) - if parent_el.tag == "resource_set": - return _is_last_element(parent_el, "resource_ref") - if is_set_constraint(parent_el): - return _is_last_element(parent_el, "resource_set") - if is_location_constraint(parent_el): - return _is_last_element(parent_el, "rule") - return False + +def _validate_elements_to_remove( + element_to_remove: ElementsToRemove, +) -> reports.ReportItemList: + report_list = [] + for missing_id in sorted(element_to_remove.missing_ids): + report_list.append( + reports.ReportItem.error( + reports.messages.IdNotFound( + missing_id, ["configuration element"] + ) + ) + ) + + unsupported_elements = element_to_remove.unsupported_elements + for unsupported_id in sorted(unsupported_elements.id_tag_map): + report_list.append( + reports.ReportItem.error( + reports.messages.IdBelongsToUnexpectedType( + unsupported_id, + list(unsupported_elements.supported_element_types), + unsupported_elements.id_tag_map[unsupported_id], + ) + ) + ) + + return report_list + + +def _ensure_not_guest_remote( + cib: _Element, ids: StringCollection +) -> reports.ReportItemList: + report_list = [] + elements_to_process, _ = get_elements_by_ids(cib, ids) + for element in elements_to_process: + if is_guest_node(element): + report_list.append( + reports.ReportItem.error( + reports.messages.UseCommandNodeRemoveGuest( + str(element.attrib["id"]) + ) + ) + ) + if get_node_name_from_remote_resource(element) is not None: + report_list.append( + reports.ReportItem.error( + reports.messages.UseCommandNodeRemoveRemote( + str(element.attrib["id"]) + ) + ) + ) + return report_list diff --git a/pcs/resource.py b/pcs/resource.py index 008b122fa..2d7d0c920 100644 --- a/pcs/resource.py +++ b/pcs/resource.py @@ -35,6 +35,7 @@ Argv, InputModifiers, KeyValueParser, + ensure_unique_args, group_by_keywords, wait_to_timeout, ) @@ -75,7 +76,11 @@ ) from pcs.common.interface import dto from pcs.common.pacemaker.defaults import CibDefaultsDto -from pcs.common.pacemaker.resource.list import CibResourcesDto +from pcs.common.pacemaker.resource.list import ( + CibResourcesDto, + get_all_resources_ids, + get_stonith_resources_ids, +) from pcs.common.pacemaker.resource.operations import ( OCF_CHECK_LEVEL_INSTANCE_ATTRIBUTE_NAME, ) @@ -84,6 +89,7 @@ format_list, format_list_custom_last_separator, format_optional, + format_plural, ) from pcs.lib.cib.resource import ( guest_node, @@ -1986,14 +1992,41 @@ def resource_remove_cmd( """ Options: * -f - CIB file - * --force - don't stop a resource before its deletion + * --force - don't stop resources before deletion """ modifiers.ensure_only_supported("-f", "--force") - if len(argv) != 1: + if not argv: raise CmdLineInputError() - resource_id = argv[0] - _check_is_not_stonith(lib, [resource_id], "pcs stonith delete") - resource_remove(resource_id) + ensure_unique_args(argv) + + resources_to_remove = set(argv) + resources_dto = lib.resource.get_configured_resources() + missing_ids = resources_to_remove - get_all_resources_ids(resources_dto) + if missing_ids: + raise CmdLineInputError( + "Unable to find {resource}: {id_list}".format( + resource=format_plural(missing_ids, "resource"), + id_list=format_list(missing_ids), + ) + ) + + stonith_ids = resources_to_remove & get_stonith_resources_ids(resources_dto) + if stonith_ids: + raise CmdLineInputError( + ( + "This command cannot remove stonith {resource}: {id_list}. Use " + "'pcs stonith remove' instead." + ).format( + resource=format_plural(stonith_ids, "resource"), + id_list=format_list(stonith_ids), + ) + ) + + force_flags = set() + if modifiers.is_specified("--force"): + force_flags.add(reports.codes.FORCE) + + lib.cib.remove_elements(resources_to_remove, force_flags) # TODO move to lib (complete rewrite) diff --git a/pcs/stonith.py b/pcs/stonith.py index f97ff5475..fdf11b412 100644 --- a/pcs/stonith.py +++ b/pcs/stonith.py @@ -15,6 +15,7 @@ Argv, InputModifiers, KeyValueParser, + ensure_unique_args, ) from pcs.cli.fencing_topology import target_type_map_cli_to_lib from pcs.cli.reports.output import ( @@ -33,10 +34,15 @@ TARGET_TYPE_NODE, TARGET_TYPE_REGEXP, ) +from pcs.common.pacemaker.resource.list import ( + get_all_resources_ids, + get_stonith_resources_ids, +) from pcs.common.resource_agent.dto import ResourceAgentNameDto from pcs.common.str_tools import ( format_list, format_optional, + format_plural, indent, ) from pcs.lib.errors import LibraryError @@ -989,11 +995,40 @@ def delete_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: * --force - don't stop a resource before its deletion """ modifiers.ensure_only_supported("-f", "--force") - if len(argv) != 1: + if not argv: raise CmdLineInputError() - resource_id = argv[0] - _check_is_stonith(lib, [resource_id], "pcs resource delete") - resource.resource_remove(resource_id) + ensure_unique_args(argv) + + resources_to_remove = set(argv) + resources_dto = lib.resource.get_configured_resources() + missing_ids = resources_to_remove - get_all_resources_ids(resources_dto) + if missing_ids: + raise CmdLineInputError( + "Unable to find stonith {resource}: {id_list}".format( + resource=format_plural(missing_ids, "resource"), + id_list=format_list(missing_ids), + ) + ) + + non_stonith_ids = resources_to_remove - get_stonith_resources_ids( + resources_dto + ) + if non_stonith_ids: + raise CmdLineInputError( + ( + "This command cannot remove {resource}: {id_list}. Use 'pcs " + "resource remove' instead." + ).format( + resource=format_plural(non_stonith_ids, "resource"), + id_list=format_list(non_stonith_ids), + ) + ) + + force_flags = set() + if modifiers.is_specified("--force"): + force_flags.add(reports.codes.FORCE) + + lib.cib.remove_elements(resources_to_remove, force_flags) def enable_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: diff --git a/pcs_test/Makefile.am b/pcs_test/Makefile.am index f50f0faf8..2bbe3514a 100644 --- a/pcs_test/Makefile.am +++ b/pcs_test/Makefile.am @@ -102,6 +102,7 @@ EXTRA_DIST = \ tier0/cli/resource/test_defaults.py \ tier0/cli/resource/test_parse_args.py \ tier0/cli/resource/test_relations.py \ + tier0/cli/resource/test_remove.py \ tier0/cli/tag/__init__.py \ tier0/cli/tag/test_command.py \ tier0/cli/test_booth.py \ @@ -118,6 +119,8 @@ EXTRA_DIST = \ tier0/common/interface/test_dto.py \ tier0/common/pacemaker/constraint/__init__.py \ tier0/common/pacemaker/constraint/all.py \ + tier0/common/pacemaker/resource/__init__.py \ + tier0/common/pacemaker/resource/list.py \ tier0/common/reports/__init__.py \ tier0/common/reports/test_item.py \ tier0/common/reports/test_messages.py \ @@ -199,6 +202,7 @@ EXTRA_DIST = \ tier0/lib/cib/test_node.py \ tier0/lib/cib/test_nvpair_multi.py \ tier0/lib/cib/test_nvpair.py \ + tier0/lib/cib/test_remove_elements.py \ tier0/lib/cib/test_resource_bundle.py \ tier0/lib/cib/test_resource_clone.py \ tier0/lib/cib/test_resource_common.py \ @@ -390,8 +394,10 @@ EXTRA_DIST = \ tier1/legacy/test_utils.py \ tier1/resource/__init__.py \ tier1/resource/test_config.py \ + tier1/resource/test_remove.py \ tier1/stonith/__init__.py \ tier1/stonith/test_config.py \ + tier1/stonith/test_remove.py \ tier1/test_booth.py \ tier1/test_cib_options.py \ tier1/test_cluster_pcmk_remote.py \ diff --git a/pcs_test/tier0/cli/reports/test_messages.py b/pcs_test/tier0/cli/reports/test_messages.py index 73fd72ea4..188964954 100644 --- a/pcs_test/tier0/cli/reports/test_messages.py +++ b/pcs_test/tier0/cli/reports/test_messages.py @@ -333,6 +333,17 @@ def test_success(self): ) +class UseCommandNodeRemoveRemote(CliReportMessageTestBase): + def test_success(self): + self.assert_message( + messages.UseCommandNodeRemoveRemote(), + ( + "this command is not sufficient for removing a remote node, use" + " 'pcs cluster node remove-remote'" + ), + ) + + class UseCommandNodeAddGuest(CliReportMessageTestBase): def test_success(self): self.assert_message( diff --git a/pcs_test/tier0/cli/resource/test_remove.py b/pcs_test/tier0/cli/resource/test_remove.py new file mode 100644 index 000000000..8bbe0b1d8 --- /dev/null +++ b/pcs_test/tier0/cli/resource/test_remove.py @@ -0,0 +1,86 @@ +from unittest import ( + TestCase, + mock, +) + +from pcs.cli.common.errors import CmdLineInputError +from pcs.resource import resource_remove_cmd + +from pcs_test.tools.misc import dict_to_modifiers +from pcs_test.tools.resources_dto import ALL_RESOURCES + + +class RemoveResource(TestCase): + def setUp(self): + self.lib = mock.Mock(spec_set=["cib", "resource"]) + self.lib.cib = mock.Mock(spec_set=["remove_elements"]) + self.cib = self.lib.cib + self.lib.resource = mock.Mock(spec_set=["get_configured_resources"]) + self.resource = self.lib.resource + self.resource.get_configured_resources.return_value = ALL_RESOURCES + + def _call_cmd(self, argv, modifiers=None): + resource_remove_cmd(self.lib, argv, dict_to_modifiers(modifiers or {})) + + def test_no_args(self): + with self.assertRaises(CmdLineInputError) as cm: + self._call_cmd([]) + self.assertIsNone(cm.exception.message) + self.resource.get_configured_resources.assert_not_called() + self.cib.remove_elements.assert_not_called() + + def test_remove_one(self): + self._call_cmd(["R1"]) + self.resource.get_configured_resources.assert_called_once_with() + self.cib.remove_elements.assert_called_once_with({"R1"}, set()) + + def test_remove_multiple(self): + self._call_cmd(["R1", "R2", "R3"]) + self.resource.get_configured_resources.assert_called_once_with() + self.cib.remove_elements.assert_called_once_with( + {"R1", "R2", "R3"}, set() + ) + + def test_duplicate_args(self): + with self.assertRaises(CmdLineInputError) as cm: + self._call_cmd(["R1", "R1", "R2", "R3", "R2"]) + self.assertEqual( + cm.exception.message, "duplicate arguments: 'R1', 'R2'" + ) + self.resource.get_configured_resources.assert_not_called() + self.cib.remove_elements.assert_not_called() + + def test_not_resource_id(self): + with self.assertRaises(CmdLineInputError) as cm: + self._call_cmd(["nonexistent"]) + self.assertEqual( + cm.exception.message, "Unable to find resource: 'nonexistent'" + ) + self.resource.get_configured_resources.assert_called_once_with() + self.cib.remove_elements.assert_not_called() + + def test_stonith_id(self): + with self.assertRaises(CmdLineInputError) as cm: + self._call_cmd(["S1"]) + self.assertEqual( + cm.exception.message, + ( + "This command cannot remove stonith resource: 'S1'. " + "Use 'pcs stonith remove' instead." + ), + ) + self.resource.get_configured_resources.assert_called_once_with() + self.cib.remove_elements.assert_not_called() + + def test_multiple_stonith_ids(self): + with self.assertRaises(CmdLineInputError) as cm: + self._call_cmd(["S1", "R1", "R2", "R3", "S2"]) + self.assertEqual( + cm.exception.message, + ( + "This command cannot remove stonith resources: 'S1', 'S2'. " + "Use 'pcs stonith remove' instead." + ), + ) + self.resource.get_configured_resources.assert_called_once_with() + self.cib.remove_elements.assert_not_called() diff --git a/pcs_test/tier0/cli/test_stonith.py b/pcs_test/tier0/cli/test_stonith.py index 368d68eb4..97d912a2d 100644 --- a/pcs_test/tier0/cli/test_stonith.py +++ b/pcs_test/tier0/cli/test_stonith.py @@ -7,8 +7,10 @@ from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.parse_args import InputModifiers from pcs.common import reports +from pcs.stonith import delete_cmd as stonith_delete_cmd from pcs_test.tools.misc import dict_to_modifiers +from pcs_test.tools.resources_dto import ALL_RESOURCES def _dict_to_modifiers(options): @@ -348,3 +350,79 @@ def test_remove_delete_devices(self): self.assert_add_remove_called_with( "stonith-id", [], ["d4", "d3", "d2", "d1"], [] ) + + +class StonithRemove(TestCase): + def setUp(self): + self.lib = mock.Mock(spec_set=["cib", "resource"]) + self.lib.cib = mock.Mock(spec_set=["remove_elements"]) + self.cib = self.lib.cib + self.lib.resource = mock.Mock(spec_set=["get_configured_resources"]) + self.resource = self.lib.resource + self.resource.get_configured_resources.return_value = ALL_RESOURCES + + def _call_cmd(self, argv, modifiers=None): + stonith_delete_cmd(self.lib, argv, dict_to_modifiers(modifiers or {})) + + def test_no_args(self): + with self.assertRaises(CmdLineInputError) as cm: + self._call_cmd([]) + self.assertIsNone(cm.exception.message) + self.resource.get_configured_resources.assert_not_called() + self.cib.remove_elements.assert_not_called() + + def test_remove_one(self): + self._call_cmd(["S1"]) + self.resource.get_configured_resources.assert_called_once_with() + self.cib.remove_elements.assert_called_once_with({"S1"}, set()) + + def test_remove_multiple(self): + self._call_cmd(["S1", "S2"]) + self.resource.get_configured_resources.assert_called_once_with() + self.cib.remove_elements.assert_called_once_with({"S1", "S2"}, set()) + + def test_duplicate_args(self): + with self.assertRaises(CmdLineInputError) as cm: + self._call_cmd(["S1", "S2", "S1", "S2"]) + self.assertEqual( + cm.exception.message, "duplicate arguments: 'S1', 'S2'" + ) + + self.resource.get_configured_resources.assert_not_called() + self.cib.remove_elements.assert_not_called() + + def test_not_resource_id(self): + with self.assertRaises(CmdLineInputError) as cm: + self._call_cmd(["nonexistent"]) + self.assertEqual( + cm.exception.message, + "Unable to find stonith resource: 'nonexistent'", + ) + self.resource.get_configured_resources.assert_called_once_with() + self.cib.remove_elements.assert_not_called() + + def test_not_stonith_id(self): + with self.assertRaises(CmdLineInputError) as cm: + self._call_cmd(["R1"]) + self.assertEqual( + cm.exception.message, + ( + "This command cannot remove resource: 'R1'. " + "Use 'pcs resource remove' instead." + ), + ) + self.resource.get_configured_resources.assert_called_once_with() + self.cib.remove_elements.assert_not_called() + + def test_multiple_not_stonith_id(self): + with self.assertRaises(CmdLineInputError) as cm: + self._call_cmd(["S1", "R1", "R2", "R3", "S2"]) + self.assertEqual( + cm.exception.message, + ( + "This command cannot remove resources: 'R1', 'R2', 'R3'. " + "Use 'pcs resource remove' instead." + ), + ) + self.resource.get_configured_resources.assert_called_once_with() + self.cib.remove_elements.assert_not_called() diff --git a/pcs_test/tier0/common/pacemaker/resource/__init__.py b/pcs_test/tier0/common/pacemaker/resource/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pcs_test/tier0/common/pacemaker/resource/list.py b/pcs_test/tier0/common/pacemaker/resource/list.py new file mode 100644 index 000000000..2963d1a8a --- /dev/null +++ b/pcs_test/tier0/common/pacemaker/resource/list.py @@ -0,0 +1,60 @@ +from unittest import TestCase + +from pcs.common.pacemaker.resource.list import ( + CibResourcesDto, + get_all_resources_ids, + get_stonith_resources_ids, +) + +from pcs_test.tools.resources_dto import ( + ALL_RESOURCES, + PRIMITIVE_R1, + PRIMITIVE_R2, +) + + +class GetAllResourcesIds(TestCase): + def test_resources(self): + self.assertEqual( + get_all_resources_ids(ALL_RESOURCES), + { + "R1", + "R2", + "R3", + "R4", + "R5", + "R6", + "R7", + "S1", + "S2", + "G1", + "G2", + "B1", + "B2", + "G1-clone", + "R6-clone", + }, + ) + + def test_no_resources(self): + self.assertEqual( + get_all_resources_ids(CibResourcesDto([], [], [], [])), set() + ) + + +class GetStonithResourcesIds(TestCase): + def test_resources(self): + self.assertEqual(get_stonith_resources_ids(ALL_RESOURCES), {"S1", "S2"}) + + def test_no_resources(self): + self.assertEqual( + get_stonith_resources_ids(CibResourcesDto([], [], [], [])), set() + ) + + def test_no_stonith_resources(self): + self.assertEqual( + get_stonith_resources_ids( + CibResourcesDto([PRIMITIVE_R1, PRIMITIVE_R2], [], [], []) + ), + set(), + ) diff --git a/pcs_test/tier0/common/reports/test_messages.py b/pcs_test/tier0/common/reports/test_messages.py index f46bced1e..4eb982776 100644 --- a/pcs_test/tier0/common/reports/test_messages.py +++ b/pcs_test/tier0/common/reports/test_messages.py @@ -3791,6 +3791,77 @@ def test_multiple_element_types_with_multiple_ids(self): ) +class CibRemoveReferences(NameBuildTest): + def test_one_element_single_reference(self): + self.assert_message_from_report( + ( + "Removing references:\n" + " Resource 'id1' from:\n" + " Tag: 'id2'" + ), + reports.CibRemoveReferences( + {"id1": "primitive", "id2": "tag"}, {"id1": ["id2"]} + ), + ) + + def test_missing_tag_mapping(self): + self.assert_message_from_report( + ( + "Removing references:\n" + " Element 'id1' from:\n" + " Element: 'id2'" + ), + reports.CibRemoveReferences({}, {"id1": ["id2"]}), + ) + + def test_one_element_multiple_references_same_type(self): + self.assert_message_from_report( + ( + "Removing references:\n" + " Resource 'id1' from:\n" + " Tags: 'id2', 'id3'" + ), + reports.CibRemoveReferences( + {"id1": "primitive", "id2": "tag", "id3": "tag"}, + {"id1": ["id2", "id3"]}, + ), + ) + + def test_one_element_multiple_references_multiple_types(self): + self.assert_message_from_report( + ( + "Removing references:\n" + " Resource 'id1' from:\n" + " Group: 'id3'\n" + " Tag: 'id2'" + ), + reports.CibRemoveReferences( + {"id1": "primitive", "id2": "tag", "id3": "group"}, + {"id1": ["id2", "id3"]}, + ), + ) + + def test_multiple_elements_single_reference(self): + self.assert_message_from_report( + ( + "Removing references:\n" + " Resource 'id1' from:\n" + " Tag: 'id2'\n" + " Resource 'id3' from:\n" + " Tag: 'id4'" + ), + reports.CibRemoveReferences( + { + "id1": "primitive", + "id2": "tag", + "id3": "primitive", + "id4": "tag", + }, + {"id1": ["id2"], "id3": ["id4"]}, + ), + ) + + class UseCommandNodeAddRemote(NameBuildTest): def test_build_messages(self): self.assert_message_from_report( @@ -3807,6 +3878,14 @@ def test_build_messages(self): ) +class UseCommandNodeRemoveRemote(NameBuildTest): + def test_build_messages(self): + self.assert_message_from_report( + "this command is not sufficient for removing a remote node", + reports.UseCommandNodeRemoveRemote(), + ) + + class UseCommandNodeRemoveGuest(NameBuildTest): def test_build_messages(self): self.assert_message_from_report( @@ -5882,3 +5961,35 @@ def test_message(self) -> str: "resourceId", "parentId" ), ) + + +class StoppingResourcesBeforeDeleting(NameBuildTest): + def test_one_resource(self) -> str: + self.assert_message_from_report( + "Stopping resource 'resourceId' before deleting", + reports.StoppingResourcesBeforeDeleting(["resourceId"]), + ) + + def test_multiple_resources(self) -> str: + self.assert_message_from_report( + "Stopping resources 'resourceId1', 'resourceId2' before deleting", + reports.StoppingResourcesBeforeDeleting( + ["resourceId1", "resourceId2"] + ), + ) + + +class CannotStopResourcesBeforeDeleting(NameBuildTest): + def test_one_resource(self) -> str: + self.assert_message_from_report( + "Cannot stop resource 'resourceId' before deleting", + reports.CannotStopResourcesBeforeDeleting(["resourceId"]), + ) + + def test_multiple_resources(self) -> str: + self.assert_message_from_report( + "Cannot stop resources 'resourceId1', 'resourceId2' before deleting", + reports.CannotStopResourcesBeforeDeleting( + ["resourceId1", "resourceId2"] + ), + ) diff --git a/pcs_test/tier0/lib/cib/test_fencing_topology.py b/pcs_test/tier0/lib/cib/test_fencing_topology.py index 7c16e15dd..9d8f29e80 100644 --- a/pcs_test/tier0/lib/cib/test_fencing_topology.py +++ b/pcs_test/tier0/lib/cib/test_fencing_topology.py @@ -16,6 +16,7 @@ from pcs.common.reports import codes as report_codes from pcs.common.reports.item import ReportItem from pcs.lib.cib import fencing_topology as lib +from pcs.lib.cib.tools import ElementNotFound from pcs.lib.errors import LibraryError from pcs.lib.pacemaker.state import ClusterState @@ -36,6 +37,17 @@ # pylint: disable=protected-access +FIXTURE_NON_UNIQUE_DEVICES = """ + + + + +""" + class CibMixin: @staticmethod @@ -533,6 +545,100 @@ def test_no_such_level_ignore_missing(self): ) +class RemoveDeviceFromAllLevelsDontRemoveElement(TestCase, CibMixin): + def setUp(self): + self.cib = self.get_cib() + self.tree = self.cib.find("configuration/fencing-topology") + + def test_success(self): + elements = lib.remove_device_from_all_levels_dont_remove_elements( + self.tree, "d3" + ) + assert_xml_equal( + """ + + + + + + + + + + + + + """, + etree_to_str(self.tree), + ) + self.assertEqual( + [ + self.tree.find(".//fencing-level[@id='fl2']"), + self.tree.find(".//fencing-level[@id='fl4']"), + self.tree.find(".//fencing-level[@id='fl5']"), + self.tree.find(".//fencing-level[@id='fl7']"), + ], + elements, + ) + + def test_non_unique_device_ids(self): + tree = etree.fromstring(FIXTURE_NON_UNIQUE_DEVICES) + elements = lib.remove_device_from_all_levels_dont_remove_elements( + tree, "d1" + ) + assert_xml_equal( + """ + + + + + """, + etree_to_str(tree), + ) + self.assertEqual( + [ + tree.find(".//fencing-level[@id='fl1']"), + tree.find(".//fencing-level[@id='fl2']"), + ], + elements, + ) + + def test_no_such_device(self): + original_xml = etree_to_str(self.tree) + elements = lib.remove_device_from_all_levels_dont_remove_elements( + self.tree, "dX" + ) + assert_xml_equal(original_xml, etree_to_str(self.tree)) + self.assertEqual([], elements) + + class RemoveDeviceFromAllLevels(TestCase, CibMixin): def setUp(self): self.cib = self.get_cib() @@ -575,12 +681,120 @@ def test_success(self): etree_to_str(self.tree), ) + def test_non_unique_device_ids(self): + # pylint: disable=no-self-use + tree = etree.fromstring(FIXTURE_NON_UNIQUE_DEVICES) + lib.remove_device_from_all_levels(tree, "d1") + assert_xml_equal( + """ + + + + """, + etree_to_str(tree), + ) + def test_no_such_device(self): original_xml = etree_to_str(self.tree) lib.remove_device_from_all_levels(self.tree, "dX") assert_xml_equal(original_xml, etree_to_str(self.tree)) +class RemoveDeviceFromOneLevel(TestCase, CibMixin): + def setUp(self): + self.cib = self.get_cib() + self.tree = self.cib.find("configuration/fencing-topology") + + def test_keep_fencing_level(self): + lib.remove_device_from_one_level(self.tree, "fl1", "d1") + assert_xml_equal( + """ + + + + + + + + + + + + + """, + etree_to_str(self.tree), + ) + + def test_remove_fencing_level(self): + lib.remove_device_from_one_level(self.tree, "fl2", "d3") + assert_xml_equal( + """ + + + + + + + + + + + + """, + etree_to_str(self.tree), + ) + + def test_nonexistent_level(self): + with self.assertRaises(ElementNotFound): + lib.remove_device_from_one_level(self.tree, "nonexistent", "d1") + + class Export(TestCase, CibMixin): def setUp(self): self.cib = self.get_cib() diff --git a/pcs_test/tier0/lib/cib/test_remove_elements.py b/pcs_test/tier0/lib/cib/test_remove_elements.py new file mode 100644 index 000000000..212e96e00 --- /dev/null +++ b/pcs_test/tier0/lib/cib/test_remove_elements.py @@ -0,0 +1,1674 @@ +# pylint: disable=too-many-lines +from unittest import ( + TestCase, + mock, +) + +from lxml import etree + +from pcs.common import reports +from pcs.lib.cib import const +from pcs.lib.cib import remove_elements as lib + +from pcs_test.tools import fixture +from pcs_test.tools.assertions import ( + assert_report_item_list_equal, + assert_xml_equal, +) +from pcs_test.tools.fixture_cib import modify_cib +from pcs_test.tools.fixture_crm_mon import complete_state +from pcs_test.tools.misc import read_test_resource +from pcs_test.tools.xml import etree_to_str + + +def _constraints(*argv): + return f"{''.join(argv)}" + + +FIXTURE_LOC_CONSTRAINT_WITH_1_RULE = """ + + + + + + +""" + +FIXTURE_LOC_CONSTRAINT_WITH_2_RULES = """ + + + + + + + + +""" + +FIXTURE_TWO_LOC_CONSTRAINTS_WITH_RULES = _constraints( + FIXTURE_LOC_CONSTRAINT_WITH_1_RULE, + FIXTURE_LOC_CONSTRAINT_WITH_2_RULES, +) + +EXPECTED_TYPES_FOR_REMOVE = ["constraint", "location rule", "resource"] + + +class GetCibMixin: + def get_cib(self, **modifier_shortcuts): + return etree.fromstring(modify_cib(self.cib, **modifier_shortcuts)) + + +class ElementsToRemoveFindElements(TestCase, GetCibMixin): + # pylint: disable=too-many-public-methods + def setUp(self): + self.cib = read_test_resource("cib-empty.xml") + + def assert_elements_to_remove( + self, + elements_to_remove: lib.ElementsToRemove, + ids_to_remove: set[str], + resources_to_disable: set[str] = set(), + dependant_elements: lib.DependantElements = lib.DependantElements({}), + element_references: lib.ElementReferences = lib.ElementReferences( + {}, {} + ), + missing_ids: set[str] = set(), + unsupported_elements: lib.UnsupportedElements = lib.UnsupportedElements( + {}, EXPECTED_TYPES_FOR_REMOVE + ), + ): + # pylint: disable=dangerous-default-value + self.assertEqual(elements_to_remove.ids_to_remove, ids_to_remove) + self.assertEqual( + elements_to_remove.resources_to_disable, resources_to_disable + ) + self.assertEqual( + elements_to_remove.dependant_elements, dependant_elements + ) + self.assertEqual( + elements_to_remove.element_references, element_references + ) + self.assertEqual(elements_to_remove.missing_ids, missing_ids) + self.assertEqual( + elements_to_remove.unsupported_elements, unsupported_elements + ) + + def test_location_constraint(self): + cib = self.get_cib( + constraints=""" + + + + + """ + ) + + elements_to_remove = lib.ElementsToRemove(cib, ["l1"]) + self.assert_elements_to_remove(elements_to_remove, {"l1"}) + + def test_order_constraint(self): + cib = self.get_cib( + constraints=""" + + + + + """ + ) + elements_to_remove = lib.ElementsToRemove(cib, ["o2"]) + self.assert_elements_to_remove(elements_to_remove, {"o2"}) + + def test_colocation_constraints(self): + cib = self.get_cib( + constraints=""" + + + + + """ + ) + elements_to_remove = lib.ElementsToRemove(cib, ["c1", "c2"]) + self.assert_elements_to_remove(elements_to_remove, {"c1", "c2"}) + + def test_ticket_constraints(self): + cib = self.get_cib( + constraints=""" + + + + + """, + ) + elements_to_remove = lib.ElementsToRemove(cib, ["t1", "t2"]) + self.assert_elements_to_remove(elements_to_remove, {"t1", "t2"}) + + def test_location_constraint_with_one_rule_by_id(self): + cib = self.get_cib(constraints=FIXTURE_TWO_LOC_CONSTRAINTS_WITH_RULES) + elements_to_remove = lib.ElementsToRemove(cib, ["lr1"]) + self.assert_elements_to_remove(elements_to_remove, {"lr1"}) + + def test_location_constraint_with_more_rules_by_id(self): + cib = self.get_cib(constraints=FIXTURE_TWO_LOC_CONSTRAINTS_WITH_RULES) + elements_to_remove = lib.ElementsToRemove(cib, ["lr2"]) + self.assert_elements_to_remove(elements_to_remove, {"lr2"}) + + def test_one_rule_from_location_constraint_with_one_rule(self): + cib = self.get_cib(constraints=FIXTURE_TWO_LOC_CONSTRAINTS_WITH_RULES) + elements_to_remove = lib.ElementsToRemove(cib, ["r1"]) + self.assert_elements_to_remove( + elements_to_remove, + {"lr1", "r1"}, + dependant_elements=lib.DependantElements( + {"lr1": const.TAG_CONSTRAINT_LOCATION} + ), + ) + + def test_one_rule_from_location_constraint_with_two_rules(self): + cib = self.get_cib(constraints=FIXTURE_TWO_LOC_CONSTRAINTS_WITH_RULES) + elements_to_remove = lib.ElementsToRemove(cib, ["r2"]) + self.assert_elements_to_remove( + elements_to_remove, + {"r2"}, + element_references=lib.ElementReferences( + {"r2": {"lr2"}}, + { + "lr2": const.TAG_CONSTRAINT_LOCATION, + "r2": const.TAG_RULE, + }, + ), + ) + + def test_more_location_rules(self): + cib = self.get_cib(constraints=FIXTURE_TWO_LOC_CONSTRAINTS_WITH_RULES) + elements_to_remove = lib.ElementsToRemove(cib, ["r1", "r2", "r3"]) + self.assert_elements_to_remove( + elements_to_remove, + {"r1", "r2", "r3", "lr1", "lr2"}, + dependant_elements=lib.DependantElements( + { + "lr1": const.TAG_CONSTRAINT_LOCATION, + "lr2": const.TAG_CONSTRAINT_LOCATION, + } + ), + ) + + def test_resource_primitive(self): + cib = self.get_cib( + resources=""" + + + + + """ + ) + elements_to_remove = lib.ElementsToRemove(cib, ["A"]) + self.assert_elements_to_remove( + elements_to_remove, + {"A"}, + resources_to_disable={"A"}, + ) + + def test_resource_primitive_in_tag(self): + cib = self.get_cib( + resources=""" + + + + + """, + tags=""" + + + + + + + + + + """, + ) + elements_to_remove = lib.ElementsToRemove(cib, ["A"]) + self.assert_elements_to_remove( + elements_to_remove, + {"A", "T2"}, + resources_to_disable={"A"}, + dependant_elements=lib.DependantElements({"T2": const.TAG_TAG}), + element_references=lib.ElementReferences( + {"A": {"T1"}}, + {"A": const.TAG_RESOURCE_PRIMITIVE, "T1": const.TAG_TAG}, + ), + ) + + def test_resource_primitive_constraints(self): + cib = self.get_cib( + resources=""" + + + + + """, + constraints=""" + + + + + + + + """, + ) + + elements_to_remove = lib.ElementsToRemove(cib, ["A"]) + self.assert_elements_to_remove( + elements_to_remove, + {"A", "l1", "o1", "c1", "t1"}, + resources_to_disable={"A"}, + dependant_elements=lib.DependantElements( + { + "l1": const.TAG_CONSTRAINT_LOCATION, + "o1": const.TAG_CONSTRAINT_ORDER, + "c1": const.TAG_CONSTRAINT_COLOCATION, + "t1": const.TAG_CONSTRAINT_TICKET, + } + ), + ) + + def test_resource_group(self): + cib = self.get_cib( + resources=""" + + + + + + + """ + ) + elements_to_remove = lib.ElementsToRemove(cib, ["G"]) + + self.assert_elements_to_remove( + elements_to_remove, + {"G", "A", "B"}, + resources_to_disable={"G", "A", "B"}, + dependant_elements=lib.DependantElements( + { + "A": const.TAG_RESOURCE_PRIMITIVE, + "B": const.TAG_RESOURCE_PRIMITIVE, + } + ), + ) + + def test_resource_group_member(self): + cib = self.get_cib( + resources=""" + + + + + + + """ + ) + elements_to_remove = lib.ElementsToRemove(cib, ["A"]) + + self.assert_elements_to_remove( + elements_to_remove, + {"A"}, + resources_to_disable={"A"}, + element_references=lib.ElementReferences( + {"A": {"G"}}, + { + "A": const.TAG_RESOURCE_PRIMITIVE, + "G": const.TAG_RESOURCE_GROUP, + }, + ), + ) + + def test_resource_group_all_members(self): + cib = self.get_cib( + resources=""" + + + + + + + """ + ) + elements_to_remove = lib.ElementsToRemove(cib, ["A", "B"]) + + self.assert_elements_to_remove( + elements_to_remove, + {"A", "B", "G"}, + resources_to_disable={"A", "B", "G"}, + dependant_elements=lib.DependantElements( + {"G": const.TAG_RESOURCE_GROUP} + ), + ) + + def test_resource_group_constraints(self): + cib = self.get_cib( + resources=""" + + + + + + + """, + constraints=""" + + + + + + + + """, + ) + elements_to_remove = lib.ElementsToRemove(cib, ["G"]) + + self.assert_elements_to_remove( + elements_to_remove, + {"G", "A", "B", "l1", "o1", "c1", "t1"}, + resources_to_disable={"G", "A", "B"}, + dependant_elements=lib.DependantElements( + { + "A": const.TAG_RESOURCE_PRIMITIVE, + "B": const.TAG_RESOURCE_PRIMITIVE, + "l1": const.TAG_CONSTRAINT_LOCATION, + "o1": const.TAG_CONSTRAINT_ORDER, + "c1": const.TAG_CONSTRAINT_COLOCATION, + "t1": const.TAG_CONSTRAINT_TICKET, + } + ), + ) + + def test_group_in_tag(self): + cib = self.get_cib( + resources=""" + + + + + + + """, + tags=""" + + + + + + + """, + ) + elements_to_remove = lib.ElementsToRemove(cib, ["G"]) + self.assert_elements_to_remove( + elements_to_remove, + {"G", "A", "B", "T"}, + resources_to_disable={"G", "A", "B"}, + dependant_elements=lib.DependantElements( + { + "A": const.TAG_RESOURCE_PRIMITIVE, + "B": const.TAG_RESOURCE_PRIMITIVE, + "T": const.TAG_TAG, + } + ), + ) + + def test_resource_clone(self): + cib = self.get_cib( + resources=""" + + + + + + """ + ) + elements_to_remove = lib.ElementsToRemove(cib, ["C"]) + self.assert_elements_to_remove( + elements_to_remove, + {"C", "A"}, + resources_to_disable={"C", "A"}, + dependant_elements=lib.DependantElements( + {"A": const.TAG_RESOURCE_PRIMITIVE} + ), + ) + + def test_resource_clone_primitive(self): + cib = self.get_cib( + resources=""" + + + + + + """ + ) + elements_to_remove = lib.ElementsToRemove(cib, ["A"]) + + self.assert_elements_to_remove( + elements_to_remove, + {"C", "A"}, + resources_to_disable={"C", "A"}, + dependant_elements=lib.DependantElements( + {"C": const.TAG_RESOURCE_CLONE} + ), + ) + + def test_resource_clone_constraints(self): + cib = self.get_cib( + resources=""" + + + + + + """, + constraints=""" + + + + + + """, + ) + elements_to_remove = lib.ElementsToRemove(cib, ["C"]) + self.assert_elements_to_remove( + elements_to_remove, + {"C", "A", "l1", "l2"}, + resources_to_disable={"C", "A"}, + dependant_elements=lib.DependantElements( + { + "A": const.TAG_RESOURCE_PRIMITIVE, + "l1": const.TAG_CONSTRAINT_LOCATION, + "l2": const.TAG_CONSTRAINT_LOCATION, + } + ), + ) + + def test_resource_clone_in_tag(self): + cib = self.get_cib( + resources=""" + + + + + + """, + tags=""" + + + + + + + """, + ) + elements_to_remove = lib.ElementsToRemove(cib, ["C"]) + self.assert_elements_to_remove( + elements_to_remove, + {"C", "A", "T"}, + resources_to_disable={"C", "A"}, + dependant_elements=lib.DependantElements( + {"A": const.TAG_RESOURCE_PRIMITIVE, "T": const.TAG_TAG} + ), + ) + + def test_resource_bundle(self): + cib = self.get_cib( + resources=""" + + + + """ + ) + elements_to_remove = lib.ElementsToRemove(cib, ["B"]) + self.assert_elements_to_remove( + elements_to_remove, {"B"}, resources_to_disable={"B"} + ) + + def test_resource_bundle_with_primitive(self): + cib = self.get_cib( + resources=""" + + + + + + """ + ) + elements_to_remove = lib.ElementsToRemove(cib, ["B"]) + self.assert_elements_to_remove( + elements_to_remove, + {"B", "A"}, + resources_to_disable={"B", "A"}, + dependant_elements=lib.DependantElements( + {"A": const.TAG_RESOURCE_PRIMITIVE} + ), + ) + + def test_resource_bundle_primitive(self): + cib = self.get_cib( + resources=""" + + + + + + """ + ) + elements_to_remove = lib.ElementsToRemove(cib, ["A"]) + self.assert_elements_to_remove( + elements_to_remove, + {"A"}, + resources_to_disable={"A"}, + element_references=lib.ElementReferences( + {"A": {"B"}}, + { + "A": const.TAG_RESOURCE_PRIMITIVE, + "B": const.TAG_RESOURCE_BUNDLE, + }, + ), + ) + + def test_resource_referenced_in_acl(self): + cib = self.get_cib( + resources=""" + + + + """, + optional_in_conf=""" + + + + + + + + + + """, + ) + elements_to_remove = lib.ElementsToRemove(cib, ["vohrablo"]) + self.assert_elements_to_remove( + elements_to_remove, + {"vohrablo", "ucesat_se2"}, + resources_to_disable={"vohrablo"}, + dependant_elements=lib.DependantElements( + {"ucesat_se2": const.TAG_ACL_PERMISSION} + ), + element_references=lib.ElementReferences( + {"ucesat_se2": {"zenich"}}, + { + "ucesat_se2": const.TAG_ACL_PERMISSION, + "zenich": const.TAG_ACL_ROLE, + }, + ), + ) + + def test_resource_referenced_in_acl_indirectly(self): + cib = self.get_cib( + resources=""" + + + + + + + """, + optional_in_conf=""" + + + + + + + + + """, + ) + elements_to_remove = lib.ElementsToRemove(cib, ["G"]) + self.assert_elements_to_remove( + elements_to_remove, + {"G", "A", "B", "PERMISSION"}, + resources_to_disable={"G", "A", "B"}, + dependant_elements=lib.DependantElements( + { + "A": const.TAG_RESOURCE_PRIMITIVE, + "B": const.TAG_RESOURCE_PRIMITIVE, + "PERMISSION": const.TAG_ACL_PERMISSION, + } + ), + element_references=lib.ElementReferences( + {"PERMISSION": {"ROLE"}}, + { + "PERMISSION": const.TAG_ACL_PERMISSION, + "ROLE": const.TAG_ACL_ROLE, + }, + ), + ) + + def test_resource_remove_fencing_level(self): + cib = self.get_cib( + resources=""" + + + + + """, + fencing_topology=""" + + + + """, + ) + elements_to_remove = lib.ElementsToRemove(cib, ["A"]) + self.assert_elements_to_remove( + elements_to_remove, + {"A"}, + resources_to_disable={"A"}, + element_references=lib.ElementReferences( + {"A": {"fl"}}, + { + "A": const.TAG_RESOURCE_PRIMITIVE, + "fl": const.TAG_FENCING_LEVEL, + }, + ), + ) + + def test_resource_keep_fencing_level(self): + cib = self.get_cib( + resources=""" + + + + """, + fencing_topology=""" + + + + """, + ) + elements_to_remove = lib.ElementsToRemove(cib, ["A"]) + self.assert_elements_to_remove( + elements_to_remove, + {"A", "fl-NODE-A-1"}, + resources_to_disable={"A"}, + dependant_elements=lib.DependantElements( + {"fl-NODE-A-1": const.TAG_FENCING_LEVEL} + ), + ) + + def test_resource_in_constraint_set(self): + cib = self.get_cib( + resources=""" + + + + + """, + constraints=""" + + + + + + + + + """, + ) + elements_to_remove = lib.ElementsToRemove(cib, ["A"]) + self.assert_elements_to_remove( + elements_to_remove, + {"A"}, + resources_to_disable={"A"}, + element_references=lib.ElementReferences( + {"A": {"set1"}}, + { + "A": const.TAG_RESOURCE_PRIMITIVE, + "set1": const.TAG_RESOURCE_SET, + }, + ), + ) + + def test_resource_in_constraint_set_remove_set_keep_constraint(self): + cib = self.get_cib( + resources=""" + + + + + """, + constraints=""" + + + + + + + + + + + """, + ) + elements_to_remove = lib.ElementsToRemove(cib, ["A"]) + self.assert_elements_to_remove( + elements_to_remove, + {"A", "set1"}, + resources_to_disable={"A"}, + dependant_elements=lib.DependantElements( + {"set1": const.TAG_RESOURCE_SET} + ), + element_references=lib.ElementReferences( + {"set1": {"c1"}}, + { + "set1": const.TAG_RESOURCE_SET, + "c1": const.TAG_CONSTRAINT_COLOCATION, + }, + ), + ) + + def test_resource_in_constraint_set_remove_set_remove_constraint(self): + cib = self.get_cib( + resources=""" + + + + """, + constraints=""" + + + + + + + + """, + ) + elements_to_remove = lib.ElementsToRemove(cib, ["A"]) + self.assert_elements_to_remove( + elements_to_remove, + {"A", "set1", "c1"}, + resources_to_disable={"A"}, + dependant_elements=lib.DependantElements( + { + "set1": const.TAG_RESOURCE_SET, + "c1": const.TAG_CONSTRAINT_COLOCATION, + } + ), + ) + + def test_resource_in_multiple_sets(self): + cib = self.get_cib( + resources=""" + + + + """, + constraints=""" + + + + + + + + + + + + + """, + ) + elements_to_remove = lib.ElementsToRemove(cib, ["A"]) + self.assert_elements_to_remove( + elements_to_remove, + {"A", "set1", "c1", "set2", "c2"}, + resources_to_disable={"A"}, + dependant_elements=lib.DependantElements( + { + "set1": const.TAG_RESOURCE_SET, + "c1": const.TAG_CONSTRAINT_COLOCATION, + "set2": const.TAG_RESOURCE_SET, + "c2": const.TAG_CONSTRAINT_COLOCATION, + } + ), + ) + + def test_resource_legacy_promotable_clone(self): + cib = self.get_cib( + resources=""" + + + + + + """, + ) + elements_to_remove = lib.ElementsToRemove(cib, ["MS"]) + self.assert_elements_to_remove( + elements_to_remove, + {"MS", "A"}, + resources_to_disable={"MS", "A"}, + dependant_elements=lib.DependantElements( + {"A": const.TAG_RESOURCE_PRIMITIVE} + ), + ) + + def test_resource_legacy_promotable_clone_inner_element(self): + cib = self.get_cib( + resources=""" + + + + + + """, + ) + elements_to_remove = lib.ElementsToRemove(cib, ["A"]) + self.assert_elements_to_remove( + elements_to_remove, + {"A", "MS"}, + resources_to_disable={"A", "MS"}, + dependant_elements=lib.DependantElements({"MS": "master"}), + ) + + def test_missing_elements(self): + cib = self.get_cib( + resources=""" + + + + """ + ) + elements_to_remove = lib.ElementsToRemove(cib, ["A", "B", "C", "D"]) + self.assert_elements_to_remove( + elements_to_remove, + {"A"}, + missing_ids={"B", "C", "D"}, + resources_to_disable={"A"}, + ) + + def test_unsupported_id_types(self): + cib = self.get_cib( + tags=""" + + + + + + """, + fencing_topology=""" + + + + """, + optional_in_conf=""" + + + + + + """, + ) + elements_to_remove = lib.ElementsToRemove( + cib, ["T", "fl", "role", "role-read"] + ) + self.assert_elements_to_remove( + elements_to_remove, + set(), + unsupported_elements=lib.UnsupportedElements( + { + "T": const.TAG_TAG, + "fl": const.TAG_FENCING_LEVEL, + "role": const.TAG_ACL_ROLE, + "role-read": const.TAG_ACL_PERMISSION, + }, + EXPECTED_TYPES_FOR_REMOVE, + ), + ) + + +class RemoveSpecifiedElements(TestCase, GetCibMixin): + def setUp(self): + self.elements_to_remove_mock = mock.Mock() + self.elements_to_remove_mock.ids_to_remove = set() + self.elements_to_remove_mock.element_references = lib.ElementReferences( + {}, {} + ) + self.cib = read_test_resource("cib-empty.xml") + + def test_remove_nothing_to_remove(self): + cib = self.get_cib( + resources=""" + + + + + + + + """, + constraints=""" + + + + + """, + tags=""" + + + + + """, + ) + + initial_cib = etree_to_str(cib) + + lib.remove_specified_elements(cib, self.elements_to_remove_mock) + + assert_xml_equal(initial_cib, etree_to_str(cib)) + + def test_remove_only_by_id(self): + self.elements_to_remove_mock.ids_to_remove = {"A", "B", "C", "D"} + + cib = self.get_cib( + resources=""" + + + + + + + + """, + constraints=""" + + + + + """, + tags=""" + + + + + """, + ) + + lib.remove_specified_elements(cib, self.elements_to_remove_mock) + + assert_xml_equal( + modify_cib( + self.cib, + resources=""" + + + + """, + constraints=""" + + + + """, + tags=""" + + + + """, + ), + etree_to_str(cib), + ) + + def test_remove_reference_from_tag(self): + self.elements_to_remove_mock.element_references = lib.ElementReferences( + {"A": {"T1"}}, + {"A": const.TAG_RESOURCE_PRIMITIVE, "T1": const.TAG_TAG}, + ) + + cib = self.get_cib( + tags=""" + + + + + + + + + + + """, + ) + + lib.remove_specified_elements(cib, self.elements_to_remove_mock) + + assert_xml_equal( + modify_cib( + self.cib, + tags=""" + + + + + + + + + + """, + ), + etree_to_str(cib), + ) + + def test_remove_reference_from_resource_set(self): + self.elements_to_remove_mock.element_references = lib.ElementReferences( + {"A": {"set1"}}, + { + "A": const.TAG_RESOURCE_PRIMITIVE, + "set1": const.TAG_RESOURCE_SET, + }, + ) + + cib = self.get_cib( + constraints=""" + + + + + + + + + + + + + + + """, + ) + + lib.remove_specified_elements(cib, self.elements_to_remove_mock) + + assert_xml_equal( + modify_cib( + self.cib, + constraints=""" + + + + + + + + + + + + + + """, + ), + etree_to_str(cib), + ) + + def test_remove_references_from_fencing_level(self): + self.elements_to_remove_mock.element_references = lib.ElementReferences( + {"A": {"FL1"}}, + {"A": const.TAG_RESOURCE_PRIMITIVE, "FL1": const.TAG_FENCING_LEVEL}, + ) + + cib = self.get_cib( + fencing_topology=""" + + + + + """, + ) + + lib.remove_specified_elements(cib, self.elements_to_remove_mock) + + assert_xml_equal( + modify_cib( + self.cib, + fencing_topology=""" + + + + + """, + ), + etree_to_str(cib), + ) + + def test_remove_references_from_ignored_type(self): + self.elements_to_remove_mock.element_references = lib.ElementReferences( + {"A": {"G"}}, + {"A": const.TAG_RESOURCE_PRIMITIVE, "G": const.TAG_RESOURCE_GROUP}, + ) + + cib = self.get_cib( + resources=""" + + + + + + """, + ) + + initial_cib = etree_to_str(cib) + + lib.remove_specified_elements(cib, self.elements_to_remove_mock) + + assert_xml_equal(initial_cib, etree_to_str(cib)) + + +class StopResources(TestCase, GetCibMixin): + def setUp(self): + self.elements_to_remove_mock = mock.Mock() + self.cib = read_test_resource("cib-empty.xml") + self.state = read_test_resource("crm_mon.minimal.xml") + + def test_nothing_to_stop(self): + cib = self.get_cib( + resources=""" + + + + """ + ) + state = complete_state(self.state) + initial_cib = etree_to_str(cib) + + self.elements_to_remove_mock.resources_to_disable = [] + lib.stop_resources(cib, state, self.elements_to_remove_mock) + + assert_xml_equal(initial_cib, etree_to_str(cib)) + + def test_one_resource(self): + cib = self.get_cib( + resources=""" + + + + + """ + ) + state = complete_state( + self.state, + resources_xml=""" + + + + """, + ) + + self.elements_to_remove_mock.resources_to_disable = ["A"] + report_list = lib.stop_resources( + cib, state, self.elements_to_remove_mock + ) + + assert_xml_equal( + modify_cib( + self.cib, + resources=""" + + + + + + + + + """, + ), + etree_to_str(cib), + ) + assert_report_item_list_equal(report_list, []) + + def test_unmanaged_resource(self): + cib = self.get_cib( + resources=""" + + + + + """ + ) + state = complete_state( + self.state, + resources_xml=""" + + + + + """, + ) + + self.elements_to_remove_mock.resources_to_disable = ["A", "B"] + report_list = lib.stop_resources( + cib, state, self.elements_to_remove_mock + ) + + assert_xml_equal( + modify_cib( + self.cib, + resources=""" + + + + + + + + + + + + + """, + ), + etree_to_str(cib), + ) + assert_report_item_list_equal( + report_list, + [ + fixture.warn( + reports.codes.RESOURCE_IS_UNMANAGED, resource_id="A" + ) + ], + ) + + def test_stop_inner_elements(self): + cib = self.get_cib( + resources=""" + + + + + + + + """ + ) + state = complete_state( + self.state, + resources_xml=""" + + + + + + + + + + + """, + ) + + self.elements_to_remove_mock.resources_to_disable = ["A", "G", "C"] + report_list = lib.stop_resources( + cib, state, self.elements_to_remove_mock + ) + assert_xml_equal( + modify_cib( + self.cib, + resources=""" + + + + + + + + + + + + + + + + + + """, + ), + etree_to_str(cib), + ) + + assert_report_item_list_equal(report_list, []) + + +class EnsureStoppedAfterDisable(TestCase): + def setUp(self): + self.state_xml = read_test_resource("crm_mon.minimal.xml") + self.elements_to_remove_mock = mock.Mock() + + def test_ok(self): + state = complete_state( + self.state_xml, + """ + + + + + + + + + + """, + ) + + self.elements_to_remove_mock.resources_to_disable = ["A", "B", "C"] + + report_list = lib.ensure_resources_stopped( + state, self.elements_to_remove_mock + ) + self.assertEqual(report_list, []) + + def test_some_not_stopped(self): + state = complete_state( + self.state_xml, + """ + + + + + + + + + + """, + ) + + self.elements_to_remove_mock.resources_to_disable = ["A", "B", "C"] + + report_list = lib.ensure_resources_stopped( + state, self.elements_to_remove_mock + ) + self.assertEqual( + report_list, + [ + reports.ReportItem.error( + reports.messages.CannotStopResourcesBeforeDeleting(["B"]), + force_code=reports.codes.FORCE, + ) + ], + ) + + def test_multiinstance_some_not_stopped_clone_id(self): + state = complete_state( + self.state_xml, + """ + + + + + + + """, + ) + + self.elements_to_remove_mock.resources_to_disable = ["C"] + + report_list = lib.ensure_resources_stopped( + state, self.elements_to_remove_mock + ) + self.assertEqual( + report_list, + [ + reports.ReportItem.error( + reports.messages.CannotStopResourcesBeforeDeleting(["C"]), + force_code=reports.codes.FORCE, + ) + ], + ) + + def test_multiinstance_some_not_stopped_primitive_id(self): + state = complete_state( + self.state_xml, + """ + + + + + + + """, + ) + + self.elements_to_remove_mock.resources_to_disable = ["A"] + + report_list = lib.ensure_resources_stopped( + state, self.elements_to_remove_mock + ) + self.assertEqual( + report_list, + [ + reports.ReportItem.error( + reports.messages.CannotStopResourcesBeforeDeleting(["A"]), + force_code=reports.codes.FORCE, + ) + ], + ) + + def test_works_with_clones_and_bundle_in_status(self): + state = complete_state( + self.state_xml, + """ + + + + + + + + + + + + + + """, + ) + + self.elements_to_remove_mock.resources_to_disable = ["C"] + + report_list = lib.ensure_resources_stopped( + state, self.elements_to_remove_mock + ) + self.assertEqual(report_list, []) + + +class DependantElementsToReports(TestCase): + # pylint: disable=no-self-use + def test_no_reports(self): + elements = lib.DependantElements({}) + assert_report_item_list_equal(elements.to_reports(), []) + + def test_reports(self): + elements = lib.DependantElements( + {"A": const.TAG_RESOURCE_PRIMITIVE, "B": const.TAG_TAG} + ) + assert_report_item_list_equal( + elements.to_reports(), + [ + fixture.info( + reports.codes.CIB_REMOVE_DEPENDANT_ELEMENTS, + id_tag_map={ + "A": const.TAG_RESOURCE_PRIMITIVE, + "B": const.TAG_TAG, + }, + ) + ], + ) + + +class ElementReferencesToReports(TestCase): + # pylint: disable=no-self-use + def test_no_reports(self): + elements = lib.ElementReferences({}, {}) + assert_report_item_list_equal(elements.to_reports(), []) + + def test_reports(self): + elements = lib.ElementReferences( + {"A": {"B"}, "C": {"B"}}, + { + "A": const.TAG_RESOURCE_PRIMITIVE, + "B": const.TAG_TAG, + "C": const.TAG_RESOURCE_PRIMITIVE, + }, + ) + assert_report_item_list_equal( + elements.to_reports(), + [ + fixture.info( + reports.codes.CIB_REMOVE_REFERENCES, + removing_references_from={"A": {"B"}, "C": {"B"}}, + id_tag_map={ + "A": const.TAG_RESOURCE_PRIMITIVE, + "B": const.TAG_TAG, + "C": const.TAG_RESOURCE_PRIMITIVE, + }, + ) + ], + ) + + +class GetInnerReferences(TestCase): + # pylint: disable=protected-access + def test_no_inner_references(self): + self.assertEqual( + [], lib._get_inner_references(etree.fromstring("")) + ) + + def test_not_supported_inner_references(self): + element = etree.fromstring( + """ + + + + + + + + + """ + ) + self.assertEqual([], lib._get_inner_references(element)) + + def test_resource_primitive(self): + primitive = etree.fromstring('') + self.assertEqual([], lib._get_inner_references(primitive)) + + def test_resource_group(self): + element = etree.fromstring('') + child1 = etree.SubElement(element, "primitive", id="A") + child2 = etree.SubElement(element, "primitive", id="B") + + self.assertEqual([child1, child2], lib._get_inner_references(element)) + + def test_resource_clone(self): + element = etree.fromstring('') + child = etree.SubElement(element, "primitive", id="A") + + self.assertEqual([child], lib._get_inner_references(element)) + + def test_resource_bundle(self): + element = etree.fromstring('') + child = etree.SubElement(element, "primitive", id="A") + + self.assertEqual([child], lib._get_inner_references(element)) + + def test_resource_bundle_no_primitive(self): + element = etree.fromstring('') + + self.assertEqual([], lib._get_inner_references(element)) + + +class IsLastElement(TestCase): + # pylint: disable=protected-access + def test_last_element_true(self): + for element, tag in ( + (etree.fromstring(""), "a"), + (etree.fromstring(""), "a"), + ): + with self.subTest(element=element, tag=tag): + self.assertTrue(lib._is_last_element(element, tag)) + + def test_last_element_false(self): + for element, tag in ( + (etree.fromstring(""), "a"), + (etree.fromstring(" "), "a"), + (etree.fromstring(" "), "a"), + ): + with self.subTest(element=element, tag=tag): + self.assertFalse(lib._is_last_element(element, tag)) + + +class IsEmptyAfterInnerElRemoval(TestCase): + # pylint: disable=protected-access + def test_last_element_true(self): + for parent in ( + etree.fromstring(""), + etree.fromstring(""), + etree.fromstring(""), + etree.fromstring(""), + etree.fromstring(""), + etree.fromstring(""), + etree.fromstring(""), + ): + with self.subTest(parent=parent.tag): + self.assertTrue(lib._is_empty_after_inner_el_removal(parent)) + + def test_last_element_false(self): + for parent in ( + etree.fromstring(""), + etree.fromstring(""), + etree.fromstring(""), + etree.fromstring(""), + etree.fromstring(""), + etree.fromstring(""), + etree.fromstring(""), + etree.fromstring( + """ + + + + + """ + ), + etree.fromstring(""), + etree.fromstring( + "" + ), + etree.fromstring( + """ + + + " + + """ + ), + etree.fromstring(""), + etree.fromstring(""), + ): + with self.subTest(parent=parent): + self.assertFalse(lib._is_empty_after_inner_el_removal(parent)) diff --git a/pcs_test/tier0/lib/cib/test_tools.py b/pcs_test/tier0/lib/cib/test_tools.py index 2ca248935..52c623027 100644 --- a/pcs_test/tier0/lib/cib/test_tools.py +++ b/pcs_test/tier0/lib/cib/test_tools.py @@ -1,3 +1,4 @@ +# pylint: disable=too-many-lines from functools import partial from unittest import ( TestCase, @@ -89,6 +90,7 @@ """ + FIXTURE_REFERENCES_IN_TAGS = """ @@ -141,6 +143,29 @@ ] ) +FIXTURE_NODE_REFERENCES_IN_CONSTRAINTS = """ + + + + + +""" + +FIXTURE_NODE_REFERENCES_IN_FENCING_LEVELS = """ + + + + + +""" +FIXTURE_ALL_SECTION_WITH_NODE_REFERENCES = "".join( + [ + FIXTURE_NODE_REFERENCES_IN_CONSTRAINTS, + FIXTURE_NODE_REFERENCES_IN_FENCING_LEVELS, + FIXTURE_REFERENCES_IN_TAGS, + ] +) + class CibToolsTest(TestCase): def setUp(self): @@ -1069,79 +1094,6 @@ def _configuration_fixture(configuration_content): """ -class FindElementsWithoutIdReferencingId(TestCase): - # pylint: disable=protected-access - def test_constraint_set_reference(self): - cib = etree.fromstring( - _configuration_fixture(FIXTURE_REFERENCES_IN_CONSTRAINTS) - ) - self.assertEqual( - [ - cib.find( - "./configuration/constraints/rsc_location/resource_set/resource_ref[@id='A']" - ), - cib.find( - "./configuration/constraints/rsc_colocation/resource_set/resource_ref[@id='A']" - ), - ], - list(lib._find_elements_without_id_referencing_id(cib, "A")), - ) - - def test_tag_reference(self): - cib = etree.fromstring( - _configuration_fixture(FIXTURE_REFERENCES_IN_TAGS) - ) - self.assertEqual( - [ - cib.find("./configuration/tags/tag[@id='X']/obj_ref[@id='A']"), - cib.find("./configuration/tags/tag[@id='Y']/obj_ref[@id='A']"), - ], - list(lib._find_elements_without_id_referencing_id(cib, "A")), - ) - - def test_acl_reference(self): - cib = etree.fromstring( - _configuration_fixture(FIXTURE_REFERENCES_IN_ACLS) - ) - self.assertEqual( - [ - cib.find("./configuration/acls/acl_target/role[@id='A']"), - cib.find("./configuration/acls/acl_group/role[@id='A']"), - ], - list(lib._find_elements_without_id_referencing_id(cib, "A")), - ) - - def test_all_references_types(self): - cib = etree.fromstring( - _configuration_fixture(FIXTURE_ALL_SECTIONS_WITH_REFERENCES) - ) - self.assertEqual( - [ - cib.find( - "./configuration/constraints/rsc_location/resource_set/" - "resource_ref[@id='A']" - ), - cib.find( - "./configuration/constraints/rsc_colocation/resource_set/" - "resource_ref[@id='A']" - ), - cib.find("./configuration/tags/tag[@id='X']/obj_ref[@id='A']"), - cib.find("./configuration/tags/tag[@id='Y']/obj_ref[@id='A']"), - cib.find("./configuration/acls/acl_target/role[@id='A']"), - cib.find("./configuration/acls/acl_group/role[@id='A']"), - ], - list(lib._find_elements_without_id_referencing_id(cib, "A")), - ) - - def test_no_reference_to_id(self): - cib = etree.fromstring( - _configuration_fixture(FIXTURE_ALL_SECTIONS_WITH_REFERENCES) - ) - self.assertEqual( - [], list(lib._find_elements_without_id_referencing_id(cib, "N")) - ) - - class FindElementsReferencingId(TestCase): def test_constraint_reference(self): cib = etree.fromstring( @@ -1310,7 +1262,9 @@ def test_remove_element_with_references(self): - + + + @@ -1319,15 +1273,19 @@ def test_remove_element_with_references(self): + + + + diff --git a/pcs_test/tier0/lib/commands/test_cib.py b/pcs_test/tier0/lib/commands/test_cib.py index 9b5933d3c..7cee72fa6 100644 --- a/pcs_test/tier0/lib/commands/test_cib.py +++ b/pcs_test/tier0/lib/commands/test_cib.py @@ -1,12 +1,20 @@ -from unittest import TestCase - -from lxml import etree +from unittest import ( + TestCase, + mock, +) from pcs.common import reports from pcs.lib.commands import cib as lib from pcs_test.tools import fixture +from pcs_test.tools.assertions import assert_xml_equal from pcs_test.tools.command_env import get_env_tools +from pcs_test.tools.custom_mock import ( + TmpFileCall, + TmpFileMock, +) +from pcs_test.tools.fixture_cib import modify_cib +from pcs_test.tools.misc import read_test_resource def _constraints(*argv): @@ -42,56 +50,38 @@ def _constraints(*argv): FIXTURE_LOC_CONSTRAINT_WITH_2_RULES, ) -EXPECTED_TYPES_FOR_REMOVE = ["constraint", "location rule"] +EXPECTED_TYPES_FOR_REMOVE = ["constraint", "location rule", "resource"] class RemoveElements(TestCase): def setUp(self): self.env_assist, self.config = get_env_tools(self) - def test_ids_not_found(self): - self.config.runner.cib.load() - self.env_assist.assert_raise_library_error( - lambda: lib.remove_elements( - self.env_assist.get_env(), - ["missing-id1", "missing-id2"], - ) - ) - self.env_assist.assert_reports( - [ - fixture.report_not_found( - _id, expected_types=["configuration element"] - ) - for _id in ["missing-id1", "missing-id2"] - ] - ) - - def test_not_constraints_ids(self): + def test_ids_not_found_and_unsupported_types(self): self.config.runner.cib.load( - resources=""" - - - - - - - """, + tags=""" + + + + + + """ ) self.env_assist.assert_raise_library_error( lambda: lib.remove_elements( - self.env_assist.get_env(), ["A", "B", "C"] + self.env_assist.get_env(), ["A", "T", "C"] ) ) self.env_assist.assert_reports( [ - fixture.report_unexpected_element( - "A", "primitive", EXPECTED_TYPES_FOR_REMOVE + fixture.report_not_found( + "A", expected_types=["configuration element"] ), - fixture.report_unexpected_element( - "B", "clone", EXPECTED_TYPES_FOR_REMOVE + fixture.report_not_found( + "C", expected_types=["configuration element"] ), fixture.report_unexpected_element( - "C", "primitive", EXPECTED_TYPES_FOR_REMOVE + "T", "tag", EXPECTED_TYPES_FOR_REMOVE ), ] ) @@ -114,252 +104,577 @@ def test_duplicate_ids_specified(self): ) lib.remove_elements(self.env_assist.get_env(), ["l1", "l1"]) - def test_remove_location_constraint(self): + def test_remove_constraints(self): self.config.runner.cib.load( constraints=""" - - """ - ) - self.config.env.push_cib( - constraints=""" - - - - """ - ) - lib.remove_elements(self.env_assist.get_env(), ["l1"]) - - def test_remove_order_constraint(self): - self.config.runner.cib.load( - constraints=""" - - - """ - ) - self.config.env.push_cib( - constraints=""" - - - - """ - ) - lib.remove_elements(self.env_assist.get_env(), ["o2"]) - - def test_remove_colocation_constraints(self): - self.config.runner.cib.load( - constraints=""" - + + """ ) - self.config.env.push_cib(constraints="") - lib.remove_elements(self.env_assist.get_env(), ["c1", "c2"]) - - def test_remove_ticket_constraints(self): - self.config.runner.cib.load( + self.config.env.push_cib( constraints=""" - + + """ ) - self.config.env.push_cib(constraints="") - lib.remove_elements(self.env_assist.get_env(), ["t1", "t2"]) + lib.remove_elements( + self.env_assist.get_env(), ["l1", "o2", "c1", "c2", "t1"] + ) - def test_remove_location_constraint_with_one_rule_by_id(self): + def test_remove_location_rules(self): self.config.runner.cib.load( constraints=FIXTURE_TWO_LOC_CONSTRAINTS_WITH_RULES ) - self.config.env.push_cib( - constraints=_constraints(FIXTURE_LOC_CONSTRAINT_WITH_2_RULES) + self.config.env.push_cib(constraints="") + lib.remove_elements(self.env_assist.get_env(), ["r1", "r2", "r3"]) + self.env_assist.assert_reports( + [ + fixture.info( + reports.codes.CIB_REMOVE_DEPENDANT_ELEMENTS, + id_tag_map={"lr1": "rsc_location", "lr2": "rsc_location"}, + ), + ] ) - lib.remove_elements(self.env_assist.get_env(), ["lr1"]) - def test_remove_location_constraint_with_more_rules_by_id(self): + def test_remove_location_rule_expressions(self): self.config.runner.cib.load( constraints=FIXTURE_TWO_LOC_CONSTRAINTS_WITH_RULES ) - self.config.env.push_cib( - constraints=_constraints(FIXTURE_LOC_CONSTRAINT_WITH_1_RULE) + self.env_assist.assert_raise_library_error( + lambda: lib.remove_elements( + self.env_assist.get_env(), ["r1e1", "r1e2"] + ) + ) + self.env_assist.assert_reports( + [ + fixture.report_unexpected_element( + "r1e1", "expression", EXPECTED_TYPES_FOR_REMOVE + ), + fixture.report_unexpected_element( + "r1e2", "date_expression", EXPECTED_TYPES_FOR_REMOVE + ), + ] ) - lib.remove_elements(self.env_assist.get_env(), ["lr2"]) - def test_remove_one_rule_from_location_constraint_with_one_rule(self): + def test_remove_resources(self): self.config.runner.cib.load( - constraints=FIXTURE_TWO_LOC_CONSTRAINTS_WITH_RULES + resources=""" + + + + + + + + + + + + + + + + + + """ ) self.config.env.push_cib( - constraints=_constraints(FIXTURE_LOC_CONSTRAINT_WITH_2_RULES) + resources=""" + + + + """ + ) + lib.remove_elements( + self.env_assist.get_env(), + ["P-1", "G", "C", "B"], + [reports.codes.FORCE], ) - lib.remove_elements(self.env_assist.get_env(), ["r1"]) self.env_assist.assert_reports( [ fixture.info( reports.codes.CIB_REMOVE_DEPENDANT_ELEMENTS, - id_tag_map={"lr1": "rsc_location"}, - ), + id_tag_map={ + "G-1": "primitive", + "G-2": "primitive", + "C-G": "group", + "C-G-1": "primitive", + "C-G-2": "primitive", + "B-1": "primitive", + }, + ) + ] + ) + + def test_remove_resource_guest(self): + self.config.runner.cib.load(filename="cib-largefile.xml") + self.env_assist.assert_raise_library_error( + lambda: lib.remove_elements( + self.env_assist.get_env(), ["container1"] + ) + ) + self.env_assist.assert_reports( + [ + fixture.error( + reports.codes.USE_COMMAND_NODE_REMOVE_GUEST, + resource_id="container1", + ) + ] + ) + + def test_remove_resource_remote(self): + self.config.runner.cib.load(filename="cib-remote.xml") + self.env_assist.assert_raise_library_error( + lambda: lib.remove_elements( + self.env_assist.get_env(), ["rh93-remote"] + ) + ) + self.env_assist.assert_reports( + [ + fixture.error( + reports.codes.USE_COMMAND_NODE_REMOVE_REMOTE, + resource_id="rh93-remote", + ) ] ) - def test_remove_one_rule_from_location_constraint_with_two_rules(self): + def test_remove_resource_multiple_dependencies(self): self.config.runner.cib.load( - constraints=FIXTURE_TWO_LOC_CONSTRAINTS_WITH_RULES + resources=""" + + + + + """, + constraints=""" + + + + + + + + + + + """, + tags=""" + + + + + + + """, + fencing_topology=""" + + + + """, + optional_in_conf=""" + + + + + + + + + """, ) self.config.env.push_cib( + resources=""" + + + + """, constraints=""" - - - - - - - - - - - + + + + + + - """ + """, + tags=""" + + + + + + """, + fencing_topology="", + optional_in_conf=""" + + + + + + + + """, ) - lib.remove_elements(self.env_assist.get_env(), ["r2"]) - - def test_remove_more_location_rules(self): - self.config.runner.cib.load( - constraints=FIXTURE_TWO_LOC_CONSTRAINTS_WITH_RULES + lib.remove_elements( + self.env_assist.get_env(), ["A"], [reports.codes.FORCE] ) - self.config.env.push_cib(constraints="") - lib.remove_elements(self.env_assist.get_env(), ["r1", "r2", "r3"]) self.env_assist.assert_reports( [ fixture.info( reports.codes.CIB_REMOVE_DEPENDANT_ELEMENTS, - id_tag_map={"lr1": "rsc_location", "lr2": "rsc_location"}, + id_tag_map={ + "L1": "rsc_location", + "FL": "fencing-level", + "PERMISSION": "acl_permission", + }, + ), + fixture.info( + reports.codes.CIB_REMOVE_REFERENCES, + id_tag_map={ + "A": "primitive", + "T": "tag", + "PERMISSION": "acl_permission", + "ROLE": "acl_role", + "SET": "resource_set", + }, + removing_references_from={ + "A": {"T", "SET"}, + "PERMISSION": {"ROLE"}, + }, ), ] ) - def test_remove_location_rule_expressions(self): + +class RemoveElementsStopResources(TestCase): + def setUp(self): + self.tmp_file_mock_obj = TmpFileMock( + file_content_checker=assert_xml_equal, + ) + self.addCleanup(self.tmp_file_mock_obj.assert_all_done) + tmp_file_patcher = mock.patch("pcs.lib.tools.get_tmp_file") + self.addCleanup(tmp_file_patcher.stop) + tmp_file_mock = tmp_file_patcher.start() + tmp_file_mock.side_effect = ( + self.tmp_file_mock_obj.get_mock_side_effect() + ) + self.env_assist, self.config = get_env_tools(self) + + def fixture_env( + self, + initial_cib_modifiers: dict[str, str], + initial_state_modifiers: dict[str, str], + after_disable_cib_modifiers: dict[str, str], + after_disable_state_modifiers: dict[str, str], + after_delete_cib_modifiers: dict[str, str], + successful_stop=True, + ): + self.config.runner.cib.load( - constraints=FIXTURE_TWO_LOC_CONSTRAINTS_WITH_RULES + name="load.disable", **initial_cib_modifiers ) - self.env_assist.assert_raise_library_error( - lambda: lib.remove_elements( - self.env_assist.get_env(), ["r1e1", "r1e2"] + self.config.runner.pcmk.load_state( + name="state.disable", **initial_state_modifiers + ) + self.config.runner.cib.diff( + "cib.disable.before", + "cib.disable.after", + name="diff.disable", + stdout="diff_disable", + ) + self.config.runner.cib.push_diff( + name="push.disable", cib_diff="diff_disable" + ) + + original_cib = self.config.calls.get("load.disable").stdout + after_disable_cib = modify_cib( + original_cib, **after_disable_cib_modifiers + ) + + self.config.runner.pcmk.wait(timeout=0) + self.config.runner.pcmk.load_state( + name="state.delete", **after_disable_state_modifiers + ) + + mock_files = [ + TmpFileCall( + "cib.disable.before", + orig_content=self.config.calls.get("load.disable").stdout, + ), + TmpFileCall( + "cib.disable.after", + orig_content=after_disable_cib, + ), + ] + + if successful_stop: + self.config.runner.cib.load( + name="load.delete", **after_disable_cib_modifiers + ) + self.config.runner.cib.diff( + "cib.delete.before", + "cib.delete.after", + name="diff.delete", + stdout="diff_delete", + ) + + self.config.runner.cib.push_diff( + name="push.delete", cib_diff="diff_delete" + ) + + mock_files.extend( + [ + TmpFileCall( + "cib.delete.before", orig_content=after_disable_cib + ), + TmpFileCall( + "cib.delete.after", + orig_content=modify_cib( + read_test_resource("cib-empty.xml"), + **after_delete_cib_modifiers, + ), + ), + ] ) + + self.tmp_file_mock_obj.set_calls(mock_files) + + def test_one_resource(self): + self.fixture_env( + initial_cib_modifiers={ + "resources": """ + + + + """ + }, + initial_state_modifiers={ + "resources": """ + + + + """ + }, + after_disable_cib_modifiers={ + "resources": """ + + + + + + + + """ + }, + after_disable_state_modifiers={ + "resources": """ + + + + """ + }, + after_delete_cib_modifiers={"resources": ""}, ) + + lib.remove_elements(self.env_assist.get_env(), ["A"]) self.env_assist.assert_reports( [ - fixture.report_unexpected_element( - "r1e1", "expression", EXPECTED_TYPES_FOR_REMOVE - ), - fixture.report_unexpected_element( - "r1e2", "date_expression", EXPECTED_TYPES_FOR_REMOVE + fixture.info( + reports.codes.STOPPING_RESOURCES_BEFORE_DELETING, + resource_id_list=["A"], ), + fixture.info(reports.codes.WAIT_FOR_IDLE_STARTED, timeout=0), ] ) - -class GetInnerReferences(TestCase): - # pylint: disable=protected-access - def test_no_inner_references(self): - self.assertEqual( - [], lib._get_inner_references(etree.fromstring("")) + def test_resource_unmanaged(self): + self.fixture_env( + initial_cib_modifiers={ + "resources": """ + + + + """ + }, + initial_state_modifiers={ + "resources": """ + + + + """ + }, + after_disable_cib_modifiers={ + "resources": """ + + + + + + + + """ + }, + after_disable_state_modifiers={ + "resources": """ + + + + """ + }, + after_delete_cib_modifiers={"resources": ""}, ) - def test_not_supported_inner_references(self): - element = etree.fromstring( - """ - - - - - - - - - """ + lib.remove_elements(self.env_assist.get_env(), ["A"]) + self.env_assist.assert_reports( + [ + fixture.warn( + reports.codes.RESOURCE_IS_UNMANAGED, resource_id="A" + ), + fixture.info( + reports.codes.STOPPING_RESOURCES_BEFORE_DELETING, + resource_id_list=["A"], + ), + fixture.info(reports.codes.WAIT_FOR_IDLE_STARTED, timeout=0), + ] ) - self.assertEqual([], lib._get_inner_references(element)) - - -class IsLastElement(TestCase): - # pylint: disable=protected-access - def test_last_element_true(self): - for element, tag in ( - (etree.fromstring(""), "a"), - (etree.fromstring(""), "a"), - ): - with self.subTest(element=element, tag=tag): - self.assertTrue(lib._is_last_element(element, tag)) - - def test_last_element_false(self): - for element, tag in ( - (etree.fromstring(""), "a"), - (etree.fromstring(" "), "a"), - (etree.fromstring(" "), "a"), - ): - with self.subTest(element=element, tag=tag): - self.assertFalse(lib._is_last_element(element, tag)) - - -class IsEmptyAfterInnerElRemoval(TestCase): - # pylint: disable=protected-access - def test_last_element_true(self): - for parent in ( - etree.fromstring(""), - etree.fromstring(""), - etree.fromstring(""), - etree.fromstring(""), - etree.fromstring(""), - etree.fromstring(""), - etree.fromstring(""), - etree.fromstring(""), - etree.fromstring(""), - ): - with self.subTest(parent=parent): - self.assertTrue(lib._is_empty_after_inner_el_removal(parent)) - - def test_last_element_false(self): - for parent in ( - etree.fromstring(""), - etree.fromstring(""), - etree.fromstring(""), - etree.fromstring(""), - etree.fromstring(""), - etree.fromstring( + + def test_resource_remove_failed_to_stop(self): + self.fixture_env( + initial_cib_modifiers={ + "resources": """ + + + """ - - - - - """ - ), - etree.fromstring(""), - etree.fromstring( - "" - ), - etree.fromstring( + }, + initial_state_modifiers={ + "resources": """ + + + """ - - - " - + }, + after_disable_cib_modifiers={ + "resources": """ + + + + + + + """ - ), - etree.fromstring(""), - etree.fromstring(""), - ): - with self.subTest(parent=parent): - self.assertFalse(lib._is_empty_after_inner_el_removal(parent)) + }, + after_disable_state_modifiers={ + "resources": """ + + + + """ + }, + after_delete_cib_modifiers={}, + successful_stop=False, + ) + + self.env_assist.assert_raise_library_error( + lambda: lib.remove_elements(self.env_assist.get_env(), ["A"]) + ) + self.env_assist.assert_reports( + [ + fixture.info( + reports.codes.STOPPING_RESOURCES_BEFORE_DELETING, + resource_id_list=["A"], + ), + fixture.info(reports.codes.WAIT_FOR_IDLE_STARTED, timeout=0), + fixture.error( + reports.codes.CANNOT_STOP_RESOURCES_BEFORE_DELETING, + resource_id_list=["A"], + force_code=reports.codes.FORCE, + ), + ] + ) + + def test_disable_only_resources(self): + constraints = """ + + + + """ + tags = """ + + + + + + """ + + self.fixture_env( + initial_cib_modifiers={ + "resources": """ + + + + """, + "constraints": constraints, + "tags": tags, + }, + initial_state_modifiers={ + "resources": """ + + + + """ + }, + after_disable_cib_modifiers={ + "resources": """ + + + + + + + + """, + "constraints": constraints, + "tags": tags, + }, + after_disable_state_modifiers={ + "resources": """ + + + + """ + }, + after_delete_cib_modifiers={ + "resources": "", + "constraints": "", + "tags": "", + }, + ) + + lib.remove_elements(self.env_assist.get_env(), ["A"]) + self.env_assist.assert_reports( + [ + fixture.info( + reports.codes.STOPPING_RESOURCES_BEFORE_DELETING, + resource_id_list=["A"], + ), + fixture.info(reports.codes.WAIT_FOR_IDLE_STARTED, timeout=0), + fixture.info( + reports.codes.CIB_REMOVE_DEPENDANT_ELEMENTS, + id_tag_map={"L": "rsc_location", "T": "tag"}, + ), + ] + ) diff --git a/pcs_test/tier0/lib/commands/test_status.py b/pcs_test/tier0/lib/commands/test_status.py index 8781507d1..f837dd5bd 100644 --- a/pcs_test/tier0/lib/commands/test_status.py +++ b/pcs_test/tier0/lib/commands/test_status.py @@ -1350,7 +1350,7 @@ def test_bad_xml(self): fixture.error( report_codes.BAD_CLUSTER_STATE_DATA, reason="Resource 'R7' contains an unknown role 'NotPcmkRole'", - ), + ) ], False, ) diff --git a/pcs_test/tier1/legacy/test_constraints.py b/pcs_test/tier1/legacy/test_constraints.py index 4d73888c1..4ac5adce2 100644 --- a/pcs_test/tier1/legacy/test_constraints.py +++ b/pcs_test/tier1/legacy/test_constraints.py @@ -1180,9 +1180,9 @@ def test_colocation_sets(self): stderr, outdent( """\ - Removing D5 from set colocation_set_D5D6D7_set - Removing D5 from set colocation_set_D5D6D7-1_set - Deleting Resource - D5 + Removing references: + Resource 'D5' from: + Resource sets: 'colocation_set_D5D6D7-1_set', 'colocation_set_D5D6D7_set' """ ), ) @@ -1197,10 +1197,13 @@ def test_colocation_sets(self): stderr, outdent( """\ - Removing D6 from set colocation_set_D5D6D7_set - Removing D6 from set colocation_set_D5D6D7-1_set - Removing set colocation_set_D5D6D7-1_set - Deleting Resource - D6 + Removing dependant element: + Resource set: 'colocation_set_D5D6D7-1_set' + Removing references: + Resource 'D6' from: + Resource set: 'colocation_set_D5D6D7_set' + Resource set 'colocation_set_D5D6D7-1_set' from: + Colocation constraint: 'colocation_set_D5D6D7-1' """ ), ) @@ -1704,9 +1707,9 @@ def test_order_sets(self): stderr, outdent( """\ - Removing D5 from set order_set_D5D6D7_set - Removing D5 from set order_set_D5D6D7-1_set - Deleting Resource - D5 + Removing references: + Resource 'D5' from: + Resource sets: 'order_set_D5D6D7-1_set', 'order_set_D5D6D7_set' """ ), ) @@ -1720,10 +1723,13 @@ def test_order_sets(self): stderr, outdent( """\ - Removing D6 from set order_set_D5D6D7_set - Removing D6 from set order_set_D5D6D7-1_set - Removing set order_set_D5D6D7-1_set - Deleting Resource - D6 + Removing dependant element: + Resource set: 'order_set_D5D6D7-1_set' + Removing references: + Resource 'D6' from: + Resource set: 'order_set_D5D6D7_set' + Resource set 'order_set_D5D6D7-1_set' from: + Order constraint: 'order_set_D5D6D7-1' """ ), ) @@ -2800,7 +2806,7 @@ def test_constraint_group_clone_update(self): ), ) - def test_remote_node_constraints_remove(self): + def test_guest_node_constraints_remove(self): # pylint: disable=too-many-statements self.temp_corosync_conf = get_tmp_file("tier1_test_constraints") write_file_to_tmpfile(rc("corosync.conf"), self.temp_corosync_conf) @@ -2863,75 +2869,6 @@ def test_remote_node_constraints_remove(self): ), ) - stdout, stderr, retval = pcs( - self.temp_cib.name, "resource delete vm-guest1".split() - ) - self.assertEqual( - stderr, - outdent( - """\ - Removing Constraint - location-D1-guest1-200 - Removing Constraint - location-D2-guest1--400 - Deleting Resource - vm-guest1 - """ - ), - ) - self.assertEqual(stdout, "") - self.assertEqual(retval, 0) - - self.assert_pcs_success( - "constraint --full".split(), - stdout_full=outdent( - """\ - Location Constraints: - resource 'D1' prefers node 'node1' with score 100 (id: location-D1-node1-100) - resource 'D2' avoids node 'node2' with score 300 (id: location-D2-node2--300) - """ - ), - ) - - # constraints referencing the remote node's name, - # removing the remote node - self.assert_pcs_success( - ( - "resource create vm-guest1 ocf:pcsmock:minimal " - "meta remote-node=guest1 --force" - ).split(), - stderr_full=( - "Warning: this command is not sufficient for creating a guest " - "node, use 'pcs cluster node add-guest'\n" - ), - ) - - stdout, stderr, retval = pcs( - self.temp_cib.name, - "constraint location D1 prefers guest1=200".split(), - ) - self.assertEqual(stderr, LOCATION_NODE_VALIDATION_SKIP_WARNING) - self.assertEqual(stdout, "") - self.assertEqual(retval, 0) - - stdout, stderr, retval = pcs( - self.temp_cib.name, - "constraint location D2 avoids guest1=400".split(), - ) - self.assertEqual(stderr, LOCATION_NODE_VALIDATION_SKIP_WARNING) - self.assertEqual(stdout, "") - self.assertEqual(retval, 0) - - self.assert_pcs_success( - "constraint --full".split(), - stdout_full=outdent( - """\ - Location Constraints: - resource 'D1' prefers node 'node1' with score 100 (id: location-D1-node1-100) - resource 'D2' avoids node 'node2' with score 300 (id: location-D2-node2--300) - resource 'D1' prefers node 'guest1' with score 200 (id: location-D1-guest1-200) - resource 'D2' avoids node 'guest1' with score 400 (id: location-D2-guest1--400) - """ - ), - ) - stdout, stderr, retval = pcs( self.temp_cib.name, "cluster node remove-guest guest1".split(), @@ -2955,58 +2892,13 @@ def test_remote_node_constraints_remove(self): """\ Location Constraints: resource 'D1' prefers node 'node1' with score 100 (id: location-D1-node1-100) - resource 'D2' avoids node 'node2' with score 300 (id: location-D2-node2--300) resource 'D1' prefers node 'guest1' with score 200 (id: location-D1-guest1-200) + resource 'D2' avoids node 'node2' with score 300 (id: location-D2-node2--300) resource 'D2' avoids node 'guest1' with score 400 (id: location-D2-guest1--400) """ ), ) - stdout, stderr, retval = pcs( - self.temp_cib.name, "resource delete vm-guest1".split() - ) - self.assertEqual(stdout, "") - self.assertEqual(stderr, "Deleting Resource - vm-guest1\n") - self.assertEqual(retval, 0) - - # constraints referencing the remote node resource - # deleting the remote node resource - self.assert_pcs_success( - ( - "resource create vm-guest1 ocf:pcsmock:minimal " - "meta remote-node=guest1 --force" - ).split(), - stderr_full=( - "Warning: this command is not sufficient for creating a guest " - "node, use 'pcs cluster node add-guest'\n" - ), - ) - - stdout, stderr, retval = pcs( - self.temp_cib.name, - "constraint location vm-guest1 prefers node1".split(), - ) - self.assertEqual(stderr, LOCATION_NODE_VALIDATION_SKIP_WARNING) - self.assertEqual(stdout, "") - self.assertEqual(retval, 0) - - stdout, stderr, retval = pcs( - self.temp_cib.name, "resource delete vm-guest1".split() - ) - self.assertEqual(stdout, "") - ac( - stderr, - outdent( - """\ - Removing Constraint - location-vm-guest1-node1-INFINITY - Removing Constraint - location-D1-guest1-200 - Removing Constraint - location-D2-guest1--400 - Deleting Resource - vm-guest1 - """ - ), - ) - self.assertEqual(retval, 0) - def test_duplicate_order(self): self.fixture_resources() stdout, stderr, retval = pcs( diff --git a/pcs_test/tier1/legacy/test_resource.py b/pcs_test/tier1/legacy/test_resource.py index b0350edec..c9640d6e3 100644 --- a/pcs_test/tier1/legacy/test_resource.py +++ b/pcs_test/tier1/legacy/test_resource.py @@ -580,53 +580,6 @@ def test_add_resources_large_cib(self): ), ) - def _test_delete_remove_resources(self, command): - assert command in {"delete", "remove"} - - self.assert_pcs_success( - "resource create --no-default-ops ClusterIP ocf:pcsmock:minimal".split() - ) - - self.assert_pcs_success( - f"resource {command} ClusterIP".split(), - stderr_full="Deleting Resource - ClusterIP\n", - ) - - self.assert_pcs_fail( - "resource config ClusterIP".split(), - "Warning: Unable to find resource 'ClusterIP'\nError: No resource found\n", - ) - - self.assert_pcs_success( - "resource status".split(), - "NO resources configured\n", - ) - - self.assert_pcs_fail( - f"resource {command} ClusterIP".split(), - "Error: Resource 'ClusterIP' does not exist.\n", - ) - - def test_delete_resources(self): - # Verify deleting resources works - # Additional tests are in class BundleDeleteTest - self.assert_pcs_fail( - "resource delete".split(), - stderr_start="\nUsage: pcs resource delete...", - ) - - self._test_delete_remove_resources("delete") - - def test_remove_resources(self): - # Verify deleting resources works - # Additional tests are in class BundleDeleteTest - self.assert_pcs_fail( - "resource remove".split(), - stderr_start="\nUsage: pcs resource remove...", - ) - - self._test_delete_remove_resources("remove") - def test_resource_show(self): self.assert_pcs_success( ( @@ -1520,98 +1473,8 @@ def test_update_operation(self): ), ) - def test_group_delete_test(self): - self.assert_pcs_success( - "resource create --no-default-ops A1 ocf:pcsmock:minimal --group AGroup".split(), - stderr_full=DEPRECATED_DASH_DASH_GROUP, - ) - self.assert_pcs_success( - "resource create --no-default-ops A2 ocf:pcsmock:minimal --group AGroup".split(), - stderr_full=DEPRECATED_DASH_DASH_GROUP, - ) - self.assert_pcs_success( - "resource create --no-default-ops A3 ocf:pcsmock:minimal --group AGroup".split(), - stderr_full=DEPRECATED_DASH_DASH_GROUP, - ) - - stdout, stderr, returncode = self.pcs_runner.run( - "resource status".split() - ) - self.assertEqual(stderr, "") - self.assertEqual(returncode, 0) - if is_pacemaker_21_without_20_compatibility(): - self.assertEqual( - stdout, - outdent( - """\ - * Resource Group: AGroup: - * A1\t(ocf:pcsmock:minimal):\t Stopped - * A2\t(ocf:pcsmock:minimal):\t Stopped - * A3\t(ocf:pcsmock:minimal):\t Stopped - """ - ), - ) - elif PCMK_2_0_3_PLUS: - assert_pcs_status( - stdout, - """\ - * Resource Group: AGroup: - * A1\t(ocf::pcsmock:minimal):\tStopped - * A2\t(ocf::pcsmock:minimal):\tStopped - * A3\t(ocf::pcsmock:minimal):\tStopped -""", - ) - else: - self.assertEqual( - stdout, - """\ - Resource Group: AGroup - A1\t(ocf::pcsmock:minimal):\tStopped - A2\t(ocf::pcsmock:minimal):\tStopped - A3\t(ocf::pcsmock:minimal):\tStopped -""", - ) - - self.assert_pcs_success( - "resource delete AGroup".split(), - stderr_full=dedent( - """\ - Removing group: AGroup (and all resources within group) - Stopping all resources in group: AGroup... - Deleting Resource - A1 - Deleting Resource - A2 - Deleting Resource (and group) - A3 - """ - ), - ) - - self.assert_pcs_success( - "resource status".split(), "NO resources configured\n" - ) - @skip_unless_crm_rule() def test_group_ungroup(self): - self.setup_cluster_a() - self.assert_pcs_success( - "constraint location ClusterIP3 prefers rh7-1".split(), - stderr_full=LOCATION_NODE_VALIDATION_SKIP_WARNING, - ) - - self.assert_pcs_success( - "resource delete ClusterIP2".split(), - stderr_full="Deleting Resource - ClusterIP2\n", - ) - - self.assert_pcs_success( - "resource delete ClusterIP3".split(), - stderr_full=dedent( - """\ - Removing Constraint - location-ClusterIP3-rh7-1-INFINITY - Deleting Resource (and group) - ClusterIP3 - """ - ), - ) - self.assert_pcs_success( "resource create --no-default-ops A1 ocf:pcsmock:minimal".split(), ) @@ -1661,23 +1524,6 @@ def test_group_ungroup(self): ), ) - def test_group_large_resource_remove(self): - self.pcs_runner = PcsRunner(self.temp_large_cib.name) - self.pcs_runner.mock_settings = get_mock_settings() - self.assert_pcs_success( - "resource group add dummies dummylarge".split(), - ) - self.assert_pcs_success( - "resource delete dummies".split(), - stderr_full=dedent( - """\ - Removing group: dummies (and all resources within group) - Stopping all resources in group: dummies... - Deleting Resource (and group) - dummylarge - """ - ), - ) - def test_group_order(self): # This was cosidered for removing during 'resource group add' command # and tests overhaul. However, this is the only test where "resource @@ -1835,236 +1681,6 @@ def test_cluster_config(self): ), ) - def test_clone_remove(self): - self.assert_pcs_success( - "resource create --no-default-ops D1 ocf:pcsmock:minimal clone".split(), - ) - - self.assert_pcs_success( - "constraint location D1-clone prefers rh7-1".split(), - stderr_full=LOCATION_NODE_VALIDATION_SKIP_WARNING, - ) - - self.assert_pcs_success( - "constraint location D1 prefers rh7-1 --force".split(), - stderr_full=LOCATION_NODE_VALIDATION_SKIP_WARNING, - ) - - self.assert_pcs_success( - "resource config".split(), - dedent( - """\ - Clone: D1-clone - Resource: D1 (class=ocf provider=pcsmock type=minimal) - Operations: - monitor: D1-monitor-interval-10s - interval=10s timeout=20s - """ - ), - ) - - self.assert_pcs_success( - "resource delete D1-clone".split(), - stderr_full=dedent( - """\ - Removing Constraint - location-D1-clone-rh7-1-INFINITY - Removing Constraint - location-D1-rh7-1-INFINITY - Deleting Resource - D1 - """ - ), - ) - - self.assert_pcs_success( - "resource config".split(), - ) - - self.assert_pcs_success( - "resource create d99 ocf:pcsmock:minimal clone globally-unique=true".split(), - stderr_full=( - "Deprecation Warning: Configuring clone meta attributes without " - "specifying the 'meta' keyword after the 'clone' keyword is " - "deprecated and will be removed in a future release. Specify " - "--future to switch to the future behavior.\n" - ), - ) - - self.assert_pcs_success( - "resource delete d99".split(), - stderr_full="Deleting Resource - d99\n", - ) - - def test_clone_remove_large(self): - self.pcs_runner = PcsRunner(self.temp_large_cib.name) - self.pcs_runner.mock_settings = get_mock_settings() - self.assert_pcs_success("resource clone dummylarge".split()) - self.assert_pcs_success( - "resource delete dummylarge".split(), - stderr_full="Deleting Resource - dummylarge\n", - ) - - def test_clone_group_large_resource_remove(self): - self.pcs_runner = PcsRunner(self.temp_large_cib.name) - self.pcs_runner.mock_settings = get_mock_settings() - self.assert_pcs_success( - "resource group add dummies dummylarge".split(), - ) - self.assert_pcs_success("resource clone dummies".split()) - self.assert_pcs_success( - "resource delete dummies".split(), - stderr_full=dedent( - """\ - Removing group: dummies (and all resources within group) - Stopping all resources in group: dummies... - Deleting Resource (and group and clone) - dummylarge - """ - ), - ) - - @skip_unless_crm_rule() - def test_master_slave_remove(self): - self.setup_cluster_a() - self.assert_pcs_success( - "constraint location ClusterIP5 prefers rh7-1 --force".split(), - stderr_full=LOCATION_NODE_VALIDATION_SKIP_WARNING, - ) - - self.assert_pcs_success( - "constraint location Master prefers rh7-2".split(), - stderr_full=LOCATION_NODE_VALIDATION_SKIP_WARNING, - ) - - self.assert_pcs_success( - "resource delete Master".split(), - stderr_full=dedent( - """\ - Removing Constraint - location-ClusterIP5-rh7-1-INFINITY - Removing Constraint - location-Master-rh7-2-INFINITY - Deleting Resource - ClusterIP5 - """ - ), - ) - - self.assert_pcs_success( - "resource create --no-default-ops ClusterIP5 ocf:pcsmock:minimal".split(), - ) - - self.assert_pcs_success( - "constraint location ClusterIP5 prefers rh7-1".split(), - stderr_full=LOCATION_NODE_VALIDATION_SKIP_WARNING, - ) - - self.assert_pcs_success( - "constraint location ClusterIP5 prefers rh7-2".split(), - stderr_full=LOCATION_NODE_VALIDATION_SKIP_WARNING, - ) - - self.assert_pcs_success( - "resource delete ClusterIP5".split(), - stderr_full=dedent( - """\ - Removing Constraint - location-ClusterIP5-rh7-1-INFINITY - Removing Constraint - location-ClusterIP5-rh7-2-INFINITY - Deleting Resource - ClusterIP5 - """ - ), - ) - - self.assert_pcs_success( - "resource create --no-default-ops ClusterIP5 ocf:pcsmock:minimal".split() - ) - - self.assert_pcs_success( - "constraint location ClusterIP5 prefers rh7-1".split(), - stderr_full=LOCATION_NODE_VALIDATION_SKIP_WARNING, - ) - - self.assert_pcs_success( - "constraint location ClusterIP5 prefers rh7-2".split(), - stderr_full=LOCATION_NODE_VALIDATION_SKIP_WARNING, - ) - - self.pcs_runner.mock_settings = { - "corosync_conf_file": rc("corosync.conf"), - } - self.assert_pcs_success( - ["config"], - dedent( - """\ - Cluster Name: test99 - Corosync Nodes: - rh7-1 rh7-2 - Pacemaker Nodes: - - Resources: - Resource: ClusterIP6 (class=ocf provider=pcsmock type=minimal) - Operations: - monitor: ClusterIP6-monitor-interval-10s - interval=10s timeout=20s - Resource: ClusterIP5 (class=ocf provider=pcsmock type=minimal) - Operations: - monitor: ClusterIP5-monitor-interval-10s - interval=10s timeout=20s - Group: TestGroup1 - Resource: ClusterIP (class=ocf provider=pcsmock type=minimal) - Operations: - monitor: ClusterIP-monitor-interval-10s - interval=10s timeout=20s - Group: TestGroup2 - Resource: ClusterIP2 (class=ocf provider=pcsmock type=minimal) - Operations: - monitor: ClusterIP2-monitor-interval-10s - interval=10s timeout=20s - Resource: ClusterIP3 (class=ocf provider=pcsmock type=minimal) - Operations: - monitor: ClusterIP3-monitor-interval-10s - interval=10s timeout=20s - Clone: ClusterIP4-clone - Resource: ClusterIP4 (class=ocf provider=pcsmock type=minimal) - Operations: - monitor: ClusterIP4-monitor-interval-10s - interval=10s timeout=20s - - Location Constraints: - resource 'ClusterIP5' prefers node 'rh7-1' with score INFINITY (id: location-ClusterIP5-rh7-1-INFINITY) - resource 'ClusterIP5' prefers node 'rh7-2' with score INFINITY (id: location-ClusterIP5-rh7-2-INFINITY) - """ - ), - ) - del self.pcs_runner.mock_settings["corosync_conf_file"] - - # pcs no longer allows turning resources into masters but supports - # existing ones. In order to test it, we need to put a master in the - # CIB without pcs. - wrap_element_by_master(self.temp_large_cib, "dummylarge") - - self.pcs_runner = PcsRunner(self.temp_large_cib.name) - self.pcs_runner.mock_settings = get_mock_settings() - self.assert_pcs_success( - "resource delete dummylarge".split(), - stderr_full="Deleting Resource - dummylarge\n", - ) - - def test_master_slave_group_large_resource_remove(self): - self.pcs_runner = PcsRunner(self.temp_large_cib.name) - self.pcs_runner.mock_settings = get_mock_settings() - self.assert_pcs_success( - "resource group add dummies dummylarge".split(), - ) - # pcs no longer allows turning resources into masters but supports - # existing ones. In order to test it, we need to put a master in the - # CIB without pcs. - wrap_element_by_master(self.temp_large_cib, "dummies") - self.assert_pcs_success( - "resource delete dummies".split(), - stderr_full=dedent( - """\ - Removing group: dummies (and all resources within group) - Stopping all resources in group: dummies... - Deleting Resource (and group and M/S) - dummylarge - """ - ), - ) - def test_ms_group(self): self.assert_pcs_success( "resource create --no-default-ops D0 ocf:pcsmock:minimal".split(), @@ -2098,14 +1714,6 @@ def test_ms_group(self): """ ), ) - self.assert_pcs_success( - "resource delete D0".split(), - stderr_full="Deleting Resource - D0\n", - ) - self.assert_pcs_success( - "resource delete D1".split(), - stderr_full="Deleting Resource (and group and M/S) - D1\n", - ) def test_unclone(self): # see also BundleClone @@ -2665,97 +2273,35 @@ def test_clone_master(self): self.assert_pcs_success("resource clone D0".split()) self.assert_pcs_fail( - "resource promotable D3 meta promotable=false".split(), - "Error: you cannot specify both promotable option and promotable keyword\n", - ) - - self.assert_pcs_success("resource promotable D3".split()) - - # pcs no longer allows turning resources into masters but supports - # existing ones. In order to test it, we need to put a master in the - # CIB without pcs. - wrap_element_by_master( - self.temp_cib, "D1", master_id="D1-master-custom" - ) - - # pcs no longer allows turning resources into masters but supports - # existing ones. In order to test it, we need to put a master in the - # CIB without pcs. - wrap_element_by_master(self.temp_cib, "D2") - - self.assert_pcs_success( - "resource config".split(), - dedent( - """\ - Clone: D0-clone - Resource: D0 (class=ocf provider=pcsmock type=stateful) - Operations: - monitor: D0-monitor-interval-10s - interval=10s timeout=20s role=Promoted - monitor: D0-monitor-interval-11s - interval=11s timeout=20s role=Unpromoted - Clone: D3-clone - Meta Attributes: D3-clone-meta_attributes - promotable=true - Resource: D3 (class=ocf provider=pcsmock type=stateful) - Operations: - monitor: D3-monitor-interval-10s - interval=10s timeout=20s role=Promoted - monitor: D3-monitor-interval-11s - interval=11s timeout=20s role=Unpromoted - Clone: D1-master-custom - Meta Attributes: - promotable=true - Resource: D1 (class=ocf provider=pcsmock type=stateful) - Operations: - monitor: D1-monitor-interval-10s - interval=10s timeout=20s role=Promoted - monitor: D1-monitor-interval-11s - interval=11s timeout=20s role=Unpromoted - Clone: D2-master - Meta Attributes: - promotable=true - Resource: D2 (class=ocf provider=pcsmock type=stateful) - Operations: - monitor: D2-monitor-interval-10s - interval=10s timeout=20s role=Promoted - monitor: D2-monitor-interval-11s - interval=11s timeout=20s role=Unpromoted - """ - ), - ) - - self.assert_pcs_success( - "resource delete D0".split(), - stderr_full="Deleting Resource - D0\n", - ) - self.assert_pcs_success( - "resource delete D2".split(), - stderr_full="Deleting Resource - D2\n", + "resource promotable D3 meta promotable=false".split(), + "Error: you cannot specify both promotable option and promotable keyword\n", ) - self.assert_pcs_success( - "resource create --no-default-ops D0 ocf:pcsmock:stateful".split(), - ) - self.assert_pcs_success( - "resource create --no-default-ops D2 ocf:pcsmock:stateful".split(), + self.assert_pcs_success("resource promotable D3".split()) + + # pcs no longer allows turning resources into masters but supports + # existing ones. In order to test it, we need to put a master in the + # CIB without pcs. + wrap_element_by_master( + self.temp_cib, "D1", master_id="D1-master-custom" ) + + # pcs no longer allows turning resources into masters but supports + # existing ones. In order to test it, we need to put a master in the + # CIB without pcs. + wrap_element_by_master(self.temp_cib, "D2") + self.assert_pcs_success( "resource config".split(), dedent( """\ - Resource: D0 (class=ocf provider=pcsmock type=stateful) - Operations: - monitor: D0-monitor-interval-10s - interval=10s timeout=20s role=Promoted - monitor: D0-monitor-interval-11s - interval=11s timeout=20s role=Unpromoted - Resource: D2 (class=ocf provider=pcsmock type=stateful) - Operations: - monitor: D2-monitor-interval-10s - interval=10s timeout=20s role=Promoted - monitor: D2-monitor-interval-11s - interval=11s timeout=20s role=Unpromoted + Clone: D0-clone + Resource: D0 (class=ocf provider=pcsmock type=stateful) + Operations: + monitor: D0-monitor-interval-10s + interval=10s timeout=20s role=Promoted + monitor: D0-monitor-interval-11s + interval=11s timeout=20s role=Unpromoted Clone: D3-clone Meta Attributes: D3-clone-meta_attributes promotable=true @@ -2774,6 +2320,15 @@ def test_clone_master(self): interval=10s timeout=20s role=Promoted monitor: D1-monitor-interval-11s interval=11s timeout=20s role=Unpromoted + Clone: D2-master + Meta Attributes: + promotable=true + Resource: D2 (class=ocf provider=pcsmock type=stateful) + Operations: + monitor: D2-monitor-interval-10s + interval=10s timeout=20s role=Promoted + monitor: D2-monitor-interval-11s + interval=11s timeout=20s role=Unpromoted """ ), ) @@ -2936,107 +2491,13 @@ def test_group_promotable_creation(self): "Error: cannot clone a group that has already been cloned\n", ) - @skip_unless_crm_rule() - def test_group_remove_with_constraints1(self): - # Load nodes into cib so move will work - self.temp_cib.seek(0) - xml = etree.fromstring(self.temp_cib.read()) - nodes_el = xml.find(".//nodes") - etree.SubElement(nodes_el, "node", {"id": "1", "uname": "rh7-1"}) - etree.SubElement(nodes_el, "node", {"id": "2", "uname": "rh7-2"}) - write_data_to_tmpfile(etree.tounicode(xml), self.temp_cib) - - self.assert_pcs_success( - "resource create --no-default-ops D1 ocf:pcsmock:minimal --group DGroup".split(), - stderr_full=DEPRECATED_DASH_DASH_GROUP, - ) - self.assert_pcs_success( - "resource create --no-default-ops D2 ocf:pcsmock:minimal --group DGroup".split(), - stderr_full=DEPRECATED_DASH_DASH_GROUP, - ) - - stdout, stderr, returncode = self.pcs_runner.run( - "resource status".split() - ) - self.assertEqual(stderr, "") - self.assertEqual(returncode, 0) - if is_pacemaker_21_without_20_compatibility(): - self.assertEqual( - stdout, - outdent( - """\ - * Resource Group: DGroup: - * D1\t(ocf:pcsmock:minimal):\t Stopped - * D2\t(ocf:pcsmock:minimal):\t Stopped - """ - ), - ) - elif PCMK_2_0_3_PLUS: - assert_pcs_status( - stdout, - """\ - * Resource Group: DGroup: - * D1\t(ocf::pcsmock:minimal):\tStopped - * D2\t(ocf::pcsmock:minimal):\tStopped -""", - ) - else: - self.assertEqual( - stdout, - """\ - Resource Group: DGroup - D1\t(ocf::pcsmock:minimal):\tStopped - D2\t(ocf::pcsmock:minimal):\tStopped -""", - ) - - # The mock executable for crm_resource does not support the - # `move-with-constraint` command, and so the real executable is used. - self.pcs_runner.mock_settings = {} - self.assert_pcs_success( - "resource move-with-constraint DGroup rh7-1".split(), - stderr_full=( - "Warning: A move constraint has been created and the resource " - "'DGroup' may or may not move depending on other configuration" - "\n" - ), - ) - self.pcs_runner.mock_settings = get_mock_settings() - self.assert_pcs_success( - ["constraint"], - outdent( - """\ - Location Constraints: - Started resource 'DGroup' prefers node 'rh7-1' with score INFINITY - """ - ), - ) - self.assert_pcs_success( - "resource delete D1".split(), - stderr_full="Deleting Resource - D1\n", - ) - self.assert_pcs_success( - "resource delete D2".split(), - stderr_full=dedent( - """\ - Removing Constraint - cli-prefer-DGroup - Deleting Resource (and group) - D2 - """ - ), - ) - - self.assert_pcs_success( - "resource status".split(), - "NO resources configured\n", - ) - def test_resource_clone_creation(self): self.pcs_runner = PcsRunner(self.temp_large_cib.name) self.pcs_runner.mock_settings = get_mock_settings() # resource "dummy1" is already in "temp_large_cib self.assert_pcs_success("resource clone dummy1".split()) - def test_resource_clone_id(self): + def test_resource_clone_id_clone_command(self): self.assert_pcs_success( "resource create --no-default-ops dummy-clone ocf:pcsmock:minimal".split(), ) @@ -3061,9 +2522,9 @@ def test_resource_clone_id(self): ), ) + def test_resource_clone_id_create_command(self): self.assert_pcs_success( - "resource delete dummy".split(), - stderr_full="Deleting Resource - dummy\n", + "resource create --no-default-ops dummy-clone ocf:pcsmock:minimal".split(), ) self.assert_pcs_success( "resource create --no-default-ops dummy ocf:pcsmock:minimal clone".split(), @@ -3085,7 +2546,7 @@ def test_resource_clone_id(self): ), ) - def test_resource_promotable_id(self): + def test_resource_promotable_id_promotable_command(self): self.assert_pcs_success( "resource create --no-default-ops dummy-clone ocf:pcsmock:stateful".split(), ) @@ -3116,9 +2577,9 @@ def test_resource_promotable_id(self): ), ) + def test_resource_promotable_id_create_command(self): self.assert_pcs_success( - "resource delete dummy".split(), - stderr_full="Deleting Resource - dummy\n", + "resource create --no-default-ops dummy-clone ocf:pcsmock:stateful".split(), ) self.assert_pcs_success( "resource create --no-default-ops dummy ocf:pcsmock:stateful promotable".split(), @@ -3212,74 +2673,6 @@ def test_resource_clone_update(self): ), ) - def test_group_remove_with_constraints2(self): - self.assert_pcs_success( - "resource create --no-default-ops A ocf:pcsmock:minimal --group AG".split(), - stderr_full=DEPRECATED_DASH_DASH_GROUP, - ) - self.assert_pcs_success( - "resource create --no-default-ops B ocf:pcsmock:minimal --group AG".split(), - stderr_full=DEPRECATED_DASH_DASH_GROUP, - ) - self.assert_pcs_success( - "constraint location AG prefers rh7-1".split(), - stderr_full=LOCATION_NODE_VALIDATION_SKIP_WARNING, - ) - - self.assert_pcs_success( - "resource ungroup AG".split(), - stderr_full="Removing Constraint - location-AG-rh7-1-INFINITY\n", - ) - - self.assert_pcs_success( - "resource config".split(), - dedent( - """\ - Resource: A (class=ocf provider=pcsmock type=minimal) - Operations: - monitor: A-monitor-interval-10s - interval=10s timeout=20s - Resource: B (class=ocf provider=pcsmock type=minimal) - Operations: - monitor: B-monitor-interval-10s - interval=10s timeout=20s - """ - ), - ) - - self.assert_pcs_success( - "resource create --no-default-ops A1 ocf:pcsmock:minimal --group AA".split(), - stderr_full=DEPRECATED_DASH_DASH_GROUP, - ) - self.assert_pcs_success( - "resource create --no-default-ops A2 ocf:pcsmock:minimal --group AA".split(), - stderr_full=DEPRECATED_DASH_DASH_GROUP, - ) - # pcs no longer allows turning resources into masters but supports - # existing ones. In order to test it, we need to put a master in the - # CIB without pcs. - wrap_element_by_master(self.temp_cib, "AA") - self.assert_pcs_success( - "constraint location AA-master prefers rh7-1".split(), - stderr_full=( - "Warning: Validation for node existence in the cluster will be skipped\n" - ), - ) - - self.assert_pcs_success( - "resource delete A1".split(), - stderr_full="Deleting Resource - A1\n", - ) - self.assert_pcs_success( - "resource delete A2".split(), - stderr_full=dedent( - """\ - Removing Constraint - location-AA-master-rh7-1-INFINITY - Deleting Resource (and group and M/S) - A2 - """ - ), - ) - def test_mastered_group(self): self.assert_pcs_success( "resource create --no-default-ops A ocf:pcsmock:minimal --group AG".split(), @@ -3318,11 +2711,23 @@ def test_mastered_group(self): self.assert_pcs_success( "resource delete B".split(), - stderr_full="Deleting Resource - B\n", + stderr_full=dedent( + """\ + Removing references: + Resource 'B' from: + Group: 'AG' + """ + ), ) self.assert_pcs_success( "resource delete C".split(), - stderr_full="Deleting Resource - C\n", + stderr_full=dedent( + """\ + Removing references: + Resource 'C' from: + Group: 'AG' + """ + ), ) self.assert_pcs_success("resource ungroup AG".split()) self.assert_pcs_success( @@ -3488,17 +2893,6 @@ def test_group_ms_and_clone(self): + "Error: you can specify only one of clone, promotable, bundle or --group\n", ) - def test_resource_clone_group(self): - self.assert_pcs_success( - "resource create --no-default-ops dummy0 ocf:pcsmock:minimal --group group".split(), - stderr_full=DEPRECATED_DASH_DASH_GROUP, - ) - self.assert_pcs_success("resource clone group".split()) - self.assert_pcs_success( - "resource delete dummy0".split(), - stderr_full="Deleting Resource (and group and clone) - dummy0\n", - ) - def test_resource_missing_values(self): self.assert_pcs_success( "resource create --no-default-ops myip params --force".split(), @@ -3637,11 +3031,9 @@ def test_cloned_mastered_group(self): "resource delete dummies-clone".split(), stderr_full=dedent( """\ - Removing group: dummies (and all resources within group) - Stopping all resources in group: dummies... - Deleting Resource - dummy1 - Deleting Resource - dummy2 - Deleting Resource (and group and clone) - dummy3 + Removing dependant elements: + Group: 'dummies' + Resources: 'dummy1', 'dummy2', 'dummy3' """ ), ) @@ -3765,11 +3157,9 @@ def test_cloned_mastered_group(self): "resource delete dummies-master".split(), stderr_full=dedent( """\ - Removing group: dummies (and all resources within group) - Stopping all resources in group: dummies... - Deleting Resource - dummy1 - Deleting Resource - dummy2 - Deleting Resource (and group and M/S) - dummy3 + Removing dependant elements: + Group: 'dummies' + Resources: 'dummy1', 'dummy2', 'dummy3' """ ), ) @@ -5062,75 +4452,6 @@ def test_clone_promotable_unsupported(self): ) -class ResourcesReferencedFromAcl(TestCase, AssertPcsMixin): - def setUp(self): - self.temp_cib = get_tmp_file("tier1_resource_referenced_from_acl") - write_file_to_tmpfile(empty_cib, self.temp_cib) - self.pcs_runner = PcsRunner(self.temp_cib.name) - self.pcs_runner.mock_settings = get_mock_settings() - - def tearDown(self): - self.temp_cib.close() - - def test_remove_referenced_primitive_resource(self): - self.assert_pcs_success( - "resource create dummy ocf:pcsmock:minimal".split() - ) - self.assert_pcs_success( - "acl role create read-dummy read id dummy".split() - ) - self.assert_pcs_success( - "resource delete dummy".split(), - stderr_full="Deleting Resource - dummy\n", - ) - - def test_remove_group_with_referenced_primitive_resource(self): - self.assert_pcs_success( - "resource create dummy1 ocf:pcsmock:minimal".split() - ) - self.assert_pcs_success( - "resource create dummy2 ocf:pcsmock:minimal".split() - ) - self.assert_pcs_success( - "resource group add dummy-group dummy1 dummy2".split() - ) - self.assert_pcs_success( - "acl role create read-dummy read id dummy2".split() - ) - self.assert_pcs_success( - "resource delete dummy-group".split(), - stderr_full=( - "Removing group: dummy-group (and all resources within group)\n" - "Stopping all resources in group: dummy-group...\n" - "Deleting Resource - dummy1\n" - "Deleting Resource (and group) - dummy2\n" - ), - ) - - def test_remove_referenced_group(self): - self.assert_pcs_success( - "resource create dummy1 ocf:pcsmock:minimal".split() - ) - self.assert_pcs_success( - "resource create dummy2 ocf:pcsmock:minimal".split() - ) - self.assert_pcs_success( - "resource group add dummy-group dummy1 dummy2".split() - ) - self.assert_pcs_success( - "acl role create acl-role-a read id dummy-group".split() - ) - self.assert_pcs_success( - "resource delete dummy-group".split(), - stderr_full=( - "Removing group: dummy-group (and all resources within group)\n" - "Stopping all resources in group: dummy-group...\n" - "Deleting Resource - dummy1\n" - "Deleting Resource (and group) - dummy2\n" - ), - ) - - class CloneMasterUpdate(TestCase, AssertPcsMixin): def setUp(self): self.temp_cib = get_tmp_file("tier1_resource_clone_master_update") @@ -5419,45 +4740,6 @@ def test_transform_master_with_meta_on_update(self): ) -class ResourceRemoveWithTicket(TestCase, AssertPcsMixin): - def setUp(self): - self.temp_cib = get_tmp_file("tier1_resource_remove_with_ticket") - write_file_to_tmpfile(empty_cib, self.temp_cib) - self.pcs_runner = PcsRunner(self.temp_cib.name) - self.pcs_runner.mock_settings = get_mock_settings() - - def tearDown(self): - self.temp_cib.close() - - def test_remove_ticket(self): - self.assert_pcs_success("resource create A ocf:pcsmock:minimal".split()) - role = str(const.PCMK_ROLE_PROMOTED_LEGACY).lower() - self.assert_pcs_success( - f"constraint ticket add T {role} A loss-policy=fence".split(), - stderr_full=( - f"Deprecation Warning: Value '{role}' of option role is " - "deprecated and might be removed in a future release, therefore it " - "should not be used, use " - f"'{const.PCMK_ROLE_PROMOTED}' value instead\n" - ), - ) - self.assert_pcs_success( - "constraint ticket config".split(), - ( - "Ticket Constraints:\n" - f" {const.PCMK_ROLE_PROMOTED_PRIMARY} resource 'A' depends on ticket 'T'\n" - " loss-policy=fence\n" - ), - ) - self.assert_pcs_success( - "resource delete A".split(), - stderr_full=( - "Removing Constraint - ticket-T-A-Master\n" - "Deleting Resource - A\n" - ), - ) - - class BundleCommon( TestCase, get_assert_pcs_effect_mixin( @@ -5538,44 +4820,6 @@ def test_rkt(self): ) -class BundleDelete(BundleCommon): - def test_without_primitive(self): - self.fixture_bundle("B") - self.assert_effect( - "resource delete B".split(), - "", - stderr_full="Deleting bundle 'B'\n", - ) - - def test_with_primitive(self): - self.fixture_bundle("B") - self.fixture_primitive("R", "B") - self.assert_effect( - "resource delete B".split(), - "", - stderr_full=( - "Deleting bundle 'B' and its inner resource 'R'\n" - "Deleting Resource - R\n" - ), - ) - - def test_remove_primitive(self): - self.fixture_bundle("B") - self.fixture_primitive("R", "B") - self.assert_effect( - "resource delete R".split(), - """ - - - - - - - """, - stderr_full="Deleting Resource - R\n", - ) - - class BundleGroup(BundleCommon): def test_group_delete_primitive(self): self.fixture_bundle("B") diff --git a/pcs_test/tier1/legacy/test_stonith.py b/pcs_test/tier1/legacy/test_stonith.py index 2dbd38925..e62f001db 100644 --- a/pcs_test/tier1/legacy/test_stonith.py +++ b/pcs_test/tier1/legacy/test_stonith.py @@ -1,6 +1,5 @@ # pylint: disable=too-many-lines import json -import shutil from textwrap import dedent from threading import Lock from unittest import TestCase @@ -1360,314 +1359,6 @@ def test_stonith_fence_confirm(self): "Error: must specify one (and only one) node to confirm fenced\n", ) - def test_stonith_delete_removes_level(self): - shutil.copyfile(rc("cib-empty-with3nodes.xml"), self.temp_cib.name) - - self.assert_pcs_success( - "stonith create n1-ipmi fence_pcsmock_minimal".split(), - ) - self.assert_pcs_success( - "stonith create n2-ipmi fence_pcsmock_minimal".split(), - ) - self.assert_pcs_success( - "stonith create n1-apc1 fence_pcsmock_minimal".split(), - ) - self.assert_pcs_success( - "stonith create n1-apc2 fence_pcsmock_minimal".split(), - ) - self.assert_pcs_success( - "stonith create n2-apc1 fence_pcsmock_minimal".split(), - ) - self.assert_pcs_success( - "stonith create n2-apc2 fence_pcsmock_minimal".split(), - ) - self.assert_pcs_success( - "stonith create n2-apc3 fence_pcsmock_minimal".split(), - ) - self.assert_pcs_success_all( - [ - "stonith level add 1 rh7-1 n1-ipmi".split(), - "stonith level add 2 rh7-1 n1-apc1,n1-apc2,n2-apc2".split(), - "stonith level add 1 rh7-2 n2-ipmi".split(), - "stonith level add 2 rh7-2 n2-apc1,n2-apc2,n2-apc3".split(), - ] - ) - - if PCMK_2_0_3_PLUS: - self.assert_pcs_success( - ["stonith"], - outdent( - """\ - * n1-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped - * n2-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped - * n1-apc1\t(stonith:fence_pcsmock_minimal):\tStopped - * n1-apc2\t(stonith:fence_pcsmock_minimal):\tStopped - * n2-apc1\t(stonith:fence_pcsmock_minimal):\tStopped - * n2-apc2\t(stonith:fence_pcsmock_minimal):\tStopped - * n2-apc3\t(stonith:fence_pcsmock_minimal):\tStopped - - Fencing Levels: - Target: rh7-1 - Level 1 - n1-ipmi - Level 2 - n1-apc1,n1-apc2,n2-apc2 - Target: rh7-2 - Level 1 - n2-ipmi - Level 2 - n2-apc1,n2-apc2,n2-apc3 - """ - ), - despace=True, - ) - else: - self.assert_pcs_success( - ["stonith"], - outdent( - """\ - n1-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped - n2-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped - n1-apc1\t(stonith:fence_pcsmock_minimal):\tStopped - n1-apc2\t(stonith:fence_pcsmock_minimal):\tStopped - n2-apc1\t(stonith:fence_pcsmock_minimal):\tStopped - n2-apc2\t(stonith:fence_pcsmock_minimal):\tStopped - n2-apc3\t(stonith:fence_pcsmock_minimal):\tStopped - - Fencing Levels: - Target: rh7-1 - Level 1 - n1-ipmi - Level 2 - n1-apc1,n1-apc2,n2-apc2 - Target: rh7-2 - Level 1 - n2-ipmi - Level 2 - n2-apc1,n2-apc2,n2-apc3 - """ - ), - ) - - self.assert_pcs_success( - "stonith delete n2-apc2".split(), - stderr_full="Deleting Resource - n2-apc2\n", - ) - - if PCMK_2_0_3_PLUS: - self.assert_pcs_success( - ["stonith"], - outdent( - """\ - * n1-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped - * n2-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped - * n1-apc1\t(stonith:fence_pcsmock_minimal):\tStopped - * n1-apc2\t(stonith:fence_pcsmock_minimal):\tStopped - * n2-apc1\t(stonith:fence_pcsmock_minimal):\tStopped - * n2-apc3\t(stonith:fence_pcsmock_minimal):\tStopped - - Fencing Levels: - Target: rh7-1 - Level 1 - n1-ipmi - Level 2 - n1-apc1,n1-apc2 - Target: rh7-2 - Level 1 - n2-ipmi - Level 2 - n2-apc1,n2-apc3 - """ - ), - despace=True, - ) - else: - self.assert_pcs_success( - ["stonith"], - outdent( - """\ - n1-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped - n2-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped - n1-apc1\t(stonith:fence_pcsmock_minimal):\tStopped - n1-apc2\t(stonith:fence_pcsmock_minimal):\tStopped - n2-apc1\t(stonith:fence_pcsmock_minimal):\tStopped - n2-apc3\t(stonith:fence_pcsmock_minimal):\tStopped - - Fencing Levels: - Target: rh7-1 - Level 1 - n1-ipmi - Level 2 - n1-apc1,n1-apc2 - Target: rh7-2 - Level 1 - n2-ipmi - Level 2 - n2-apc1,n2-apc3 - """ - ), - ) - - self.assert_pcs_success( - "stonith remove n2-apc1".split(), - stderr_full="Deleting Resource - n2-apc1\n", - ) - - if PCMK_2_0_3_PLUS: - self.assert_pcs_success( - ["stonith"], - outdent( - """\ - * n1-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped - * n2-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped - * n1-apc1\t(stonith:fence_pcsmock_minimal):\tStopped - * n1-apc2\t(stonith:fence_pcsmock_minimal):\tStopped - * n2-apc3\t(stonith:fence_pcsmock_minimal):\tStopped - - Fencing Levels: - Target: rh7-1 - Level 1 - n1-ipmi - Level 2 - n1-apc1,n1-apc2 - Target: rh7-2 - Level 1 - n2-ipmi - Level 2 - n2-apc3 - """ - ), - despace=True, - ) - else: - self.assert_pcs_success( - ["stonith"], - outdent( - """\ - n1-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped - n2-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped - n1-apc1\t(stonith:fence_pcsmock_minimal):\tStopped - n1-apc2\t(stonith:fence_pcsmock_minimal):\tStopped - n2-apc3\t(stonith:fence_pcsmock_minimal):\tStopped - - Fencing Levels: - Target: rh7-1 - Level 1 - n1-ipmi - Level 2 - n1-apc1,n1-apc2 - Target: rh7-2 - Level 1 - n2-ipmi - Level 2 - n2-apc3 - """ - ), - ) - - self.assert_pcs_success( - "stonith delete n2-apc3".split(), - stderr_full="Deleting Resource - n2-apc3\n", - ) - - if PCMK_2_0_3_PLUS: - self.assert_pcs_success( - ["stonith"], - outdent( - """\ - * n1-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped - * n2-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped - * n1-apc1\t(stonith:fence_pcsmock_minimal):\tStopped - * n1-apc2\t(stonith:fence_pcsmock_minimal):\tStopped - - Fencing Levels: - Target: rh7-1 - Level 1 - n1-ipmi - Level 2 - n1-apc1,n1-apc2 - Target: rh7-2 - Level 1 - n2-ipmi - """ - ), - despace=True, - ) - else: - self.assert_pcs_success( - ["stonith"], - outdent( - """\ - n1-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped - n2-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped - n1-apc1\t(stonith:fence_pcsmock_minimal):\tStopped - n1-apc2\t(stonith:fence_pcsmock_minimal):\tStopped - - Fencing Levels: - Target: rh7-1 - Level 1 - n1-ipmi - Level 2 - n1-apc1,n1-apc2 - Target: rh7-2 - Level 1 - n2-ipmi - """ - ), - ) - - self.assert_pcs_success( - "stonith remove n1-apc1".split(), - stderr_full="Deleting Resource - n1-apc1\n", - ) - - if PCMK_2_0_3_PLUS: - self.assert_pcs_success( - ["stonith"], - outdent( - """\ - * n1-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped - * n2-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped - * n1-apc2\t(stonith:fence_pcsmock_minimal):\tStopped - - Fencing Levels: - Target: rh7-1 - Level 1 - n1-ipmi - Level 2 - n1-apc2 - Target: rh7-2 - Level 1 - n2-ipmi - """ - ), - despace=True, - ) - else: - self.assert_pcs_success( - ["stonith"], - outdent( - """\ - n1-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped - n2-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped - n1-apc2\t(stonith:fence_pcsmock_minimal):\tStopped - - Fencing Levels: - Target: rh7-1 - Level 1 - n1-ipmi - Level 2 - n1-apc2 - Target: rh7-2 - Level 1 - n2-ipmi - """ - ), - ) - - self.assert_pcs_success( - "stonith delete n1-apc2".split(), - stderr_full="Deleting Resource - n1-apc2\n", - ) - - if PCMK_2_0_3_PLUS: - self.assert_pcs_success( - ["stonith"], - outdent( - """\ - * n1-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped - * n2-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped - - Fencing Levels: - Target: rh7-1 - Level 1 - n1-ipmi - Target: rh7-2 - Level 1 - n2-ipmi - """ - ), - despace=True, - ) - else: - self.assert_pcs_success( - ["stonith"], - outdent( - """\ - n1-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped - n2-ipmi\t(stonith:fence_pcsmock_minimal):\tStopped - - Fencing Levels: - Target: rh7-1 - Level 1 - n1-ipmi - Target: rh7-2 - Level 1 - n2-ipmi - """ - ), - ) - def test_no_stonith_warning(self): self.pcs_runner.corosync_conf_opt = self.temp_corosync_conf.name self.assert_pcs_success( @@ -1689,10 +1380,7 @@ def test_no_stonith_warning(self): ) self.pcs_runner.corosync_conf_opt = None - self.assert_pcs_success( - "stonith delete test_stonith".split(), - stderr_full="Deleting Resource - test_stonith\n", - ) + self.assert_pcs_success("stonith delete test_stonith".split()) self.pcs_runner.corosync_conf_opt = self.temp_corosync_conf.name self.assert_pcs_success( diff --git a/pcs_test/tier1/resource/test_remove.py b/pcs_test/tier1/resource/test_remove.py new file mode 100644 index 000000000..8d7266d56 --- /dev/null +++ b/pcs_test/tier1/resource/test_remove.py @@ -0,0 +1,292 @@ +from textwrap import dedent +from unittest import TestCase + +from lxml import etree + +from pcs_test.tools.assertions import AssertPcsMixin +from pcs_test.tools.cib import get_assert_pcs_effect_mixin +from pcs_test.tools.fixture_cib import modify_cib_file +from pcs_test.tools.misc import ( + get_test_resource, + get_tmp_file, + write_data_to_tmpfile, +) +from pcs_test.tools.pcs_runner import PcsRunner + + +def fixture_primtive_xml(resource_id: str) -> str: + return f""" + + """ + + +FIXTURE_CLONED_GROUP_XML = f""" + + + {fixture_primtive_xml("R2")} + + +""" +FIXTURE_GROUP_XML = f""" + + {fixture_primtive_xml("R3")} + {fixture_primtive_xml("R4")} + +""" +FIXTURE_TAG_XML = """ + + + + +""" +FIXTURE_LOCATION_CONSTRAINT1 = """ + +""" +FIXTURE_LOCATION_CONSTRAINT2 = """ + +""" +FIXTURE_LOCATION_CONSTRAINT3 = """ + +""" +FIXTURE_ALL_LOCATION_CONSTRAINTS = f""" + {FIXTURE_LOCATION_CONSTRAINT1} + {FIXTURE_LOCATION_CONSTRAINT2} + {FIXTURE_LOCATION_CONSTRAINT3} +""" +FIXTURE_SET_CONSTRAINT = """ + + + + + + +""" +FIXTURE_ALL_CONSTRAINTS_XML = f""" + {FIXTURE_ALL_LOCATION_CONSTRAINTS} + {FIXTURE_SET_CONSTRAINT} +""" + + +class ResourceRemoveDeleteBase( + get_assert_pcs_effect_mixin( + lambda cib: etree.tostring(etree.parse(cib).find(".//resources")) + ), +): + command = "" + + def setUp(self): + self.temp_cib = get_tmp_file("tier1_resource_remove") + cib_data = modify_cib_file( + get_test_resource("cib-empty.xml"), + resources=f""" + + {fixture_primtive_xml("R1")} + {FIXTURE_GROUP_XML} + {FIXTURE_CLONED_GROUP_XML} + + """, + constraints=f""" + + {FIXTURE_ALL_CONSTRAINTS_XML} + + """, + tags=f""" + + {FIXTURE_TAG_XML} + + """, + ) + write_data_to_tmpfile(cib_data, self.temp_cib) + self.pcs_runner = PcsRunner(self.temp_cib.name) + + def assert_constraints(self, constraints_xml): + self.assert_resources_xml_in_cib( + constraints_xml, + lambda cib: etree.tostring(etree.parse(cib).find(".//constraints")), + ) + + def assert_tags(self, tags_xml): + self.assert_resources_xml_in_cib( + tags_xml, + lambda cib: etree.tostring(etree.parse(cib).find(".//tags")), + ) + + def tearDown(self): + self.temp_cib.close() + + def test_no_args(self): + self.assert_pcs_fail( + ["resource", self.command], + stderr_start=f"\nUsage: pcs resource {self.command}...", + ) + + def test_nonexistent_resource(self): + self.assert_pcs_fail( + ["resource", self.command, "nonexistent"], + stderr_full="Error: Unable to find resource: 'nonexistent'\n", + ) + + def test_primitive(self): + self.assert_effect_single( + ["resource", self.command, "R1"], + f""" + + {FIXTURE_GROUP_XML} + {FIXTURE_CLONED_GROUP_XML} + + """, + stderr_full="", + ) + self.assert_constraints( + f""" + + {FIXTURE_ALL_CONSTRAINTS_XML} + + """ + ) + self.assert_tags( + f""" + + {FIXTURE_TAG_XML} + + """ + ) + + def test_remove_dependencies(self): + self.assert_effect_single( + ["resource", self.command, "R2"], + f""" + + {fixture_primtive_xml("R1")} + {FIXTURE_GROUP_XML} + + """, + stderr_full=dedent( + """\ + Removing dependant elements: + Clone: 'R2-clone' + Group: 'R2-group' + Location constraints: 'location-constraint1', 'location-constraint2', 'location-constraint3' + """ + ), + ) + self.assert_constraints( + f""" + + {FIXTURE_SET_CONSTRAINT} + + """ + ) + + def test_remove_references(self): + self.assert_effect_single( + ["resource", self.command, "R3"], + f""" + + {fixture_primtive_xml("R1")} + + {fixture_primtive_xml("R4")} + + {FIXTURE_CLONED_GROUP_XML} + + """, + stderr_full=dedent( + """\ + Removing references: + Resource 'R3' from: + Group: 'R3R4-group' + Resource set: 'set1' + Tag: 'TAG' + """ + ), + ) + self.assert_constraints( + f""" + + {FIXTURE_ALL_LOCATION_CONSTRAINTS} + + + + + + + """ + ) + self.assert_tags( + """ + + + + + + """ + ) + + def test_remove_all_resources(self): + self.assert_effect_single( + ["resource", self.command, "R1", "R2", "R3", "R4"], + "", + stderr_full=dedent( + """\ + Removing dependant elements: + Clone: 'R2-clone' + Colocation constraint: 'colocation-constraint' + Groups: 'R2-group', 'R3R4-group' + Location constraints: 'location-constraint1', 'location-constraint2', 'location-constraint3' + Resource set: 'set1' + Tag: 'TAG' + """ + ), + ) + self.assert_constraints("") + self.assert_tags("") + + +class ResourceRemove(ResourceRemoveDeleteBase, TestCase): + command = "remove" + + +class ResourceDelete(ResourceRemoveDeleteBase, TestCase): + command = "delete" + + +class ResourceReferencedInAcl(AssertPcsMixin, TestCase): + def setUp(self): + self.temp_cib = get_tmp_file("tier1_resource_remove_referenced_in_acl") + cib_data = modify_cib_file( + get_test_resource("cib-empty.xml"), + resources=f""" + + {fixture_primtive_xml("R1")} + + """, + optional_in_conf=""" + + + + + + + + + """, + ) + write_data_to_tmpfile(cib_data, self.temp_cib) + self.pcs_runner = PcsRunner(self.temp_cib.name) + + def tearDown(self): + self.temp_cib.close() + + def test_remove_primitive(self): + self.assert_pcs_success( + ["resource", "delete", "R1"], + stderr_full=dedent( + """\ + Removing dependant element: + Acl permission: 'PERMISSION' + Removing references: + Acl permission 'PERMISSION' from: + Acl role: 'ROLE' + """ + ), + ) diff --git a/pcs_test/tier1/stonith/test_remove.py b/pcs_test/tier1/stonith/test_remove.py new file mode 100644 index 000000000..75a2ec6db --- /dev/null +++ b/pcs_test/tier1/stonith/test_remove.py @@ -0,0 +1,260 @@ +from textwrap import dedent +from unittest import TestCase + +from lxml import etree + +from pcs_test.tools.assertions import AssertPcsMixin +from pcs_test.tools.cib import get_assert_pcs_effect_mixin +from pcs_test.tools.fixture_cib import modify_cib_file +from pcs_test.tools.misc import ( + get_test_resource, + get_tmp_file, + write_data_to_tmpfile, +) +from pcs_test.tools.pcs_runner import PcsRunner + + +def fixture_stonith_primitive_xml(resource_id: str) -> str: + return f""" + + """ + + +FIXTURE_TAG_XML = """ + + + + +""" +FIXTURE_SET_CONSTRAINT = """ + + + + + + +""" +FIXTURE_FENCING_LEVEL_XML = """ + +""" + + +class StonithRemoveDeleteBase( + get_assert_pcs_effect_mixin( + lambda cib: etree.tostring(etree.parse(cib).find(".//resources")) + ), +): + command = "" + + def setUp(self): + self.temp_cib = get_tmp_file("tier1_stonith_remove") + cib_data = modify_cib_file( + get_test_resource("cib-empty.xml"), + resources=f""" + + {fixture_stonith_primitive_xml("S1")} + {fixture_stonith_primitive_xml("S2")} + {fixture_stonith_primitive_xml("S3")} + + """, + constraints=f""" + + {FIXTURE_SET_CONSTRAINT} + + """, + tags=f""" + + {FIXTURE_TAG_XML} + + """, + fencing_topology=f""" + + {FIXTURE_FENCING_LEVEL_XML} + + """, + ) + write_data_to_tmpfile(cib_data, self.temp_cib) + self.pcs_runner = PcsRunner(self.temp_cib.name) + + def assert_constraints(self, constraints_xml): + self.assert_resources_xml_in_cib( + constraints_xml, + lambda cib: etree.tostring(etree.parse(cib).find(".//constraints")), + ) + + def assert_tags(self, tags_xml): + self.assert_resources_xml_in_cib( + tags_xml, + lambda cib: etree.tostring(etree.parse(cib).find(".//tags")), + ) + + def assert_fencing_topology(self, fencing_xml): + self.assert_resources_xml_in_cib( + fencing_xml, + lambda cib: etree.tostring( + etree.parse(cib).find(".//fencing-topology") + ), + ) + + def tearDown(self): + self.temp_cib.close() + + def test_no_args(self): + self.assert_pcs_fail( + ["stonith", self.command], + stderr_start=f"\nUsage: pcs stonith {self.command}...", + ) + + def test_nonexistent_resource(self): + self.assert_pcs_fail( + ["stonith", self.command, "nonexistent"], + stderr_full="Error: Unable to find stonith resource: 'nonexistent'\n", + ) + + def test_single_resource(self): + self.assert_effect_single( + ["stonith", self.command, "S1"], + f""" + + {fixture_stonith_primitive_xml("S2")} + {fixture_stonith_primitive_xml("S3")} + + """, + stderr_full="", + ) + self.assert_constraints( + f""" + + {FIXTURE_SET_CONSTRAINT} + + """ + ) + self.assert_tags( + f""" + + {FIXTURE_TAG_XML} + + """ + ) + self.assert_fencing_topology( + f""" + + {FIXTURE_FENCING_LEVEL_XML} + + """ + ) + + def test_remove_references(self): + self.assert_effect_single( + ["stonith", self.command, "S2"], + f""" + + {fixture_stonith_primitive_xml("S1")} + {fixture_stonith_primitive_xml("S3")} + + """, + stderr_full=dedent( + """\ + Removing references: + Resource 'S2' from: + Fencing level: 'fencing-level' + Resource set: 'set1' + Tag: 'TAG' + """ + ), + ) + self.assert_constraints( + """ + + + + + + + + """ + ) + self.assert_tags( + """ + + + + + + """ + ) + self.assert_fencing_topology( + """ + + + + """ + ) + + def test_remove_all_resources(self): + self.assert_effect_single( + ["stonith", self.command, "S1", "S2", "S3"], + "", + stderr_full=dedent( + """\ + Removing dependant elements: + Colocation constraint: 'colocation-constraint' + Fencing level: 'fencing-level' + Resource set: 'set1' + Tag: 'TAG' + """ + ), + ) + self.assert_constraints("") + self.assert_tags("") + self.assert_fencing_topology("") + + +class StonithRemove(StonithRemoveDeleteBase, TestCase): + command = "remove" + + +class StonithDelete(StonithRemoveDeleteBase, TestCase): + command = "delete" + + +class StonithReferencedInAcl(AssertPcsMixin, TestCase): + def setUp(self): + self.temp_cib = get_tmp_file("tier1_stonith_remove_referenced_in_acl") + cib_data = modify_cib_file( + get_test_resource("cib-empty.xml"), + resources=f""" + + {fixture_stonith_primitive_xml("S1")} + + """, + optional_in_conf=""" + + + + + + + + + """, + ) + write_data_to_tmpfile(cib_data, self.temp_cib) + self.pcs_runner = PcsRunner(self.temp_cib.name) + + def tearDown(self): + self.temp_cib.close() + + def test_remove_primitive(self): + self.assert_pcs_success( + ["stonith", "delete", "S1"], + stderr_full=dedent( + """\ + Removing dependant element: + Acl permission: 'PERMISSION' + Removing references: + Acl permission 'PERMISSION' from: + Acl role: 'ROLE' + """ + ), + ) diff --git a/pcs_test/tier1/test_tag.py b/pcs_test/tier1/test_tag.py index 96d62d7fa..e45c4f0c6 100644 --- a/pcs_test/tier1/test_tag.py +++ b/pcs_test/tier1/test_tag.py @@ -607,63 +607,6 @@ class TagDelete( command = "delete" -class ResourceRemoveDeleteBase(TestTagMixin): - command = None - - @staticmethod - def fixture_error_message(resource, tags): - return ( - "Error: Unable to remove resource '{resource}' because it is " - "referenced in {tags}: {tag_list}\n".format( - resource=resource, - tags="tags" if len(tags) > 1 else "the tag", - tag_list=", ".join(f"'{tag}'" for tag in tags), - ) - ) - - def test_resource_not_referenced_in_tags(self): - self.assert_pcs_success( - ["resource", self.command, "not-in-tags"], - stderr_full="Deleting Resource - not-in-tags\n", - ) - self.assert_resources_xml_in_cib(self.fixture_tags_xml()) - - def test_resource_referenced_in_a_single_tag(self): - self.assert_pcs_fail( - ["resource", self.command, "x1"], - self.fixture_error_message("x1", ["tag1"]), - ) - self.assert_resources_xml_in_cib(self.fixture_tags_xml()) - - def test_resource_referenced_in_multiple_tags(self): - self.assert_pcs_fail( - ["resource", self.command, "x2"], - self.fixture_error_message("x2", ["tag1", "tag2"]), - ) - self.assert_resources_xml_in_cib(self.fixture_tags_xml()) - - def test_related_clone_resource_in_tag(self): - self.assert_pcs_fail( - ["resource", self.command, "y2"], - self.fixture_error_message("y2", ["tag3"]), - ) - self.assert_resources_xml_in_cib(self.fixture_tags_xml()) - - -class ResourceRemove( - ResourceRemoveDeleteBase, - TestCase, -): - command = "remove" - - -class ResourceDelete( - ResourceRemoveDeleteBase, - TestCase, -): - command = "delete" - - class TagUpdate(TestTagMixin, TestCase): def test_success_add_new_existing_before_and_remove(self): self.assert_effect( diff --git a/pcs_test/tools/resources_dto.py b/pcs_test/tools/resources_dto.py index 21fc534cc..22ea18e5f 100644 --- a/pcs_test/tools/resources_dto.py +++ b/pcs_test/tools/resources_dto.py @@ -28,6 +28,9 @@ STATEFUL_AGENT_NAME = ResourceAgentNameDto( standard="ocf", provider="pcsmock", type="stateful" ) +REMOTE_AGENT_NAME = ResourceAgentNameDto( + standard="ocf", provider="pacemaker", type="remote" +) STONITH_AGENT_NAME = ResourceAgentNameDto( standard="stonith", provider=None, type="fence_pcsmock_minimal" From fc8e22be1d3cd4c50bfee933ba184116dc2ce04d Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Mon, 23 Sep 2024 14:38:11 +0200 Subject: [PATCH 032/227] Improve fencing level deletion --- pcs/lib/cib/fencing_topology.py | 103 ++----- pcs/lib/cib/remove_elements.py | 30 +- pcs/lib/cib/tools.py | 64 ++-- pcs/resource.py | 1 - .../tier0/lib/cib/test_fencing_topology.py | 285 +++++------------- pcs_test/tier0/lib/cib/test_tools.py | 202 +++++-------- 6 files changed, 239 insertions(+), 446 deletions(-) diff --git a/pcs/lib/cib/fencing_topology.py b/pcs/lib/cib/fencing_topology.py index bb628d006..27081fe75 100644 --- a/pcs/lib/cib/fencing_topology.py +++ b/pcs/lib/cib/fencing_topology.py @@ -28,8 +28,9 @@ from pcs.lib.cib.resource.stonith import is_stonith_resource from pcs.lib.cib.tools import ( find_unique_id, - get_element_by_id, - get_root, + multivalue_attr_contains_value, + multivalue_attr_delete_value, + multivalue_attr_has_any_values, ) from pcs.lib.errors import LibraryError from pcs.lib.pacemaker.state import _Element as StateElement @@ -101,7 +102,7 @@ def remove_all_levels(topology_el): # the whole change to be rejected by pacemaker with a "permission denied" # message. # https://bugzilla.redhat.com/show_bug.cgi?id=1642514 - for level_el in topology_el.findall("fencing-level"): + for level_el in topology_el.findall(TAG_FENCING_LEVEL): level_el.getparent().remove(level_el) @@ -170,82 +171,42 @@ def remove_levels_by_params( return report_list -def remove_device_from_all_levels_dont_remove_elements( +def find_levels_with_device( topology_el: _Element, device_id: str ) -> list[_Element]: """ - Remove specified stonith device from all fencing levels. Do not remove - fencing-level elements with empty devices attribute. Instead, return a list - of all the elements from which the device was removed. + Return list of all fencing-level elements that reference the specified + device - topology_el -- etree element with levels to remove the device from - device_id -- stonith device to remove + topology_el -- etree element with fencing levels + device_id -- id of the stonith device """ - elements = [] - for level_el in topology_el.findall(TAG_FENCING_LEVEL): - old_devices = level_el.get(_DEVICES_ATTRIBUTE, "") - _remove_device_from_level(level_el, device_id) - new_devices = level_el.get(_DEVICES_ATTRIBUTE, "") - - if old_devices != new_devices: - elements.append(level_el) - - return elements + return [ + level_el + for level_el in topology_el.findall(TAG_FENCING_LEVEL) + if multivalue_attr_contains_value( + level_el, _DEVICES_ATTRIBUTE, device_id + ) + ] -def remove_device_from_all_levels( - topology_el: _Element, device_id: str -) -> None: +def remove_device_from_level(level_el: _Element, device_id: str) -> None: """ - Remove specified stonith device from all fencing levels. + Remove specified stonith device from fencing level. - topology_el -- etree element with levels to remove the device from + level_el -- level element from which the device is removed device_id -- stonith device to remove """ - # Do not ever remove a fencing-topology element, even if it is empty. There - # may be ACLs set in pacemaker which allow "write" for fencing-level - # elements (adding, changing and removing) but not fencing-topology - # elements. In such a case, removing a fencing-topology element would cause - # the whole change to be rejected by pacemaker with a "permission denied" - # message. - # https://bugzilla.redhat.com/show_bug.cgi?id=1642514 - for level_el in remove_device_from_all_levels_dont_remove_elements( - topology_el, device_id - ): - _remove_fencing_level_if_empty(level_el) + multivalue_attr_delete_value(level_el, _DEVICES_ATTRIBUTE, device_id) -def remove_device_from_one_level( - topology_el: _Element, level_id: str, device_id: str -) -> None: +def has_any_devices(level_el: _Element) -> bool: """ - Remove specified stonith device from specified fencing level. + Return whether there are any devices references in the fencing level - topology_el -- etree element with levels to remove the device from - level_id -- level element from which the device is removed - device_id -- stonith device to remove + level_el -- fencing level element """ - level_el = get_element_by_id(get_root(topology_el), level_id) - _remove_device_from_level(level_el, device_id) - _remove_fencing_level_if_empty(level_el) - - -def _remove_device_from_level(level_el: _Element, device_id: str) -> None: - new_devices = [ - dev - for dev in level_el.get(_DEVICES_ATTRIBUTE, "").split(",") - if dev != device_id - ] - level_el.set(_DEVICES_ATTRIBUTE, ",".join(new_devices)) - - -def _remove_fencing_level_if_empty(level_el: _Element) -> None: - if level_el.get(_DEVICES_ATTRIBUTE, "") != "": - return - - parent = level_el.getparent() - if parent is not None: - parent.remove(level_el) + return multivalue_attr_has_any_values(level_el, _DEVICES_ATTRIBUTE) def export(topology_el): @@ -258,7 +219,7 @@ def export(topology_el): etree topology_el -- etree element to export """ export_levels = [] - for level_el in topology_el.iterfind("fencing-level"): + for level_el in topology_el.iterfind(TAG_FENCING_LEVEL): target_type = target_value = None if "target" in level_el.attrib: target_type = TARGET_TYPE_NODE @@ -278,7 +239,7 @@ def export(topology_el): "target_type": target_type, "target_value": target_value, "level": level_el.get("index"), - "devices": level_el.get("devices").split(","), + "devices": level_el.get(_DEVICES_ATTRIBUTE).split(","), } ) return export_levels @@ -300,8 +261,10 @@ def verify( used_nodes: Set[str] = set() used_devices: Set[str] = set() - for level_el in topology_el.iterfind("fencing-level"): - used_devices.update(str(level_el.get("devices", "")).split(",")) + for level_el in topology_el.iterfind(TAG_FENCING_LEVEL): + used_devices.update( + str(level_el.get(_DEVICES_ATTRIBUTE, "")).split(",") + ) if "target" in level_el.attrib: used_nodes.add(str(level_el.get("target", ""))) @@ -458,7 +421,7 @@ def _validate_level_target_devices_does_not_exist( def _append_level_element(tree, level, target_type, target_value, devices): level_el = etree.SubElement( - tree, "fencing-level", index=str(level), devices=",".join(devices) + tree, TAG_FENCING_LEVEL, index=str(level), devices=",".join(devices) ) if target_type == TARGET_TYPE_NODE: level_el.set("target", target_value) @@ -509,9 +472,9 @@ def _find_level_elements( ) if xpath_attrs: return tree.xpath( - f"fencing-level[{xpath_attrs}]", + f"{TAG_FENCING_LEVEL}[{xpath_attrs}]", var_devices=(",".join(devices) if devices else ""), var_level=level, **xpath_vars, ) - return tree.findall("fencing-level") + return tree.findall(TAG_FENCING_LEVEL) diff --git a/pcs/lib/cib/remove_elements.py b/pcs/lib/cib/remove_elements.py index 6e0856b44..e4ca03700 100644 --- a/pcs/lib/cib/remove_elements.py +++ b/pcs/lib/cib/remove_elements.py @@ -20,10 +20,7 @@ StringCollection, StringSequence, ) -from pcs.lib.cib import ( - const, - sections, -) +from pcs.lib.cib import const from pcs.lib.cib.constraint.common import ( is_constraint, is_set_constraint, @@ -33,8 +30,9 @@ is_location_rule, ) from pcs.lib.cib.fencing_topology import ( - remove_device_from_all_levels_dont_remove_elements, - remove_device_from_one_level, + find_levels_with_device, + has_any_devices, + remove_device_from_level, ) from pcs.lib.cib.resource.clone import is_any_clone from pcs.lib.cib.resource.common import ( @@ -336,22 +334,17 @@ def _remove_element_reference( # need to be removed using this reference mapping. Therefore, we need to # only remove elements that do not have id here, such as obj_ref and # resource_ref. + try: + element = get_element_by_id(cib, referenced_in_id) + except ElementNotFound: + return if referenced_in_tag == const.TAG_FENCING_LEVEL: - if not sections.exists(cib, sections.FENCING_TOPOLOGY): - return - - remove_device_from_one_level( - get_fencing_topology(cib), referenced_in_id, element_id - ) + remove_device_from_level(element, element_id) return if referenced_in_tag not in _REFERENCE_TAG_XPATH_MAP: return - try: - element = get_element_by_id(cib, referenced_in_id) - except ElementNotFound: - return for el in cast( list[_Element], element.xpath( @@ -433,13 +426,14 @@ def _get_dependencies_to_remove( elements_to_process.extend(_get_element_references(el)) elements_to_process.extend(_get_inner_references(el)) - for level_el in remove_device_from_all_levels_dont_remove_elements( + for level_el in find_levels_with_device( get_fencing_topology(get_root(el)), element_id ): removing_references_from[element_id].add( str(level_el.attrib["id"]) ) - if level_el.get("devices", "") == "": + remove_device_from_level(level_el, element_id) + if not has_any_devices(level_el): elements_to_process.append(level_el) parent_el = el.getparent() diff --git a/pcs/lib/cib/tools.py b/pcs/lib/cib/tools.py index ce341cf3f..979d517cc 100644 --- a/pcs/lib/cib/tools.py +++ b/pcs/lib/cib/tools.py @@ -559,25 +559,6 @@ def _get_configuration(element: _Element) -> _Element: return get_configuration(get_root(element)) -def _find_elements_without_id_referencing_id( - element: _Element, - referenced_id: str, -) -> list[_Element]: - """ - Find elements which are referencing specified id (resource or tag). - - element -- any element within CIB tree - referenced_id -- id which references should be found - """ - return cast( - list[_Element], - _get_configuration(element).xpath( - _ELEMENTS_WITH_IDREF_WITHOUT_ID_XPATH, - referenced_id=referenced_id, - ), - ) - - def find_elements_referencing_id( element: _Element, referenced_id: str, @@ -629,10 +610,49 @@ def remove_element_by_id(cib: _Element, element_id: str) -> None: """ Remove element with specified id from cib element. """ - # raise LibraryError error if configuration section is not in cib - _ = get_configuration(cib) - try: remove_one_element(get_element_by_id(cib, element_id)) except ElementNotFound: pass + + +def multivalue_attr_contains_value( + element: _Element, attr_name: str, value: str +) -> bool: + """ + Return whether attribute, that can contain multiple comma separated values, + contains specified value + + element -- any element + attribute_name -- name of the multivalue attribute + value -- value that should be present in the attribute + """ + return value in str(element.attrib[attr_name]).split(",") + + +def multivalue_attr_has_any_values(element: _Element, attr_name: str) -> bool: + """ + Return whether attribute, that can contain multiple comma separated values, + contains any value + + element -- any element + attribute_name -- name of the multivalue attribute + """ + return element.attrib[attr_name] != "" + + +def multivalue_attr_delete_value( + element: _Element, attr_name: str, value: str +) -> None: + """ + Remove value from attribute, that can contain multiple comma separated + values. + + element -- any element + attribute_name -- name of the multivalue attribute + value -- value to remove + """ + new_attribute_value = [ + val for val in str(element.attrib[attr_name]).split(",") if val != value + ] + element.set(attr_name, ",".join(new_attribute_value)) diff --git a/pcs/resource.py b/pcs/resource.py index 2d7d0c920..75d351d9b 100644 --- a/pcs/resource.py +++ b/pcs/resource.py @@ -2363,7 +2363,6 @@ def is_bundle_running(bundle_id): return True -# moved to pcs.lib.cib.fencing_topology.remove_device_from_all_levels def stonith_level_rm_device(cib_dom, stn_id): """ Commandline options: no options diff --git a/pcs_test/tier0/lib/cib/test_fencing_topology.py b/pcs_test/tier0/lib/cib/test_fencing_topology.py index 9d8f29e80..bf89ba979 100644 --- a/pcs_test/tier0/lib/cib/test_fencing_topology.py +++ b/pcs_test/tier0/lib/cib/test_fencing_topology.py @@ -16,7 +16,6 @@ from pcs.common.reports import codes as report_codes from pcs.common.reports.item import ReportItem from pcs.lib.cib import fencing_topology as lib -from pcs.lib.cib.tools import ElementNotFound from pcs.lib.errors import LibraryError from pcs.lib.pacemaker.state import ClusterState @@ -545,254 +544,114 @@ def test_no_such_level_ignore_missing(self): ) -class RemoveDeviceFromAllLevelsDontRemoveElement(TestCase, CibMixin): +class FindLevelsReferencingDevice(TestCase, CibMixin): def setUp(self): self.cib = self.get_cib() self.tree = self.cib.find("configuration/fencing-topology") - def test_success(self): - elements = lib.remove_device_from_all_levels_dont_remove_elements( - self.tree, "d3" - ) - assert_xml_equal( - """ - - - - - - - - - - - - - """, - etree_to_str(self.tree), - ) + def test_find_levels(self): + elements = lib.find_levels_with_device(self.tree, "d1") + self.assertEqual( [ - self.tree.find(".//fencing-level[@id='fl2']"), - self.tree.find(".//fencing-level[@id='fl4']"), - self.tree.find(".//fencing-level[@id='fl5']"), - self.tree.find(".//fencing-level[@id='fl7']"), + self.tree.find("./fencing-level[@id='fl1']"), + self.tree.find("./fencing-level[@id='fl3']"), + self.tree.find("./fencing-level[@id='fl6']"), ], elements, ) - def test_non_unique_device_ids(self): - tree = etree.fromstring(FIXTURE_NON_UNIQUE_DEVICES) - elements = lib.remove_device_from_all_levels_dont_remove_elements( - tree, "d1" + def test_find_nonexistent_level(self): + elements = lib.find_levels_with_device(self.tree, "device") + + self.assertEqual([], elements) + + +class RemoveDeviceFromLevel(TestCase): + # pylint: disable=no-self-use + def test_remove_single(self): + element = etree.fromstring( + """ + + """ ) + + lib.remove_device_from_level(element, "d1") + assert_xml_equal( """ - - - - + """, - etree_to_str(tree), - ) - self.assertEqual( - [ - tree.find(".//fencing-level[@id='fl1']"), - tree.find(".//fencing-level[@id='fl2']"), - ], - elements, + etree_to_str(element), ) - def test_no_such_device(self): - original_xml = etree_to_str(self.tree) - elements = lib.remove_device_from_all_levels_dont_remove_elements( - self.tree, "dX" + def test_remove_single_multiple_devices(self): + element = etree.fromstring( + """ + + """ ) - assert_xml_equal(original_xml, etree_to_str(self.tree)) - self.assertEqual([], elements) + lib.remove_device_from_level(element, "d1") -class RemoveDeviceFromAllLevels(TestCase, CibMixin): - def setUp(self): - self.cib = self.get_cib() - self.tree = self.cib.find("configuration/fencing-topology") - - def test_success(self): - lib.remove_device_from_all_levels(self.tree, "d3") assert_xml_equal( """ - - - - - - - - - - + """, - etree_to_str(self.tree), + etree_to_str(element), ) - def test_non_unique_device_ids(self): - # pylint: disable=no-self-use - tree = etree.fromstring(FIXTURE_NON_UNIQUE_DEVICES) - lib.remove_device_from_all_levels(tree, "d1") - assert_xml_equal( + def test_remove_multiple_occurrences(self): + element = etree.fromstring( + """ + """ - - - - """, - etree_to_str(tree), ) - def test_no_such_device(self): - original_xml = etree_to_str(self.tree) - lib.remove_device_from_all_levels(self.tree, "dX") - assert_xml_equal(original_xml, etree_to_str(self.tree)) - + lib.remove_device_from_level(element, "d1") -class RemoveDeviceFromOneLevel(TestCase, CibMixin): - def setUp(self): - self.cib = self.get_cib() - self.tree = self.cib.find("configuration/fencing-topology") - - def test_keep_fencing_level(self): - lib.remove_device_from_one_level(self.tree, "fl1", "d1") assert_xml_equal( """ - - - - - - - - - - - - + """, - etree_to_str(self.tree), + etree_to_str(element), ) - def test_remove_fencing_level(self): - lib.remove_device_from_one_level(self.tree, "fl2", "d3") - assert_xml_equal( + +class HasAnyDevices(TestCase): + def test_true(self): + element = etree.fromstring( + """ + + """ + ) + + self.assertTrue(lib.has_any_devices(element)) + + def test_false(self): + element = etree.fromstring( + """ + """ - - - - - - - - - - - - """, - etree_to_str(self.tree), ) - def test_nonexistent_level(self): - with self.assertRaises(ElementNotFound): - lib.remove_device_from_one_level(self.tree, "nonexistent", "d1") + self.assertFalse(lib.has_any_devices(element)) class Export(TestCase, CibMixin): diff --git a/pcs_test/tier0/lib/cib/test_tools.py b/pcs_test/tier0/lib/cib/test_tools.py index 52c623027..10bd30b85 100644 --- a/pcs_test/tier0/lib/cib/test_tools.py +++ b/pcs_test/tier0/lib/cib/test_tools.py @@ -143,29 +143,6 @@ ] ) -FIXTURE_NODE_REFERENCES_IN_CONSTRAINTS = """ - - - - - -""" - -FIXTURE_NODE_REFERENCES_IN_FENCING_LEVELS = """ - - - - - -""" -FIXTURE_ALL_SECTION_WITH_NODE_REFERENCES = "".join( - [ - FIXTURE_NODE_REFERENCES_IN_CONSTRAINTS, - FIXTURE_NODE_REFERENCES_IN_FENCING_LEVELS, - FIXTURE_REFERENCES_IN_TAGS, - ] -) - class CibToolsTest(TestCase): def setUp(self): @@ -1190,7 +1167,7 @@ def test_element_not_found(self): lib.remove_element_by_id(cib, "id-not-found") assert_xml_equal(expected_cib, etree_to_str(cib)) - def test_remove_element_without_references(self): + def test_remove_element(self): cib = etree.fromstring( """ @@ -1212,92 +1189,6 @@ def test_remove_element_without_references(self): lib.remove_element_by_id(cib, "id-without-references") assert_xml_equal(expected_cib, etree_to_str(cib)) - def test_remove_element_with_references(self): - cib = etree.fromstring( - """ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - """ - ) - expected_cib = """ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - """ - lib.remove_element_by_id(cib, "id-with-references") - assert_xml_equal(expected_cib, etree_to_str(cib)) - def test_assert_raised_for_duplicate_ids(self): expected_cib = """ @@ -1332,16 +1223,83 @@ def test_assert_not_raised_for_duplicate_ids(self): lib.remove_element_by_id(cib, "duplicate-id") assert_xml_equal(expected_cib, etree_to_str(cib)) - def test_missing_configuration_section(self): - expected_cib = """ - - """ - cib = etree.fromstring(expected_cib) - assert_raise_library_error( - lambda: lib.remove_element_by_id(cib, "missing-config-section"), - fixture.error( - report_codes.CIB_CANNOT_FIND_MANDATORY_SECTION, - section="configuration", - ), + +class MultivalueAttrContainsValue(TestCase): + def test_true(self): + element = etree.fromstring( + '' + ) + for attr in ["attr1", "attr2", "attr3"]: + with self.subTest(value=attr): + self.assertTrue( + lib.multivalue_attr_contains_value(element, attr, "A") + ) + + def test_false(self): + element = etree.fromstring( + '' + ) + for attr in ["attr1", "attr2", "attr3"]: + with self.subTest(value=attr): + self.assertFalse( + lib.multivalue_attr_contains_value(element, attr, "A") + ) + + def test_no_attribute(self): + element = etree.fromstring('') + self.assertRaises( + KeyError, + lambda: lib.multivalue_attr_contains_value(element, "attr1", "A"), + ) + + +class MultivalueAttrDeleteValue(TestCase): + # pylint: disable=no-self-use + def test_remove_one(self): + element = etree.fromstring('') + lib.multivalue_attr_delete_value(element, "attr1", "A") + assert_xml_equal('', etree_to_str(element)) + + def test_remove_one_multiple_from_multiple(self): + element = etree.fromstring('') + lib.multivalue_attr_delete_value(element, "attr1", "A") + assert_xml_equal( + '', etree_to_str(element) + ) + + def test_remove_multiple_occurrences(self): + element = etree.fromstring('') + lib.multivalue_attr_delete_value(element, "attr1", "A") + assert_xml_equal( + '', etree_to_str(element) + ) + + def test_no_attribute(self): + element = etree.fromstring('') + self.assertRaises( + KeyError, + lambda: lib.multivalue_attr_delete_value(element, "attr1", "A"), + ) + + +class MultivalueAttrHasAnyValues(TestCase): + def test_true(self): + element = etree.fromstring( + '' + ) + for attr in ["attr1", "attr2"]: + with self.subTest(value=attr): + self.assertTrue( + lib.multivalue_attr_has_any_values(element, attr) + ) + + def test_false(self): + element = etree.fromstring('') + self.assertFalse(lib.multivalue_attr_has_any_values(element, "attr1")) + + def test_no_attribute(self): + element = etree.fromstring('') + self.assertRaises( + KeyError, + lambda: lib.multivalue_attr_has_any_values(element, "attr1"), ) - assert_xml_equal(expected_cib, etree_to_str(cib)) From 67554b5662993e834e01c849cebef14290056c57 Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Tue, 24 Sep 2024 16:25:57 +0200 Subject: [PATCH 033/227] fix code after review --- mypy.ini | 2 - pcs/common/reports/messages.py | 2 +- pcs/lib/cib/remove_elements.py | 137 ++++--- pcs/lib/commands/cib.py | 41 +- .../tier0/lib/cib/test_fencing_topology.py | 2 +- .../tier0/lib/cib/test_remove_elements.py | 375 ++++++++++-------- pcs_test/tier0/lib/cib/test_tools.py | 1 - pcs_test/tier0/lib/commands/test_cib.py | 193 ++++++++- pcs_test/tools/resources_dto.py | 3 - 9 files changed, 498 insertions(+), 258 deletions(-) diff --git a/mypy.ini b/mypy.ini index 94ef949ec..b6e385cf0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -89,7 +89,6 @@ disallow_untyped_defs = True [mypy-pcs.lib.cib.remove_elements] disallow_untyped_defs = True -ignore_errors = False [mypy-pcs.lib.cib.resource.clone] disallow_untyped_defs = True @@ -127,7 +126,6 @@ disallow_untyped_defs = True [mypy-pcs.lib.commands.cib] disallow_untyped_defs = True -ignore_errors = False [mypy-pcs.lib.commands.cib_options] disallow_untyped_defs = True diff --git a/pcs/common/reports/messages.py b/pcs/common/reports/messages.py index 4d3ee69e6..b14ed7493 100644 --- a/pcs/common/reports/messages.py +++ b/pcs/common/reports/messages.py @@ -6350,7 +6350,7 @@ def message(self) -> str: @dataclass(frozen=True) class CannotStopResourcesBeforeDeleting(ReportItemMessage): """ - Cannot stop a resource that is being removed + Cannot stop resources that are being removed resource_id_list -- ids of resources that cannot be stopped """ diff --git a/pcs/lib/cib/remove_elements.py b/pcs/lib/cib/remove_elements.py index e4ca03700..10c7f9d9d 100644 --- a/pcs/lib/cib/remove_elements.py +++ b/pcs/lib/cib/remove_elements.py @@ -4,6 +4,7 @@ from typing import ( Iterable, Mapping, + Sequence, cast, ) @@ -130,6 +131,9 @@ def __init__(self, cib: _Element, ids: StringCollection): _get_dependencies_to_remove(supported_elements) ) + # We need to use ids of the elements, since we will work with cib, but + # the the elements were instantiated using the wip_cib, which means we + # cannot reuse the elements self._ids_to_remove = element_ids_to_remove self._dependant_element_ids = self._ids_to_remove - initial_ids self._missing_ids = set(missing_ids) @@ -149,15 +153,15 @@ def __init__(self, cib: _Element, ids: StringCollection): for el in get_elements_by_ids(cib, all_ids)[0] } self._element_references = removing_references_from - self._resource_ids_to_disable = set( - str(el.attrib["id"]) - for el in get_elements_by_ids(cib, element_ids_to_remove)[0] + self._resources_to_disable = [ + el + for el in get_elements_by_ids(cib, sorted(element_ids_to_remove))[0] if is_resource(el) - ) + ] @property - def resources_to_disable(self) -> set[str]: - return set(self._resource_ids_to_disable) + def resources_to_disable(self) -> list[_Element]: + return list(self._resources_to_disable) @property def ids_to_remove(self) -> set[str]: @@ -196,31 +200,71 @@ def unsupported_elements(self) -> UnsupportedElements: element_id: self._id_tag_map[element_id] for element_id in self._unsupported_ids }, + # the list of tags should match the validations done in + # _validate_element_types function supported_element_types=["constraint", "location rule", "resource"], ) -def stop_resources( - cib: _Element, state: _Element, elements: ElementsToRemove +def warn_resource_unmanaged( + state: _Element, resource_ids: StringSequence ) -> reports.ReportItemList: """ + + state -- state of the cluster + resource_ids -- ids of resources to be checked + """ + report_list: reports.ReportItemList = [] + try: + parser = ClusterStatusParser(state) + try: + status_dto = parser.status_xml_to_dto() + except ClusterStatusParsingError as e: + report_list.append(cluster_status_parsing_error_to_report(e)) + return report_list + report_list.extend(parser.get_warnings()) + + status = ResourcesStatusFacade.from_resources_status_dto(status_dto) + report_list.extend( + reports.ReportItem.warning( + reports.messages.ResourceIsUnmanaged(resource_id) + ) + for resource_id in resource_ids + if status.is_state( + resource_id, + None, + ResourceState.UNMANAGED, + ) + ) + except NotImplementedError: + # TODO remove when issue with bundles in status is fixed + report_list.extend( + reports.ReportItem.warning( + reports.messages.ResourceIsUnmanaged(resource_id) + ) + for resource_id in resource_ids + if not is_resource_managed(state, resource_id) + ) + + return report_list + + +def stop_resources( + cib: _Element, resource_elements: Sequence[_Element] +) -> None: + """ Stop all resources that are going to be removed. cib -- the whole cib - state -- state of the cluster - elements -- elements planned to be removed + resource_elements -- sequence of elements that should be stopped """ - resources_to_disable = sorted(elements.resources_to_disable) - report_list = _warn_resource_unmanaged(state, resources_to_disable) - resources, _ = get_elements_by_ids(cib, resources_to_disable) provider = IdProvider(cib) - for el in resources: + for el in resource_elements: disable(el, provider) - return report_list def ensure_resources_stopped( - state: _Element, elements: ElementsToRemove + state: _Element, resource_ids: StringSequence ) -> reports.ReportItemList: """ Ensure that all resources that should be stopped are stopped. @@ -228,7 +272,6 @@ def ensure_resources_stopped( state -- state of the cluster elements -- elements planned to be removed """ - resources_to_disable = sorted(elements.resources_to_disable) not_stopped_ids = [] report_list: reports.ReportItemList = [] try: @@ -243,7 +286,7 @@ def ensure_resources_stopped( status = ResourcesStatusFacade.from_resources_status_dto(status_dto) not_stopped_ids = [ resource_id - for resource_id in resources_to_disable + for resource_id in resource_ids if not status.is_state( resource_id, None, @@ -259,7 +302,7 @@ def ensure_resources_stopped( # TODO remove when issue with bundles in status is fixed not_stopped_ids = [ resource_id - for resource_id in resources_to_disable + for resource_id in resource_ids if ensure_resource_state(False, state, resource_id).severity.level == reports.item.ReportItemSeverity.ERROR ] @@ -310,6 +353,8 @@ def _validate_element_types( unsupported_elements = [] for el in elements: + # valid elements should match the valid tags reported from + # ElementsToRemove.unsupported_elements property if is_constraint(el) or is_location_rule(el) or is_resource(el): supported_elements.append(el) else: @@ -355,44 +400,6 @@ def _remove_element_reference( remove_one_element(el) -def _warn_resource_unmanaged( - state: _Element, resource_ids: StringSequence -) -> reports.ReportItemList: - report_list: reports.ReportItemList = [] - try: - parser = ClusterStatusParser(state) - try: - status_dto = parser.status_xml_to_dto() - except ClusterStatusParsingError as e: - report_list.append(cluster_status_parsing_error_to_report(e)) - return report_list - report_list.extend(parser.get_warnings()) - - status = ResourcesStatusFacade.from_resources_status_dto(status_dto) - report_list.extend( - reports.ReportItem.warning( - reports.messages.ResourceIsUnmanaged(resource_id) - ) - for resource_id in resource_ids - if status.is_state( - resource_id, - None, - ResourceState.UNMANAGED, - ) - ) - except NotImplementedError: - # TODO remove when issue with bundles in status is fixed - report_list.extend( - reports.ReportItem.warning( - reports.messages.ResourceIsUnmanaged(resource_id) - ) - for resource_id in resource_ids - if not is_resource_managed(state, resource_id) - ) - - return report_list - - def _get_dependencies_to_remove( elements: Iterable[_Element], ) -> tuple[set[str], dict[str, set[str]]]: @@ -415,6 +422,11 @@ def _get_dependencies_to_remove( el = elements_to_process.pop(0) element_id = str(el.attrib["id"]) + # Elements with these tags are only used for referencing other elements. + # The 'id' attribute in these does not represent the id of the element + # itself, but the id of the element that they refer to. + # Therefore, it does not make sense to try finding any references to + # these elements. if el.tag not in ( const.TAG_OBJREF, const.TAG_RESOURCE_REF, @@ -438,6 +450,13 @@ def _get_dependencies_to_remove( parent_el = el.getparent() if parent_el is not None: + # We only want to remove parent elements that are invalid when + # empty. There may be ACLs set in pacemaker which allow "write" for + # the child elements (adding, changing and removing) but not their + # parent elements. In such case, removing the parent element would + # cause the whole change to be rejected by pacemaker with a + # "permission denied" message. + # https://bugzilla.redhat.com/show_bug.cgi?id=1642514 if _is_empty_after_inner_el_removal(parent_el): elements_to_process.append(parent_el) parent_el.remove(el) @@ -446,6 +465,10 @@ def _get_dependencies_to_remove( if parent_id is not None: removing_references_from[element_id].add(parent_id) + # Removing references from parent elements that are going to be removed + # (they are present in the 'element_ids_to_remove') is unncesary, since all + # of the child elements are removed when parent is removed. This means we + # can filter out such references from the resulting mapping. for key in list(removing_references_from): removing_references_from[key].difference_update(element_ids_to_remove) if not removing_references_from[key]: diff --git a/pcs/lib/commands/cib.py b/pcs/lib/commands/cib.py index 25a7b7b66..c81b65fb4 100644 --- a/pcs/lib/commands/cib.py +++ b/pcs/lib/commands/cib.py @@ -1,4 +1,7 @@ -from typing import Collection +from typing import ( + Collection, + Sequence, +) from lxml.etree import _Element @@ -9,6 +12,7 @@ ensure_resources_stopped, remove_specified_elements, stop_resources, + warn_resource_unmanaged, ) from pcs.lib.cib.resource.guest_node import is_guest_node from pcs.lib.cib.resource.remote_node import ( @@ -39,11 +43,7 @@ def remove_elements( if report_processor.report_list( _validate_elements_to_remove(elements_to_remove) - ).has_errors: - raise LibraryError() - - if report_processor.report_list( - _ensure_not_guest_remote(cib, ids) + + _ensure_not_guest_remote(cib, ids) ).has_errors: raise LibraryError() @@ -55,14 +55,18 @@ def remove_elements( ) if env.is_cib_live and reports.codes.FORCE not in force_flags: - cib = _stop_resources_wait(env, cib, elements_to_remove) + cib = _stop_resources_wait( + env, cib, elements_to_remove.resources_to_disable + ) remove_specified_elements(cib, elements_to_remove) env.push_cib() def _stop_resources_wait( - env: LibraryEnvironment, cib: _Element, elements_to_remove: ElementsToRemove + env: LibraryEnvironment, + cib: _Element, + resource_elements: Sequence[_Element], ) -> _Element: """ Stop all resources that are going to be removed. Push cib, wait for the @@ -70,28 +74,29 @@ def _stop_resources_wait( If not, report errors. Return cib with the applied changes. cib -- whole cib - elements -- elements planned to be removed + resource_elements -- resources that should be stopped """ - resources_to_disable = elements_to_remove.resources_to_disable - if not resources_to_disable: + if not resource_elements: return cib + + resource_ids = [str(el.attrib["id"]) for el in resource_elements] + env.report_processor.report( reports.ReportItem.info( - reports.messages.StoppingResourcesBeforeDeleting( - sorted(resources_to_disable) - ) + reports.messages.StoppingResourcesBeforeDeleting(resource_ids) ) ) if env.report_processor.report_list( - stop_resources(cib, env.get_cluster_state(), elements_to_remove) + warn_resource_unmanaged(env.get_cluster_state(), resource_ids) ).has_errors: raise LibraryError() + stop_resources(cib, resource_elements) env.push_cib() env.wait_for_idle() if env.report_processor.report_list( - ensure_resources_stopped(env.get_cluster_state(), elements_to_remove) + ensure_resources_stopped(env.get_cluster_state(), resource_ids) ).has_errors: raise LibraryError() @@ -105,9 +110,7 @@ def _validate_elements_to_remove( for missing_id in sorted(element_to_remove.missing_ids): report_list.append( reports.ReportItem.error( - reports.messages.IdNotFound( - missing_id, ["configuration element"] - ) + reports.messages.IdNotFound(missing_id, []) ) ) diff --git a/pcs_test/tier0/lib/cib/test_fencing_topology.py b/pcs_test/tier0/lib/cib/test_fencing_topology.py index bf89ba979..bf9e60164 100644 --- a/pcs_test/tier0/lib/cib/test_fencing_topology.py +++ b/pcs_test/tier0/lib/cib/test_fencing_topology.py @@ -544,7 +544,7 @@ def test_no_such_level_ignore_missing(self): ) -class FindLevelsReferencingDevice(TestCase, CibMixin): +class FindLevelsWithDevice(TestCase, CibMixin): def setUp(self): self.cib = self.get_cib() self.tree = self.cib.find("configuration/fencing-topology") diff --git a/pcs_test/tier0/lib/cib/test_remove_elements.py b/pcs_test/tier0/lib/cib/test_remove_elements.py index 212e96e00..5f51514ce 100644 --- a/pcs_test/tier0/lib/cib/test_remove_elements.py +++ b/pcs_test/tier0/lib/cib/test_remove_elements.py @@ -1,10 +1,12 @@ # pylint: disable=too-many-lines +from typing import Optional from unittest import ( TestCase, mock, ) from lxml import etree +from lxml.etree import _Element from pcs.common import reports from pcs.lib.cib import const @@ -57,6 +59,40 @@ def _constraints(*argv): EXPECTED_TYPES_FOR_REMOVE = ["constraint", "location rule", "resource"] +def fixture_primitive_to_disable(cib: _Element) -> list[_Element]: + return [cib.find("./configuration/resources/primitive[@id='A']")] + + +FIXTURE_GROUP = """ + + + + +""" + + +def fixture_group_to_disable(cib: _Element) -> list[_Element]: + return [ + cib.find("./configuration/resources/group[@id='G']/primitive[@id='A']"), + cib.find("./configuration/resources/group[@id='G']/primitive[@id='B']"), + cib.find("./configuration/resources/group[@id='G']"), + ] + + +FIXTURE_CLONE = """ + + + +""" + + +def fixture_clone_to_disable(cib: _Element) -> list[_Element]: + return [ + cib.find("./configuration/resources/clone[@id='C']/primitive[@id='A']"), + cib.find("./configuration/resources/clone[@id='C']"), + ] + + class GetCibMixin: def get_cib(self, **modifier_shortcuts): return etree.fromstring(modify_cib(self.cib, **modifier_shortcuts)) @@ -71,20 +107,20 @@ def assert_elements_to_remove( self, elements_to_remove: lib.ElementsToRemove, ids_to_remove: set[str], - resources_to_disable: set[str] = set(), + resources_to_disable: Optional[list[etree._Element]] = None, dependant_elements: lib.DependantElements = lib.DependantElements({}), element_references: lib.ElementReferences = lib.ElementReferences( {}, {} ), - missing_ids: set[str] = set(), + missing_ids: Optional[set[str]] = None, unsupported_elements: lib.UnsupportedElements = lib.UnsupportedElements( {}, EXPECTED_TYPES_FOR_REMOVE ), ): - # pylint: disable=dangerous-default-value self.assertEqual(elements_to_remove.ids_to_remove, ids_to_remove) self.assertEqual( - elements_to_remove.resources_to_disable, resources_to_disable + elements_to_remove.resources_to_disable, + resources_to_disable or [], ) self.assertEqual( elements_to_remove.dependant_elements, dependant_elements @@ -92,7 +128,7 @@ def assert_elements_to_remove( self.assertEqual( elements_to_remove.element_references, element_references ) - self.assertEqual(elements_to_remove.missing_ids, missing_ids) + self.assertEqual(elements_to_remove.missing_ids, missing_ids or set()) self.assertEqual( elements_to_remove.unsupported_elements, unsupported_elements ) @@ -209,7 +245,7 @@ def test_resource_primitive(self): self.assert_elements_to_remove( elements_to_remove, {"A"}, - resources_to_disable={"A"}, + resources_to_disable=fixture_primitive_to_disable(cib), ) def test_resource_primitive_in_tag(self): @@ -236,7 +272,7 @@ def test_resource_primitive_in_tag(self): self.assert_elements_to_remove( elements_to_remove, {"A", "T2"}, - resources_to_disable={"A"}, + resources_to_disable=fixture_primitive_to_disable(cib), dependant_elements=lib.DependantElements({"T2": const.TAG_TAG}), element_references=lib.ElementReferences( {"A": {"T1"}}, @@ -267,7 +303,7 @@ def test_resource_primitive_constraints(self): self.assert_elements_to_remove( elements_to_remove, {"A", "l1", "o1", "c1", "t1"}, - resources_to_disable={"A"}, + resources_to_disable=fixture_primitive_to_disable(cib), dependant_elements=lib.DependantElements( { "l1": const.TAG_CONSTRAINT_LOCATION, @@ -280,12 +316,9 @@ def test_resource_primitive_constraints(self): def test_resource_group(self): cib = self.get_cib( - resources=""" + resources=f""" - - - - + {FIXTURE_GROUP} """ ) @@ -294,7 +327,7 @@ def test_resource_group(self): self.assert_elements_to_remove( elements_to_remove, {"G", "A", "B"}, - resources_to_disable={"G", "A", "B"}, + resources_to_disable=fixture_group_to_disable(cib), dependant_elements=lib.DependantElements( { "A": const.TAG_RESOURCE_PRIMITIVE, @@ -305,12 +338,9 @@ def test_resource_group(self): def test_resource_group_member(self): cib = self.get_cib( - resources=""" + resources=f""" - - - - + {FIXTURE_GROUP} """ ) @@ -319,7 +349,9 @@ def test_resource_group_member(self): self.assert_elements_to_remove( elements_to_remove, {"A"}, - resources_to_disable={"A"}, + resources_to_disable=[ + cib.find("./configuration/resources/group/primitive[@id='A']"), + ], element_references=lib.ElementReferences( {"A": {"G"}}, { @@ -331,12 +363,9 @@ def test_resource_group_member(self): def test_resource_group_all_members(self): cib = self.get_cib( - resources=""" + resources=f""" - - - - + {FIXTURE_GROUP} """ ) @@ -345,7 +374,7 @@ def test_resource_group_all_members(self): self.assert_elements_to_remove( elements_to_remove, {"A", "B", "G"}, - resources_to_disable={"A", "B", "G"}, + resources_to_disable=fixture_group_to_disable(cib), dependant_elements=lib.DependantElements( {"G": const.TAG_RESOURCE_GROUP} ), @@ -353,12 +382,9 @@ def test_resource_group_all_members(self): def test_resource_group_constraints(self): cib = self.get_cib( - resources=""" + resources=f""" - - - - + {FIXTURE_GROUP} """, constraints=""" @@ -376,7 +402,7 @@ def test_resource_group_constraints(self): self.assert_elements_to_remove( elements_to_remove, {"G", "A", "B", "l1", "o1", "c1", "t1"}, - resources_to_disable={"G", "A", "B"}, + resources_to_disable=fixture_group_to_disable(cib), dependant_elements=lib.DependantElements( { "A": const.TAG_RESOURCE_PRIMITIVE, @@ -391,12 +417,9 @@ def test_resource_group_constraints(self): def test_group_in_tag(self): cib = self.get_cib( - resources=""" + resources=f""" - - - - + {FIXTURE_GROUP} """, tags=""" @@ -412,7 +435,7 @@ def test_group_in_tag(self): self.assert_elements_to_remove( elements_to_remove, {"G", "A", "B", "T"}, - resources_to_disable={"G", "A", "B"}, + resources_to_disable=fixture_group_to_disable(cib), dependant_elements=lib.DependantElements( { "A": const.TAG_RESOURCE_PRIMITIVE, @@ -424,11 +447,9 @@ def test_group_in_tag(self): def test_resource_clone(self): cib = self.get_cib( - resources=""" + resources=f""" - - - + {FIXTURE_CLONE} """ ) @@ -436,7 +457,7 @@ def test_resource_clone(self): self.assert_elements_to_remove( elements_to_remove, {"C", "A"}, - resources_to_disable={"C", "A"}, + resources_to_disable=fixture_clone_to_disable(cib), dependant_elements=lib.DependantElements( {"A": const.TAG_RESOURCE_PRIMITIVE} ), @@ -444,11 +465,9 @@ def test_resource_clone(self): def test_resource_clone_primitive(self): cib = self.get_cib( - resources=""" + resources=f""" - - - + {FIXTURE_CLONE} """ ) @@ -457,7 +476,7 @@ def test_resource_clone_primitive(self): self.assert_elements_to_remove( elements_to_remove, {"C", "A"}, - resources_to_disable={"C", "A"}, + resources_to_disable=fixture_clone_to_disable(cib), dependant_elements=lib.DependantElements( {"C": const.TAG_RESOURCE_CLONE} ), @@ -465,11 +484,9 @@ def test_resource_clone_primitive(self): def test_resource_clone_constraints(self): cib = self.get_cib( - resources=""" + resources=f""" - - - + {FIXTURE_CLONE} """, constraints=""" @@ -484,7 +501,7 @@ def test_resource_clone_constraints(self): self.assert_elements_to_remove( elements_to_remove, {"C", "A", "l1", "l2"}, - resources_to_disable={"C", "A"}, + resources_to_disable=fixture_clone_to_disable(cib), dependant_elements=lib.DependantElements( { "A": const.TAG_RESOURCE_PRIMITIVE, @@ -496,11 +513,9 @@ def test_resource_clone_constraints(self): def test_resource_clone_in_tag(self): cib = self.get_cib( - resources=""" + resources=f""" - - - + {FIXTURE_CLONE} """, tags=""" @@ -516,7 +531,7 @@ def test_resource_clone_in_tag(self): self.assert_elements_to_remove( elements_to_remove, {"C", "A", "T"}, - resources_to_disable={"C", "A"}, + resources_to_disable=fixture_clone_to_disable(cib), dependant_elements=lib.DependantElements( {"A": const.TAG_RESOURCE_PRIMITIVE, "T": const.TAG_TAG} ), @@ -532,7 +547,11 @@ def test_resource_bundle(self): ) elements_to_remove = lib.ElementsToRemove(cib, ["B"]) self.assert_elements_to_remove( - elements_to_remove, {"B"}, resources_to_disable={"B"} + elements_to_remove, + {"B"}, + resources_to_disable=[ + cib.find("./configuration/resources/bundle[@id='B']") + ], ) def test_resource_bundle_with_primitive(self): @@ -549,7 +568,10 @@ def test_resource_bundle_with_primitive(self): self.assert_elements_to_remove( elements_to_remove, {"B", "A"}, - resources_to_disable={"B", "A"}, + resources_to_disable=[ + cib.find("./configuration/resources/bundle/primitive[@id='A']"), + cib.find("./configuration/resources/bundle[@id='B']"), + ], dependant_elements=lib.DependantElements( {"A": const.TAG_RESOURCE_PRIMITIVE} ), @@ -569,7 +591,9 @@ def test_resource_bundle_primitive(self): self.assert_elements_to_remove( elements_to_remove, {"A"}, - resources_to_disable={"A"}, + resources_to_disable=[ + cib.find("./configuration/resources/bundle/primitive[@id='A']") + ], element_references=lib.ElementReferences( {"A": {"B"}}, { @@ -602,7 +626,9 @@ def test_resource_referenced_in_acl(self): self.assert_elements_to_remove( elements_to_remove, {"vohrablo", "ucesat_se2"}, - resources_to_disable={"vohrablo"}, + resources_to_disable=[ + cib.find("./configuration/resources/primitive[@id='vohrablo']") + ], dependant_elements=lib.DependantElements( {"ucesat_se2": const.TAG_ACL_PERMISSION} ), @@ -617,12 +643,9 @@ def test_resource_referenced_in_acl(self): def test_resource_referenced_in_acl_indirectly(self): cib = self.get_cib( - resources=""" + resources=f""" - - - - + {FIXTURE_GROUP} """, optional_in_conf=""" @@ -640,7 +663,7 @@ def test_resource_referenced_in_acl_indirectly(self): self.assert_elements_to_remove( elements_to_remove, {"G", "A", "B", "PERMISSION"}, - resources_to_disable={"G", "A", "B"}, + resources_to_disable=fixture_group_to_disable(cib), dependant_elements=lib.DependantElements( { "A": const.TAG_RESOURCE_PRIMITIVE, @@ -657,7 +680,7 @@ def test_resource_referenced_in_acl_indirectly(self): ), ) - def test_resource_remove_fencing_level(self): + def test_resource_keep_fencing_level(self): cib = self.get_cib( resources=""" @@ -675,7 +698,7 @@ def test_resource_remove_fencing_level(self): self.assert_elements_to_remove( elements_to_remove, {"A"}, - resources_to_disable={"A"}, + resources_to_disable=fixture_primitive_to_disable(cib), element_references=lib.ElementReferences( {"A": {"fl"}}, { @@ -685,7 +708,7 @@ def test_resource_remove_fencing_level(self): ), ) - def test_resource_keep_fencing_level(self): + def test_resource_remove_fencing_level(self): cib = self.get_cib( resources=""" @@ -702,7 +725,7 @@ def test_resource_keep_fencing_level(self): self.assert_elements_to_remove( elements_to_remove, {"A", "fl-NODE-A-1"}, - resources_to_disable={"A"}, + resources_to_disable=fixture_primitive_to_disable(cib), dependant_elements=lib.DependantElements( {"fl-NODE-A-1": const.TAG_FENCING_LEVEL} ), @@ -731,7 +754,7 @@ def test_resource_in_constraint_set(self): self.assert_elements_to_remove( elements_to_remove, {"A"}, - resources_to_disable={"A"}, + resources_to_disable=fixture_primitive_to_disable(cib), element_references=lib.ElementReferences( {"A": {"set1"}}, { @@ -766,7 +789,7 @@ def test_resource_in_constraint_set_remove_set_keep_constraint(self): self.assert_elements_to_remove( elements_to_remove, {"A", "set1"}, - resources_to_disable={"A"}, + resources_to_disable=fixture_primitive_to_disable(cib), dependant_elements=lib.DependantElements( {"set1": const.TAG_RESOURCE_SET} ), @@ -800,7 +823,7 @@ def test_resource_in_constraint_set_remove_set_remove_constraint(self): self.assert_elements_to_remove( elements_to_remove, {"A", "set1", "c1"}, - resources_to_disable={"A"}, + resources_to_disable=fixture_primitive_to_disable(cib), dependant_elements=lib.DependantElements( { "set1": const.TAG_RESOURCE_SET, @@ -835,7 +858,7 @@ def test_resource_in_multiple_sets(self): self.assert_elements_to_remove( elements_to_remove, {"A", "set1", "c1", "set2", "c2"}, - resources_to_disable={"A"}, + resources_to_disable=fixture_primitive_to_disable(cib), dependant_elements=lib.DependantElements( { "set1": const.TAG_RESOURCE_SET, @@ -860,7 +883,10 @@ def test_resource_legacy_promotable_clone(self): self.assert_elements_to_remove( elements_to_remove, {"MS", "A"}, - resources_to_disable={"MS", "A"}, + resources_to_disable=[ + cib.find("./configuration/resources/master/primitive[@id='A']"), + cib.find("./configuration/resources/master/[@id='MS']"), + ], dependant_elements=lib.DependantElements( {"A": const.TAG_RESOURCE_PRIMITIVE} ), @@ -880,7 +906,10 @@ def test_resource_legacy_promotable_clone_inner_element(self): self.assert_elements_to_remove( elements_to_remove, {"A", "MS"}, - resources_to_disable={"A", "MS"}, + resources_to_disable=[ + cib.find("./configuration/resources/master/primitive[@id='A']"), + cib.find("./configuration/resources/master/[@id='MS']"), + ], dependant_elements=lib.DependantElements({"MS": "master"}), ) @@ -897,7 +926,7 @@ def test_missing_elements(self): elements_to_remove, {"A"}, missing_ids={"B", "C", "D"}, - resources_to_disable={"A"}, + resources_to_disable=fixture_primitive_to_disable(cib), ) def test_unsupported_id_types(self): @@ -1177,11 +1206,80 @@ def test_remove_references_from_ignored_type(self): assert_xml_equal(initial_cib, etree_to_str(cib)) +class WarnResourcesUnmanaged(TestCase): + def setUp(self): + self.state = read_test_resource("crm_mon.minimal.xml") + + def test_no_reports(self): + state = complete_state( + self.state, + resources_xml=""" + + + + + + + """, + ) + + assert_report_item_list_equal( + lib.warn_resource_unmanaged(state, ["A", "B", "C"]), [] + ) + + def test_unmanaged(self): + state = complete_state( + self.state, + resources_xml=""" + + + + + + + """, + ) + + assert_report_item_list_equal( + lib.warn_resource_unmanaged(state, ["A", "B", "C"]), + [ + fixture.warn( + reports.codes.RESOURCE_IS_UNMANAGED, resource_id="B" + ), + fixture.warn( + reports.codes.RESOURCE_IS_UNMANAGED, resource_id="C" + ), + ], + ) + + def test_works_with_bundle_in_status(self): + state = complete_state( + self.state, + resources_xml=""" + + + + + + + + + + + + + + """, + ) + + assert_report_item_list_equal( + lib.warn_resource_unmanaged(state, ["A", "B"]), [] + ) + + class StopResources(TestCase, GetCibMixin): def setUp(self): - self.elements_to_remove_mock = mock.Mock() self.cib = read_test_resource("cib-empty.xml") - self.state = read_test_resource("crm_mon.minimal.xml") def test_nothing_to_stop(self): cib = self.get_cib( @@ -1191,12 +1289,9 @@ def test_nothing_to_stop(self): """ ) - state = complete_state(self.state) initial_cib = etree_to_str(cib) - self.elements_to_remove_mock.resources_to_disable = [] - lib.stop_resources(cib, state, self.elements_to_remove_mock) - + lib.stop_resources(cib, []) assert_xml_equal(initial_cib, etree_to_str(cib)) def test_one_resource(self): @@ -1208,18 +1303,9 @@ def test_one_resource(self): """ ) - state = complete_state( - self.state, - resources_xml=""" - - - - """, - ) - self.elements_to_remove_mock.resources_to_disable = ["A"] - report_list = lib.stop_resources( - cib, state, self.elements_to_remove_mock + lib.stop_resources( + cib, [cib.find("./configuration/resources/primitive[@id='A']")] ) assert_xml_equal( @@ -1238,30 +1324,24 @@ def test_one_resource(self): ), etree_to_str(cib), ) - assert_report_item_list_equal(report_list, []) - def test_unmanaged_resource(self): + def test_multiple_resources(self): cib = self.get_cib( resources=""" + """ ) - state = complete_state( - self.state, - resources_xml=""" - - - - - """, - ) - self.elements_to_remove_mock.resources_to_disable = ["A", "B"] - report_list = lib.stop_resources( - cib, state, self.elements_to_remove_mock + lib.stop_resources( + cib, + [ + cib.find("./configuration/resources/primitive[@id='A']"), + cib.find("./configuration/resources/primitive[@id='B']"), + ], ) assert_xml_equal( @@ -1279,51 +1359,35 @@ def test_unmanaged_resource(self): + """, ), etree_to_str(cib), ) - assert_report_item_list_equal( - report_list, - [ - fixture.warn( - reports.codes.RESOURCE_IS_UNMANAGED, resource_id="A" - ) - ], - ) - def test_stop_inner_elements(self): + def test_stop_elements_in_subtree(self): cib = self.get_cib( resources=""" + """ ) - state = complete_state( - self.state, - resources_xml=""" - - - - - - - - - - - """, - ) - self.elements_to_remove_mock.resources_to_disable = ["A", "G", "C"] - report_list = lib.stop_resources( - cib, state, self.elements_to_remove_mock + lib.stop_resources( + cib, + [ + cib.find( + "./configuration/resources/clone/group/primitive[@id='A']" + ), + cib.find("./configuration/resources/clone/group[@id='G']"), + ], ) assert_xml_equal( modify_cib( @@ -1331,9 +1395,6 @@ def test_stop_inner_elements(self): resources=""" - - - @@ -1343,6 +1404,7 @@ def test_stop_inner_elements(self): + @@ -1351,13 +1413,10 @@ def test_stop_inner_elements(self): etree_to_str(cib), ) - assert_report_item_list_equal(report_list, []) - class EnsureStoppedAfterDisable(TestCase): def setUp(self): self.state_xml = read_test_resource("crm_mon.minimal.xml") - self.elements_to_remove_mock = mock.Mock() def test_ok(self): state = complete_state( @@ -1375,11 +1434,7 @@ def test_ok(self): """, ) - self.elements_to_remove_mock.resources_to_disable = ["A", "B", "C"] - - report_list = lib.ensure_resources_stopped( - state, self.elements_to_remove_mock - ) + report_list = lib.ensure_resources_stopped(state, ["A", "B", "C"]) self.assertEqual(report_list, []) def test_some_not_stopped(self): @@ -1398,11 +1453,7 @@ def test_some_not_stopped(self): """, ) - self.elements_to_remove_mock.resources_to_disable = ["A", "B", "C"] - - report_list = lib.ensure_resources_stopped( - state, self.elements_to_remove_mock - ) + report_list = lib.ensure_resources_stopped(state, ["A", "B", "C"]) self.assertEqual( report_list, [ @@ -1426,11 +1477,7 @@ def test_multiinstance_some_not_stopped_clone_id(self): """, ) - self.elements_to_remove_mock.resources_to_disable = ["C"] - - report_list = lib.ensure_resources_stopped( - state, self.elements_to_remove_mock - ) + report_list = lib.ensure_resources_stopped(state, ["C"]) self.assertEqual( report_list, [ @@ -1454,11 +1501,7 @@ def test_multiinstance_some_not_stopped_primitive_id(self): """, ) - self.elements_to_remove_mock.resources_to_disable = ["A"] - - report_list = lib.ensure_resources_stopped( - state, self.elements_to_remove_mock - ) + report_list = lib.ensure_resources_stopped(state, ["A"]) self.assertEqual( report_list, [ @@ -1489,11 +1532,7 @@ def test_works_with_clones_and_bundle_in_status(self): """, ) - self.elements_to_remove_mock.resources_to_disable = ["C"] - - report_list = lib.ensure_resources_stopped( - state, self.elements_to_remove_mock - ) + report_list = lib.ensure_resources_stopped(state, ["C"]) self.assertEqual(report_list, []) diff --git a/pcs_test/tier0/lib/cib/test_tools.py b/pcs_test/tier0/lib/cib/test_tools.py index 10bd30b85..44dde4fef 100644 --- a/pcs_test/tier0/lib/cib/test_tools.py +++ b/pcs_test/tier0/lib/cib/test_tools.py @@ -90,7 +90,6 @@ """ - FIXTURE_REFERENCES_IN_TAGS = """ diff --git a/pcs_test/tier0/lib/commands/test_cib.py b/pcs_test/tier0/lib/commands/test_cib.py index 7cee72fa6..13f9bf492 100644 --- a/pcs_test/tier0/lib/commands/test_cib.py +++ b/pcs_test/tier0/lib/commands/test_cib.py @@ -74,12 +74,8 @@ def test_ids_not_found_and_unsupported_types(self): ) self.env_assist.assert_reports( [ - fixture.report_not_found( - "A", expected_types=["configuration element"] - ), - fixture.report_not_found( - "C", expected_types=["configuration element"] - ), + fixture.report_not_found("A", expected_types=[]), + fixture.report_not_found("C", expected_types=[]), fixture.report_unexpected_element( "T", "tag", EXPECTED_TYPES_FOR_REMOVE ), @@ -678,3 +674,188 @@ def test_disable_only_resources(self): ), ] ) + + def test_stop_inner_elements(self): + self.fixture_env( + initial_cib_modifiers={ + "resources": """ + + + + + + + + + """ + }, + initial_state_modifiers={ + "resources": """ + + + + + + + + + + + + + """ + }, + after_disable_cib_modifiers={ + "resources": """ + + + + + + + + + + + + + + + + + + + + + + + """ + }, + after_disable_state_modifiers={ + "resources": """ + + + + + + + + + + + + + """ + }, + after_delete_cib_modifiers={"resources": ""}, + ) + + lib.remove_elements(self.env_assist.get_env(), ["C"]) + self.env_assist.assert_reports( + [ + fixture.info( + reports.codes.STOPPING_RESOURCES_BEFORE_DELETING, + resource_id_list=["A", "B", "C", "G"], + ), + fixture.info(reports.codes.WAIT_FOR_IDLE_STARTED, timeout=0), + fixture.info( + reports.codes.CIB_REMOVE_DEPENDANT_ELEMENTS, + id_tag_map={ + "G": "group", + "A": "primitive", + "B": "primitive", + }, + ), + ] + ) + + def test_disable_only_needed_resources(self): + self.fixture_env( + initial_cib_modifiers={ + "resources": """ + + + + + + + + + """ + }, + initial_state_modifiers={ + "resources": """ + + + + + + + + + + + + + """ + }, + after_disable_cib_modifiers={ + "resources": """ + + + + + + + + + + + + + """ + }, + after_disable_state_modifiers={ + "resources": """ + + + + + + + + + + + + + """ + }, + after_delete_cib_modifiers={ + "resources": """ + + + + + + + + """ + }, + ) + + lib.remove_elements(self.env_assist.get_env(), ["A"]) + self.env_assist.assert_reports( + [ + fixture.info( + reports.codes.STOPPING_RESOURCES_BEFORE_DELETING, + resource_id_list=["A"], + ), + fixture.info(reports.codes.WAIT_FOR_IDLE_STARTED, timeout=0), + fixture.info( + reports.codes.CIB_REMOVE_REFERENCES, + id_tag_map={"A": "primitive", "G": "group"}, + removing_references_from={"A": {"G"}}, + ), + ] + ) diff --git a/pcs_test/tools/resources_dto.py b/pcs_test/tools/resources_dto.py index 22ea18e5f..21fc534cc 100644 --- a/pcs_test/tools/resources_dto.py +++ b/pcs_test/tools/resources_dto.py @@ -28,9 +28,6 @@ STATEFUL_AGENT_NAME = ResourceAgentNameDto( standard="ocf", provider="pcsmock", type="stateful" ) -REMOTE_AGENT_NAME = ResourceAgentNameDto( - standard="ocf", provider="pacemaker", type="remote" -) STONITH_AGENT_NAME = ResourceAgentNameDto( standard="stonith", provider=None, type="fence_pcsmock_minimal" From b34bd6e5a24619b38c384ea04b374ab851649794 Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Tue, 1 Oct 2024 14:13:35 +0200 Subject: [PATCH 034/227] update documentation --- CHANGELOG.md | 8 ++++++++ pcs/pcs.8.in | 16 ++++++++-------- pcs/usage.py | 10 +++++----- pcsd/capabilities.xml.in | 17 +++++++++++++++-- 4 files changed, 36 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89720716c..aa2633c2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,13 @@ `resource.get_configured_resources` to API v2 - Add lib command `status.full_cluster_status_plaintext` to API v1 ([RHEL-61738]) +- Lib command `cib.remove_elements` can now remove resources + +### Changed +- Commands `pcs resource delete | remove` and `pcs stonith delete | remove` + now allow ([RHEL-61901]): + - deletion of multiple resources or stonith resources with one command + - deletion of resources or stonith resources included in tags ### Fixed - Do not end with error when using the instances quantifier in `pcs status @@ -26,6 +33,7 @@ [RHEL-46293]: https://issues.redhat.com/browse/RHEL-46293 [RHEL-55441]: https://issues.redhat.com/browse/RHEL-55441 [RHEL-61738]: https://issues.redhat.com/browse/RHEL-61738 +[RHEL-61901]: https://issues.redhat.com/browse/RHEL-61901 ## [0.11.8] - 2024-07-09 diff --git a/pcs/pcs.8.in b/pcs/pcs.8.in index a67540cb5..6a390f375 100644 --- a/pcs/pcs.8.in +++ b/pcs/pcs.8.in @@ -120,11 +120,11 @@ Example: Create a new resource called 'VirtualIP' with IP address 192.168.0.99, .br pcs resource create VirtualIP ocf:heartbeat:IPaddr2 ip=192.168.0.99 cidr_netmask=32 nic=eth2 op monitor interval=30s .TP -delete -Deletes the resource, group, bundle or clone (and all resources within the group/bundle/clone). +delete ... +Deletes the specified resources, groups, bundles or clones (and all resources within the groups/bundles/clones). .TP -remove -Deletes the resource, group, bundle or clone (and all resources within the group/bundle/clone). +remove ... +Deletes the specified resources, groups, bundles or clones (and all resources within the groups/bundles/clones). .TP enable ... [\fB\-\-wait\fR[=n]] Allow the cluster to start the resources. Depending on the rest of the configuration (constraints, options, failures, etc), the resources may remain stopped. If \fB\-\-wait\fR is specified, pcs will wait up to 'n' seconds for the resources to start and then return 0 if the resources are started, or 1 if the resources have not yet started. If 'n' is not specified it defaults to 60 minutes. @@ -734,11 +734,11 @@ If \fB\-\-wait\fR is specified, pcs will wait up to 'n' seconds for the changes update\-scsi\-devices (set [...]) | (add [...] delete|remove [...] ) Update scsi fencing devices without affecting other resources. You must specify either list of set devices or at least one device for add or delete/remove devices. Stonith resource must be running on one cluster node. Each device will be unfenced on each cluster node running cluster. Supported fence agents: fence_scsi, fence_mpath. .TP -delete -Remove stonith id from configuration. +delete ... +Remove stonith resources from configuration. .TP -remove -Remove stonith id from configuration. +remove ... +Remove stonith resources from configuration. .TP op add [operation properties] Add operation for specified stonith device. diff --git a/pcs/usage.py b/pcs/usage.py index 3aab9ae98..e8d0d1a03 100644 --- a/pcs/usage.py +++ b/pcs/usage.py @@ -312,11 +312,11 @@ def _output_format_desc(cmd: bool = True) -> str: _DELETE_CMD = "delete" _REMOVE_CMD = "remove" -_RESOURCE_DELETE_SYNTAX = "" +_RESOURCE_DELETE_SYNTAX = "..." _RESOURCE_DELETE_DESC = ( """ - Deletes the resource, group, bundle or clone (and all resources within the - group/bundle/clone). + Deletes the specified resources, groups, bundles or clones (and all + resources within the groups/bundles/clones). """, ) @@ -755,8 +755,8 @@ def _resource_config_desc(obj: str) -> tuple[str, ...]: ) -_STONITH_DELETE_SYNTAX = "" -_STONITH_DELETE_DESC = ("Remove stonith id from configuration.",) +_STONITH_DELETE_SYNTAX = "..." +_STONITH_DELETE_DESC = ("Remove stonith resources from configuration.",) _CLUSTER_CONFIG_SHOW_SYNTAX = ( f"config [show] [{_output_format_syntax()}] [--corosync_conf ]" diff --git a/pcsd/capabilities.xml.in b/pcsd/capabilities.xml.in index c1696226a..6b6139d45 100644 --- a/pcsd/capabilities.xml.in +++ b/pcsd/capabilities.xml.in @@ -904,6 +904,13 @@ API v2: cib.remove_elements + + + Remove resources by ids. + + API v2: cib.remove_elements + + Remove cib elements by ids. @@ -1451,13 +1458,16 @@ pcs commands: resource ( delete | remove ) daemon urls: remove_resource + API v2: cib.remove_elements - + Delete several resources at once. + pcs commands: resource ( delete | remove ) daemon urls: remove_resource + API v2: cib.remove_elements @@ -2099,13 +2109,16 @@ pcs commands: stonith ( delete | remove ) daemon urls: currently missing, remove_resource is used instead which works for now + API v2: cib.remove_elements - + Delete several stonith resources at once. + pcs commands: stonith ( delete | remove ) daemon urls: remove_resource + API v2: cib.remove_elements From a4fa08c6e495c4a1d76b5db10119d96d6b8be928 Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Wed, 16 Oct 2024 15:58:49 +0200 Subject: [PATCH 035/227] fixup tests --- pcs/common/reports/messages.py | 2 ++ pcs_test/tier1/legacy/test_constraints.py | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/pcs/common/reports/messages.py b/pcs/common/reports/messages.py index b14ed7493..021242578 100644 --- a/pcs/common/reports/messages.py +++ b/pcs/common/reports/messages.py @@ -146,6 +146,8 @@ def _key_numeric(item: str) -> Tuple[int, str]: "resource_set": "resource set", "rsc_colocation": "colocation constraint", "rsc_location": "location constraint", + "rsc_order": "order constraint", + "rsc_ticket": "ticket constraint", } _type_articles = { "ACL group": "an", diff --git a/pcs_test/tier1/legacy/test_constraints.py b/pcs_test/tier1/legacy/test_constraints.py index 4ac5adce2..af9037283 100644 --- a/pcs_test/tier1/legacy/test_constraints.py +++ b/pcs_test/tier1/legacy/test_constraints.py @@ -1955,7 +1955,14 @@ def test_location_constraint_rule(self): "constraint rule remove location-D1-rh7-1-INFINITY-rule-1".split(), ) self.assertEqual(stdout, "") - self.assertEqual(stderr, "") + self.assertEqual( + stderr, + ( + "Removing references:\n" + " Rule 'location-D1-rh7-1-INFINITY-rule-1' from:\n" + " Location constraint: 'location-D1-rh7-1-INFINITY'\n" + ), + ) self.assertEqual(retval, 0) stdout, stderr, retval = pcs( @@ -1963,7 +1970,14 @@ def test_location_constraint_rule(self): "constraint rule remove location-D1-rh7-1-INFINITY-rule-2".split(), ) self.assertEqual(stdout, "") - self.assertEqual(stderr, "") + self.assertEqual( + stderr, + ( + "Removing references:\n" + " Rule 'location-D1-rh7-1-INFINITY-rule-2' from:\n" + " Location constraint: 'location-D1-rh7-1-INFINITY'\n" + ), + ) self.assertEqual(retval, 0) self.assert_pcs_success( From 1f6f95f201708cd5df3ac1ad1e3fc51f1da42ca6 Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Wed, 16 Oct 2024 16:21:29 +0200 Subject: [PATCH 036/227] change error to deprecation warning --- pcs/resource.py | 13 ++--- pcs/stonith.py | 13 ++--- pcs_test/tier0/cli/resource/test_remove.py | 56 +++++++++++----------- pcs_test/tier0/cli/test_stonith.py | 56 +++++++++++----------- 4 files changed, 68 insertions(+), 70 deletions(-) diff --git a/pcs/resource.py b/pcs/resource.py index 75d351d9b..b74f8023d 100644 --- a/pcs/resource.py +++ b/pcs/resource.py @@ -2012,14 +2012,11 @@ def resource_remove_cmd( stonith_ids = resources_to_remove & get_stonith_resources_ids(resources_dto) if stonith_ids: - raise CmdLineInputError( - ( - "This command cannot remove stonith {resource}: {id_list}. Use " - "'pcs stonith remove' instead." - ).format( - resource=format_plural(stonith_ids, "resource"), - id_list=format_list(stonith_ids), - ) + deprecation_warning( + reports.messages.ResourceStonithCommandsMismatch( + "stonith resources" + ).message + + " Please use 'pcs stonith remove' instead." ) force_flags = set() diff --git a/pcs/stonith.py b/pcs/stonith.py index fdf11b412..79d360a93 100644 --- a/pcs/stonith.py +++ b/pcs/stonith.py @@ -1014,14 +1014,11 @@ def delete_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: resources_dto ) if non_stonith_ids: - raise CmdLineInputError( - ( - "This command cannot remove {resource}: {id_list}. Use 'pcs " - "resource remove' instead." - ).format( - resource=format_plural(non_stonith_ids, "resource"), - id_list=format_list(non_stonith_ids), - ) + deprecation_warning( + reports.messages.ResourceStonithCommandsMismatch( + "resources" + ).message + + " Please use 'pcs resource remove' instead." ) force_flags = set() diff --git a/pcs_test/tier0/cli/resource/test_remove.py b/pcs_test/tier0/cli/resource/test_remove.py index 8bbe0b1d8..945195308 100644 --- a/pcs_test/tier0/cli/resource/test_remove.py +++ b/pcs_test/tier0/cli/resource/test_remove.py @@ -10,6 +10,7 @@ from pcs_test.tools.resources_dto import ALL_RESOURCES +@mock.patch("pcs.resource.deprecation_warning") class RemoveResource(TestCase): def setUp(self): self.lib = mock.Mock(spec_set=["cib", "resource"]) @@ -22,26 +23,29 @@ def setUp(self): def _call_cmd(self, argv, modifiers=None): resource_remove_cmd(self.lib, argv, dict_to_modifiers(modifiers or {})) - def test_no_args(self): + def test_no_args(self, mock_deprecation_warning): with self.assertRaises(CmdLineInputError) as cm: self._call_cmd([]) self.assertIsNone(cm.exception.message) self.resource.get_configured_resources.assert_not_called() self.cib.remove_elements.assert_not_called() + mock_deprecation_warning.assert_not_called() - def test_remove_one(self): + def test_remove_one(self, mock_deprecation_warning): self._call_cmd(["R1"]) self.resource.get_configured_resources.assert_called_once_with() self.cib.remove_elements.assert_called_once_with({"R1"}, set()) + mock_deprecation_warning.assert_not_called() - def test_remove_multiple(self): + def test_remove_multiple(self, mock_deprecation_warning): self._call_cmd(["R1", "R2", "R3"]) self.resource.get_configured_resources.assert_called_once_with() self.cib.remove_elements.assert_called_once_with( {"R1", "R2", "R3"}, set() ) + mock_deprecation_warning.assert_not_called() - def test_duplicate_args(self): + def test_duplicate_args(self, mock_deprecation_warning): with self.assertRaises(CmdLineInputError) as cm: self._call_cmd(["R1", "R1", "R2", "R3", "R2"]) self.assertEqual( @@ -49,8 +53,9 @@ def test_duplicate_args(self): ) self.resource.get_configured_resources.assert_not_called() self.cib.remove_elements.assert_not_called() + mock_deprecation_warning.assert_not_called() - def test_not_resource_id(self): + def test_not_resource_id(self, mock_deprecation_warning): with self.assertRaises(CmdLineInputError) as cm: self._call_cmd(["nonexistent"]) self.assertEqual( @@ -58,29 +63,26 @@ def test_not_resource_id(self): ) self.resource.get_configured_resources.assert_called_once_with() self.cib.remove_elements.assert_not_called() + mock_deprecation_warning.assert_not_called() - def test_stonith_id(self): - with self.assertRaises(CmdLineInputError) as cm: - self._call_cmd(["S1"]) - self.assertEqual( - cm.exception.message, - ( - "This command cannot remove stonith resource: 'S1'. " - "Use 'pcs stonith remove' instead." - ), - ) + def test_stonith_id(self, mock_deprecation_warning): + self._call_cmd(["S1"]) self.resource.get_configured_resources.assert_called_once_with() - self.cib.remove_elements.assert_not_called() - - def test_multiple_stonith_ids(self): - with self.assertRaises(CmdLineInputError) as cm: - self._call_cmd(["S1", "R1", "R2", "R3", "S2"]) - self.assertEqual( - cm.exception.message, - ( - "This command cannot remove stonith resources: 'S1', 'S2'. " - "Use 'pcs stonith remove' instead." - ), + self.cib.remove_elements.assert_called_once_with({"S1"}, set()) + mock_deprecation_warning.assert_called_once_with( + "Ability of this command to accept stonith resources is deprecated " + "and will be removed in a future release. Please use " + "'pcs stonith remove' instead." ) + + def test_multiple_stonith_ids(self, mock_deprecation_warning): + self._call_cmd(["S1", "R1", "R2", "R3", "S2"]) self.resource.get_configured_resources.assert_called_once_with() - self.cib.remove_elements.assert_not_called() + self.cib.remove_elements.assert_called_once_with( + {"R1", "R2", "R3", "S1", "S2"}, set() + ) + mock_deprecation_warning.assert_called_once_with( + "Ability of this command to accept stonith resources is deprecated " + "and will be removed in a future release. Please use " + "'pcs stonith remove' instead." + ) diff --git a/pcs_test/tier0/cli/test_stonith.py b/pcs_test/tier0/cli/test_stonith.py index 97d912a2d..ffcac5d6d 100644 --- a/pcs_test/tier0/cli/test_stonith.py +++ b/pcs_test/tier0/cli/test_stonith.py @@ -352,6 +352,7 @@ def test_remove_delete_devices(self): ) +@mock.patch("pcs.stonith.deprecation_warning") class StonithRemove(TestCase): def setUp(self): self.lib = mock.Mock(spec_set=["cib", "resource"]) @@ -364,24 +365,27 @@ def setUp(self): def _call_cmd(self, argv, modifiers=None): stonith_delete_cmd(self.lib, argv, dict_to_modifiers(modifiers or {})) - def test_no_args(self): + def test_no_args(self, mock_deprecation_warning): with self.assertRaises(CmdLineInputError) as cm: self._call_cmd([]) self.assertIsNone(cm.exception.message) self.resource.get_configured_resources.assert_not_called() self.cib.remove_elements.assert_not_called() + mock_deprecation_warning.assert_not_called() - def test_remove_one(self): + def test_remove_one(self, mock_deprecation_warning): self._call_cmd(["S1"]) self.resource.get_configured_resources.assert_called_once_with() self.cib.remove_elements.assert_called_once_with({"S1"}, set()) + mock_deprecation_warning.assert_not_called() - def test_remove_multiple(self): + def test_remove_multiple(self, mock_deprecation_warning): self._call_cmd(["S1", "S2"]) self.resource.get_configured_resources.assert_called_once_with() self.cib.remove_elements.assert_called_once_with({"S1", "S2"}, set()) + mock_deprecation_warning.assert_not_called() - def test_duplicate_args(self): + def test_duplicate_args(self, mock_deprecation_warning): with self.assertRaises(CmdLineInputError) as cm: self._call_cmd(["S1", "S2", "S1", "S2"]) self.assertEqual( @@ -390,8 +394,9 @@ def test_duplicate_args(self): self.resource.get_configured_resources.assert_not_called() self.cib.remove_elements.assert_not_called() + mock_deprecation_warning.assert_not_called() - def test_not_resource_id(self): + def test_not_resource_id(self, mock_deprecation_warning): with self.assertRaises(CmdLineInputError) as cm: self._call_cmd(["nonexistent"]) self.assertEqual( @@ -400,29 +405,26 @@ def test_not_resource_id(self): ) self.resource.get_configured_resources.assert_called_once_with() self.cib.remove_elements.assert_not_called() + mock_deprecation_warning.assert_not_called() - def test_not_stonith_id(self): - with self.assertRaises(CmdLineInputError) as cm: - self._call_cmd(["R1"]) - self.assertEqual( - cm.exception.message, - ( - "This command cannot remove resource: 'R1'. " - "Use 'pcs resource remove' instead." - ), - ) + def test_not_stonith_id(self, mock_deprecation_warning): + self._call_cmd(["R1"]) self.resource.get_configured_resources.assert_called_once_with() - self.cib.remove_elements.assert_not_called() - - def test_multiple_not_stonith_id(self): - with self.assertRaises(CmdLineInputError) as cm: - self._call_cmd(["S1", "R1", "R2", "R3", "S2"]) - self.assertEqual( - cm.exception.message, - ( - "This command cannot remove resources: 'R1', 'R2', 'R3'. " - "Use 'pcs resource remove' instead." - ), + self.cib.remove_elements.assert_called_once_with({"R1"}, set()) + mock_deprecation_warning.assert_called_once_with( + "Ability of this command to accept resources is deprecated and " + "will be removed in a future release. Please use " + "'pcs resource remove' instead." ) + + def test_multiple_not_stonith_id(self, mock_deprecation_warning): + self._call_cmd(["S1", "R1", "R2", "R3", "S2"]) self.resource.get_configured_resources.assert_called_once_with() - self.cib.remove_elements.assert_not_called() + self.cib.remove_elements.assert_called_once_with( + {"R1", "R2", "R3", "S1", "S2"}, set() + ) + mock_deprecation_warning.assert_called_once_with( + "Ability of this command to accept resources is deprecated and " + "will be removed in a future release. Please use " + "'pcs resource remove' instead." + ) From 27dcb7682ea95b568f85b2282cd4f78c46ad334d Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Wed, 16 Oct 2024 18:05:26 +0200 Subject: [PATCH 037/227] remove location constrains referencing remote node name --- pcs/lib/cib/remove_elements.py | 34 +++++++++++++++++++ pcs/lib/cib/tools.py | 17 ++++++++++ .../tier0/lib/cib/test_remove_elements.py | 33 ++++++++++++++++++ 3 files changed, 84 insertions(+) diff --git a/pcs/lib/cib/remove_elements.py b/pcs/lib/cib/remove_elements.py index 10c7f9d9d..1640e40a6 100644 --- a/pcs/lib/cib/remove_elements.py +++ b/pcs/lib/cib/remove_elements.py @@ -42,11 +42,19 @@ is_resource, ) from pcs.lib.cib.resource.group import is_group +from pcs.lib.cib.resource.guest_node import ( + get_node_name_from_resource as get_node_name_from_guest, +) +from pcs.lib.cib.resource.guest_node import is_guest_node +from pcs.lib.cib.resource.remote_node import ( + get_node_name_from_resource as get_node_name_from_remote, +) from pcs.lib.cib.tag import is_tag from pcs.lib.cib.tools import ( ElementNotFound, IdProvider, find_elements_referencing_id, + find_location_constraints_referencing_node_name, get_element_by_id, get_elements_by_ids, get_fencing_topology, @@ -437,6 +445,9 @@ def _get_dependencies_to_remove( element_ids_to_remove.add(element_id) elements_to_process.extend(_get_element_references(el)) elements_to_process.extend(_get_inner_references(el)) + elements_to_process.extend( + _get_remote_node_name_constraint_references(el) + ) for level_el in find_levels_with_device( get_fencing_topology(get_root(el)), element_id @@ -528,3 +539,26 @@ def _is_empty_after_inner_el_removal(parent_el: _Element) -> bool: if is_location_constraint(parent_el): return _is_last_element(parent_el, const.TAG_RULE) return False + + +def _get_remote_node_name_constraint_references( + element: _Element, +) -> Iterable[_Element]: + """ + Return all location constraints referencing remote or guest node name. + """ + if not is_resource(element): + return [] + + if is_guest_node(element): + return find_location_constraints_referencing_node_name( + element, get_node_name_from_guest(element) + ) + + remote_node_name = get_node_name_from_remote(element) + if remote_node_name is not None: + return find_location_constraints_referencing_node_name( + element, remote_node_name + ) + + return [] diff --git a/pcs/lib/cib/tools.py b/pcs/lib/cib/tools.py index 979d517cc..de3593def 100644 --- a/pcs/lib/cib/tools.py +++ b/pcs/lib/cib/tools.py @@ -606,6 +606,23 @@ def find_elements_referencing_id( ) +def find_location_constraints_referencing_node_name( + element: _Element, node_name: str +) -> list[_Element]: + """ + Find location constraints which are referencing specified node name. + + element -- any element within CIB tree + node_name -- name of the node which references should be found + """ + return cast( + list[_Element], + _get_configuration(element).xpath( + "./constraints/rsc_location[@node=$node_name]", node_name=node_name + ), + ) + + def remove_element_by_id(cib: _Element, element_id: str) -> None: """ Remove element with specified id from cib element. diff --git a/pcs_test/tier0/lib/cib/test_remove_elements.py b/pcs_test/tier0/lib/cib/test_remove_elements.py index 5f51514ce..288e46400 100644 --- a/pcs_test/tier0/lib/cib/test_remove_elements.py +++ b/pcs_test/tier0/lib/cib/test_remove_elements.py @@ -968,6 +968,39 @@ def test_unsupported_id_types(self): ), ) + def test_remote_guest_node_name_constraint(self): + cib = self.get_cib( + resources=""" + + + + + + + + + """, + constraints=""" + + + + + + """, + ) + elements_to_remove = lib.ElementsToRemove(cib, ["R1", "R2"]) + self.assert_elements_to_remove( + elements_to_remove, + {"R1", "R2", "l1", "l2"}, + resources_to_disable=[ + cib.find("./configuration/resources/primitive[@id='R1']"), + cib.find("./configuration/resources/primitive[@id='R2']"), + ], + dependant_elements=lib.DependantElements( + {"l1": "rsc_location", "l2": "rsc_location"} + ), + ) + class RemoveSpecifiedElements(TestCase, GetCibMixin): def setUp(self): From 91490b59a1505dd416976445583a290f46f7506f Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Mon, 21 Oct 2024 15:17:49 +0200 Subject: [PATCH 038/227] allow deletion of remote/guest via resource delete --- CHANGELOG.md | 5 + pcs/common/reports/codes.py | 2 + pcs/common/reports/messages.py | 50 +++++- pcs/lib/commands/cib.py | 87 +++++++--- .../tier0/common/reports/test_messages.py | 26 +++ pcs_test/tier0/lib/commands/test_cib.py | 148 +++++++++++++++--- 6 files changed, 267 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa2633c2d..8913b0fb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,11 @@ - Displaying status of local and remote cluster sites in `pcs dr status` command. ([RHEL-61738]) +### Deprecated +- Using `pcs resource delete | remove` to delete resources representing remote + and guest nodes. Use `pcs cluster node remove-remote` and `pcs cluster node + remove-guest` instead. + [RHEL-46284]: https://issues.redhat.com/browse/RHEL-46284 [RHEL-46286]: https://issues.redhat.com/browse/RHEL-46286 [RHEL-46293]: https://issues.redhat.com/browse/RHEL-46293 diff --git a/pcs/common/reports/codes.py b/pcs/common/reports/codes.py index 1089958ed..bccb99003 100644 --- a/pcs/common/reports/codes.py +++ b/pcs/common/reports/codes.py @@ -616,6 +616,8 @@ USE_COMMAND_NODE_ADD_GUEST = M("USE_COMMAND_NODE_ADD_GUEST") USE_COMMAND_NODE_REMOVE_REMOTE = M("USE_COMMAND_NODE_REMOVE_REMOTE") USE_COMMAND_NODE_REMOVE_GUEST = M("USE_COMMAND_NODE_REMOVE_GUEST") +REMOTE_NODE_REMOVAL_INCOMPLETE = M("REMOTE_NODE_REMOVAL_INCOMPLETE") +GUEST_NODE_REMOVAL_INCOMPLETE = M("GUEST_NODE_REMOVAL_INCOMPLETE") USING_DEFAULT_ADDRESS_FOR_HOST = M("USING_DEFAULT_ADDRESS_FOR_HOST") USING_DEFAULT_WATCHDOG = M("USING_DEFAULT_WATCHDOG") WAIT_FOR_IDLE_STARTED = M("WAIT_FOR_IDLE_STARTED") diff --git a/pcs/common/reports/messages.py b/pcs/common/reports/messages.py index 021242578..a6878e1a8 100644 --- a/pcs/common/reports/messages.py +++ b/pcs/common/reports/messages.py @@ -5234,14 +5234,11 @@ class UseCommandNodeRemoveRemote(ReportItemMessage): Advise the user for more appropriate command. """ - resource_id: Optional[str] = None _code = codes.USE_COMMAND_NODE_REMOVE_REMOTE @property def message(self) -> str: - return "this command is not sufficient for removing a remote node{id}".format( - id=format_optional(self.resource_id, template=": '{}'") - ) + return "this command is not sufficient for removing a remote node" @dataclass(frozen=True) @@ -5250,14 +5247,51 @@ class UseCommandNodeRemoveGuest(ReportItemMessage): Advise the user for more appropriate command. """ - resource_id: Optional[str] = None _code = codes.USE_COMMAND_NODE_REMOVE_GUEST @property def message(self) -> str: - return "this command is not sufficient for removing a guest node{id}".format( - id=format_optional(self.resource_id, template=": '{}") - ) + return "this command is not sufficient for removing a guest node" + + +@dataclass(frozen=True) +class RemoteNodeRemovalIncomplete(ReportItemMessage): + """ + Warn the user about needed manual steps after removal of remote node. + + node_name -- name of the remote node + """ + + node_name: str + _code = codes.REMOTE_NODE_REMOVAL_INCOMPLETE + + @property + def message(self) -> str: + return ( + "This command is not sufficient for removing remote node: " + "'{name}'. To complete the removal, remove pacemaker authkey and " + "stop and disable pacemaker_remote on the node manually." + ).format(name=self.node_name) + + +@dataclass(frozen=True) +class GuestNodeRemovalIncomplete(ReportItemMessage): + """ + Warn the user about needed manual steps after removal of guest node. + + node_name -- name of the guest node + """ + + node_name: str + _code = codes.GUEST_NODE_REMOVAL_INCOMPLETE + + @property + def message(self) -> str: + return ( + "This command is not sufficient for removing guest node: '{name}'. " + "To complete the removal, remove pacemaker authkey and stop and " + "disable pacemaker_remote on the node manually." + ).format(name=self.node_name) @dataclass(frozen=True) diff --git a/pcs/lib/commands/cib.py b/pcs/lib/commands/cib.py index c81b65fb4..59ef6752f 100644 --- a/pcs/lib/commands/cib.py +++ b/pcs/lib/commands/cib.py @@ -1,12 +1,16 @@ from typing import ( Collection, + Iterable, Sequence, ) from lxml.etree import _Element from pcs.common import reports -from pcs.common.types import StringCollection +from pcs.common.types import ( + StringCollection, + StringSequence, +) from pcs.lib.cib.remove_elements import ( ElementsToRemove, ensure_resources_stopped, @@ -14,13 +18,16 @@ stop_resources, warn_resource_unmanaged, ) +from pcs.lib.cib.resource.guest_node import ( + get_node_name_from_resource as get_node_name_from_guest_resource, +) from pcs.lib.cib.resource.guest_node import is_guest_node from pcs.lib.cib.resource.remote_node import ( get_node_name_from_resource as get_node_name_from_remote_resource, ) -from pcs.lib.cib.tools import get_elements_by_ids from pcs.lib.env import LibraryEnvironment from pcs.lib.errors import LibraryError +from pcs.lib.pacemaker.live import remove_node def remove_elements( @@ -40,10 +47,29 @@ def remove_elements( report_processor = env.report_processor elements_to_remove = ElementsToRemove(cib, ids) + remote_node_names = _get_remote_node_names( + elements_to_remove.resources_to_disable + ) + guest_node_names = _get_guest_node_names( + elements_to_remove.resources_to_disable + ) + + if remote_node_names: + report_processor.report( + reports.ReportItem.deprecation( + reports.messages.UseCommandNodeRemoveRemote() + ) + ) + if guest_node_names: + report_processor.report( + reports.ReportItem.deprecation( + reports.messages.UseCommandNodeRemoveGuest() + ) + ) if report_processor.report_list( _validate_elements_to_remove(elements_to_remove) - + _ensure_not_guest_remote(cib, ids) + + _warn_remote_guest(remote_node_names, guest_node_names) ).has_errors: raise LibraryError() @@ -62,6 +88,10 @@ def remove_elements( remove_specified_elements(cib, elements_to_remove) env.push_cib() + if env.is_cib_live: + for node_name in remote_node_names + guest_node_names: + remove_node(env.cmd_runner(), node_name) + def _stop_resources_wait( env: LibraryEnvironment, @@ -129,26 +159,33 @@ def _validate_elements_to_remove( return report_list -def _ensure_not_guest_remote( - cib: _Element, ids: StringCollection +def _warn_remote_guest( + remote_node_names: StringSequence, guest_node_names: StringSequence ) -> reports.ReportItemList: - report_list = [] - elements_to_process, _ = get_elements_by_ids(cib, ids) - for element in elements_to_process: - if is_guest_node(element): - report_list.append( - reports.ReportItem.error( - reports.messages.UseCommandNodeRemoveGuest( - str(element.attrib["id"]) - ) - ) - ) - if get_node_name_from_remote_resource(element) is not None: - report_list.append( - reports.ReportItem.error( - reports.messages.UseCommandNodeRemoveRemote( - str(element.attrib["id"]) - ) - ) - ) - return report_list + return [ + reports.ReportItem.warning( + reports.messages.RemoteNodeRemovalIncomplete(node_name) + ) + for node_name in remote_node_names + ] + [ + reports.ReportItem.warning( + reports.messages.GuestNodeRemovalIncomplete(node_name) + ) + for node_name in guest_node_names + ] + + +def _get_remote_node_names(resource_elements: Iterable[_Element]) -> list[str]: + return [ + get_node_name_from_remote_resource(el) + for el in resource_elements + if get_node_name_from_remote_resource(el) is not None + ] + + +def _get_guest_node_names(resource_elements: Iterable[_Element]) -> list[str]: + return [ + get_node_name_from_guest_resource(el) + for el in resource_elements + if is_guest_node(el) + ] diff --git a/pcs_test/tier0/common/reports/test_messages.py b/pcs_test/tier0/common/reports/test_messages.py index 4eb982776..dbd507b7a 100644 --- a/pcs_test/tier0/common/reports/test_messages.py +++ b/pcs_test/tier0/common/reports/test_messages.py @@ -5993,3 +5993,29 @@ def test_multiple_resources(self) -> str: ["resourceId1", "resourceId2"] ), ) + + +class RemoteNodeRemovalIncomplete(NameBuildTest): + def test_success(self) -> str: + self.assert_message_from_report( + ( + "This command is not sufficient for removing remote node: " + "'remote-node'. To complete the removal, remove pacemaker " + "authkey and stop and disable pacemaker_remote on the node " + "manually." + ), + reports.RemoteNodeRemovalIncomplete("remote-node"), + ) + + +class GuestNodeRemovalIncomplete(NameBuildTest): + def test_success(self) -> str: + self.assert_message_from_report( + ( + "This command is not sufficient for removing guest node: " + "'guest-node'. To complete the removal, remove pacemaker " + "authkey and stop and disable pacemaker_remote on the node " + "manually." + ), + reports.GuestNodeRemovalIncomplete("guest-node"), + ) diff --git a/pcs_test/tier0/lib/commands/test_cib.py b/pcs_test/tier0/lib/commands/test_cib.py index 13f9bf492..45f9d076a 100644 --- a/pcs_test/tier0/lib/commands/test_cib.py +++ b/pcs_test/tier0/lib/commands/test_cib.py @@ -53,6 +53,20 @@ def _constraints(*argv): EXPECTED_TYPES_FOR_REMOVE = ["constraint", "location rule", "resource"] +def fixture_remote_resource(resource_id: str) -> str: + return f'' + + +def fixture_guest_resource(resource_id: str) -> str: + return f""" + + + + + + """ + + class RemoveElements(TestCase): def setUp(self): self.env_assist, self.config = get_env_tools(self) @@ -214,34 +228,132 @@ def test_remove_resources(self): ) def test_remove_resource_guest(self): - self.config.runner.cib.load(filename="cib-largefile.xml") - self.env_assist.assert_raise_library_error( - lambda: lib.remove_elements( - self.env_assist.get_env(), ["container1"] - ) + cib = modify_cib( + read_test_resource("cib-empty.xml"), + resources=f""" + + {fixture_guest_resource("R1")} + + {fixture_guest_resource("R2")} + + + {fixture_guest_resource("R3")} + + + + {fixture_guest_resource("R4")} + + + + """, + ) + self.config.runner.cib.load_content(cib) + self.config.env.push_cib( + load_key="runner.cib.load_content", resources="" + ) + self.config.runner.pcmk.remove_node("R1-remote", name="remove_node-R1") + self.config.runner.pcmk.remove_node("R2-remote", name="remove_node-R2") + self.config.runner.pcmk.remove_node("R3-remote", name="remove_node-R3") + self.config.runner.pcmk.remove_node("R4-remote", name="remove_node-R4") + + lib.remove_elements( + self.env_assist.get_env(), + ["R1", "R2", "R3", "R4"], + [reports.codes.FORCE], ) self.env_assist.assert_reports( [ - fixture.error( - reports.codes.USE_COMMAND_NODE_REMOVE_GUEST, - resource_id="container1", - ) + fixture.deprecation( + reports.codes.USE_COMMAND_NODE_REMOVE_GUEST + ), + fixture.warn( + reports.codes.GUEST_NODE_REMOVAL_INCOMPLETE, + node_name="R1-remote", + ), + fixture.warn( + reports.codes.GUEST_NODE_REMOVAL_INCOMPLETE, + node_name="R2-remote", + ), + fixture.warn( + reports.codes.GUEST_NODE_REMOVAL_INCOMPLETE, + node_name="R3-remote", + ), + fixture.warn( + reports.codes.GUEST_NODE_REMOVAL_INCOMPLETE, + node_name="R4-remote", + ), + fixture.info( + reports.codes.CIB_REMOVE_DEPENDANT_ELEMENTS, + id_tag_map={ + "C1": "clone", + "C2": "clone", + "G1": "group", + "G2": "group", + }, + ), ] ) def test_remove_resource_remote(self): - self.config.runner.cib.load(filename="cib-remote.xml") - self.env_assist.assert_raise_library_error( - lambda: lib.remove_elements( - self.env_assist.get_env(), ["rh93-remote"] - ) + cib = modify_cib( + read_test_resource("cib-empty.xml"), + resources=f""" + + {fixture_remote_resource("R1")} + + {fixture_remote_resource("R2")} + + + {fixture_remote_resource("R3")} + + + + {fixture_remote_resource("R4")} + + + + """, + ) + self.config.runner.cib.load_content(cib) + self.config.env.push_cib( + resources="", load_key="runner.cib.load_content" + ) + self.config.runner.pcmk.remove_node("R1", name="remove_node-R1") + self.config.runner.pcmk.remove_node("R2", name="remove_node-R2") + self.config.runner.pcmk.remove_node("R3", name="remove_node-R3") + self.config.runner.pcmk.remove_node("R4", name="remove_node-R4") + + lib.remove_elements( + self.env_assist.get_env(), + ["R1", "R2", "R3", "R4"], + [reports.codes.FORCE], ) self.env_assist.assert_reports( [ - fixture.error( - reports.codes.USE_COMMAND_NODE_REMOVE_REMOTE, - resource_id="rh93-remote", - ) + fixture.deprecation( + reports.codes.USE_COMMAND_NODE_REMOVE_REMOTE + ), + fixture.warn( + reports.codes.REMOTE_NODE_REMOVAL_INCOMPLETE, node_name="R1" + ), + fixture.warn( + reports.codes.REMOTE_NODE_REMOVAL_INCOMPLETE, node_name="R2" + ), + fixture.warn( + reports.codes.REMOTE_NODE_REMOVAL_INCOMPLETE, node_name="R3" + ), + fixture.warn( + reports.codes.REMOTE_NODE_REMOVAL_INCOMPLETE, node_name="R4" + ), + fixture.info( + reports.codes.CIB_REMOVE_DEPENDANT_ELEMENTS, + id_tag_map={ + "C1": "clone", + "C2": "clone", + "G1": "group", + "G2": "group", + }, + ), ] ) From 71a3510e5cd3c55d70ae2ed28387d10a26582567 Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Tue, 1 Oct 2024 14:40:37 +0200 Subject: [PATCH 039/227] remove resource_remove callback from node_remove_remote lib command --- pcs/cli/cluster/command.py | 65 +++++----- pcs/cli/routing/cluster.py | 11 +- pcs/common/reports/codes.py | 1 + pcs/common/reports/messages.py | 17 +++ pcs/lib/cib/resource/guest_node.py | 71 ++++++----- pcs/lib/cib/resource/remote_node.py | 18 ++- pcs/lib/commands/remote_node.py | 98 +++++++++------ pcs_test/tier0/cli/cluster/test_command.py | 55 +++++++- .../tier0/common/reports/test_messages.py | 13 ++ .../remote_node/test_node_remove_remote.py | 119 ++++++++++-------- pcs_test/tier1/test_cluster_pcmk_remote.py | 15 +-- 11 files changed, 302 insertions(+), 181 deletions(-) diff --git a/pcs/cli/cluster/command.py b/pcs/cli/cluster/command.py index 8df36b881..4efde680b 100644 --- a/pcs/cli/cluster/command.py +++ b/pcs/cli/cluster/command.py @@ -12,6 +12,7 @@ from pcs.cli.resource.parse_args import ( parse_primitive as parse_primitive_resource, ) +from pcs.common.reports import codes as report_codes def _node_add_remote_separate_name_and_addr( @@ -83,39 +84,37 @@ def node_add_remote( ) -def create_node_remove_remote(remove_resource): # type:ignore - def node_remove_remote( - lib: Any, arg_list: Argv, modifiers: InputModifiers - ) -> None: - """ - Options: - * --force - allow multiple nodes removal, allow pcmk remote service - to fail, don't stop a resource before its deletion (this is side - effect of old resource delete command used here) - * --skip-offline - skip offline nodes - * --request-timeout - HTTP request timeout - For tests: - * --corosync_conf - * -f - """ - modifiers.ensure_only_supported( - "--force", - "--skip-offline", - "--request-timeout", - "--corosync_conf", - "-f", - ) - if len(arg_list) != 1: - raise CmdLineInputError() - lib.remote_node.node_remove_remote( - arg_list[0], - remove_resource, - skip_offline_nodes=modifiers.get("--skip-offline"), - allow_remove_multiple_nodes=modifiers.get("--force"), - allow_pacemaker_remote_service_fail=modifiers.get("--force"), - ) - - return node_remove_remote +def node_remove_remote( + lib: Any, arg_list: Argv, modifiers: InputModifiers +) -> None: + """ + Options: + * --force - allow multiple nodes removal, allow pcmk remote service + to fail, don't stop a resource before its deletion (this is side + effect of old resource delete command used here) + * --skip-offline - skip offline nodes + * --request-timeout - HTTP request timeout + For tests: + * --corosync_conf + * -f + """ + modifiers.ensure_only_supported( + "--force", + "--skip-offline", + "--request-timeout", + "--corosync_conf", + "-f", + ) + if len(arg_list) != 1: + raise CmdLineInputError() + + force_flags = [] + if modifiers.get("--force"): + force_flags.append(report_codes.FORCE) + if modifiers.get("--skip-offline"): + force_flags.append(report_codes.SKIP_OFFLINE_NODES) + + lib.remote_node.node_remove_remote(arg_list[0], force_flags) def node_add_guest(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: diff --git a/pcs/cli/routing/cluster.py b/pcs/cli/routing/cluster.py index 31bf0c668..ca5697290 100644 --- a/pcs/cli/routing/cluster.py +++ b/pcs/cli/routing/cluster.py @@ -4,7 +4,6 @@ from pcs import ( cluster, pcsd, - resource, status, usage, ) @@ -104,16 +103,10 @@ def pcsd_status(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: "clear": cluster_command.node_clear, "delete": cluster.node_remove, "delete-guest": cluster_command.node_remove_guest, - # ignoring mypy errors, these functions need to be fixed, they - # are passing a function to pcs.lib - "delete-remote": cluster_command.create_node_remove_remote( - resource.resource_remove - ), # type:ignore + "delete-remote": cluster_command.node_remove_remote, "remove": cluster.node_remove, "remove-guest": cluster_command.node_remove_guest, - "remove-remote": cluster_command.create_node_remove_remote( - resource.resource_remove - ), # type:ignore + "remove-remote": cluster_command.node_remove_remote, }, ["cluster", "node"], ), diff --git a/pcs/common/reports/codes.py b/pcs/common/reports/codes.py index bccb99003..c2c4626d3 100644 --- a/pcs/common/reports/codes.py +++ b/pcs/common/reports/codes.py @@ -144,6 +144,7 @@ CIB_LOAD_ERROR_SCOPE_MISSING = M("CIB_LOAD_ERROR_SCOPE_MISSING") CIB_PUSH_ERROR = M("CIB_PUSH_ERROR") CIB_REMOVE_REFERENCES = M("CIB_REMOVE_REFERENCES") +CIB_REMOVE_RESOURCES = M("CIB_REMOVE_RESOURCES") CIB_REMOVE_DEPENDANT_ELEMENTS = M("CIB_REMOVE_DEPENDANT_ELEMENTS") CIB_SAVE_TMP_ERROR = M("CIB_SAVE_TMP_ERROR") CIB_SIMULATE_ERROR = M("CIB_SIMULATE_ERROR") diff --git a/pcs/common/reports/messages.py b/pcs/common/reports/messages.py index a6878e1a8..7eedb2d16 100644 --- a/pcs/common/reports/messages.py +++ b/pcs/common/reports/messages.py @@ -5135,6 +5135,23 @@ def message(self) -> str: ) +@dataclass(frozen=True) +class CibRemoveResources(ReportItemMessage): + """ + Information about removal of resources from cib. + """ + + id_list: list[str] + _code = codes.CIB_REMOVE_RESOURCES + + @property + def message(self) -> str: + return "Removing {resource_pl}: {resource_list}".format( + resource_pl=format_plural(self.id_list, "resource"), + resource_list=format_list(self.id_list), + ) + + @dataclass(frozen=True) class CibRemoveDependantElements(ReportItemMessage): """ diff --git a/pcs/lib/cib/resource/guest_node.py b/pcs/lib/cib/resource/guest_node.py index 496b96541..ef8c03acd 100644 --- a/pcs/lib/cib/resource/guest_node.py +++ b/pcs/lib/cib/resource/guest_node.py @@ -1,4 +1,7 @@ -from typing import Mapping +from typing import ( + Mapping, + cast, +) from lxml.etree import _Element @@ -212,47 +215,51 @@ def find_node_list(tree): return node_list -def find_node_resources(resources_section, node_identifier): +def find_node_resources( + resources_section: _Element, node_identifier: str +) -> list[_Element]: """ - Return list of etree.Element primitives that are guest nodes. + Return list of primitive elements that are guest nodes. - etree.Element resources_section is a researched element - string node_identifier could be id of resource, node name or node address + resources_section -- searched element + node_identifier -- could be id of resource, node name or node address """ - resources = resources_section.xpath( - """ - .//primitive[ - ( - @id=$node_id - and + return cast( + list[_Element], + resources_section.xpath( + """ + .//primitive[ + ( + @id=$node_id + and + meta_attributes[ + nvpair[ + @name="remote-node" + and + string-length(@value) > 0 + ] + ] + ) + or meta_attributes[ nvpair[ @name="remote-node" and string-length(@value) > 0 ] - ] - ) - or - meta_attributes[ - nvpair[ - @name="remote-node" and - string-length(@value) > 0 - ] - and - nvpair[ - ( - @name="remote-addr" - or - @name="remote-node" - ) - and - @value=$node_id + nvpair[ + ( + @name="remote-addr" + or + @name="remote-node" + ) + and + @value=$node_id + ] ] ] - ] - """, - node_id=node_identifier, + """, + node_id=node_identifier, + ), ) - return resources diff --git a/pcs/lib/cib/resource/remote_node.py b/pcs/lib/cib/resource/remote_node.py index 0495ba9be..00af55091 100644 --- a/pcs/lib/cib/resource/remote_node.py +++ b/pcs/lib/cib/resource/remote_node.py @@ -2,6 +2,7 @@ Iterable, Mapping, Optional, + cast, ) from lxml.etree import _Element @@ -70,16 +71,20 @@ def find_node_list(tree): return node_list -def find_node_resources(resources_section, node_identifier): +def find_node_resources( + resources_section: _Element, node_identifier: str +) -> list[_Element]: """ Return list of resource elements that match to node_identifier - etree.Element resources_section is a search element - string node_identifier could be id of the resource or its instance attribute + resources_section -- search element + node_identifier -- could be id of the resource or its instance attribute "server" """ - return resources_section.xpath( - f""" + return cast( + list[_Element], + resources_section.xpath( + f""" .//primitive[ {_IS_REMOTE_AGENT_XPATH_SNIPPET} and ( @id=$identifier @@ -92,7 +97,8 @@ def find_node_resources(resources_section, node_identifier): ) ] """, - identifier=node_identifier, + identifier=node_identifier, + ), ) diff --git a/pcs/lib/commands/remote_node.py b/pcs/lib/commands/remote_node.py index ace07730f..522bf5317 100644 --- a/pcs/lib/commands/remote_node.py +++ b/pcs/lib/commands/remote_node.py @@ -1,9 +1,13 @@ from typing import ( + Callable, + Collection, Iterable, Mapping, Optional, ) +from lxml.etree import _Element + from pcs import settings from pcs.common import reports from pcs.common.file import RawFileError @@ -12,6 +16,10 @@ ReportProcessor, ) from pcs.lib import node_communication_format +from pcs.lib.cib.remove_elements import ( + ElementsToRemove, + remove_specified_elements, +) from pcs.lib.cib.resource import ( guest_node, primitive, @@ -22,6 +30,7 @@ IdProvider, get_resources, ) +from pcs.lib.commands.cib import _stop_resources_wait # TODO lib.commands should never import each other. This is to be removed when # the 'resource create' commands are overhauled. @@ -605,13 +614,13 @@ def node_add_guest( def _find_resources_to_remove( - cib, + cib: _Element, report_processor: ReportProcessor, - node_type, - node_identifier, - allow_remove_multiple_nodes, - find_resources, -): + node_type: str, + node_identifier: str, + allow_remove_multiple_nodes: bool, + find_resources: Callable[[_Element, str], list[_Element]], +) -> list[_Element]: resource_element_list = find_resources(get_resources(cib), node_identifier) if not resource_element_list: @@ -631,7 +640,7 @@ def _find_resources_to_remove( message=reports.messages.MultipleResultsFound( "resource", [ - resource.attrib["id"] + str(resource.attrib["id"]) for resource in resource_element_list ], node_identifier, @@ -699,35 +708,27 @@ def _report_skip_live_parts_in_remove(node_names_list): def node_remove_remote( - env, - node_identifier, - remove_resource, - skip_offline_nodes=False, - allow_remove_multiple_nodes=False, - allow_pacemaker_remote_service_fail=False, + env: LibraryEnvironment, + node_identifier: str, + force_flags: Collection[reports.types.ForceCode] = (), ): """ remove a resource representing remote node and destroy remote node - LibraryEnvironment env provides all for communication with externals - string node_identifier -- node name or hostname - callable remove_resource -- function for remove resource - bool skip_offline_nodes -- a flag for ignoring when some nodes are offline - bool allow_remove_multiple_nodes -- is a flag for allowing - remove unexpected multiple occurrence of remote node for node_identifier - bool allow_pacemaker_remote_service_fail -- is a flag for allowing - successfully finish this command even if stoping/disabling - pacemaker_remote not succeeded + env -- provides all for communication with externals + node_identifier -- node name or hostname + force_flags -- list of flags codes """ - cib = env.get_cib() + report_processor = env.report_processor + resource_element_list = _find_resources_to_remove( cib, - env.report_processor, + report_processor, "remote", node_identifier, - allow_remove_multiple_nodes, - remote_node.find_node_resources, + allow_remove_multiple_nodes=reports.codes.FORCE in force_flags, + find_resources=remote_node.find_node_resources, ) node_names_list = sorted( @@ -737,25 +738,50 @@ def node_remove_remote( } ) + resource_ids = [str(el.attrib["id"]) for el in resource_element_list] + elements_to_remove = ElementsToRemove(cib, resource_ids) + + # the user could have provided hostname, so we want to show them which + # resources are going to be removed + report_processor.report( + reports.ReportItem.info( + reports.messages.CibRemoveResources(resource_ids) + ) + ) + + report_processor.report_list( + elements_to_remove.dependant_elements.to_reports() + ) + report_processor.report_list( + elements_to_remove.element_references.to_reports() + ) + + if env.is_cib_live and reports.codes.FORCE not in force_flags: + # we use private function from lib.commands.cib to reduce code repetition + cib = _stop_resources_wait( + env, cib, elements_to_remove.resources_to_disable + ) + if not env.is_cib_live: - env.report_processor.report_list( + report_processor.report_list( _report_skip_live_parts_in_remove(node_names_list) ) else: _destroy_pcmk_remote_env( env, node_names_list, - skip_offline_nodes, - allow_pacemaker_remote_service_fail, + skip_offline_nodes=reports.codes.SKIP_OFFLINE_NODES in force_flags, + allow_fails=reports.codes.FORCE in force_flags, ) - # remove node from pcmk caches is currently integrated in remove_resource - # function - for resource_element in resource_element_list: - remove_resource( - resource_element.attrib["id"], - is_remove_remote_context=True, - ) + remove_specified_elements(cib, elements_to_remove) + + env.push_cib() + + # remove node from pcmk caches + if env.is_cib_live: + for node_name in node_names_list: + remove_node(env.cmd_runner(), node_name) def node_remove_guest( diff --git a/pcs_test/tier0/cli/cluster/test_command.py b/pcs_test/tier0/cli/cluster/test_command.py index 9ab7792df..13246f690 100644 --- a/pcs_test/tier0/cli/cluster/test_command.py +++ b/pcs_test/tier0/cli/cluster/test_command.py @@ -1,6 +1,13 @@ -from unittest import TestCase +from unittest import ( + TestCase, + mock, +) from pcs.cli.cluster import command +from pcs.cli.common.errors import CmdLineInputError +from pcs.common.reports import codes as report_codes + +from pcs_test.tools.misc import dict_to_modifiers class ParseNodeAddRemote(TestCase): @@ -18,3 +25,49 @@ def test_deal_with_implicit_address(self): command._node_add_remote_separate_name_and_addr(["name", "a=b"]), ("name", None, ["a=b"]), ) + + +class NodeRemoveRemote(TestCase): + def setUp(self): + self.lib = mock.Mock(spec_set=["remote_node"]) + self.remote_node = mock.Mock(spec_set=["node_remove_remote"]) + self.lib.remote_node = self.remote_node + + def _call_cmd(self, argv, modifiers=None): + command.node_remove_remote( + self.lib, argv, dict_to_modifiers(modifiers or {}) + ) + + def test_no_args(self): + with self.assertRaises(CmdLineInputError) as cm: + self._call_cmd([]) + self.assertIsNone(cm.exception.message) + self.remote_node.node_remove_remote.assert_not_called() + + def test_too_many_args(self): + with self.assertRaises(CmdLineInputError) as cm: + self._call_cmd(["A", "B"]) + self.assertIsNone(cm.exception.message) + self.remote_node.node_remove_remote.assert_not_called() + + def test_success(self): + self._call_cmd(["A"]) + self.remote_node.node_remove_remote.assert_called_once_with("A", []) + + def test_skip_offline(self): + self._call_cmd(["A"], {"skip-offline": True}) + self.remote_node.node_remove_remote.assert_called_once_with( + "A", [report_codes.SKIP_OFFLINE_NODES] + ) + + def test_force(self): + self._call_cmd(["A"], {"force": True}) + self.remote_node.node_remove_remote.assert_called_once_with( + "A", [report_codes.FORCE] + ) + + def test_all_flags(self): + self._call_cmd(["A"], {"skip-offline": True, "force": True}) + self.remote_node.node_remove_remote.assert_called_once_with( + "A", [report_codes.FORCE, report_codes.SKIP_OFFLINE_NODES] + ) diff --git a/pcs_test/tier0/common/reports/test_messages.py b/pcs_test/tier0/common/reports/test_messages.py index dbd507b7a..b027755aa 100644 --- a/pcs_test/tier0/common/reports/test_messages.py +++ b/pcs_test/tier0/common/reports/test_messages.py @@ -3740,6 +3740,19 @@ def test_no_info(self): ) +class CibRemoveResources(NameBuildTest): + def test_single_id(self): + self.assert_message_from_report( + "Removing resource: 'id1'", reports.CibRemoveResources(["id1"]) + ) + + def test_multiple_ids(self): + self.assert_message_from_report( + "Removing resources: 'id1', 'id2', 'id3'", + reports.CibRemoveResources(["id1", "id2", "id3"]), + ) + + class CibRemoveDependantElements(NameBuildTest): def test_single_element_type_with_single_id(self): self.assert_message_from_report( diff --git a/pcs_test/tier0/lib/commands/remote_node/test_node_remove_remote.py b/pcs_test/tier0/lib/commands/remote_node/test_node_remove_remote.py index 08f344cb4..6f93bcd72 100644 --- a/pcs_test/tier0/lib/commands/remote_node/test_node_remove_remote.py +++ b/pcs_test/tier0/lib/commands/remote_node/test_node_remove_remote.py @@ -4,6 +4,7 @@ mock, ) +from pcs.common import reports from pcs.common.host import Destination from pcs.common.reports import codes as report_codes from pcs.lib.commands.remote_node import ( @@ -53,13 +54,26 @@ def node_remove_remote(env, *args, node_identifier=REMOTE_HOST, **kwargs): REMOTE_HOST, ) -REPORTS = base_reports_for_host(NODE_NAME) +REPORTS = fixture.ReportSequenceBuilder().info( + report_codes.CIB_REMOVE_RESOURCES, + id_list=[NODE_NAME], + _name="cib_remove_resources", +).fixtures + base_reports_for_host(NODE_NAME) + get_env_tools = partial( get_env_tools, local_extensions={"local": EnvConfigMixin} ) +def _stop_resources_wait_mock(_env, cib, _elements_to_remove): + return cib + + +@mock.patch( + "pcs.lib.commands.remote_node._stop_resources_wait", + _stop_resources_wait_mock, +) class RemoveRemote(TestCase): def setUp(self): self.env_assist, self.config = get_env_tools(self) @@ -68,7 +82,6 @@ def setUp(self): NODE_NAME: NODE_DEST_LIST, } ) - self.remove_resource = mock.Mock() def find_by(self, identifier): # Instance of 'Config' has no 'local' member @@ -82,14 +95,11 @@ def find_by(self, identifier): dict(label=NODE_NAME, dest_list=NODE_DEST_LIST) ], ) - node_remove_remote( - self.env_assist.get_env(), - node_identifier=identifier, - remove_resource=self.remove_resource, - ) - self.remove_resource.assert_called_once_with( - NODE_NAME, is_remove_remote_context=True - ) + self.config.env.push_cib(resources="") + self.config.runner.pcmk.remove_node(NODE_NAME) + + env = self.env_assist.get_env() + node_remove_remote(env, node_identifier=identifier) self.env_assist.assert_reports(REPORTS) def test_success_base(self): @@ -99,10 +109,14 @@ def test_can_find_by_node_name(self): self.find_by(NODE_NAME) +@mock.patch( + "pcs.lib.commands.remote_node._stop_resources_wait", + _stop_resources_wait_mock, +) class RemoveRemoteOthers(TestCase): def setUp(self): self.env_assist, self.config = get_env_tools(self) - self.remove_resource = mock.Mock() + # self.remove_resource = mock.Mock() self.config.env.set_known_hosts_dests( { NODE_NAME: NODE_DEST_LIST, @@ -122,14 +136,11 @@ def test_can_skip_all_offline(self): ], **FAIL_HTTP_KWARGS, ) + self.config.env.push_cib(resources="") + self.config.runner.pcmk.remove_node(NODE_NAME) node_remove_remote( - self.env_assist.get_env(), - remove_resource=self.remove_resource, - skip_offline_nodes=True, - ) - self.remove_resource.assert_called_once_with( - NODE_NAME, is_remove_remote_context=True + self.env_assist.get_env(), [reports.codes.SKIP_OFFLINE_NODES] ) my_reports = REPORTS.copy() my_reports.replace( @@ -147,9 +158,7 @@ def test_fail_when_identifier_not_found(self): (self.config.runner.cib.load(resources=FIXTURE_RESOURCES)) self.env_assist.assert_raise_library_error( lambda: node_remove_remote( - self.env_assist.get_env(), - remove_resource=self.remove_resource, - node_identifier="NOEXISTENT", + self.env_assist.get_env(), node_identifier="NOEXISTENT" ), [ fixture.error( @@ -188,7 +197,7 @@ class MultipleResults(TestCase): def setUp(self): self.env_assist, self.config = get_env_tools(self) - self.remove_resource = mock.Mock() + # self.remove_resource = mock.Mock() (self.config.runner.cib.load(resources=self.fixture_multi_resources)) self.report_multiple_results = fixture.error( report_codes.MULTIPLE_RESULTS_FOUND, @@ -210,9 +219,7 @@ def setUp(self): def test_fail(self): self.env_assist.assert_raise_library_error( lambda: node_remove_remote( - self.env_assist.get_env(), - node_identifier=REMOTE_HOST, - remove_resource=self.remove_resource, + self.env_assist.get_env(), node_identifier=REMOTE_HOST ) ) self.env_assist.assert_reports([self.report_multiple_results]) @@ -232,14 +239,26 @@ def test_force(self): dict(label=REMOTE_HOST, dest_list=REMOTE_DEST_LIST), ], ) + self.config.env.push_cib(resources="") + self.config.runner.pcmk.remove_node( + NODE_NAME, name="remove_node.node_name" + ) + self.config.runner.pcmk.remove_node( + REMOTE_HOST, name="remove_node.remote_host" + ) node_remove_remote( self.env_assist.get_env(), node_identifier=REMOTE_HOST, - remove_resource=self.remove_resource, - allow_remove_multiple_nodes=True, + force_flags=[reports.codes.FORCE], ) my_reports = REPORTS.copy() + my_reports.replace( + "cib_remove_resources", + REPORTS["cib_remove_resources"].adapt( + id_list=[NODE_NAME, REMOTE_HOST] + ), + ) my_reports.replace( "pcmk_remote_disable_stop_started", REPORTS["pcmk_remote_disable_stop_started"].adapt( @@ -265,6 +284,10 @@ def test_force(self): self.env_assist.assert_reports(my_reports) +@mock.patch( + "pcs.lib.commands.remote_node._stop_resources_wait", + _stop_resources_wait_mock, +) class AuthkeyRemove(TestCase): def setUp(self): # Instance of 'Config' has no 'local' member @@ -279,7 +302,6 @@ def setUp(self): self.config.local.destroy_pacemaker_remote( label=NODE_NAME, dest_list=NODE_DEST_LIST ) - self.remove_resource = mock.Mock() def test_fails_when_offline(self): # Instance of 'Config' has no 'local' member @@ -291,10 +313,7 @@ def test_fails_when_offline(self): **FAIL_HTTP_KWARGS, ) self.env_assist.assert_raise_library_error( - lambda: node_remove_remote( - self.env_assist.get_env(), - remove_resource=self.remove_resource, - ) + lambda: node_remove_remote(self.env_assist.get_env()) ) my_reports = REPORTS.copy() my_reports.replace( @@ -316,10 +335,7 @@ def test_fails_when_remotely_fails(self): }, ) self.env_assist.assert_raise_library_error( - lambda: node_remove_remote( - self.env_assist.get_env(), - remove_resource=self.remove_resource, - ) + lambda: node_remove_remote(self.env_assist.get_env()) ) my_reports = REPORTS.copy() my_reports.replace( @@ -340,11 +356,10 @@ def test_forceable_when_remotely_fail(self): "message": "Access denied", }, ) - node_remove_remote( - self.env_assist.get_env(), - remove_resource=self.remove_resource, - allow_pacemaker_remote_service_fail=True, - ) + self.config.env.push_cib(resources="") + self.config.runner.pcmk.remove_node(NODE_NAME) + + node_remove_remote(self.env_assist.get_env(), [reports.codes.FORCE]) my_reports = REPORTS.copy() my_reports.replace( "authkey_remove_success", @@ -353,11 +368,14 @@ def test_forceable_when_remotely_fail(self): self.env_assist.assert_reports(my_reports) +@mock.patch( + "pcs.lib.commands.remote_node._stop_resources_wait", + _stop_resources_wait_mock, +) class PcmkRemoteServiceDestroy(TestCase): def setUp(self): self.env_assist, self.config = get_env_tools(self) self.config.runner.cib.load(resources=FIXTURE_RESOURCES) - self.remove_resource = mock.Mock() self.config.env.set_known_hosts_dests( { NODE_NAME: NODE_DEST_LIST, @@ -371,10 +389,7 @@ def test_fails_when_offline(self): label=NODE_NAME, dest_list=NODE_DEST_LIST, **FAIL_HTTP_KWARGS ) self.env_assist.assert_raise_library_error( - lambda: node_remove_remote( - self.env_assist.get_env(), - remove_resource=self.remove_resource, - ) + lambda: node_remove_remote(self.env_assist.get_env()) ) my_reports = REPORTS[:"pcmk_remote_disable_success"] my_reports.append(report_manage_services_connection_failed(NODE_NAME)) @@ -392,10 +407,7 @@ def test_fails_when_remotely_fails(self): }, ) self.env_assist.assert_raise_library_error( - lambda: node_remove_remote( - self.env_assist.get_env(), - remove_resource=self.remove_resource, - ) + lambda: node_remove_remote(self.env_assist.get_env()) ) my_reports = REPORTS[:"pcmk_remote_disable_success"] my_reports.append(report_pcmk_remote_disable_failed(NODE_NAME)) @@ -417,11 +429,10 @@ def test_forceable_when_remotely_fail(self): dict(label=NODE_NAME, dest_list=NODE_DEST_LIST) ], ) - node_remove_remote( - self.env_assist.get_env(), - remove_resource=self.remove_resource, - allow_pacemaker_remote_service_fail=True, - ) + self.config.env.push_cib(resources="") + self.config.runner.pcmk.remove_node(NODE_NAME) + + node_remove_remote(self.env_assist.get_env(), [reports.codes.FORCE]) my_reports = REPORTS.copy() my_reports.replace( "pcmk_remote_disable_success", diff --git a/pcs_test/tier1/test_cluster_pcmk_remote.py b/pcs_test/tier1/test_cluster_pcmk_remote.py index b7bec7a66..8979e8df5 100644 --- a/pcs_test/tier1/test_cluster_pcmk_remote.py +++ b/pcs_test/tier1/test_cluster_pcmk_remote.py @@ -518,8 +518,8 @@ def _test_success_remove_by_host(self): ["cluster", "node", self.command, "NODE-HOST"], "", stderr_full=( - fixture_nolive_remove_report(["NODE-NAME"]) - + "Deleting Resource - NODE-NAME\n" + "Removing resource: 'NODE-NAME'\n" + + fixture_nolive_remove_report(["NODE-NAME"]) ), ) @@ -529,8 +529,8 @@ def _test_success_remove_by_node_name(self): ["cluster", "node", self.command, "NODE-NAME"], "", stderr_full=( - fixture_nolive_remove_report(["NODE-NAME"]) - + "Deleting Resource - NODE-NAME\n" + "Removing resource: 'NODE-NAME'\n" + + fixture_nolive_remove_report(["NODE-NAME"]) ), ) @@ -551,13 +551,8 @@ def _test_success_remove_multiple_nodes(self): stderr_full=( "Warning: more than one resource for 'HOST-A' found: " "'HOST-A', 'NODE-NAME'\n" + + "Removing resources: 'HOST-A', 'NODE-NAME'\n" + fixture_nolive_remove_report(["HOST-A", "NODE-NAME"]) - + dedent( - """\ - Deleting Resource - NODE-NAME - Deleting Resource - HOST-A - """ - ) ), ) From 98e238d756e26a6941843ae473f457a6e64c0049 Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Tue, 1 Oct 2024 14:58:00 +0200 Subject: [PATCH 040/227] remove resource_remove callback from booth.remove_from_cluster lib command --- pcs/cli/booth/command.py | 42 +++++----- pcs/cli/routing/booth.py | 7 +- pcs/lib/booth/resource.py | 25 +++--- pcs/lib/commands/booth.py | 47 +++++++++--- pcs_test/tier0/cli/test_booth.py | 53 +++++-------- pcs_test/tier0/lib/booth/test_resource.py | 47 ++++++------ pcs_test/tier0/lib/commands/test_booth.py | 94 ++++++++++++----------- 7 files changed, 166 insertions(+), 149 deletions(-) diff --git a/pcs/cli/booth/command.py b/pcs/cli/booth/command.py index 7da5fc82c..db2db5803 100644 --- a/pcs/cli/booth/command.py +++ b/pcs/cli/booth/command.py @@ -10,6 +10,7 @@ KeyValueParser, group_by_keywords, ) +from pcs.common.reports import codes as report_codes def config_setup(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: @@ -211,29 +212,26 @@ def create_in_cluster( ) -def get_remove_from_cluster(resource_remove): # type:ignore - # TODO resource_remove is provisional hack until resources are not moved to - # lib - def remove_from_cluster( - lib: Any, arg_list: Argv, modifiers: InputModifiers - ) -> None: - """ - Options: - * --force - allow remove of multiple - * -f - CIB file - * --name - name of a booth instance - """ - modifiers.ensure_only_supported("--force", "-f", "--name") - if arg_list: - raise CmdLineInputError() - - lib.booth.remove_from_cluster( - resource_remove, - instance_name=modifiers.get("--name"), - allow_remove_multiple=modifiers.get("--force"), - ) +def remove_from_cluster( + lib: Any, arg_list: Argv, modifiers: InputModifiers +) -> None: + """ + Options: + * --force - allow remove of multiple + * -f - CIB file + * --name - name of a booth instance + """ + modifiers.ensure_only_supported("--force", "-f", "--name") + if arg_list: + raise CmdLineInputError() + + force_flags = [] + if modifiers.get("--force"): + force_flags.append(report_codes.FORCE) - return remove_from_cluster + lib.booth.remove_from_cluster( + instance_name=modifiers.get("--name"), force_flags=force_flags + ) def restart(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: diff --git a/pcs/cli/routing/booth.py b/pcs/cli/routing/booth.py index 2cc330089..ec1dd9d9b 100644 --- a/pcs/cli/routing/booth.py +++ b/pcs/cli/routing/booth.py @@ -4,7 +4,6 @@ ) from pcs.cli.booth import command from pcs.cli.common.routing import create_router -from pcs.resource import resource_remove mapping = { "help": lambda lib, argv, modifiers: print(usage.booth(argv)), @@ -23,10 +22,8 @@ ["booth", "ticket"], ), "create": command.create_in_cluster, - # ignoring mypy errors, these functions need to be fixed, they are passing - # a function to pcs.lib - "delete": command.get_remove_from_cluster(resource_remove), # type:ignore - "remove": command.get_remove_from_cluster(resource_remove), # type:ignore + "delete": command.remove_from_cluster, + "remove": command.remove_from_cluster, "restart": command.restart, "sync": command.sync, "pull": command.pull, diff --git a/pcs/lib/booth/resource.py b/pcs/lib/booth/resource.py index fd6cacd9b..e5609c335 100644 --- a/pcs/lib/booth/resource.py +++ b/pcs/lib/booth/resource.py @@ -1,4 +1,7 @@ -from typing import cast +from typing import ( + Iterable, + cast, +) from lxml.etree import _Element @@ -33,15 +36,17 @@ def find_grouped_ip_element_to_remove(booth_element): return None -def get_remover(resource_remove): - def remove_from_cluster(booth_element_list): - for element in booth_element_list: - ip_resource_to_remove = find_grouped_ip_element_to_remove(element) - if ip_resource_to_remove is not None: - resource_remove(ip_resource_to_remove.attrib["id"]) - resource_remove(element.attrib["id"]) - - return remove_from_cluster +def find_elements_to_remove( + booth_element_list: Iterable[_Element], +) -> list[_Element]: + elements_to_remove = [] + for element in booth_element_list: + ip_resource_to_remove = find_grouped_ip_element_to_remove(element) + if ip_resource_to_remove is not None: + elements_to_remove.append(ip_resource_to_remove) + elements_to_remove.append(element) + + return elements_to_remove def find_for_config( diff --git a/pcs/lib/commands/booth.py b/pcs/lib/commands/booth.py index bb9752da9..729364e6a 100644 --- a/pcs/lib/commands/booth.py +++ b/pcs/lib/commands/booth.py @@ -2,6 +2,7 @@ import os.path from functools import partial from typing import ( + Collection, Optional, cast, ) @@ -37,6 +38,10 @@ status, ) from pcs.lib.booth.env import BoothEnv +from pcs.lib.cib.remove_elements import ( + ElementsToRemove, + remove_specified_elements, +) from pcs.lib.cib.resource import ( group, hierarchy, @@ -46,6 +51,7 @@ IdProvider, get_resources, ) +from pcs.lib.commands.cib import _stop_resources_wait from pcs.lib.communication.booth import ( BoothGetConfig, BoothSendConfig, @@ -533,33 +539,54 @@ def create_in_cluster( def remove_from_cluster( env: LibraryEnvironment, - resource_remove, - instance_name=None, - allow_remove_multiple=False, + instance_name: Optional[str] = None, + force_flags: Collection[reports.types.ForceCode] = (), ): """ Remove group with ip resource and booth resource env -- provides all for communication with externals - function resource_remove -- provisional hack til resources are moved to lib - string instance_name -- booth instance name - bool allow_remove_multiple -- remove all resources if more than one found + instance_name -- booth instance name + force_flags -- list of flags codes """ - # TODO resource_remove is provisional hack til resources are moved to lib report_processor = env.report_processor booth_env = env.get_booth_env(instance_name) # This command does not work with booth config files at all, let's reject # them then. _ensure_live_booth_env(booth_env) - resource.get_remover(resource_remove)( + cib = env.get_cib() + booth_elements_to_remove = resource.find_elements_to_remove( _find_resource_elements_for_operation( report_processor, - get_resources(env.get_cib()), + get_resources(cib), booth_env, - allow_remove_multiple, + allow_multiple=reports.codes.FORCE in force_flags, + ) + ) + + resource_ids = [str(el.attrib["id"]) for el in booth_elements_to_remove] + elements_to_remove = ElementsToRemove(cib, resource_ids) + + report_processor.report( + reports.ReportItem.info( + reports.messages.CibRemoveResources(resource_ids) ) ) + report_processor.report_list( + elements_to_remove.dependant_elements.to_reports() + ) + report_processor.report_list( + elements_to_remove.element_references.to_reports() + ) + + if env.is_cib_live and reports.codes.FORCE not in force_flags: + cib = _stop_resources_wait( + env, cib, elements_to_remove.resources_to_disable + ) + + remove_specified_elements(cib, elements_to_remove) + env.push_cib() def restart( diff --git a/pcs_test/tier0/cli/test_booth.py b/pcs_test/tier0/cli/test_booth.py index fa2ec6e3c..1b9e2dc59 100644 --- a/pcs_test/tier0/cli/test_booth.py +++ b/pcs_test/tier0/cli/test_booth.py @@ -5,6 +5,7 @@ from pcs.cli.booth import command as booth_cmd from pcs.cli.common.errors import CmdLineInputError +from pcs.common.reports import codes as report_codes from pcs_test.tools.assertions import AssertPcsMixin from pcs_test.tools.misc import dict_to_modifiers @@ -177,46 +178,34 @@ def test_lib_call_full(self): ) -class DeleteRemoveTestMixin(AssertPcsMixin): - command = None - +class RemoveFromCluster(TestCase): def setUp(self): self.lib = mock.Mock(spec_set=["booth"]) - self.lib.booth = mock.Mock(spec_set=["remove_from_cluster"]) + self.booth = mock.Mock(spec_set=["remove_from_cluster"]) + self.lib.booth = self.booth - def test_lib_call_minimal(self): - def resource_remove(something): - return something - - booth_cmd.get_remove_from_cluster(resource_remove)( - self.lib, [], dict_to_modifiers({}) - ) - self.lib.booth.remove_from_cluster.assert_called_once_with( - resource_remove, - instance_name=None, - allow_remove_multiple=False, + def _call_cmd(self, argv, modifiers=None): + booth_cmd.remove_from_cluster( + self.lib, argv, dict_to_modifiers(modifiers or {}) ) - def test_lib_call_full(self): - def resource_remove(something): - return something + def test_args(self): + with self.assertRaises(CmdLineInputError) as cm: + self._call_cmd(["A"]) + self.assertIsNone(cm.exception.message) + self.booth.remove_from_cluster.assert_not_called() - booth_cmd.get_remove_from_cluster(resource_remove)( - self.lib, [], dict_to_modifiers(dict(name="my_booth", force=True)) - ) - self.lib.booth.remove_from_cluster.assert_called_once_with( - resource_remove, - instance_name="my_booth", - allow_remove_multiple=True, + def test_name(self): + self._call_cmd([], {"name": "A"}) + self.booth.remove_from_cluster.assert_called_once_with( + instance_name="A", force_flags=[] ) - -class DeleteTest(DeleteRemoveTestMixin, TestCase): - command = "delete" - - -class RemoveTest(DeleteRemoveTestMixin, TestCase): - command = "remove" + def test_force(self): + self._call_cmd([], {"force": True}) + self.booth.remove_from_cluster.assert_called_once_with( + instance_name=None, force_flags=[report_codes.FORCE] + ) class TicketGrantTest(TestCase): diff --git a/pcs_test/tier0/lib/booth/test_resource.py b/pcs_test/tier0/lib/booth/test_resource.py index d2840cff7..9c3a0b214 100644 --- a/pcs_test/tier0/lib/booth/test_resource.py +++ b/pcs_test/tier0/lib/booth/test_resource.py @@ -95,13 +95,7 @@ def test_returns_all_found_resource_elements(self): ) -class RemoveFromClusterTest(TestCase): - @staticmethod - def call(element_list): - mock_resource_remove = mock.Mock() - booth_resource.get_remover(mock_resource_remove)(element_list) - return mock_resource_remove - +class FindElementsToRemove(TestCase): @staticmethod def find_booth_resources(tree): return tree.xpath('.//primitive[@type="booth-site"]') @@ -120,13 +114,15 @@ def test_remove_ip_when_is_only_booth_sibling_in_group(self): """ ) - mock_resource_remove = self.call(self.find_booth_resources(group)) + elements = booth_resource.find_elements_to_remove( + self.find_booth_resources(group) + ) self.assertEqual( - mock_resource_remove.mock_calls, [ - mock.call("ip"), - mock.call("booth"), + group.find("./primitive[@id='ip']"), + group.find("./primitive[@id='booth']"), ], + elements, ) def test_remove_ip_when_group_is_disabled_1(self): @@ -146,13 +142,15 @@ def test_remove_ip_when_group_is_disabled_1(self): """ ) - mock_resource_remove = self.call(self.find_booth_resources(group)) + elements = booth_resource.find_elements_to_remove( + self.find_booth_resources(group) + ) self.assertEqual( - mock_resource_remove.mock_calls, [ - mock.call("ip"), - mock.call("booth"), + group.find("./primitive[@id='ip']"), + group.find("./primitive[@id='booth']"), ], + elements, ) def test_remove_ip_when_group_is_disabled_2(self): @@ -172,13 +170,15 @@ def test_remove_ip_when_group_is_disabled_2(self): """ ) - mock_resource_remove = self.call(self.find_booth_resources(group)) + elements = booth_resource.find_elements_to_remove( + self.find_booth_resources(group) + ) self.assertEqual( - mock_resource_remove.mock_calls, [ - mock.call("ip"), - mock.call("booth"), + group.find("./primitive[@id='ip']"), + group.find("./primitive[@id='booth']"), ], + elements, ) def test_dont_remove_ip_when_group_has_other_resources(self): @@ -196,13 +196,10 @@ def test_dont_remove_ip_when_group_has_other_resources(self): """ ) - mock_resource_remove = self.call(self.find_booth_resources(group)) - self.assertEqual( - mock_resource_remove.mock_calls, - [ - mock.call("booth"), - ], + elements = booth_resource.find_elements_to_remove( + self.find_booth_resources(group) ) + self.assertEqual([group.find("./primitive[@id='booth']")], elements) class FindBoundIpTest(TestCase): diff --git a/pcs_test/tier0/lib/commands/test_booth.py b/pcs_test/tier0/lib/commands/test_booth.py index d152407f8..c29f2b3d5 100644 --- a/pcs_test/tier0/lib/commands/test_booth.py +++ b/pcs_test/tier0/lib/commands/test_booth.py @@ -2059,36 +2059,39 @@ def test_agents_missing_forced(self): ) +@mock.patch( + "pcs.lib.commands.booth._stop_resources_wait", + lambda env, cib, elements: cib, +) class RemoveFromCluster(TestCase, FixtureMixin): def setUp(self): self.env_assist, self.config = get_env_tools(self) - # mock pcs.resource.remove function which does all the CIB editing - self.resource_remove = mock.Mock() def test_invalid_instance(self): instance_name = "/tmp/booth/booth" self.env_assist.assert_raise_library_error( lambda: commands.remove_from_cluster( - self.env_assist.get_env(), - self.resource_remove, - instance_name=instance_name, + self.env_assist.get_env(), instance_name=instance_name ), - [ - fixture_report_invalid_name(instance_name), - ], + [fixture_report_invalid_name(instance_name)], expected_in_processor=False, ) - self.resource_remove.assert_not_called() def test_success_default_instance(self): self.config.runner.cib.load(resources=self.fixture_cib_booth_group()) - commands.remove_from_cluster( - self.env_assist.get_env(), self.resource_remove - ) - self.resource_remove.assert_has_calls( + self.config.env.push_cib(resources="") + + commands.remove_from_cluster(self.env_assist.get_env()) + self.env_assist.assert_reports( [ - mock.call("booth-booth-ip"), - mock.call("booth-booth-service"), + fixture.info( + reports.codes.CIB_REMOVE_RESOURCES, + id_list=["booth-booth-ip", "booth-booth-service"], + ), + fixture.info( + reports.codes.CIB_REMOVE_DEPENDANT_ELEMENTS, + id_tag_map={"booth-booth-group": "group"}, + ), ] ) @@ -2097,15 +2100,21 @@ def test_success_custom_instance(self): self.config.runner.cib.load( resources=self.fixture_cib_booth_group(instance_name) ) + self.config.env.push_cib(resources="") commands.remove_from_cluster( self.env_assist.get_env(), - self.resource_remove, instance_name=instance_name, ) - self.resource_remove.assert_has_calls( + self.env_assist.assert_reports( [ - mock.call(f"booth-{instance_name}-ip"), - mock.call(f"booth-{instance_name}-service"), + fixture.info( + reports.codes.CIB_REMOVE_RESOURCES, + id_list=["booth-my_booth-ip", "booth-my_booth-service"], + ), + fixture.info( + reports.codes.CIB_REMOVE_DEPENDANT_ELEMENTS, + id_tag_map={"booth-my_booth-group": "group"}, + ), ] ) @@ -2120,13 +2129,20 @@ def test_success_not_live_cib(self): self.config.env.set_cib_data(str(cib_xml_man), cib_tempfile=tmp_file) # This instructs the runner to actually return our mocked cib self.config.runner.cib.load_content(str(cib_xml_man), env=env) - commands.remove_from_cluster( - self.env_assist.get_env(), self.resource_remove + self.config.env.push_cib( + resources="", load_key="runner.cib.load_content" ) - self.resource_remove.assert_has_calls( + commands.remove_from_cluster(self.env_assist.get_env()) + self.env_assist.assert_reports( [ - mock.call("booth-booth-ip"), - mock.call("booth-booth-service"), + fixture.info( + reports.codes.CIB_REMOVE_RESOURCES, + id_list=["booth-booth-ip", "booth-booth-service"], + ), + fixture.info( + reports.codes.CIB_REMOVE_DEPENDANT_ELEMENTS, + id_tag_map={"booth-booth-group": "group"}, + ), ] ) @@ -2139,9 +2155,7 @@ def test_not_live_booth(self): } ) self.env_assist.assert_raise_library_error( - lambda: commands.remove_from_cluster( - self.env_assist.get_env(), self.resource_remove - ), + lambda: commands.remove_from_cluster(self.env_assist.get_env()), [ fixture.error( reports.codes.LIVE_ENVIRONMENT_REQUIRED, @@ -2153,14 +2167,11 @@ def test_not_live_booth(self): ], expected_in_processor=False, ) - self.resource_remove.assert_not_called() def test_booth_resource_does_not_exist(self): (self.config.runner.cib.load()) self.env_assist.assert_raise_library_error( - lambda: commands.remove_from_cluster( - self.env_assist.get_env(), self.resource_remove - ), + lambda: commands.remove_from_cluster(self.env_assist.get_env()), ) self.env_assist.assert_reports( [ @@ -2170,14 +2181,11 @@ def test_booth_resource_does_not_exist(self): ), ] ) - self.resource_remove.assert_not_called() def test_more_booth_resources(self): self.config.runner.cib.load(resources=self.fixture_cib_more_resources()) self.env_assist.assert_raise_library_error( - lambda: commands.remove_from_cluster( - self.env_assist.get_env(), self.resource_remove - ), + lambda: commands.remove_from_cluster(self.env_assist.get_env()), ) self.env_assist.assert_reports( [ @@ -2188,29 +2196,25 @@ def test_more_booth_resources(self): ), ] ) - self.resource_remove.assert_not_called() def test_more_booth_resources_forced(self): self.config.runner.cib.load(resources=self.fixture_cib_more_resources()) + self.config.env.push_cib(resources="") commands.remove_from_cluster( - self.env_assist.get_env(), - self.resource_remove, - allow_remove_multiple=True, + self.env_assist.get_env(), force_flags=[reports.codes.FORCE] ) self.env_assist.assert_reports( [ + fixture.info( + reports.codes.CIB_REMOVE_RESOURCES, + id_list=["booth1", "booth2"], + ), fixture.warn( reports.codes.BOOTH_MULTIPLE_TIMES_IN_CIB, name="booth", ), ] ) - self.resource_remove.assert_has_calls( - [ - mock.call("booth1"), - mock.call("booth2"), - ] - ) class Restart(TestCase, FixtureMixin): From 95e882bf95373b36966f65b28547d0e7c27ea730 Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Tue, 1 Oct 2024 16:33:11 +0200 Subject: [PATCH 041/227] remove old code --- pcs/constraint.py | 24 - pcs/lib/cib/resource/common.py | 36 -- pcs/resource.py | 432 +----------------- pcs/utils.py | 23 - .../tier0/lib/cib/test_resource_common.py | 92 ---- 5 files changed, 1 insertion(+), 606 deletions(-) diff --git a/pcs/constraint.py b/pcs/constraint.py index c22675ba5..55e11e882 100644 --- a/pcs/constraint.py +++ b/pcs/constraint.py @@ -1492,30 +1492,6 @@ def remove_constraints_containing( return None -def remove_constraints_containing_node(dom, node, output=False): - """ - Commandline options: no options - """ - for constraint in find_constraints_containing_node(dom, node): - if output: - print_to_stderr( - "Removing Constraint - {}".format(constraint.getAttribute("id")) - ) - constraint.parentNode.removeChild(constraint) - return dom - - -def find_constraints_containing_node(dom, node): - """ - Commandline options: no options - """ - return [ - constraint - for constraint in dom.getElementsByTagName("rsc_location") - if constraint.getAttribute("node") == node - ] - - # Re-assign any constraints referencing a resource to its parent (a clone # or master) def constraint_resource_update(old_id, dom): diff --git a/pcs/lib/cib/resource/common.py b/pcs/lib/cib/resource/common.py index 07370d6c9..ce259aa55 100644 --- a/pcs/lib/cib/resource/common.py +++ b/pcs/lib/cib/resource/common.py @@ -357,39 +357,3 @@ def unmanage(resource_el: _Element, id_provider: IdProvider) -> None: {"is-managed": "false"}, id_provider, ) - - -def find_resources_to_delete(resource_el: _Element) -> List[_Element]: - """ - Get resources to delete, children and parents of the given resource if - necessary. - - If element is a primitive which is in a clone and you specify one of them, - you will get elements for both of them. If you specify group element which - is in a clone then will you get clone, group, and all primitive elements in - a group and etc. - - resource_el - resource element (bundle, clone, group, primitive) - """ - result = [resource_el] - # childrens of bundle, clone, group, clone-with-group - inner_resource_list = get_inner_resources(resource_el) - if inner_resource_list: - result.extend(inner_resource_list) - inner_resource = inner_resource_list[0] - if is_group(inner_resource): - result.extend(get_inner_resources(inner_resource)) - # parents of primitive if needed (group, clone) - parent_el = get_parent_resource(resource_el) - if parent_el is None or is_bundle(parent_el): - return result - if is_any_clone(parent_el): - result.insert(0, parent_el) - if is_group(parent_el): - group_inner_resources = get_group_inner_resources(parent_el) - if len(group_inner_resources) <= 1: - result = [parent_el] + group_inner_resources - clone_el = get_parent_resource(parent_el) - if clone_el is not None: - result.insert(0, clone_el) - return result diff --git a/pcs/resource.py b/pcs/resource.py index b74f8023d..cacab4d27 100644 --- a/pcs/resource.py +++ b/pcs/resource.py @@ -3,7 +3,6 @@ import re import sys import textwrap -import time from functools import partial from typing import ( Any, @@ -95,20 +94,12 @@ guest_node, primitive, ) -from pcs.lib.cib.resource.common import ( - find_one_resource, - find_resources_to_delete, -) -from pcs.lib.cib.tools import ( - get_resources, - get_tags, -) +from pcs.lib.cib.tools import get_resources from pcs.lib.commands.resource import ( _get_nodes_to_validate_against, _validate_guest_change, ) from pcs.lib.errors import LibraryError -from pcs.lib.pacemaker.state import get_resource_state from pcs.lib.pacemaker.values import ( is_true, validate_id, @@ -2026,340 +2017,6 @@ def resource_remove_cmd( lib.cib.remove_elements(resources_to_remove, force_flags) -# TODO move to lib (complete rewrite) -def resource_remove(resource_id, output=True, is_remove_remote_context=False): - """ - Removes a resource from cluster configuration - - Commandline options: - * -f - CIB file - * --force - don't stop a resource before its deletion - * --wait - is supported by resource_disable but waiting for resource to - stop is handled also in this function - - This function contains at least three bugs: - 1) Input parameter 'output' gets overwritten with output of utilities which - can cause loose equality checks to suppress output of this function - 2) Callers don't check the return value of this function and in conjunction - with the previous bug, the command can complete successfully even though the - resource was not removed - 3) Parameter 'output' is not always correctly propagated to functions that - support it - - str resource_id -- id of resource to be removed - bool output -- suppresses output of this and subsequent commands (buggy) - bool is_remove_remote_context -- is this running on a remote node - """ - # pylint: disable=too-many-branches - # pylint: disable=too-many-locals - # pylint: disable=too-many-nested-blocks - # pylint: disable=too-many-statements - - def is_bundle_running(bundle_id): - roles_with_nodes = get_resource_state( - lib_pacemaker.get_cluster_status_dom(utils.cmd_runner()), - bundle_id, - ) - return bool(roles_with_nodes) - - # if the resource is referenced in tags then exit with an error message - cib_xml = utils.get_cib() - xml_etree = lib_pacemaker.get_cib(cib_xml) - resource_el, dummy_report_list = find_one_resource( - get_resources(xml_etree), resource_id - ) - if resource_el is not None: - tag_obj_ref_list = [] - for el in find_resources_to_delete(resource_el): - xpath_result = get_tags(xml_etree).xpath( - ".//tag/obj_ref[@id=$id]", - id=el.get("id", ""), - ) - if xpath_result: - tag_obj_ref_list.extend(xpath_result) - if tag_obj_ref_list: - tag_id_list = sorted( - { - obj_ref.getparent().get("id", "") - for obj_ref in tag_obj_ref_list - } - ) - utils.err( - "Unable to remove resource '{resource}' because it is " - "referenced in {tags}: {tag_id_list}".format( - resource=resource_id, - tags="tags" if len(tag_id_list) > 1 else "the tag", - tag_id_list=format_list(tag_id_list), - ) - ) - - dom = utils.get_cib_dom(cib_xml) - # if resource is a clone or a master, work with its child instead - cloned_resource = utils.dom_get_clone_ms_resource(dom, resource_id) - if cloned_resource: - resource_id = cloned_resource.getAttribute("id") - - bundle_el = utils.dom_get_bundle(dom, resource_id) - if bundle_el is not None: - primitive_el = utils.dom_get_resource_bundle(bundle_el) - if primitive_el is None: - print_to_stderr("Deleting bundle '{0}'".format(resource_id)) - else: - print_to_stderr( - "Deleting bundle '{0}' and its inner resource '{1}'".format( - resource_id, primitive_el.getAttribute("id") - ) - ) - - if ( - "--force" not in utils.pcs_options - and not utils.usefile - and is_bundle_running(resource_id) - ): - print_to_stderr( - "Stopping bundle '{0}'... ".format(resource_id), end="" - ) - lib = utils.get_library_wrapper() - lib.resource.disable([resource_id], False) - output, retval = utils.run(["crm_resource", "--wait"]) - # pacemaker which supports bundles supports --wait as well - if is_bundle_running(resource_id): - msg = [ - "Unable to stop: %s before deleting " - "(re-run with --force to force deletion)" % resource_id - ] - if retval != 0 and output: - msg.append("\n" + output) - utils.err("\n".join(msg).strip()) - print_to_stderr("Stopped") - - if primitive_el is not None: - resource_remove(primitive_el.getAttribute("id")) - utils.replace_cib_configuration( - remove_resource_references(utils.get_cib_dom(), resource_id, output) - ) - args = [ - "cibadmin", - "-o", - "resources", - "-D", - "--xpath", - "//bundle[@id='{0}']".format(resource_id), - ] - dummy_cmdoutput, retval = utils.run(args) - if retval != 0: - utils.err("Unable to remove resource '{0}'".format(resource_id)) - return True - - if utils.does_exist('//group[@id="' + resource_id + '"]'): - print_to_stderr( - f"Removing group: {resource_id} (and all resources within group)" - ) - group = utils.get_cib_xpath('//group[@id="' + resource_id + '"]') - group_dom = parseString(group) - print_to_stderr(f"Stopping all resources in group: {resource_id}...") - resource_disable([resource_id]) - if "--force" not in utils.pcs_options and not utils.usefile: - output, retval = utils.run(["crm_resource", "--wait"]) - if retval != 0 and "unrecognized option '--wait'" in output: - output = "" - retval = 0 - for res in reversed( - group_dom.documentElement.getElementsByTagName("primitive") - ): - res_id = res.getAttribute("id") - res_stopped = False - for _ in range(15): - time.sleep(1) - if not utils.resource_running_on(res_id)["is_running"]: - res_stopped = True - break - if not res_stopped: - break - stopped = True - state = utils.getClusterState() - for res in group_dom.documentElement.getElementsByTagName( - "primitive" - ): - res_id = res.getAttribute("id") - if utils.resource_running_on(res_id, state)["is_running"]: - stopped = False - break - if not stopped: - msg = [ - "Unable to stop group: %s before deleting " - "(re-run with --force to force deletion)" % resource_id - ] - if retval != 0 and output: - msg.append("\n" + output) - utils.err("\n".join(msg).strip()) - for res in group_dom.documentElement.getElementsByTagName("primitive"): - resource_remove(res.getAttribute("id")) - sys.exit(0) - - # now we know resource is not a group, a clone, a master nor a bundle - # because of the conditions above - if not utils.does_exist( - '//resources/descendant::primitive[@id="' + resource_id + '"]' - ): - utils.err("Resource '{0}' does not exist.".format(resource_id)) - - group_xpath = '//group/primitive[@id="' + resource_id + '"]/..' - group = utils.get_cib_xpath(group_xpath) - num_resources_in_group = 0 - - if group != "": - num_resources_in_group = len( - parseString(group).documentElement.getElementsByTagName("primitive") - ) - - if ( - "--force" not in utils.pcs_options - and not utils.usefile - and utils.resource_running_on(resource_id)["is_running"] - ): - print_to_stderr("Attempting to stop: " + resource_id + "... ", end="") - lib = utils.get_library_wrapper() - # we are not using wait from disable command, because if wait is not - # supported in pacemaker, we don't want error message but we try to - # simulate wait by waiting for resource to stop - lib.resource.disable([resource_id], False) - output, retval = utils.run(["crm_resource", "--wait"]) - if retval != 0 and "unrecognized option '--wait'" in output: - output = "" - retval = 0 - for _ in range(15): - time.sleep(1) - if not utils.resource_running_on(resource_id)["is_running"]: - break - if utils.resource_running_on(resource_id)["is_running"]: - msg = [ - "Unable to stop: %s before deleting " - "(re-run with --force to force deletion)" % resource_id - ] - if retval != 0 and output: - msg.append("\n" + output) - utils.err("\n".join(msg).strip()) - print_to_stderr("Stopped") - - utils.replace_cib_configuration( - remove_resource_references(utils.get_cib_dom(), resource_id, output) - ) - dom = utils.get_cib_dom() - resource_el = utils.dom_get_resource(dom, resource_id) - remote_node_name = None - if resource_el: - remote_node_name = utils.dom_get_resource_remote_node_name(resource_el) - if remote_node_name: - dom = constraint.remove_constraints_containing_node( - dom, remote_node_name, output - ) - utils.replace_cib_configuration(dom) - dom = utils.get_cib_dom() - - if group == "" or num_resources_in_group > 1: - master_xpath = f'//master/primitive[@id="{resource_id}"]/..' - clone_xpath = f'//clone/primitive[@id="{resource_id}"]/..' - if utils.get_cib_xpath(clone_xpath) != "": - args = ["cibadmin", "-o", "resources", "-D", "--xpath", clone_xpath] - elif utils.get_cib_xpath(master_xpath) != "": - args = [ - "cibadmin", - "-o", - "resources", - "-D", - "--xpath", - master_xpath, - ] - else: - args = [ - "cibadmin", - "-o", - "resources", - "-D", - "--xpath", - f"//primitive[@id='{resource_id}']", - ] - if output is True: - print_to_stderr("Deleting Resource - " + resource_id) - output, retval = utils.run(args) - if retval != 0: - utils.err( - f"unable to remove resource: {resource_id}, it may still be " - "referenced in constraints." - ) - else: - top_master_xpath = ( - f'//master/group/primitive[@id="{resource_id}"]/../..' - ) - top_clone_xpath = f'//clone/group/primitive[@id="{resource_id}"]/../..' - top_master = utils.get_cib_xpath(top_master_xpath) - top_clone = utils.get_cib_xpath(top_clone_xpath) - if top_master != "": - to_remove_xpath = top_master_xpath - msg = "and group and M/S" - to_remove_dom = parseString(top_master).getElementsByTagName( - "master" - ) - to_remove_id = to_remove_dom[0].getAttribute("id") - utils.replace_cib_configuration( - remove_resource_references( - utils.get_cib_dom(), - to_remove_dom[0] - .getElementsByTagName("group")[0] - .getAttribute("id"), - ) - ) - elif top_clone != "": - to_remove_xpath = top_clone_xpath - msg = "and group and clone" - to_remove_dom = parseString(top_clone).getElementsByTagName("clone") - to_remove_id = to_remove_dom[0].getAttribute("id") - utils.replace_cib_configuration( - remove_resource_references( - utils.get_cib_dom(), - to_remove_dom[0] - .getElementsByTagName("group")[0] - .getAttribute("id"), - ) - ) - else: - to_remove_xpath = group_xpath - msg = "and group" - to_remove_dom = parseString(group).getElementsByTagName("group") - to_remove_id = to_remove_dom[0].getAttribute("id") - - utils.replace_cib_configuration( - remove_resource_references( - utils.get_cib_dom(), to_remove_id, output - ) - ) - - args = ["cibadmin", "-o", "resources", "-D", "--xpath", to_remove_xpath] - if output is True: - print_to_stderr("Deleting Resource (" + msg + ") - " + resource_id) - dummy_cmdoutput, retval = utils.run(args) - if retval != 0: - if output is True: - utils.err( - "Unable to remove resource '%s' (do constraints exist?)" - % (resource_id) - ) - return False - if remote_node_name and not utils.usefile: - if not is_remove_remote_context: - warn( - "This command is not sufficient for removing remote and guest " - "nodes. To complete the removal, remove pacemaker authkey and " - "stop and disable pacemaker_remote on the node(s) manually." - ) - output, retval = utils.run(["crm_resource", "--wait"]) - output, retval = utils.run( - ["crm_node", "--force", "--remove", remote_node_name] - ) - return True - - def stonith_level_rm_device(cib_dom, stn_id): """ Commandline options: no options @@ -2766,62 +2423,6 @@ def resource_enable_cmd( lib.resource.enable(resources, modifiers.get("--wait")) -# DEPRECATED, moved to pcs.lib.commands.resource -def resource_disable(argv: Argv) -> Optional[bool]: - """ - Commandline options: - * -f - CIB file - * --wait - """ - if not argv: - utils.err("You must specify a resource to disable") - - resource = argv[0] - if not is_managed(resource): - warn(f"'{resource}' is unmanaged") - - wait_timeout = None - if "--wait" in utils.pcs_options: - wait_timeout = utils.validate_wait_get_timeout() - - args = [ - "crm_resource", - "-r", - argv[0], - "-m", - "-p", - "target-role", - "-v", - "Stopped", - ] - output, retval = utils.run(args) - if retval != 0: - utils.err(output) - - if "--wait" in utils.pcs_options: - args = ["crm_resource", "--wait"] - if wait_timeout: - args.extend(["--timeout=%s" % wait_timeout]) - output, retval = utils.run(args) - running_on = utils.resource_running_on(resource) - if retval == 0 and not running_on["is_running"]: - print_to_stderr(running_on["message"]) - return True - msg = [] - if retval == PACEMAKER_WAIT_TIMEOUT_STATUS: - msg.append("waiting timeout") - else: - msg.append( - "unable to stop: '%s', please check logs for failure " - "information" % resource - ) - msg.append(running_on["message"]) - if retval != 0 and output: - msg.append("\n" + output) - utils.err("\n".join(msg).strip()) - return None - - def resource_restart_cmd( lib: Any, argv: Argv, modifiers: InputModifiers ) -> None: @@ -2966,37 +2567,6 @@ def resource_unmanage_cmd( lib.resource.unmanage(resources, with_monitor=modifiers.get("--monitor")) -# moved to pcs.lib.pacemaker.state -def is_managed(resource_id: str) -> bool: - # pylint: disable=too-many-return-statements - """ - Commandline options: - * -f - CIB file - """ - state_dom = utils.getClusterState() - for resource_el in state_dom.getElementsByTagName("resource"): - if resource_el.getAttribute("id") in [resource_id, resource_id + ":0"]: - if resource_el.getAttribute("managed") == "false": - return False - return True - for resource_el in state_dom.getElementsByTagName("group"): - if resource_el.getAttribute("id") in [resource_id, resource_id + ":0"]: - for primitive_el in resource_el.getElementsByTagName("resource"): - if primitive_el.getAttribute("managed") == "false": - return False - return True - for resource_el in state_dom.getElementsByTagName("clone"): - if resource_el.getAttribute("id") == resource_id: - if resource_el.getAttribute("managed") == "false": - return False - for primitive_el in resource_el.getElementsByTagName("resource"): - if primitive_el.getAttribute("managed") == "false": - return False - return True - utils.err("unable to find a resource/clone/group: %s" % resource_id) - return False # pylint does not know utils.err raises - - def resource_failcount_show( lib: Any, argv: Argv, modifiers: InputModifiers ) -> None: diff --git a/pcs/utils.py b/pcs/utils.py index 5dd3777d0..e2393680c 100644 --- a/pcs/utils.py +++ b/pcs/utils.py @@ -1179,19 +1179,6 @@ def report(node, returncode, output): return node_errors -# Check if something exists in the CIB -def does_exist(xpath_query): - """ - Commandline options: - * -f - CIB file - """ - args = ["cibadmin", "-Q", "--xpath", xpath_query] - dummy_output, retval = run(args) - if retval != 0: - return False - return True - - def get_group_children(group_id): """ Commandline options: no options @@ -1364,16 +1351,6 @@ def dom_get_any_resource(dom, resource_id): ) -def is_stonith_resource(resource_id): - """ - Commandline options: - * -f - CIB file - """ - return does_exist( - "//primitive[@id='" + resource_id + "' and @class='stonith']" - ) - - def dom_get_resource_clone(dom, resource_id): """ Commandline options: no options diff --git a/pcs_test/tier0/lib/cib/test_resource_common.py b/pcs_test/tier0/lib/cib/test_resource_common.py index cda7f17bd..77b5f5583 100644 --- a/pcs_test/tier0/lib/cib/test_resource_common.py +++ b/pcs_test/tier0/lib/cib/test_resource_common.py @@ -859,95 +859,3 @@ def test_only_first_meta(self): """, ) - - -class FindResourcesToDelete(TestCase): - # pylint: disable=too-many-public-methods - def assert_element2element_list(self, element_id, element_id_list): - self.assertEqual( - common.find_resources_to_delete( - fixture_cib.xpath(f'.//*[@id="{element_id}"]')[0] - ), - [ - fixture_cib.xpath(f'.//*[@id="{_id}"]')[0] - for _id in element_id_list - ], - ) - - def test_primitive(self): - self.assert_element2element_list("A", ["A"]) - - def test_clone(self): - self.assert_element2element_list("B-clone", ["B-clone", "B"]) - - def test_primitive_in_clone(self): - self.assert_element2element_list("B", ["B-clone", "B"]) - - def test_master(self): - self.assert_element2element_list("C-master", ["C-master", "C"]) - - def test_primitive_in_master(self): - self.assert_element2element_list("C", ["C-master", "C"]) - - def test_group(self): - self.assert_element2element_list("D", ["D", "D1", "D2"]) - - def test_primitive_in_group(self): - self.assert_element2element_list("D1", ["D1"]) - - def test_clone_with_group(self): - self.assert_element2element_list( - "E-clone", - ["E-clone", "E", "E1", "E2"], - ) - - def test_group_in_clone(self): - self.assert_element2element_list("E", ["E-clone", "E", "E1", "E2"]) - - def test_primitive_in_cloned_group(self): - self.assert_element2element_list("E2", ["E2"]) - - def test_master_with_group(self): - self.assert_element2element_list( - "F-master", - ["F-master", "F", "F1", "F2"], - ) - - def test_group_in_master(self): - self.assert_element2element_list("F", ["F-master", "F", "F1", "F2"]) - - def test_primitive_in_mastered_group(self): - self.assert_element2element_list("F1", ["F1"]) - - def test_empty_bundle(self): - self.assert_element2element_list("G-bundle", ["G-bundle"]) - - def test_bundle_with_primitive(self): - self.assert_element2element_list("H-bundle", ["H-bundle", "H"]) - - def test_primitive_in_bundle(self): - self.assert_element2element_list("H", ["H"]) - - def test_group_with_single_primitive(self): - self.assert_element2element_list("I", ["I", "I1"]) - - def test_single_primitive_in_group(self): - self.assert_element2element_list("I1", ["I", "I1"]) - - def test_clone_with_group_with_single_primitive(self): - self.assert_element2element_list("J-clone", ["J-clone", "J", "J1"]) - - def test_group_with_single_primitive_in_clone(self): - self.assert_element2element_list("J", ["J-clone", "J", "J1"]) - - def test_single_primitive_in_cloned_group(self): - self.assert_element2element_list("J1", ["J-clone", "J", "J1"]) - - def test_master_with_group_with_single_primitive(self): - self.assert_element2element_list("K-master", ["K-master", "K", "K1"]) - - def test_group_with_single_primitive_in_master(self): - self.assert_element2element_list("K", ["K-master", "K", "K1"]) - - def test_single_primitive_in_mastered_group(self): - self.assert_element2element_list("K1", ["K-master", "K", "K1"]) From 64c7e75d7e495dddfe438ddb1f950aee7f5c7400 Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Wed, 2 Oct 2024 17:43:43 +0200 Subject: [PATCH 042/227] remove workaround for removing multiple resources from pcsd --- pcsd/remote.rb | 79 ++++++++++---------------------------------------- 1 file changed, 15 insertions(+), 64 deletions(-) diff --git a/pcsd/remote.rb b/pcsd/remote.rb index 932123213..5e36d11d5 100644 --- a/pcsd/remote.rb +++ b/pcsd/remote.rb @@ -1008,80 +1008,31 @@ def remove_resource(params, request, auth_user) end force = params['force'] user = PCSAuth.getSuperuserAuth() - no_error_if_not_exists = params.include?('no_error_if_not_exists') resource_list = [] - errors = '' - resource_to_remove = [] params.each { |param,_| if param.start_with?('resid-') resource_list << param.split('resid-', 2)[1] end } - tmp_file = nil - if force - resource_to_remove = resource_list + + resource_or_stonith = if params["is-stonith"] == "true" then + "stonith" else - begin - tmp_file = Tempfile.new('temp_cib') - _, err, retval = run_cmd(user, PCS, '--', 'cluster', 'cib', tmp_file.path) - if retval != 0 - return [400, 'Unable to stop resource(s).'] - end - cmd = [PCS, '-f', tmp_file.path, '--', 'resource', 'disable'] - resource_list.each { |resource| - out, err, retval = run_cmd(user, *(cmd + [resource])) - if retval != 0 - unless ( - (out + err).join('').include?(' does not exist') and - no_error_if_not_exists - ) - errors += "Unable to stop resource '#{resource}': #{err.join('')}" - end - else - resource_to_remove << resource - end - } - _, _, retval = run_cmd( - user, PCS, '--config', '--wait', '--', 'cluster', 'cib-push', tmp_file.path - ) - if retval != 0 - return [400, 'Unable to stop resource(s).'] - end - errors.strip! - unless errors.empty? - $logger.info("Stopping resource(s) errors:\n#{errors}") - return [400, errors] - end - rescue IOError - return [400, 'Unable to stop resource(s).'] - ensure - if tmp_file - tmp_file.close! - end - end + "resource" end - resource_to_remove.each { |resource| - cmd = ['resource', 'delete', resource] - flags = [] - if force - flags << '--force' - end - out, err, retval = run_cmd(auth_user, PCS, *flags, '--', *cmd) - if retval != 0 - unless ( - (out + err).join('').include?(' does not exist.') and - no_error_if_not_exists - ) - errors += err.join(' ').strip + "\n" - end - end - } - errors.strip! - if errors.empty? + + cmd = [resource_or_stonith, 'delete'] + flags = [] + if force + flags << '--force' + end + out, err, retval = run_cmd(auth_user, PCS, *flags, '--', *cmd, *resource_list) + + if retval == 0 return 200 else - $logger.info("Remove resource errors:\n"+errors) - return [400, errors] + $logger.info("Remove resource errors:\n"+err) + return [400, err] end end From dca8a611afa11b9651399cbfe35a4c30c688a4ad Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Mon, 14 Oct 2024 16:53:07 +0200 Subject: [PATCH 043/227] properly mock resource stopping --- .../remote_node/test_node_remove_remote.py | 164 ++++++--- pcs_test/tier0/lib/commands/test_booth.py | 111 +++++- pcs_test/tier0/lib/commands/test_cib.py | 348 +++++++++--------- pcs_test/tools/custom_mock.py | 5 +- 4 files changed, 404 insertions(+), 224 deletions(-) diff --git a/pcs_test/tier0/lib/commands/remote_node/test_node_remove_remote.py b/pcs_test/tier0/lib/commands/remote_node/test_node_remove_remote.py index 6f93bcd72..c9345fce8 100644 --- a/pcs_test/tier0/lib/commands/remote_node/test_node_remove_remote.py +++ b/pcs_test/tier0/lib/commands/remote_node/test_node_remove_remote.py @@ -1,8 +1,5 @@ from functools import partial -from unittest import ( - TestCase, - mock, -) +from unittest import TestCase from pcs.common import reports from pcs.common.host import Destination @@ -23,6 +20,7 @@ report_pcmk_remote_stop_failed, report_remove_file_connection_failed, ) +from pcs_test.tier0.lib.commands.test_cib import StopResourcesWaitMixin from pcs_test.tools import fixture from pcs_test.tools.command_env import get_env_tools @@ -54,27 +52,70 @@ def node_remove_remote(env, *args, node_identifier=REMOTE_HOST, **kwargs): REMOTE_HOST, ) +FIXTURE_RESOURCES_DISABLED_MODIFIERS = { + "resources": """ + + + + + + + + + + + """.format( + NODE_NAME, + REMOTE_HOST, + ) +} + +FIXTURE_RESOURCES_STATE_BEFORE_MODIFIERS = { + "resources": """ + + + + """.format( + NODE_NAME + ) +} + +FIXTURE_RESOURCES_STATE_AFTER_MODIFIERS = { + "resources": """ + + + + """.format( + NODE_NAME + ) +} + + REPORTS = fixture.ReportSequenceBuilder().info( report_codes.CIB_REMOVE_RESOURCES, id_list=[NODE_NAME], _name="cib_remove_resources", ).fixtures + base_reports_for_host(NODE_NAME) +REPORTS_WITH_DISABLE = ( + fixture.ReportSequenceBuilder() + .info( + report_codes.STOPPING_RESOURCES_BEFORE_DELETING, + resource_id_list=[NODE_NAME], + ) + .info(report_codes.WAIT_FOR_IDLE_STARTED, timeout=0) +).fixtures + REPORTS + get_env_tools = partial( get_env_tools, local_extensions={"local": EnvConfigMixin} ) -def _stop_resources_wait_mock(_env, cib, _elements_to_remove): - return cib - - -@mock.patch( - "pcs.lib.commands.remote_node._stop_resources_wait", - _stop_resources_wait_mock, -) -class RemoveRemote(TestCase): +class RemoveRemote(TestCase, StopResourcesWaitMixin): def setUp(self): self.env_assist, self.config = get_env_tools(self) self.config.env.set_known_hosts_dests( @@ -82,11 +123,18 @@ def setUp(self): NODE_NAME: NODE_DEST_LIST, } ) + self.fixture_init_tmp_file_mocker() def find_by(self, identifier): # Instance of 'Config' has no 'local' member # pylint: disable=no-member self.config.runner.cib.load(resources=FIXTURE_RESOURCES) + self.fixture_stop_resources_wait_calls( + self.config.calls.get("runner.cib.load").stdout, + FIXTURE_RESOURCES_STATE_BEFORE_MODIFIERS, + FIXTURE_RESOURCES_DISABLED_MODIFIERS, + FIXTURE_RESOURCES_STATE_AFTER_MODIFIERS, + ) self.config.local.destroy_pacemaker_remote( label=NODE_NAME, dest_list=NODE_DEST_LIST ) @@ -95,12 +143,13 @@ def find_by(self, identifier): dict(label=NODE_NAME, dest_list=NODE_DEST_LIST) ], ) - self.config.env.push_cib(resources="") + self.fixture_push_cib_after_stopping(resources="") self.config.runner.pcmk.remove_node(NODE_NAME) - env = self.env_assist.get_env() - node_remove_remote(env, node_identifier=identifier) - self.env_assist.assert_reports(REPORTS) + node_remove_remote( + self.env_assist.get_env(), node_identifier=identifier + ) + self.env_assist.assert_reports(REPORTS_WITH_DISABLE) def test_success_base(self): self.find_by(REMOTE_HOST) @@ -109,24 +158,26 @@ def test_can_find_by_node_name(self): self.find_by(NODE_NAME) -@mock.patch( - "pcs.lib.commands.remote_node._stop_resources_wait", - _stop_resources_wait_mock, -) -class RemoveRemoteOthers(TestCase): +class RemoveRemoteOthers(TestCase, StopResourcesWaitMixin): def setUp(self): self.env_assist, self.config = get_env_tools(self) - # self.remove_resource = mock.Mock() self.config.env.set_known_hosts_dests( { NODE_NAME: NODE_DEST_LIST, } ) + self.fixture_init_tmp_file_mocker() def test_can_skip_all_offline(self): # Instance of 'Config' has no 'local' member # pylint: disable=no-member self.config.runner.cib.load(resources=FIXTURE_RESOURCES) + self.fixture_stop_resources_wait_calls( + self.config.calls.get("runner.cib.load").stdout, + FIXTURE_RESOURCES_STATE_BEFORE_MODIFIERS, + FIXTURE_RESOURCES_DISABLED_MODIFIERS, + FIXTURE_RESOURCES_STATE_AFTER_MODIFIERS, + ) self.config.local.destroy_pacemaker_remote( label=NODE_NAME, dest_list=NODE_DEST_LIST, **FAIL_HTTP_KWARGS ) @@ -136,13 +187,13 @@ def test_can_skip_all_offline(self): ], **FAIL_HTTP_KWARGS, ) - self.config.env.push_cib(resources="") + self.fixture_push_cib_after_stopping(resources="") self.config.runner.pcmk.remove_node(NODE_NAME) node_remove_remote( self.env_assist.get_env(), [reports.codes.SKIP_OFFLINE_NODES] ) - my_reports = REPORTS.copy() + my_reports = REPORTS_WITH_DISABLE.copy() my_reports.replace( "pcmk_remote_disable_success", report_manage_services_connection_failed(NODE_NAME).to_warn(), @@ -155,7 +206,7 @@ def test_can_skip_all_offline(self): self.env_assist.assert_reports(my_reports) def test_fail_when_identifier_not_found(self): - (self.config.runner.cib.load(resources=FIXTURE_RESOURCES)) + self.config.runner.cib.load(resources=FIXTURE_RESOURCES) self.env_assist.assert_raise_library_error( lambda: node_remove_remote( self.env_assist.get_env(), node_identifier="NOEXISTENT" @@ -197,8 +248,7 @@ class MultipleResults(TestCase): def setUp(self): self.env_assist, self.config = get_env_tools(self) - # self.remove_resource = mock.Mock() - (self.config.runner.cib.load(resources=self.fixture_multi_resources)) + self.config.runner.cib.load(resources=self.fixture_multi_resources) self.report_multiple_results = fixture.error( report_codes.MULTIPLE_RESULTS_FOUND, force_code=report_codes.FORCE, @@ -284,11 +334,7 @@ def test_force(self): self.env_assist.assert_reports(my_reports) -@mock.patch( - "pcs.lib.commands.remote_node._stop_resources_wait", - _stop_resources_wait_mock, -) -class AuthkeyRemove(TestCase): +class AuthkeyRemove(TestCase, StopResourcesWaitMixin): def setUp(self): # Instance of 'Config' has no 'local' member # pylint: disable=no-member @@ -298,14 +344,21 @@ def setUp(self): NODE_NAME: NODE_DEST_LIST, } ) + self.fixture_init_tmp_file_mocker() self.config.runner.cib.load(resources=FIXTURE_RESOURCES) - self.config.local.destroy_pacemaker_remote( - label=NODE_NAME, dest_list=NODE_DEST_LIST - ) def test_fails_when_offline(self): # Instance of 'Config' has no 'local' member # pylint: disable=no-member + self.fixture_stop_resources_wait_calls( + self.config.calls.get("runner.cib.load").stdout, + FIXTURE_RESOURCES_STATE_BEFORE_MODIFIERS, + FIXTURE_RESOURCES_DISABLED_MODIFIERS, + FIXTURE_RESOURCES_STATE_AFTER_MODIFIERS, + ) + self.config.local.destroy_pacemaker_remote( + label=NODE_NAME, dest_list=NODE_DEST_LIST + ) self.config.local.remove_authkey( communication_list=[ dict(label=NODE_NAME, dest_list=NODE_DEST_LIST) @@ -315,7 +368,7 @@ def test_fails_when_offline(self): self.env_assist.assert_raise_library_error( lambda: node_remove_remote(self.env_assist.get_env()) ) - my_reports = REPORTS.copy() + my_reports = REPORTS_WITH_DISABLE.copy() my_reports.replace( "authkey_remove_success", report_remove_file_connection_failed(NODE_NAME), @@ -325,6 +378,15 @@ def test_fails_when_offline(self): def test_fails_when_remotely_fails(self): # Instance of 'Config' has no 'local' member # pylint: disable=no-member + self.fixture_stop_resources_wait_calls( + self.config.calls.get("runner.cib.load").stdout, + FIXTURE_RESOURCES_STATE_BEFORE_MODIFIERS, + FIXTURE_RESOURCES_DISABLED_MODIFIERS, + FIXTURE_RESOURCES_STATE_AFTER_MODIFIERS, + ) + self.config.local.destroy_pacemaker_remote( + label=NODE_NAME, dest_list=NODE_DEST_LIST + ) self.config.local.remove_authkey( communication_list=[ dict(label=NODE_NAME, dest_list=NODE_DEST_LIST) @@ -337,7 +399,7 @@ def test_fails_when_remotely_fails(self): self.env_assist.assert_raise_library_error( lambda: node_remove_remote(self.env_assist.get_env()) ) - my_reports = REPORTS.copy() + my_reports = REPORTS_WITH_DISABLE.copy() my_reports.replace( "authkey_remove_success", report_authkey_remove_failed(NODE_NAME), @@ -347,6 +409,9 @@ def test_fails_when_remotely_fails(self): def test_forceable_when_remotely_fail(self): # Instance of 'Config' has no 'local' member # pylint: disable=no-member + self.config.local.destroy_pacemaker_remote( + label=NODE_NAME, dest_list=NODE_DEST_LIST + ) self.config.local.remove_authkey( communication_list=[ dict(label=NODE_NAME, dest_list=NODE_DEST_LIST) @@ -368,11 +433,7 @@ def test_forceable_when_remotely_fail(self): self.env_assist.assert_reports(my_reports) -@mock.patch( - "pcs.lib.commands.remote_node._stop_resources_wait", - _stop_resources_wait_mock, -) -class PcmkRemoteServiceDestroy(TestCase): +class PcmkRemoteServiceDestroy(TestCase, StopResourcesWaitMixin): def setUp(self): self.env_assist, self.config = get_env_tools(self) self.config.runner.cib.load(resources=FIXTURE_RESOURCES) @@ -381,23 +442,36 @@ def setUp(self): NODE_NAME: NODE_DEST_LIST, } ) + self.fixture_init_tmp_file_mocker() def test_fails_when_offline(self): # Instance of 'Config' has no 'local' member # pylint: disable=no-member + self.fixture_stop_resources_wait_calls( + self.config.calls.get("runner.cib.load").stdout, + FIXTURE_RESOURCES_STATE_BEFORE_MODIFIERS, + FIXTURE_RESOURCES_DISABLED_MODIFIERS, + FIXTURE_RESOURCES_STATE_AFTER_MODIFIERS, + ) self.config.local.destroy_pacemaker_remote( label=NODE_NAME, dest_list=NODE_DEST_LIST, **FAIL_HTTP_KWARGS ) self.env_assist.assert_raise_library_error( lambda: node_remove_remote(self.env_assist.get_env()) ) - my_reports = REPORTS[:"pcmk_remote_disable_success"] + my_reports = REPORTS_WITH_DISABLE[:"pcmk_remote_disable_success"] my_reports.append(report_manage_services_connection_failed(NODE_NAME)) self.env_assist.assert_reports(my_reports) def test_fails_when_remotely_fails(self): # Instance of 'Config' has no 'local' member # pylint: disable=no-member + self.fixture_stop_resources_wait_calls( + self.config.calls.get("runner.cib.load").stdout, + FIXTURE_RESOURCES_STATE_BEFORE_MODIFIERS, + FIXTURE_RESOURCES_DISABLED_MODIFIERS, + FIXTURE_RESOURCES_STATE_AFTER_MODIFIERS, + ) self.config.local.destroy_pacemaker_remote( label=NODE_NAME, dest_list=NODE_DEST_LIST, @@ -409,7 +483,7 @@ def test_fails_when_remotely_fails(self): self.env_assist.assert_raise_library_error( lambda: node_remove_remote(self.env_assist.get_env()) ) - my_reports = REPORTS[:"pcmk_remote_disable_success"] + my_reports = REPORTS_WITH_DISABLE[:"pcmk_remote_disable_success"] my_reports.append(report_pcmk_remote_disable_failed(NODE_NAME)) my_reports.append(report_pcmk_remote_stop_failed(NODE_NAME)) self.env_assist.assert_reports(my_reports) diff --git a/pcs_test/tier0/lib/commands/test_booth.py b/pcs_test/tier0/lib/commands/test_booth.py index c29f2b3d5..61b4d7695 100644 --- a/pcs_test/tier0/lib/commands/test_booth.py +++ b/pcs_test/tier0/lib/commands/test_booth.py @@ -15,6 +15,7 @@ from pcs.lib.booth import constants from pcs.lib.commands import booth as commands +from pcs_test.tier0.lib.commands.test_cib import StopResourcesWaitMixin from pcs_test.tools import fixture from pcs_test.tools.command_env import get_env_tools from pcs_test.tools.misc import get_test_resource as rc @@ -70,14 +71,41 @@ def fixture_cib_booth_primitive(self, name="booth", rid="booth_resource"): """ def fixture_cib_booth_group( - self, name="booth", default_operations=False, wrap_in_resources=True + self, + name="booth", + default_operations=False, + wrap_in_resources=True, + disabled_resources=False, ): + disabled_template = """ + + + + """ return ( ("" if wrap_in_resources else "") + f""" + """ + + ( + disabled_template.format(name=f"booth-{name}-group") + if disabled_resources + else "" + ) + + f""" + """ + + ( + disabled_template.format(name=f"booth-{name}-ip") + if disabled_resources + else "" + ) + + f""" @@ -112,6 +140,13 @@ def fixture_cib_booth_group( + """ + + ( + disabled_template.format(name=f"booth-{name}-service") + if disabled_resources + else "" + ) + + f""" @@ -175,6 +210,18 @@ def fixture_cfg_content(self, key_path=None, ticket_list=None): config += "\n".join(extra_lines) + "\n" return config.encode("utf-8") + def fixture_group_status(self, name="booth", running=True): + # pylint: disable=no-self-use + role = "Started" if running else "Stopped" + return f""" + + + + + + + """ + @mock.patch( "pcs.lib.tools.generate_binary_key", @@ -2059,13 +2106,10 @@ def test_agents_missing_forced(self): ) -@mock.patch( - "pcs.lib.commands.booth._stop_resources_wait", - lambda env, cib, elements: cib, -) -class RemoveFromCluster(TestCase, FixtureMixin): +class RemoveFromCluster(TestCase, FixtureMixin, StopResourcesWaitMixin): def setUp(self): self.env_assist, self.config = get_env_tools(self) + self.fixture_init_tmp_file_mocker() def test_invalid_instance(self): instance_name = "/tmp/booth/booth" @@ -2079,7 +2123,21 @@ def test_invalid_instance(self): def test_success_default_instance(self): self.config.runner.cib.load(resources=self.fixture_cib_booth_group()) - self.config.env.push_cib(resources="") + self.fixture_stop_resources_wait_calls( + self.config.calls.get("runner.cib.load").stdout, + initial_state_modifiers={ + "resources": self.fixture_group_status(running=True) + }, + after_disable_cib_modifiers={ + "resources": self.fixture_cib_booth_group( + disabled_resources=True + ) + }, + after_disable_state_modifiers={ + "resources": self.fixture_group_status(running=False) + }, + ) + self.fixture_push_cib_after_stopping(resources="") commands.remove_from_cluster(self.env_assist.get_env()) self.env_assist.assert_reports( @@ -2092,6 +2150,15 @@ def test_success_default_instance(self): reports.codes.CIB_REMOVE_DEPENDANT_ELEMENTS, id_tag_map={"booth-booth-group": "group"}, ), + fixture.info( + reports.codes.STOPPING_RESOURCES_BEFORE_DELETING, + resource_id_list=[ + "booth-booth-group", + "booth-booth-ip", + "booth-booth-service", + ], + ), + fixture.info(reports.codes.WAIT_FOR_IDLE_STARTED, timeout=0), ] ) @@ -2100,7 +2167,26 @@ def test_success_custom_instance(self): self.config.runner.cib.load( resources=self.fixture_cib_booth_group(instance_name) ) - self.config.env.push_cib(resources="") + self.fixture_stop_resources_wait_calls( + self.config.calls.get("runner.cib.load").stdout, + initial_state_modifiers={ + "resources": self.fixture_group_status( + instance_name, running=True + ) + }, + after_disable_cib_modifiers={ + "resources": self.fixture_cib_booth_group( + instance_name, disabled_resources=True + ) + }, + after_disable_state_modifiers={ + "resources": self.fixture_group_status( + instance_name, running=False + ) + }, + ) + self.fixture_push_cib_after_stopping(resources="") + commands.remove_from_cluster( self.env_assist.get_env(), instance_name=instance_name, @@ -2115,6 +2201,15 @@ def test_success_custom_instance(self): reports.codes.CIB_REMOVE_DEPENDANT_ELEMENTS, id_tag_map={"booth-my_booth-group": "group"}, ), + fixture.info( + reports.codes.STOPPING_RESOURCES_BEFORE_DELETING, + resource_id_list=[ + "booth-my_booth-group", + "booth-my_booth-ip", + "booth-my_booth-service", + ], + ), + fixture.info(reports.codes.WAIT_FOR_IDLE_STARTED, timeout=0), ] ) diff --git a/pcs_test/tier0/lib/commands/test_cib.py b/pcs_test/tier0/lib/commands/test_cib.py index 45f9d076a..01edc0cc4 100644 --- a/pcs_test/tier0/lib/commands/test_cib.py +++ b/pcs_test/tier0/lib/commands/test_cib.py @@ -1,3 +1,4 @@ +from typing import Optional from unittest import ( TestCase, mock, @@ -67,6 +68,97 @@ def fixture_guest_resource(resource_id: str) -> str: """ +class StopResourcesWaitMixin: + def fixture_init_tmp_file_mocker(self): + self.tmp_file_mock_obj = TmpFileMock( + file_content_checker=assert_xml_equal, + ) + self.addCleanup(self.tmp_file_mock_obj.assert_all_done) + tmp_file_patcher = mock.patch("pcs.lib.tools.get_tmp_file") + self.addCleanup(tmp_file_patcher.stop) + tmp_file_mock = tmp_file_patcher.start() + tmp_file_mock.side_effect = ( + self.tmp_file_mock_obj.get_mock_side_effect() + ) + + def fixture_stop_resources_wait_calls( + self, + initial_cib: str, + initial_state_modifiers: Optional[dict[str, str]] = None, + after_disable_cib_modifiers: Optional[dict[str, str]] = None, + after_disable_state_modifiers: Optional[dict[str, str]] = None, + successful_stop: bool = True, + ): + self.config.runner.pcmk.load_state( + name="stop_wait.load_state.before", + **(initial_state_modifiers or {}), + ) + + self.__disabled_cib = modify_cib( + initial_cib, **(after_disable_cib_modifiers or {}) + ) + self.tmp_file_mock_obj.set_calls( + [ + TmpFileCall( + "stop_wait.cib.disable.before", orig_content=initial_cib + ), + TmpFileCall( + "stop_wait.cib.disable.after", + orig_content=self.__disabled_cib, + ), + ] + ) + self.config.runner.cib.diff( + "stop_wait.cib.disable.before", + "stop_wait.cib.disable.after", + name="stop_wait.cib.diff.disable", + stdout="stop_wait.cib.diff.disable", + ) + self.config.runner.cib.push_diff( + name="stop_wait.cib.push.disable", + cib_diff="stop_wait.cib.diff.disable", + ) + + self.config.runner.pcmk.wait(timeout=0) + self.config.runner.pcmk.load_state( + name="stop_wait.state.after", + **(after_disable_state_modifiers or {}), + ) + + if successful_stop: + self.config.runner.cib.load_content( + self.__disabled_cib, name="stop_wait.cib.load.after" + ) + + def fixture_push_cib_after_stopping(self, **modifiers): + self.tmp_file_mock_obj.extend_calls( + [ + TmpFileCall( + "stop_wait.cib.delete.before", + orig_content=self.__disabled_cib, + ), + TmpFileCall( + "stop_wait.cib.delete.after", + orig_content=modify_cib( + read_test_resource("cib-empty.xml"), **modifiers + ), + ), + ] + ) + + self.config.runner.cib.diff( + "stop_wait.cib.delete.before", + "stop_wait.cib.delete.after", + name="stop_wait.cib.diff.delete", + stdout="stop_wait.cib.diff.delete", + ) + + self.config.runner.cib.push_diff( + name="stop_wait.cib.push.delete", + cib_diff="stop_wait.cib.diff.delete", + ) + + class RemoveElements(TestCase): def setUp(self): self.env_assist, self.config = get_env_tools(self) @@ -466,108 +558,21 @@ def test_remove_resource_multiple_dependencies(self): ) -class RemoveElementsStopResources(TestCase): +class RemoveElementsStopResources(TestCase, StopResourcesWaitMixin): def setUp(self): - self.tmp_file_mock_obj = TmpFileMock( - file_content_checker=assert_xml_equal, - ) - self.addCleanup(self.tmp_file_mock_obj.assert_all_done) - tmp_file_patcher = mock.patch("pcs.lib.tools.get_tmp_file") - self.addCleanup(tmp_file_patcher.stop) - tmp_file_mock = tmp_file_patcher.start() - tmp_file_mock.side_effect = ( - self.tmp_file_mock_obj.get_mock_side_effect() - ) self.env_assist, self.config = get_env_tools(self) + self.fixture_init_tmp_file_mocker() - def fixture_env( - self, - initial_cib_modifiers: dict[str, str], - initial_state_modifiers: dict[str, str], - after_disable_cib_modifiers: dict[str, str], - after_disable_state_modifiers: dict[str, str], - after_delete_cib_modifiers: dict[str, str], - successful_stop=True, - ): - + def test_one_resource(self): self.config.runner.cib.load( - name="load.disable", **initial_cib_modifiers - ) - self.config.runner.pcmk.load_state( - name="state.disable", **initial_state_modifiers - ) - self.config.runner.cib.diff( - "cib.disable.before", - "cib.disable.after", - name="diff.disable", - stdout="diff_disable", - ) - self.config.runner.cib.push_diff( - name="push.disable", cib_diff="diff_disable" - ) - - original_cib = self.config.calls.get("load.disable").stdout - after_disable_cib = modify_cib( - original_cib, **after_disable_cib_modifiers - ) - - self.config.runner.pcmk.wait(timeout=0) - self.config.runner.pcmk.load_state( - name="state.delete", **after_disable_state_modifiers + resources=""" + + + + """ ) - - mock_files = [ - TmpFileCall( - "cib.disable.before", - orig_content=self.config.calls.get("load.disable").stdout, - ), - TmpFileCall( - "cib.disable.after", - orig_content=after_disable_cib, - ), - ] - - if successful_stop: - self.config.runner.cib.load( - name="load.delete", **after_disable_cib_modifiers - ) - self.config.runner.cib.diff( - "cib.delete.before", - "cib.delete.after", - name="diff.delete", - stdout="diff_delete", - ) - - self.config.runner.cib.push_diff( - name="push.delete", cib_diff="diff_delete" - ) - - mock_files.extend( - [ - TmpFileCall( - "cib.delete.before", orig_content=after_disable_cib - ), - TmpFileCall( - "cib.delete.after", - orig_content=modify_cib( - read_test_resource("cib-empty.xml"), - **after_delete_cib_modifiers, - ), - ), - ] - ) - - self.tmp_file_mock_obj.set_calls(mock_files) - - def test_one_resource(self): - self.fixture_env( - initial_cib_modifiers={ - "resources": """ - - - - """ - }, + self.fixture_stop_resources_wait_calls( + self.config.calls.get("runner.cib.load").stdout, initial_state_modifiers={ "resources": """ @@ -593,8 +598,8 @@ def test_one_resource(self): """ }, - after_delete_cib_modifiers={"resources": ""}, ) + self.fixture_push_cib_after_stopping(resources="") lib.remove_elements(self.env_assist.get_env(), ["A"]) self.env_assist.assert_reports( @@ -608,14 +613,15 @@ def test_one_resource(self): ) def test_resource_unmanaged(self): - self.fixture_env( - initial_cib_modifiers={ - "resources": """ - - - - """ - }, + self.config.runner.cib.load( + resources=""" + + + + """ + ) + self.fixture_stop_resources_wait_calls( + self.config.calls.get("runner.cib.load").stdout, initial_state_modifiers={ "resources": """ @@ -641,8 +647,8 @@ def test_resource_unmanaged(self): """ }, - after_delete_cib_modifiers={"resources": ""}, ) + self.fixture_push_cib_after_stopping(resources="") lib.remove_elements(self.env_assist.get_env(), ["A"]) self.env_assist.assert_reports( @@ -659,14 +665,15 @@ def test_resource_unmanaged(self): ) def test_resource_remove_failed_to_stop(self): - self.fixture_env( - initial_cib_modifiers={ - "resources": """ - - - - """ - }, + self.config.runner.cib.load( + resources=""" + + + + """ + ) + self.fixture_stop_resources_wait_calls( + self.config.calls.get("runner.cib.load").stdout, initial_state_modifiers={ "resources": """ @@ -692,7 +699,6 @@ def test_resource_remove_failed_to_stop(self): """ }, - after_delete_cib_modifiers={}, successful_stop=False, ) @@ -727,17 +733,17 @@ def test_disable_only_resources(self): """ - - self.fixture_env( - initial_cib_modifiers={ - "resources": """ - - - - """, - "constraints": constraints, - "tags": tags, - }, + self.config.runner.cib.load( + resources=""" + + + + """, + constraints=constraints, + tags=tags, + ) + self.fixture_stop_resources_wait_calls( + self.config.calls.get("runner.cib.load").stdout, initial_state_modifiers={ "resources": """ @@ -765,11 +771,11 @@ def test_disable_only_resources(self): """ }, - after_delete_cib_modifiers={ - "resources": "", - "constraints": "", - "tags": "", - }, + ) + self.fixture_push_cib_after_stopping( + resources="", + constraints="", + tags="", ) lib.remove_elements(self.env_assist.get_env(), ["A"]) @@ -788,19 +794,20 @@ def test_disable_only_resources(self): ) def test_stop_inner_elements(self): - self.fixture_env( - initial_cib_modifiers={ - "resources": """ - - - - - - - - - """ - }, + self.config.runner.cib.load( + resources=""" + + + + + + + + + """ + ) + self.fixture_stop_resources_wait_calls( + self.config.calls.get("runner.cib.load").stdout, initial_state_modifiers={ "resources": """ @@ -859,8 +866,8 @@ def test_stop_inner_elements(self): """ }, - after_delete_cib_modifiers={"resources": ""}, ) + self.fixture_push_cib_after_stopping(resources="") lib.remove_elements(self.env_assist.get_env(), ["C"]) self.env_assist.assert_reports( @@ -882,19 +889,20 @@ def test_stop_inner_elements(self): ) def test_disable_only_needed_resources(self): - self.fixture_env( - initial_cib_modifiers={ - "resources": """ - - - - - - - - - """ - }, + self.config.runner.cib.load( + resources=""" + + + + + + + + + """ + ) + self.fixture_stop_resources_wait_calls( + self.config.calls.get("runner.cib.load").stdout, initial_state_modifiers={ "resources": """ @@ -943,17 +951,17 @@ def test_disable_only_needed_resources(self): """ }, - after_delete_cib_modifiers={ - "resources": """ - - - - - - - - """ - }, + ) + self.fixture_push_cib_after_stopping( + resources=""" + + + + + + + + """ ) lib.remove_elements(self.env_assist.get_env(), ["A"]) diff --git a/pcs_test/tools/custom_mock.py b/pcs_test/tools/custom_mock.py index 9137adb20..0882234d6 100644 --- a/pcs_test/tools/custom_mock.py +++ b/pcs_test/tools/custom_mock.py @@ -95,9 +95,12 @@ def _assert_file_content_equal(self, name, expected, real): ) def set_calls(self, calls): - self._calls = calls + self._calls = list(calls) self._calls_iter = iter(self._calls) + def extend_calls(self, calls): + self._calls.extend(calls) + def get_mock_side_effect(self): return self._mock_side_effect From 07acfb4ca7c2685d8bece95c861fb8c56bc94aaf Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Tue, 15 Oct 2024 09:22:28 +0200 Subject: [PATCH 044/227] show warning about not stopping resources with --force --- pcs/common/reports/codes.py | 3 +++ pcs/common/reports/messages.py | 16 ++++++++++++++++ pcs/lib/commands/booth.py | 7 +++---- pcs/lib/commands/cib.py | 18 ++++++++++++++---- pcs/lib/commands/remote_node.py | 14 +++++++------- pcs_test/tier0/common/reports/test_messages.py | 11 +++++++++++ .../remote_node/test_node_remove_remote.py | 15 +++++++++++++++ pcs_test/tier0/lib/commands/test_booth.py | 7 +++++-- pcs_test/tier0/lib/commands/test_cib.py | 14 +++++++++++++- 9 files changed, 87 insertions(+), 18 deletions(-) diff --git a/pcs/common/reports/codes.py b/pcs/common/reports/codes.py index c2c4626d3..0d448f0ab 100644 --- a/pcs/common/reports/codes.py +++ b/pcs/common/reports/codes.py @@ -89,6 +89,9 @@ "CANNOT_BAN_RESOURCE_STOPPED_NO_NODE_SPECIFIED" ) STOPPING_RESOURCES_BEFORE_DELETING = M("STOPPING_RESOURCES_BEFORE_DELETING") +STOPPING_RESOURCES_BEFORE_DELETING_SKIPPED = M( + "STOPPING_RESOURCES_BEFORE_DELETING_SKIPPED" +) CANNOT_STOP_RESOURCES_BEFORE_DELETING = M( "CANNOT_STOP_RESOURCES_BEFORE_DELETING" ) diff --git a/pcs/common/reports/messages.py b/pcs/common/reports/messages.py index 7eedb2d16..6dc1b3c05 100644 --- a/pcs/common/reports/messages.py +++ b/pcs/common/reports/messages.py @@ -6400,6 +6400,22 @@ def message(self) -> str: ) +@dataclass(frozen=True) +class StoppingResourcesBeforeDeletingSkipped(ReportItemMessage): + """ + Resources are not going to be stopped before deletion. + """ + + _code = codes.STOPPING_RESOURCES_BEFORE_DELETING_SKIPPED + + @property + def message(self) -> str: + return ( + "Resources are not going to be stopped before deletion, this may " + "result in orphaned resources being present in the cluster" + ) + + @dataclass(frozen=True) class CannotStopResourcesBeforeDeleting(ReportItemMessage): """ diff --git a/pcs/lib/commands/booth.py b/pcs/lib/commands/booth.py index 729364e6a..494e7f4f3 100644 --- a/pcs/lib/commands/booth.py +++ b/pcs/lib/commands/booth.py @@ -580,10 +580,9 @@ def remove_from_cluster( elements_to_remove.element_references.to_reports() ) - if env.is_cib_live and reports.codes.FORCE not in force_flags: - cib = _stop_resources_wait( - env, cib, elements_to_remove.resources_to_disable - ) + cib = _stop_resources_wait( + env, cib, elements_to_remove.resources_to_disable, force_flags + ) remove_specified_elements(cib, elements_to_remove) env.push_cib() diff --git a/pcs/lib/commands/cib.py b/pcs/lib/commands/cib.py index 59ef6752f..bca986603 100644 --- a/pcs/lib/commands/cib.py +++ b/pcs/lib/commands/cib.py @@ -80,10 +80,9 @@ def remove_elements( elements_to_remove.element_references.to_reports() ) - if env.is_cib_live and reports.codes.FORCE not in force_flags: - cib = _stop_resources_wait( - env, cib, elements_to_remove.resources_to_disable - ) + cib = _stop_resources_wait( + env, cib, elements_to_remove.resources_to_disable, force_flags + ) remove_specified_elements(cib, elements_to_remove) env.push_cib() @@ -97,6 +96,7 @@ def _stop_resources_wait( env: LibraryEnvironment, cib: _Element, resource_elements: Sequence[_Element], + force_flags: Collection[reports.types.ForceCode] = (), ) -> _Element: """ Stop all resources that are going to be removed. Push cib, wait for the @@ -105,9 +105,19 @@ def _stop_resources_wait( cib -- whole cib resource_elements -- resources that should be stopped + force_flags -- list of flags codes """ if not resource_elements: return cib + if not env.is_cib_live: + return cib + if reports.codes.FORCE in force_flags: + env.report_processor.report( + reports.ReportItem.warning( + reports.messages.StoppingResourcesBeforeDeletingSkipped() + ) + ) + return cib resource_ids = [str(el.attrib["id"]) for el in resource_elements] diff --git a/pcs/lib/commands/remote_node.py b/pcs/lib/commands/remote_node.py index 522bf5317..f388fb0ec 100644 --- a/pcs/lib/commands/remote_node.py +++ b/pcs/lib/commands/remote_node.py @@ -721,13 +721,14 @@ def node_remove_remote( """ cib = env.get_cib() report_processor = env.report_processor + force = reports.codes.FORCE in force_flags resource_element_list = _find_resources_to_remove( cib, report_processor, "remote", node_identifier, - allow_remove_multiple_nodes=reports.codes.FORCE in force_flags, + allow_remove_multiple_nodes=force, find_resources=remote_node.find_node_resources, ) @@ -756,11 +757,10 @@ def node_remove_remote( elements_to_remove.element_references.to_reports() ) - if env.is_cib_live and reports.codes.FORCE not in force_flags: - # we use private function from lib.commands.cib to reduce code repetition - cib = _stop_resources_wait( - env, cib, elements_to_remove.resources_to_disable - ) + # we use private function from lib.commands.cib to reduce code repetition + cib = _stop_resources_wait( + env, cib, elements_to_remove.resources_to_disable, force_flags + ) if not env.is_cib_live: report_processor.report_list( @@ -771,7 +771,7 @@ def node_remove_remote( env, node_names_list, skip_offline_nodes=reports.codes.SKIP_OFFLINE_NODES in force_flags, - allow_fails=reports.codes.FORCE in force_flags, + allow_fails=force, ) remove_specified_elements(cib, elements_to_remove) diff --git a/pcs_test/tier0/common/reports/test_messages.py b/pcs_test/tier0/common/reports/test_messages.py index b027755aa..e76d171fb 100644 --- a/pcs_test/tier0/common/reports/test_messages.py +++ b/pcs_test/tier0/common/reports/test_messages.py @@ -5992,6 +5992,17 @@ def test_multiple_resources(self) -> str: ) +class StoppingResourcesBeforeDeletingSkipped(NameBuildTest): + def test_success(self) -> str: + self.assert_message_from_report( + ( + "Resources are not going to be stopped before deletion, this " + "may result in orphaned resources being present in the cluster" + ), + reports.StoppingResourcesBeforeDeletingSkipped(), + ) + + class CannotStopResourcesBeforeDeleting(NameBuildTest): def test_one_resource(self) -> str: self.assert_message_from_report( diff --git a/pcs_test/tier0/lib/commands/remote_node/test_node_remove_remote.py b/pcs_test/tier0/lib/commands/remote_node/test_node_remove_remote.py index c9345fce8..953110ad4 100644 --- a/pcs_test/tier0/lib/commands/remote_node/test_node_remove_remote.py +++ b/pcs_test/tier0/lib/commands/remote_node/test_node_remove_remote.py @@ -331,6 +331,11 @@ def test_force(self): REPORTS["authkey_remove_success"].adapt(node=REMOTE_HOST) ) my_reports.append(self.report_multiple_results.to_warn()) + my_reports.append( + fixture.warn( + reports.codes.STOPPING_RESOURCES_BEFORE_DELETING_SKIPPED + ) + ) self.env_assist.assert_reports(my_reports) @@ -430,6 +435,11 @@ def test_forceable_when_remotely_fail(self): "authkey_remove_success", report_authkey_remove_failed(NODE_NAME).to_warn(), ) + my_reports.append( + fixture.warn( + reports.codes.STOPPING_RESOURCES_BEFORE_DELETING_SKIPPED + ) + ) self.env_assist.assert_reports(my_reports) @@ -516,4 +526,9 @@ def test_forceable_when_remotely_fail(self): "pcmk_remote_stop_success", report_pcmk_remote_stop_failed(NODE_NAME).to_warn(), ) + my_reports.append( + fixture.warn( + reports.codes.STOPPING_RESOURCES_BEFORE_DELETING_SKIPPED + ) + ) self.env_assist.assert_reports(my_reports) diff --git a/pcs_test/tier0/lib/commands/test_booth.py b/pcs_test/tier0/lib/commands/test_booth.py index 61b4d7695..7ff878959 100644 --- a/pcs_test/tier0/lib/commands/test_booth.py +++ b/pcs_test/tier0/lib/commands/test_booth.py @@ -2300,13 +2300,16 @@ def test_more_booth_resources_forced(self): ) self.env_assist.assert_reports( [ + fixture.warn( + reports.codes.BOOTH_MULTIPLE_TIMES_IN_CIB, + name="booth", + ), fixture.info( reports.codes.CIB_REMOVE_RESOURCES, id_list=["booth1", "booth2"], ), fixture.warn( - reports.codes.BOOTH_MULTIPLE_TIMES_IN_CIB, - name="booth", + reports.codes.STOPPING_RESOURCES_BEFORE_DELETING_SKIPPED ), ] ) diff --git a/pcs_test/tier0/lib/commands/test_cib.py b/pcs_test/tier0/lib/commands/test_cib.py index 01edc0cc4..7c72fd047 100644 --- a/pcs_test/tier0/lib/commands/test_cib.py +++ b/pcs_test/tier0/lib/commands/test_cib.py @@ -315,7 +315,10 @@ def test_remove_resources(self): "C-G-2": "primitive", "B-1": "primitive", }, - ) + ), + fixture.warn( + reports.codes.STOPPING_RESOURCES_BEFORE_DELETING_SKIPPED + ), ] ) @@ -358,6 +361,9 @@ def test_remove_resource_guest(self): fixture.deprecation( reports.codes.USE_COMMAND_NODE_REMOVE_GUEST ), + fixture.warn( + reports.codes.STOPPING_RESOURCES_BEFORE_DELETING_SKIPPED + ), fixture.warn( reports.codes.GUEST_NODE_REMOVAL_INCOMPLETE, node_name="R1-remote", @@ -425,6 +431,9 @@ def test_remove_resource_remote(self): fixture.deprecation( reports.codes.USE_COMMAND_NODE_REMOVE_REMOTE ), + fixture.warn( + reports.codes.STOPPING_RESOURCES_BEFORE_DELETING_SKIPPED + ), fixture.warn( reports.codes.REMOTE_NODE_REMOVAL_INCOMPLETE, node_name="R1" ), @@ -554,6 +563,9 @@ def test_remove_resource_multiple_dependencies(self): "PERMISSION": {"ROLE"}, }, ), + fixture.warn( + reports.codes.STOPPING_RESOURCES_BEFORE_DELETING_SKIPPED + ), ] ) From 1ea71967264f3076c3d22fa8d11e0e02d2d6ce1e Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Tue, 22 Oct 2024 10:34:39 +0200 Subject: [PATCH 045/227] improve readability of properties --- pcs/lib/cib/remove_elements.py | 36 +++++++++-- pcs/lib/commands/booth.py | 2 +- pcs/lib/commands/cib.py | 6 +- pcs/lib/commands/remote_node.py | 2 +- .../tier0/lib/cib/test_remove_elements.py | 60 +++++++++---------- 5 files changed, 66 insertions(+), 40 deletions(-) diff --git a/pcs/lib/cib/remove_elements.py b/pcs/lib/cib/remove_elements.py index 1640e40a6..093218dac 100644 --- a/pcs/lib/cib/remove_elements.py +++ b/pcs/lib/cib/remove_elements.py @@ -161,22 +161,34 @@ def __init__(self, cib: _Element, ids: StringCollection): for el in get_elements_by_ids(cib, all_ids)[0] } self._element_references = removing_references_from - self._resources_to_disable = [ + self._resources_to_remove = [ el for el in get_elements_by_ids(cib, sorted(element_ids_to_remove))[0] if is_resource(el) ] - @property - def resources_to_disable(self) -> list[_Element]: - return list(self._resources_to_disable) - @property def ids_to_remove(self) -> set[str]: + """ + Ids of ALL cib elements with id that should be removed, including all + resource and dependant elements + """ return set(self._ids_to_remove) + @property + def resources_to_remove(self) -> list[_Element]: + """ + List of cib resources that should be removed. Used for operations + needed for resources before their deletion, e.g. disabling them. + """ + return list(self._resources_to_remove) + @property def dependant_elements(self) -> DependantElements: + """ + Information about cib elements that are removed indirectly as + dependencies of other removed cib elements + """ return DependantElements( { element_id: self._id_tag_map[element_id] @@ -186,6 +198,12 @@ def dependant_elements(self) -> DependantElements: @property def element_references(self) -> ElementReferences: + """ + Information about cib element references that need to be removed. + These references does not have their own id and need special handling + while removing, e.g. references to stonith devices in fencing-level + elements, or obj-ref elements inside tag elements. + """ return ElementReferences( dict(self._element_references), { @@ -199,10 +217,17 @@ def element_references(self) -> ElementReferences: @property def missing_ids(self) -> set[str]: + """ + Set of ids not present in the cib + """ return set(self._missing_ids) @property def unsupported_elements(self) -> UnsupportedElements: + """ + Information about cib elements that cannot be removed using this + mechanism + """ return UnsupportedElements( id_tag_map={ element_id: self._id_tag_map[element_id] @@ -218,6 +243,7 @@ def warn_resource_unmanaged( state: _Element, resource_ids: StringSequence ) -> reports.ReportItemList: """ + Warn about unmanaged resources state -- state of the cluster resource_ids -- ids of resources to be checked diff --git a/pcs/lib/commands/booth.py b/pcs/lib/commands/booth.py index 494e7f4f3..54f3f3bdb 100644 --- a/pcs/lib/commands/booth.py +++ b/pcs/lib/commands/booth.py @@ -581,7 +581,7 @@ def remove_from_cluster( ) cib = _stop_resources_wait( - env, cib, elements_to_remove.resources_to_disable, force_flags + env, cib, elements_to_remove.resources_to_remove, force_flags ) remove_specified_elements(cib, elements_to_remove) diff --git a/pcs/lib/commands/cib.py b/pcs/lib/commands/cib.py index bca986603..2eeb41a8f 100644 --- a/pcs/lib/commands/cib.py +++ b/pcs/lib/commands/cib.py @@ -48,10 +48,10 @@ def remove_elements( elements_to_remove = ElementsToRemove(cib, ids) remote_node_names = _get_remote_node_names( - elements_to_remove.resources_to_disable + elements_to_remove.resources_to_remove ) guest_node_names = _get_guest_node_names( - elements_to_remove.resources_to_disable + elements_to_remove.resources_to_remove ) if remote_node_names: @@ -81,7 +81,7 @@ def remove_elements( ) cib = _stop_resources_wait( - env, cib, elements_to_remove.resources_to_disable, force_flags + env, cib, elements_to_remove.resources_to_remove, force_flags ) remove_specified_elements(cib, elements_to_remove) diff --git a/pcs/lib/commands/remote_node.py b/pcs/lib/commands/remote_node.py index f388fb0ec..f7fa040ba 100644 --- a/pcs/lib/commands/remote_node.py +++ b/pcs/lib/commands/remote_node.py @@ -759,7 +759,7 @@ def node_remove_remote( # we use private function from lib.commands.cib to reduce code repetition cib = _stop_resources_wait( - env, cib, elements_to_remove.resources_to_disable, force_flags + env, cib, elements_to_remove.resources_to_remove, force_flags ) if not env.is_cib_live: diff --git a/pcs_test/tier0/lib/cib/test_remove_elements.py b/pcs_test/tier0/lib/cib/test_remove_elements.py index 288e46400..5b6a7c420 100644 --- a/pcs_test/tier0/lib/cib/test_remove_elements.py +++ b/pcs_test/tier0/lib/cib/test_remove_elements.py @@ -107,7 +107,7 @@ def assert_elements_to_remove( self, elements_to_remove: lib.ElementsToRemove, ids_to_remove: set[str], - resources_to_disable: Optional[list[etree._Element]] = None, + resources_to_remove: Optional[list[etree._Element]] = None, dependant_elements: lib.DependantElements = lib.DependantElements({}), element_references: lib.ElementReferences = lib.ElementReferences( {}, {} @@ -119,8 +119,8 @@ def assert_elements_to_remove( ): self.assertEqual(elements_to_remove.ids_to_remove, ids_to_remove) self.assertEqual( - elements_to_remove.resources_to_disable, - resources_to_disable or [], + elements_to_remove.resources_to_remove, + resources_to_remove or [], ) self.assertEqual( elements_to_remove.dependant_elements, dependant_elements @@ -245,7 +245,7 @@ def test_resource_primitive(self): self.assert_elements_to_remove( elements_to_remove, {"A"}, - resources_to_disable=fixture_primitive_to_disable(cib), + resources_to_remove=fixture_primitive_to_disable(cib), ) def test_resource_primitive_in_tag(self): @@ -272,7 +272,7 @@ def test_resource_primitive_in_tag(self): self.assert_elements_to_remove( elements_to_remove, {"A", "T2"}, - resources_to_disable=fixture_primitive_to_disable(cib), + resources_to_remove=fixture_primitive_to_disable(cib), dependant_elements=lib.DependantElements({"T2": const.TAG_TAG}), element_references=lib.ElementReferences( {"A": {"T1"}}, @@ -303,7 +303,7 @@ def test_resource_primitive_constraints(self): self.assert_elements_to_remove( elements_to_remove, {"A", "l1", "o1", "c1", "t1"}, - resources_to_disable=fixture_primitive_to_disable(cib), + resources_to_remove=fixture_primitive_to_disable(cib), dependant_elements=lib.DependantElements( { "l1": const.TAG_CONSTRAINT_LOCATION, @@ -327,7 +327,7 @@ def test_resource_group(self): self.assert_elements_to_remove( elements_to_remove, {"G", "A", "B"}, - resources_to_disable=fixture_group_to_disable(cib), + resources_to_remove=fixture_group_to_disable(cib), dependant_elements=lib.DependantElements( { "A": const.TAG_RESOURCE_PRIMITIVE, @@ -349,7 +349,7 @@ def test_resource_group_member(self): self.assert_elements_to_remove( elements_to_remove, {"A"}, - resources_to_disable=[ + resources_to_remove=[ cib.find("./configuration/resources/group/primitive[@id='A']"), ], element_references=lib.ElementReferences( @@ -374,7 +374,7 @@ def test_resource_group_all_members(self): self.assert_elements_to_remove( elements_to_remove, {"A", "B", "G"}, - resources_to_disable=fixture_group_to_disable(cib), + resources_to_remove=fixture_group_to_disable(cib), dependant_elements=lib.DependantElements( {"G": const.TAG_RESOURCE_GROUP} ), @@ -402,7 +402,7 @@ def test_resource_group_constraints(self): self.assert_elements_to_remove( elements_to_remove, {"G", "A", "B", "l1", "o1", "c1", "t1"}, - resources_to_disable=fixture_group_to_disable(cib), + resources_to_remove=fixture_group_to_disable(cib), dependant_elements=lib.DependantElements( { "A": const.TAG_RESOURCE_PRIMITIVE, @@ -435,7 +435,7 @@ def test_group_in_tag(self): self.assert_elements_to_remove( elements_to_remove, {"G", "A", "B", "T"}, - resources_to_disable=fixture_group_to_disable(cib), + resources_to_remove=fixture_group_to_disable(cib), dependant_elements=lib.DependantElements( { "A": const.TAG_RESOURCE_PRIMITIVE, @@ -457,7 +457,7 @@ def test_resource_clone(self): self.assert_elements_to_remove( elements_to_remove, {"C", "A"}, - resources_to_disable=fixture_clone_to_disable(cib), + resources_to_remove=fixture_clone_to_disable(cib), dependant_elements=lib.DependantElements( {"A": const.TAG_RESOURCE_PRIMITIVE} ), @@ -476,7 +476,7 @@ def test_resource_clone_primitive(self): self.assert_elements_to_remove( elements_to_remove, {"C", "A"}, - resources_to_disable=fixture_clone_to_disable(cib), + resources_to_remove=fixture_clone_to_disable(cib), dependant_elements=lib.DependantElements( {"C": const.TAG_RESOURCE_CLONE} ), @@ -501,7 +501,7 @@ def test_resource_clone_constraints(self): self.assert_elements_to_remove( elements_to_remove, {"C", "A", "l1", "l2"}, - resources_to_disable=fixture_clone_to_disable(cib), + resources_to_remove=fixture_clone_to_disable(cib), dependant_elements=lib.DependantElements( { "A": const.TAG_RESOURCE_PRIMITIVE, @@ -531,7 +531,7 @@ def test_resource_clone_in_tag(self): self.assert_elements_to_remove( elements_to_remove, {"C", "A", "T"}, - resources_to_disable=fixture_clone_to_disable(cib), + resources_to_remove=fixture_clone_to_disable(cib), dependant_elements=lib.DependantElements( {"A": const.TAG_RESOURCE_PRIMITIVE, "T": const.TAG_TAG} ), @@ -549,7 +549,7 @@ def test_resource_bundle(self): self.assert_elements_to_remove( elements_to_remove, {"B"}, - resources_to_disable=[ + resources_to_remove=[ cib.find("./configuration/resources/bundle[@id='B']") ], ) @@ -568,7 +568,7 @@ def test_resource_bundle_with_primitive(self): self.assert_elements_to_remove( elements_to_remove, {"B", "A"}, - resources_to_disable=[ + resources_to_remove=[ cib.find("./configuration/resources/bundle/primitive[@id='A']"), cib.find("./configuration/resources/bundle[@id='B']"), ], @@ -591,7 +591,7 @@ def test_resource_bundle_primitive(self): self.assert_elements_to_remove( elements_to_remove, {"A"}, - resources_to_disable=[ + resources_to_remove=[ cib.find("./configuration/resources/bundle/primitive[@id='A']") ], element_references=lib.ElementReferences( @@ -626,7 +626,7 @@ def test_resource_referenced_in_acl(self): self.assert_elements_to_remove( elements_to_remove, {"vohrablo", "ucesat_se2"}, - resources_to_disable=[ + resources_to_remove=[ cib.find("./configuration/resources/primitive[@id='vohrablo']") ], dependant_elements=lib.DependantElements( @@ -663,7 +663,7 @@ def test_resource_referenced_in_acl_indirectly(self): self.assert_elements_to_remove( elements_to_remove, {"G", "A", "B", "PERMISSION"}, - resources_to_disable=fixture_group_to_disable(cib), + resources_to_remove=fixture_group_to_disable(cib), dependant_elements=lib.DependantElements( { "A": const.TAG_RESOURCE_PRIMITIVE, @@ -698,7 +698,7 @@ def test_resource_keep_fencing_level(self): self.assert_elements_to_remove( elements_to_remove, {"A"}, - resources_to_disable=fixture_primitive_to_disable(cib), + resources_to_remove=fixture_primitive_to_disable(cib), element_references=lib.ElementReferences( {"A": {"fl"}}, { @@ -725,7 +725,7 @@ def test_resource_remove_fencing_level(self): self.assert_elements_to_remove( elements_to_remove, {"A", "fl-NODE-A-1"}, - resources_to_disable=fixture_primitive_to_disable(cib), + resources_to_remove=fixture_primitive_to_disable(cib), dependant_elements=lib.DependantElements( {"fl-NODE-A-1": const.TAG_FENCING_LEVEL} ), @@ -754,7 +754,7 @@ def test_resource_in_constraint_set(self): self.assert_elements_to_remove( elements_to_remove, {"A"}, - resources_to_disable=fixture_primitive_to_disable(cib), + resources_to_remove=fixture_primitive_to_disable(cib), element_references=lib.ElementReferences( {"A": {"set1"}}, { @@ -789,7 +789,7 @@ def test_resource_in_constraint_set_remove_set_keep_constraint(self): self.assert_elements_to_remove( elements_to_remove, {"A", "set1"}, - resources_to_disable=fixture_primitive_to_disable(cib), + resources_to_remove=fixture_primitive_to_disable(cib), dependant_elements=lib.DependantElements( {"set1": const.TAG_RESOURCE_SET} ), @@ -823,7 +823,7 @@ def test_resource_in_constraint_set_remove_set_remove_constraint(self): self.assert_elements_to_remove( elements_to_remove, {"A", "set1", "c1"}, - resources_to_disable=fixture_primitive_to_disable(cib), + resources_to_remove=fixture_primitive_to_disable(cib), dependant_elements=lib.DependantElements( { "set1": const.TAG_RESOURCE_SET, @@ -858,7 +858,7 @@ def test_resource_in_multiple_sets(self): self.assert_elements_to_remove( elements_to_remove, {"A", "set1", "c1", "set2", "c2"}, - resources_to_disable=fixture_primitive_to_disable(cib), + resources_to_remove=fixture_primitive_to_disable(cib), dependant_elements=lib.DependantElements( { "set1": const.TAG_RESOURCE_SET, @@ -883,7 +883,7 @@ def test_resource_legacy_promotable_clone(self): self.assert_elements_to_remove( elements_to_remove, {"MS", "A"}, - resources_to_disable=[ + resources_to_remove=[ cib.find("./configuration/resources/master/primitive[@id='A']"), cib.find("./configuration/resources/master/[@id='MS']"), ], @@ -906,7 +906,7 @@ def test_resource_legacy_promotable_clone_inner_element(self): self.assert_elements_to_remove( elements_to_remove, {"A", "MS"}, - resources_to_disable=[ + resources_to_remove=[ cib.find("./configuration/resources/master/primitive[@id='A']"), cib.find("./configuration/resources/master/[@id='MS']"), ], @@ -926,7 +926,7 @@ def test_missing_elements(self): elements_to_remove, {"A"}, missing_ids={"B", "C", "D"}, - resources_to_disable=fixture_primitive_to_disable(cib), + resources_to_remove=fixture_primitive_to_disable(cib), ) def test_unsupported_id_types(self): @@ -992,7 +992,7 @@ def test_remote_guest_node_name_constraint(self): self.assert_elements_to_remove( elements_to_remove, {"R1", "R2", "l1", "l2"}, - resources_to_disable=[ + resources_to_remove=[ cib.find("./configuration/resources/primitive[@id='R1']"), cib.find("./configuration/resources/primitive[@id='R2']"), ], From 3a90b474168deafe743919da79d3389756a9bea4 Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Wed, 23 Oct 2024 16:49:13 +0200 Subject: [PATCH 046/227] drop old function --- pcs/lib/cib/constraint/colocation.py | 2 +- pcs/lib/cib/constraint/location.py | 2 +- pcs/lib/cib/constraint/order.py | 2 +- pcs/lib/cib/constraint/resource_set.py | 4 ---- pcs/lib/cib/constraint/ticket.py | 2 +- 5 files changed, 4 insertions(+), 8 deletions(-) diff --git a/pcs/lib/cib/constraint/colocation.py b/pcs/lib/cib/constraint/colocation.py index cb4d9de2b..ac7b9ba96 100644 --- a/pcs/lib/cib/constraint/colocation.py +++ b/pcs/lib/cib/constraint/colocation.py @@ -11,9 +11,9 @@ from pcs.common.reports.item import ReportItem from pcs.lib.cib.const import TAG_CONSTRAINT_COLOCATION as TAG from pcs.lib.cib.constraint import constraint +from pcs.lib.cib.constraint.common import is_set_constraint from pcs.lib.cib.constraint.resource_set import ( constraint_element_to_resource_set_dto_list, - is_set_constraint, ) from pcs.lib.cib.rule import RuleInEffectEval from pcs.lib.cib.rule.cib_to_dto import rule_element_to_dto diff --git a/pcs/lib/cib/constraint/location.py b/pcs/lib/cib/constraint/location.py index 4952798df..45d548a6b 100644 --- a/pcs/lib/cib/constraint/location.py +++ b/pcs/lib/cib/constraint/location.py @@ -8,9 +8,9 @@ from pcs.common.pacemaker.types import CibResourceDiscovery from pcs.lib.cib.const import TAG_CONSTRAINT_LOCATION as TAG from pcs.lib.cib.const import TAG_RULE +from pcs.lib.cib.constraint.common import is_set_constraint from pcs.lib.cib.constraint.resource_set import ( constraint_element_to_resource_set_dto_list, - is_set_constraint, ) from pcs.lib.cib.rule import RuleInEffectEval from pcs.lib.cib.rule.cib_to_dto import rule_element_to_dto diff --git a/pcs/lib/cib/constraint/order.py b/pcs/lib/cib/constraint/order.py index 5042d2b0b..9f68861a6 100644 --- a/pcs/lib/cib/constraint/order.py +++ b/pcs/lib/cib/constraint/order.py @@ -13,9 +13,9 @@ from pcs.common.reports.item import ReportItem from pcs.lib.cib.const import TAG_CONSTRAINT_ORDER as TAG from pcs.lib.cib.constraint import constraint +from pcs.lib.cib.constraint.common import is_set_constraint from pcs.lib.cib.constraint.resource_set import ( constraint_element_to_resource_set_dto_list, - is_set_constraint, ) from pcs.lib.cib.tools import check_new_id_applicable from pcs.lib.errors import LibraryError diff --git a/pcs/lib/cib/constraint/resource_set.py b/pcs/lib/cib/constraint/resource_set.py index 2aacd41f0..4fa65d81a 100644 --- a/pcs/lib/cib/constraint/resource_set.py +++ b/pcs/lib/cib/constraint/resource_set.py @@ -112,10 +112,6 @@ def is_resource_in_same_group(cib, resource_id_list): ) -def is_set_constraint(constraint_el: etree._Element) -> bool: - return constraint_el.find("./resource_set") is not None - - def _resource_set_element_to_dto( resource_set_el: etree._Element, ) -> CibResourceSetDto: diff --git a/pcs/lib/cib/constraint/ticket.py b/pcs/lib/cib/constraint/ticket.py index bd2fbcebd..a60a170cf 100644 --- a/pcs/lib/cib/constraint/ticket.py +++ b/pcs/lib/cib/constraint/ticket.py @@ -23,9 +23,9 @@ from pcs.lib.cib import tools from pcs.lib.cib.const import TAG_CONSTRAINT_TICKET as TAG from pcs.lib.cib.constraint import constraint +from pcs.lib.cib.constraint.common import is_set_constraint from pcs.lib.cib.constraint.resource_set import ( constraint_element_to_resource_set_dto_list, - is_set_constraint, ) from pcs.lib.cib.tools import role_constructor from pcs.lib.errors import LibraryError From dffb59f981687d3b626b4c268fdbb92dc0fb3ffe Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Fri, 25 Oct 2024 14:53:48 +0200 Subject: [PATCH 047/227] deprecate multiple rules in a location constraint --- CHANGELOG.md | 9 ++++-- pcs/cli/constraint/rule/command.py | 8 +++++ pcs/constraint.py | 7 +++++ .../tier0/cli/constraint/rule/test_command.py | 20 +++++++++---- pcs_test/tier1/legacy/test_constraints.py | 30 ++++++++++++------- 5 files changed, 56 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8913b0fb2..8a230298c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ - Lib command `cib.remove_elements` can now remove resources ### Changed -- Commands `pcs resource delete | remove` and `pcs stonith delete | remove` +- Commands `pcs resource delete | remove` and `pcs stonith delete | remove` now allow ([RHEL-61901]): - deletion of multiple resources or stonith resources with one command - deletion of resources or stonith resources included in tags @@ -32,6 +32,9 @@ - Using `pcs resource delete | remove` to delete resources representing remote and guest nodes. Use `pcs cluster node remove-remote` and `pcs cluster node remove-guest` instead. +- Commands `pcs constraint rule add | delete | remove`, as support for multiple + rules in a location constraint is among the [features planned to be removed + in pacemaker 3] [RHEL-46284]: https://issues.redhat.com/browse/RHEL-46284 [RHEL-46286]: https://issues.redhat.com/browse/RHEL-46286 @@ -39,6 +42,7 @@ [RHEL-55441]: https://issues.redhat.com/browse/RHEL-55441 [RHEL-61738]: https://issues.redhat.com/browse/RHEL-61738 [RHEL-61901]: https://issues.redhat.com/browse/RHEL-61901 +[features planned to be removed in pacemaker 3]: https://projects.clusterlabs.org/w/projects/pacemaker/pacemaker_3.0_changes/pacemaker_3.0_configuration_changes/ ## [0.11.8] - 2024-07-09 @@ -70,7 +74,7 @@ - Validate SBD\_DELAY\_START and SBD\_STARTMODE options ([RHEL-17962]) ### Deprecated -- Pcs produces warnings about [features planned to be removed in pacemaker 3](https://projects.clusterlabs.org/w/projects/pacemaker/pacemaker_3.0_changes/pacemaker_3.0_configuration_changes/): +- Pcs produces warnings about [features planned to be removed in pacemaker 3]: - score in order constraints - using rkt in bundles - upstart and nagios resources @@ -88,6 +92,7 @@ [RHEL-27492]: https://issues.redhat.com/browse/RHEL-27492 [RHEL-28749]: https://issues.redhat.com/browse/RHEL-28749 [RHEL-36514]: https://issues.redhat.com/browse/RHEL-36514 +[features planned to be removed in pacemaker 3]: https://projects.clusterlabs.org/w/projects/pacemaker/pacemaker_3.0_changes/pacemaker_3.0_configuration_changes/ ## [0.11.7] - 2024-01-11 diff --git a/pcs/cli/constraint/rule/command.py b/pcs/cli/constraint/rule/command.py index 056aa9458..a94f63f80 100644 --- a/pcs/cli/constraint/rule/command.py +++ b/pcs/cli/constraint/rule/command.py @@ -6,6 +6,7 @@ InputModifiers, ensure_unique_args, ) +from pcs.cli.reports.output import deprecation_warning from pcs.common.pacemaker.constraint import get_all_location_rules_ids from pcs.common.str_tools import format_list @@ -15,6 +16,13 @@ def remove(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: Options: * -f - CIB file """ + # deprecated in pacemaker 2, removed in pacemaker 3 + # added to pcs after 0.11.8 + # the whole command removed in pcs-0.12 + deprecation_warning( + "The possibility of defining multiple rules in a single location " + "constraint is deprecated and will be removed." + ) modifiers.ensure_only_supported("-f") if not argv: raise CmdLineInputError() diff --git a/pcs/constraint.py b/pcs/constraint.py index 55e11e882..65ef6c5f3 100644 --- a/pcs/constraint.py +++ b/pcs/constraint.py @@ -1521,6 +1521,13 @@ def constraint_rule_add(lib, argv, modifiers): * -f - CIB file * --force - allow duplicate constraints """ + # deprecated in pacemaker 2, removed in pacemaker 3 + # added to pcs after 0.11.8 + # the whole command removed in pcs-0.12 + deprecation_warning( + "The possibility of defining multiple rules in a single location " + "constraint is deprecated and will be removed." + ) del lib modifiers.ensure_only_supported("-f", "--force") if not argv: diff --git a/pcs_test/tier0/cli/constraint/rule/test_command.py b/pcs_test/tier0/cli/constraint/rule/test_command.py index 75dafbc8f..a45c045ec 100644 --- a/pcs_test/tier0/cli/constraint/rule/test_command.py +++ b/pcs_test/tier0/cli/constraint/rule/test_command.py @@ -11,6 +11,7 @@ from pcs_test.tools.misc import dict_to_modifiers +@mock.patch("pcs.cli.constraint.rule.command.deprecation_warning") class TestRemoveColocationConstraint(TestCase): def setUp(self): self.lib = mock.Mock(spec_set=["constraint", "cib"]) @@ -21,18 +22,23 @@ def setUp(self): ) self.lib.cib = self.cib self.lib.constraint = self.constraint + self.deprecation_msg = ( + "The possibility of defining multiple rules in a single location " + "constraint is deprecated and will be removed." + ) def _call_cmd(self, argv, modifiers=None): rule_command.remove(self.lib, argv, dict_to_modifiers(modifiers or {})) - def test_no_args(self): + def test_no_args(self, mock_deprecation_warning): with self.assertRaises(CmdLineInputError) as cm: self._call_cmd([]) self.assertIsNone(cm.exception.message) self.constraint.get_config.assert_not_called() self.cib.remove_elements.assert_not_called() + mock_deprecation_warning.assert_called_once_with(self.deprecation_msg) - def test_duplicate_args(self): + def test_duplicate_args(self, mock_deprecation_warning): with self.assertRaises(CmdLineInputError) as cm: self._call_cmd(["id1", "id2", "id3", "id1", "id2"]) self.assertEqual( @@ -40,8 +46,9 @@ def test_duplicate_args(self): ) self.constraint.get_config.assert_not_called() self.cib.remove_elements.assert_not_called() + mock_deprecation_warning.assert_called_once_with(self.deprecation_msg) - def test_unsupported_modifier(self): + def test_unsupported_modifier(self, mock_deprecation_warning): with self.assertRaises(CmdLineInputError) as cm: self._call_cmd(["id1", "id2"], {"force": True}) self.assertEqual( @@ -50,8 +57,9 @@ def test_unsupported_modifier(self): ) self.constraint.get_config.assert_not_called() self.cib.remove_elements.assert_not_called() + mock_deprecation_warning.assert_called_once_with(self.deprecation_msg) - def test_rule_ids_not_found(self): + def test_rule_ids_not_found(self, mock_deprecation_warning): with self.assertRaises(CmdLineInputError) as cm: self._call_cmd(["id1", "id2"]) self.assertEqual( @@ -60,8 +68,9 @@ def test_rule_ids_not_found(self): ) self.constraint.get_config.assert_called_once_with(evaluate_rules=False) self.cib.remove_elements.assert_not_called() + mock_deprecation_warning.assert_called_once_with(self.deprecation_msg) - def test_rule_ids_found(self): + def test_rule_ids_found(self, mock_deprecation_warning): location_rule_ids = [ "loc_constr_with_expired_rule-rule", "loc_constr_with_not_expired_rule-rule", @@ -70,3 +79,4 @@ def test_rule_ids_found(self): self._call_cmd(location_rule_ids) self.constraint.get_config.assert_called_once_with(evaluate_rules=False) self.cib.remove_elements.assert_called_once_with(location_rule_ids) + mock_deprecation_warning.assert_called_once_with(self.deprecation_msg) diff --git a/pcs_test/tier1/legacy/test_constraints.py b/pcs_test/tier1/legacy/test_constraints.py index af9037283..d3dcdf95f 100644 --- a/pcs_test/tier1/legacy/test_constraints.py +++ b/pcs_test/tier1/legacy/test_constraints.py @@ -58,6 +58,10 @@ "Deprecation Warning: This command is deprecated and will be removed. " "Please use 'pcs constraint delete' or 'pcs constraint remove' instead.\n" ) +DEPRECATED_MULTIPLE_RULES = ( + "Deprecation Warning: The possibility of defining multiple rules " + "in a single location constraint is deprecated and will be removed.\n" +) empty_cib = rc("cib-empty-3.7.xml") large_cib = rc("cib-large.xml") @@ -1901,7 +1905,7 @@ def test_location_constraint_rule(self): "constraint rule add location-D1-rh7-1-INFINITY #uname eq rh7-1".split(), ) self.assertEqual(stdout, "") - self.assertEqual(stderr, "") + self.assertEqual(stderr, DEPRECATED_MULTIPLE_RULES) self.assertEqual(retval, 0) stdout, stderr, retval = pcs( @@ -1909,7 +1913,7 @@ def test_location_constraint_rule(self): "constraint rule add location-D1-rh7-1-INFINITY #uname eq rh7-1".split(), ) self.assertEqual(stdout, "") - self.assertEqual(stderr, "") + self.assertEqual(stderr, DEPRECATED_MULTIPLE_RULES) self.assertEqual(retval, 0) stdout, stderr, retval = pcs( @@ -1917,7 +1921,7 @@ def test_location_constraint_rule(self): "constraint rule add location-D1-rh7-1-INFINITY #uname eq rh7-1".split(), ) self.assertEqual(stdout, "") - self.assertEqual(stderr, "") + self.assertEqual(stderr, DEPRECATED_MULTIPLE_RULES) self.assertEqual(retval, 0) stdout, stderr, retval = pcs( @@ -1925,7 +1929,7 @@ def test_location_constraint_rule(self): "constraint rule add location-D2-rh7-2-INFINITY date-spec hours=9-16 weekdays=1-5".split(), ) self.assertEqual(stdout, "") - self.assertEqual(stderr, "") + self.assertEqual(stderr, DEPRECATED_MULTIPLE_RULES) self.assertEqual(retval, 0) self.assert_pcs_success( @@ -1958,7 +1962,7 @@ def test_location_constraint_rule(self): self.assertEqual( stderr, ( - "Removing references:\n" + DEPRECATED_MULTIPLE_RULES + "Removing references:\n" " Rule 'location-D1-rh7-1-INFINITY-rule-1' from:\n" " Location constraint: 'location-D1-rh7-1-INFINITY'\n" ), @@ -1973,7 +1977,7 @@ def test_location_constraint_rule(self): self.assertEqual( stderr, ( - "Removing references:\n" + DEPRECATED_MULTIPLE_RULES + "Removing references:\n" " Rule 'location-D1-rh7-1-INFINITY-rule-2' from:\n" " Location constraint: 'location-D1-rh7-1-INFINITY'\n" ), @@ -2006,7 +2010,7 @@ def test_location_constraint_rule(self): self.assertEqual( stderr, ( - "Removing dependant element:\n" + DEPRECATED_MULTIPLE_RULES + "Removing dependant element:\n" " Location constraint: 'location-D1-rh7-1-INFINITY'\n" ), ) @@ -2050,7 +2054,8 @@ def test_location_constraint_rule(self): ) ac( stderr, - "Error: Unable to find constraint: location-D1-rh7-1-INFINITY\n", + DEPRECATED_MULTIPLE_RULES + + "Error: Unable to find constraint: location-D1-rh7-1-INFINITY\n", ) self.assertEqual(stdout, "") self.assertEqual(retval, 1) @@ -2061,7 +2066,8 @@ def test_location_constraint_rule(self): ) ac( stderr, - "Error: invalid rule id '123', '1' is not a valid first character for a rule id\n", + DEPRECATED_MULTIPLE_RULES + + "Error: invalid rule id '123', '1' is not a valid first character for a rule id\n", ) self.assertEqual(stdout, "") self.assertEqual(retval, 1) @@ -5306,7 +5312,8 @@ def fixture_complex_primitive(self): ( "constraint rule add location-D2 id=test-duration score=INFINITY " "date in_range 2019-03-01 to duration weeks=2" - ).split() + ).split(), + stderr_full=DEPRECATED_MULTIPLE_RULES, ) self.assert_pcs_success( ( @@ -5318,7 +5325,8 @@ def fixture_complex_primitive(self): ( "constraint rule add location-D3 id=test-defined score=INFINITY " "not_defined pingd" - ).split() + ).split(), + stderr_full=DEPRECATED_MULTIPLE_RULES, ) def test_complex_primitive_plain(self): From 11127bf0ea863c9adfb061cff74a4479079c1857 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Mon, 9 Sep 2024 15:30:24 +0200 Subject: [PATCH 048/227] export fencing levels --- CHANGELOG.md | 3 + mypy.ini | 8 + pcs/Makefile.am | 7 + pcs/cli/common/lib_wrapper.py | 1 + pcs/cli/common/output.py | 4 + pcs/cli/resource/command.py | 68 ++++ pcs/cli/resource/output.py | 6 +- pcs/cli/routing/resource.py | 3 +- pcs/cli/routing/stonith.py | 6 +- pcs/cli/stonith/__init__.py | 0 pcs/cli/stonith/command.py | 58 ++++ pcs/cli/stonith/levels/__init__.py | 0 pcs/cli/stonith/levels/command.py | 44 +++ pcs/cli/stonith/levels/output.py | 114 +++++++ pcs/common/fencing_topology.py | 15 +- pcs/common/pacemaker/fencing_topology.py | 42 +++ pcs/common/reports/messages.py | 24 +- pcs/config.py | 6 +- .../async_tasks/worker/command_mapping.py | 4 + pcs/lib/cib/fencing_topology.py | 321 +++++++++++++----- pcs/lib/cib/resource/stonith.py | 12 - pcs/lib/cib/tools.py | 2 +- pcs/lib/commands/cluster.py | 7 +- pcs/lib/commands/fencing_topology.py | 102 +++--- pcs/pcs.8.in | 10 +- pcs/resource.py | 46 --- pcs/stonith.py | 95 +----- pcs/usage.py | 35 +- pcs_test/Makefile.am | 6 + pcs_test/resources/cib-fencing-levels.xml | 40 +++ pcs_test/tier0/cli/stonith/__init__.py | 0 pcs_test/tier0/cli/stonith/levels/__init__.py | 0 .../tier0/cli/stonith/levels/test_output.py | 120 +++++++ .../tier0/lib/cib/test_fencing_topology.py | 225 ++++++++++-- .../lib/commands/test_fencing_topology.py | 100 +++--- pcs_test/tier1/legacy/test_stonith.py | 82 ++--- pcs_test/tier1/stonith/levels/__init__.py | 0 pcs_test/tier1/stonith/levels/test_config.py | 177 ++++++++++ pcs_test/tier1/stonith/test_config.py | 149 +++++++- pcs_test/tier1/test_status.py | 24 +- pcs_test/tier1/test_tag.py | 12 +- pcsd/capabilities.xml.in | 18 + typos.toml | 1 + 43 files changed, 1549 insertions(+), 448 deletions(-) create mode 100644 pcs/cli/resource/command.py create mode 100644 pcs/cli/stonith/__init__.py create mode 100644 pcs/cli/stonith/command.py create mode 100644 pcs/cli/stonith/levels/__init__.py create mode 100644 pcs/cli/stonith/levels/command.py create mode 100644 pcs/cli/stonith/levels/output.py create mode 100644 pcs/common/pacemaker/fencing_topology.py create mode 100644 pcs_test/resources/cib-fencing-levels.xml create mode 100644 pcs_test/tier0/cli/stonith/__init__.py create mode 100644 pcs_test/tier0/cli/stonith/levels/__init__.py create mode 100644 pcs_test/tier0/cli/stonith/levels/test_output.py create mode 100644 pcs_test/tier1/stonith/levels/__init__.py create mode 100644 pcs_test/tier1/stonith/levels/test_config.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a230298c..f97b28fea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ - Add lib command `status.full_cluster_status_plaintext` to API v1 ([RHEL-61738]) - Lib command `cib.remove_elements` can now remove resources +- Support for exporting stonith levels in `json` and `cmd` formats in commands + `pcs stonith config` and `pcs stonith level config` commands ([RHEL-16232]) ### Changed - Commands `pcs resource delete | remove` and `pcs stonith delete | remove` @@ -43,6 +45,7 @@ [RHEL-61738]: https://issues.redhat.com/browse/RHEL-61738 [RHEL-61901]: https://issues.redhat.com/browse/RHEL-61901 [features planned to be removed in pacemaker 3]: https://projects.clusterlabs.org/w/projects/pacemaker/pacemaker_3.0_changes/pacemaker_3.0_configuration_changes/ +[RHEL-16232]: https://issues.redhat.com/browse/RHEL-16232 ## [0.11.8] - 2024-07-09 diff --git a/mypy.ini b/mypy.ini index b6e385cf0..403323571 100644 --- a/mypy.ini +++ b/mypy.ini @@ -84,6 +84,10 @@ disallow_untyped_calls = False disallow_untyped_defs = True disallow_untyped_calls = True +[mypy-pcs.lib.cib.fencing_topology] +disallow_untyped_defs = True +disallow_untyped_calls = True + [mypy-pcs.lib.cib.nvpair_multi] disallow_untyped_defs = True @@ -137,6 +141,10 @@ disallow_untyped_calls = True [mypy-pcs.lib.commands.dr] disallow_untyped_defs = True +[mypy-pcs.lib.commands.fencing_topology] +disallow_untyped_defs = True +disallow_untyped_calls = True + [mypy-pcs.lib.commands.status] disallow_untyped_defs = True diff --git a/pcs/Makefile.am b/pcs/Makefile.am index c8bfbf0ab..1a5a27d6d 100644 --- a/pcs/Makefile.am +++ b/pcs/Makefile.am @@ -75,6 +75,7 @@ EXTRA_DIST = \ cli/reports/preprocessor.py \ cli/reports/processor.py \ cli/resource/__init__.py \ + cli/resource/command.py \ cli/resource/parse_args.py \ cli/resource/output.py \ cli/resource/relations.py \ @@ -102,6 +103,11 @@ EXTRA_DIST = \ cli/rule.py \ cli/status/command.py \ cli/status/__init__.py \ + cli/stonith/__init__.py \ + cli/stonith/command.py \ + cli/stonith/levels/__init__.py \ + cli/stonith/levels/command.py \ + cli/stonith/levels/output.py \ cli/tag/__init__.py \ cli/tag/command.py \ cli/tag/output.py \ @@ -135,6 +141,7 @@ EXTRA_DIST = \ common/pacemaker/constraint/set.py \ common/pacemaker/constraint/ticket.py \ common/pacemaker/defaults.py \ + common/pacemaker/fencing_topology.py \ common/pacemaker/nvset.py \ common/pacemaker/resource/__init__.py \ common/pacemaker/resource/bundle.py \ diff --git a/pcs/cli/common/lib_wrapper.py b/pcs/cli/common/lib_wrapper.py index acca19838..dc590562a 100644 --- a/pcs/cli/common/lib_wrapper.py +++ b/pcs/cli/common/lib_wrapper.py @@ -284,6 +284,7 @@ def load_module(env, middleware_factory, name): { "add_level": fencing_topology.add_level, "get_config": fencing_topology.get_config, + "get_config_dto": fencing_topology.get_config_dto, "remove_all_levels": fencing_topology.remove_all_levels, "remove_levels_by_params": ( fencing_topology.remove_levels_by_params diff --git a/pcs/cli/common/output.py b/pcs/cli/common/output.py index 9dc0e1621..817d5ce59 100644 --- a/pcs/cli/common/output.py +++ b/pcs/cli/common/output.py @@ -96,3 +96,7 @@ def pairs_to_cmd(pairs: Iterable[tuple[str, str]]) -> str: def lines_to_str(lines: StringSequence) -> str: return "\n".join(smart_wrap_text(lines)) + + +def format_cmd_list(cmd_lines: StringSequence) -> str: + return ";\n".join(cmd_lines) diff --git a/pcs/cli/resource/command.py b/pcs/cli/resource/command.py new file mode 100644 index 000000000..0dcb595cb --- /dev/null +++ b/pcs/cli/resource/command.py @@ -0,0 +1,68 @@ +import json +from typing import Any + +from pcs.cli.common.output import ( + format_cmd_list, + lines_to_str, + smart_wrap_text, +) +from pcs.cli.common.parse_args import ( + OUTPUT_FORMAT_VALUE_CMD, + OUTPUT_FORMAT_VALUE_JSON, + Argv, + InputModifiers, +) +from pcs.cli.resource.output import ( + ResourcesConfigurationFacade, + resources_to_cmd, + resources_to_text, +) +from pcs.common.interface import dto +from pcs.common.pacemaker.resource.list import CibResourcesDto + + +def config(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: + output = config_common(lib, argv, modifiers, stonith=False) + if output: + print(output) + + +def config_common( + lib: Any, argv: Argv, modifiers: InputModifiers, stonith: bool +) -> str: + """ + Also used by stonith commands. + + Options: + * -f - CIB file + * --output-format - supported formats: text, cmd, json + """ + modifiers.ensure_only_supported("-f", output_format_supported=True) + resources_facade = ( + ResourcesConfigurationFacade.from_resources_dto( + lib.resource.get_configured_resources() + ) + .filter_stonith(stonith) + .filter_resources(argv) + ) + output_format = modifiers.get_output_format() + if output_format == OUTPUT_FORMAT_VALUE_CMD: + output = format_cmd_list( + [" \\\n".join(cmd) for cmd in resources_to_cmd(resources_facade)] + ) + elif output_format == OUTPUT_FORMAT_VALUE_JSON: + output = json.dumps( + dto.to_dict( + CibResourcesDto( + primitives=resources_facade.primitives, + clones=resources_facade.clones, + groups=resources_facade.groups, + bundles=resources_facade.bundles, + ) + ) + ) + else: + output = lines_to_str( + smart_wrap_text(resources_to_text(resources_facade)) + ) + return output diff --git a/pcs/cli/resource/output.py b/pcs/cli/resource/output.py index 9bfffe4ef..1df2b0e20 100644 --- a/pcs/cli/resource/output.py +++ b/pcs/cli/resource/output.py @@ -1,11 +1,13 @@ import shlex from collections import defaultdict -from typing import ( +from collections.abc import ( Container, + Sequence, +) +from typing import ( Dict, List, Optional, - Sequence, Tuple, Union, ) diff --git a/pcs/cli/routing/resource.py b/pcs/cli/routing/resource.py index 3e0ad6b6d..3775dae7c 100644 --- a/pcs/cli/routing/resource.py +++ b/pcs/cli/routing/resource.py @@ -1,5 +1,6 @@ from functools import partial +import pcs.cli.resource.command as resource_cli from pcs import ( resource, usage, @@ -33,7 +34,7 @@ # replaced with 'resource status' and 'resource config' "show": resource.resource_show, "status": resource.resource_status, - "config": resource.config, + "config": resource_cli.config, "group": create_router( { "add": resource.resource_group_add_cmd, diff --git a/pcs/cli/routing/stonith.py b/pcs/cli/routing/stonith.py index da737d2c3..370d033e8 100644 --- a/pcs/cli/routing/stonith.py +++ b/pcs/cli/routing/stonith.py @@ -1,3 +1,5 @@ +import pcs.cli.stonith.command as stonith_cli +import pcs.cli.stonith.levels.command as levels_cli from pcs import ( resource, stonith, @@ -15,7 +17,7 @@ "help": lambda lib, argv, modifiers: print(usage.stonith(argv)), "list": stonith.stonith_list_available, "describe": stonith.stonith_list_options, - "config": stonith.config_cmd, + "config": stonith_cli.config, "create": stonith.stonith_create, "update": stonith.update_cmd, "update-scsi-devices": stonith.stonith_update_scsi_devices, @@ -42,7 +44,7 @@ { "add": stonith.stonith_level_add_cmd, "clear": stonith.stonith_level_clear_cmd, - "config": stonith.stonith_level_config_cmd, + "config": levels_cli.config, "remove": stonith.stonith_level_remove_cmd, "delete": stonith.stonith_level_remove_cmd, "verify": stonith.stonith_level_verify_cmd, diff --git a/pcs/cli/stonith/__init__.py b/pcs/cli/stonith/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pcs/cli/stonith/command.py b/pcs/cli/stonith/command.py new file mode 100644 index 000000000..ac0dd5ddb --- /dev/null +++ b/pcs/cli/stonith/command.py @@ -0,0 +1,58 @@ +from typing import Any + +from pcs.cli.common.output import ( + format_cmd_list, + lines_to_str, + smart_wrap_text, +) +from pcs.cli.common.parse_args import ( + OUTPUT_FORMAT_VALUE_CMD, + OUTPUT_FORMAT_VALUE_JSON, + Argv, + InputModifiers, +) +from pcs.cli.reports.output import warn +from pcs.cli.resource import command as resource_cmd +from pcs.cli.stonith.levels.output import ( + stonith_level_config_to_cmd, + stonith_level_config_to_text, +) +from pcs.common.str_tools import indent + + +def config(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: + """ + Options: + * --output-format - supported formats: text, cmd, json + * -f CIB file + """ + output_format = modifiers.get_output_format() + output = resource_cmd.config_common(lib, argv, modifiers, stonith=True) + + if output_format == OUTPUT_FORMAT_VALUE_JSON: + # JSON output format does not include fencing levels because it would + # change the current JSON structure and break existing user tooling + warn( + "Fencing levels are not included because this command could only " + "export stonith configuration previously. This cannot be changed " + "to avoid breaking existing tooling. To export fencing levels, run " + "'pcs stonith level config --output-format=json'" + ) + print(output) + return + + fencing_topology_dto = lib.fencing_topology.get_config_dto() + if output_format == OUTPUT_FORMAT_VALUE_CMD: + # we can look at the output of config_common as one command + output = format_cmd_list( + [output, *stonith_level_config_to_cmd(fencing_topology_dto)] + ) + else: + text_output = stonith_level_config_to_text(fencing_topology_dto) + if text_output: + output += "\n\nFencing Levels:\n" + lines_to_str( + smart_wrap_text(indent(text_output)) + ) + + if output: + print(output) diff --git a/pcs/cli/stonith/levels/__init__.py b/pcs/cli/stonith/levels/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pcs/cli/stonith/levels/command.py b/pcs/cli/stonith/levels/command.py new file mode 100644 index 000000000..9301415c1 --- /dev/null +++ b/pcs/cli/stonith/levels/command.py @@ -0,0 +1,44 @@ +import json +from typing import Any + +from pcs.cli.common.errors import CmdLineInputError +from pcs.cli.common.output import ( + format_cmd_list, + lines_to_str, +) +from pcs.cli.common.parse_args import ( + OUTPUT_FORMAT_VALUE_CMD, + OUTPUT_FORMAT_VALUE_JSON, + Argv, + InputModifiers, +) +from pcs.cli.stonith.levels import output as levels_output +from pcs.common.interface.dto import to_dict + + +def config(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: + """ + Options: + * --output-format - supported formats: text, cmd, json + * -f - CIB file + """ + modifiers.ensure_only_supported("-f", output_format_supported=True) + output_format = modifiers.get_output_format() + if argv: + raise CmdLineInputError + + fencing_topology_dto = lib.fencing_topology.get_config_dto() + + if output_format == OUTPUT_FORMAT_VALUE_JSON: + output = json.dumps(to_dict(fencing_topology_dto)) + elif output_format == OUTPUT_FORMAT_VALUE_CMD: + output = format_cmd_list( + levels_output.stonith_level_config_to_cmd(fencing_topology_dto) + ) + else: + output = lines_to_str( + levels_output.stonith_level_config_to_text(fencing_topology_dto) + ) + + if output: + print(output) diff --git a/pcs/cli/stonith/levels/output.py b/pcs/cli/stonith/levels/output.py new file mode 100644 index 000000000..9f5a0cf29 --- /dev/null +++ b/pcs/cli/stonith/levels/output.py @@ -0,0 +1,114 @@ +from collections.abc import Sequence + +from pcs.common.pacemaker.fencing_topology import ( + CibFencingLevel, + CibFencingLevelAttributeDto, + CibFencingLevelRegexDto, + CibFencingTopologyDto, +) +from pcs.common.str_tools import indent +from pcs.common.types import ( + StringCollection, + StringSequence, +) + + +def _get_targets_with_levels_str( + levels: Sequence[CibFencingLevel], +) -> list[str]: + lines = [] + last_target_value = "" + for level in levels: + if isinstance(level, CibFencingLevelAttributeDto): + target_label = "attribute" + target_value = f"{level.target_attribute}={level.target_value}" + elif isinstance(level, CibFencingLevelRegexDto): + target_label = "regexp" + target_value = level.target_pattern + else: + target_label = "node" + target_value = level.target + if target_value != last_target_value: + lines.append(f"Target ({target_label}): {target_value}") + last_target_value = target_value + lines.extend( + indent( + [ + "Level {level}: {devices}".format( + level=level.index, devices=" ".join(level.devices) + ) + ] + ) + ) + return lines + + +def stonith_level_config_to_text( + fencing_topology: CibFencingTopologyDto, +) -> StringSequence: + target_node_levels = sorted( + fencing_topology.target_node, + key=lambda level: (level.target, level.index), + ) + target_regex_levels = sorted( + fencing_topology.target_regex, + key=lambda level: (level.target_pattern, level.index), + ) + target_attr_levels = sorted( + fencing_topology.target_attribute, + key=lambda level: ( + level.target_value, + level.target_attribute, + level.index, + ), + ) + + return ( + _get_targets_with_levels_str(target_node_levels) + + _get_targets_with_levels_str(target_regex_levels) + + _get_targets_with_levels_str(target_attr_levels) + ) + + +def _get_level_add_cmd( + index: int, + target: str, + device_list: StringCollection, + level_id: str, +) -> str: + devices = " ".join(device_list) + return ( + f"pcs stonith level add --force -- {index} {target} {devices} " + f"id={level_id}" + ) + + +def stonith_level_config_to_cmd( + fencing_topology: CibFencingTopologyDto, +) -> StringSequence: + lines: list[str] = [] + level = None + for level in fencing_topology.target_node: + lines.append( + _get_level_add_cmd( + level.index, level.target, level.devices, level.id + ) + ) + for level_regex in fencing_topology.target_regex: + target = f"regexp%{level_regex.target_pattern}" + lines.append( + _get_level_add_cmd( + level_regex.index, target, level_regex.devices, level_regex.id + ) + ) + for level_attr in fencing_topology.target_attribute: + target = ( + f"attrib%{level_attr.target_attribute}={level_attr.target_value}" + ) + lines.append( + _get_level_add_cmd( + level_attr.index, target, level_attr.devices, level_attr.id + ) + ) + + return lines diff --git a/pcs/common/fencing_topology.py b/pcs/common/fencing_topology.py index 8999c2b2a..4e128ac47 100644 --- a/pcs/common/fencing_topology.py +++ b/pcs/common/fencing_topology.py @@ -1,3 +1,12 @@ -TARGET_TYPE_NODE = "node" -TARGET_TYPE_REGEXP = "regexp" -TARGET_TYPE_ATTRIBUTE = "attribute" +from typing import ( + Final, + NewType, + Union, +) + +FencingTargetType = NewType("FencingTargetType", str) +FencingTargetValue = Union[str, tuple[str, str]] + +TARGET_TYPE_NODE: Final = FencingTargetType("node") +TARGET_TYPE_REGEXP: Final = FencingTargetType("regexp") +TARGET_TYPE_ATTRIBUTE: Final = FencingTargetType("attribute") diff --git a/pcs/common/pacemaker/fencing_topology.py b/pcs/common/pacemaker/fencing_topology.py new file mode 100644 index 000000000..603563cae --- /dev/null +++ b/pcs/common/pacemaker/fencing_topology.py @@ -0,0 +1,42 @@ +from collections.abc import Sequence +from dataclasses import dataclass +from typing import Union + +from pcs.common.interface.dto import DataTransferObject + + +@dataclass(frozen=True) +class CibFencingLevelNodeDto(DataTransferObject): + id: str + target: str + index: int + devices: list[str] + + +@dataclass(frozen=True) +class CibFencingLevelRegexDto(DataTransferObject): + id: str + target_pattern: str + index: int + devices: list[str] + + +@dataclass(frozen=True) +class CibFencingLevelAttributeDto(DataTransferObject): + id: str + target_attribute: str + target_value: str + index: int + devices: list[str] + + +CibFencingLevel = Union[ + CibFencingLevelNodeDto, CibFencingLevelRegexDto, CibFencingLevelAttributeDto +] + + +@dataclass(frozen=True) +class CibFencingTopologyDto(DataTransferObject): + target_node: Sequence[CibFencingLevelNodeDto] + target_regex: Sequence[CibFencingLevelRegexDto] + target_attribute: Sequence[CibFencingLevelAttributeDto] diff --git a/pcs/common/reports/messages.py b/pcs/common/reports/messages.py index 6dc1b3c05..48971a979 100644 --- a/pcs/common/reports/messages.py +++ b/pcs/common/reports/messages.py @@ -17,7 +17,11 @@ ) from pcs.common import file_type_codes -from pcs.common.fencing_topology import TARGET_TYPE_ATTRIBUTE +from pcs.common.fencing_topology import ( + TARGET_TYPE_ATTRIBUTE, + FencingTargetType, + FencingTargetValue, +) from pcs.common.file import ( FileAction, RawFileError, @@ -90,11 +94,13 @@ def _resource_move_ban_pcmk_success(stdout: str, stderr: str) -> str: def _format_fencing_level_target( - target_type: Optional[str], target_value: Any + target_type: FencingTargetType, + target_value: FencingTargetValue, ) -> str: if target_type == TARGET_TYPE_ATTRIBUTE: return f"{target_value[0]}={target_value[1]}" - return target_value + # Other target types guarantee values to be only strings + return str(target_value) def _format_booth_default(value: Optional[str], template: str) -> str: @@ -5082,9 +5088,9 @@ class CibFencingLevelAlreadyExists(ReportItemMessage): """ level: str - target_type: str - target_value: Optional[Tuple[str, str]] - devices: List[str] + target_type: FencingTargetType + target_value: FencingTargetValue + devices: list[str] _code = codes.CIB_FENCING_LEVEL_ALREADY_EXISTS @property @@ -5108,9 +5114,9 @@ class CibFencingLevelDoesNotExist(ReportItemMessage): """ level: str = "" - target_type: Optional[str] = None - target_value: Optional[Tuple[str, str]] = None - devices: List[str] = field(default_factory=list) + target_type: Optional[FencingTargetType] = None + target_value: Optional[FencingTargetValue] = None + devices: list[str] = field(default_factory=list) _code = codes.CIB_FENCING_LEVEL_DOES_NOT_EXIST @property diff --git a/pcs/config.py b/pcs/config.py index e0919cc68..bd50c5425 100644 --- a/pcs/config.py +++ b/pcs/config.py @@ -21,7 +21,6 @@ quorum, settings, status, - stonith, usage, utils, ) @@ -46,6 +45,7 @@ ResourcesConfigurationFacade, resources_to_text, ) +from pcs.cli.stonith.levels.output import stonith_level_config_to_text from pcs.cli.tag.output import tags_to_text from pcs.common.interface import dto from pcs.common.pacemaker.constraint import CibConstraintsDto @@ -152,8 +152,8 @@ def _config_show_cib_lines(lib, properties_facade=None): all_lines.append("Stonith Devices:") all_lines.extend(stonith_lines) - levels_lines = stonith.stonith_level_config_to_str( - lib.fencing_topology.get_config() + levels_lines = stonith_level_config_to_text( + lib.fencing_topology.get_config_dto() ) if levels_lines: if all_lines: diff --git a/pcs/daemon/async_tasks/worker/command_mapping.py b/pcs/daemon/async_tasks/worker/command_mapping.py index f81eb1ac1..31a78f831 100644 --- a/pcs/daemon/async_tasks/worker/command_mapping.py +++ b/pcs/daemon/async_tasks/worker/command_mapping.py @@ -188,6 +188,10 @@ class _Cmd: cmd=fencing_topology.add_level, required_permission=p.WRITE, ), + "fencing_topology.get_config_dto": _Cmd( + cmd=fencing_topology.get_config_dto, + required_permission=p.READ, + ), "fencing_topology.remove_all_levels": _Cmd( cmd=fencing_topology.remove_all_levels, required_permission=p.WRITE, diff --git a/pcs/lib/cib/fencing_topology.py b/pcs/lib/cib/fencing_topology.py index 27081fe75..55e347a0f 100644 --- a/pcs/lib/cib/fencing_topology.py +++ b/pcs/lib/cib/fencing_topology.py @@ -1,18 +1,32 @@ +from collections.abc import Sequence from typing import ( + Any, Final, Optional, - Sequence, - Set, + Type, + TypeVar, + cast, ) from lxml import etree -from lxml.etree import _Element +from lxml.etree import ( + _Attrib, + _Element, +) from pcs.common import reports from pcs.common.fencing_topology import ( TARGET_TYPE_ATTRIBUTE, TARGET_TYPE_NODE, TARGET_TYPE_REGEXP, + FencingTargetType, + FencingTargetValue, +) +from pcs.common.pacemaker.fencing_topology import ( + CibFencingLevelAttributeDto, + CibFencingLevelNodeDto, + CibFencingLevelRegexDto, + CibFencingTopologyDto, ) from pcs.common.reports import ( ReportItemList, @@ -22,12 +36,15 @@ from pcs.common.reports import codes as report_codes from pcs.common.reports import has_errors from pcs.common.reports.item import ReportItem -from pcs.common.types import StringCollection +from pcs.common.types import StringSequence from pcs.common.validate import is_integer from pcs.lib.cib.const import TAG_FENCING_LEVEL -from pcs.lib.cib.resource.stonith import is_stonith_resource +from pcs.lib.cib.resource.stonith import is_stonith from pcs.lib.cib.tools import ( - find_unique_id, + ElementNotFound, + IdProvider, + get_element_by_id, + get_fencing_topology, multivalue_attr_contains_value, multivalue_attr_delete_value, multivalue_attr_has_any_values, @@ -41,43 +58,72 @@ _DEVICES_ATTRIBUTE: Final = "devices" +FencingLevelDto = TypeVar( + "FencingLevelDto", + CibFencingLevelAttributeDto, + CibFencingLevelNodeDto, + CibFencingLevelRegexDto, +) + + +def _generate_level_id( + id_provider: IdProvider, + level: str, + target_type: FencingTargetType, + target_value: FencingTargetValue, +) -> str: + if target_type == TARGET_TYPE_ATTRIBUTE: + # For attribute target type, the value is tuple[str, str] + id_part = target_value[0] + else: + # For all other target types, the value is str + id_part = str(target_value) + return id_provider.allocate_id(sanitize_id(f"fl-{id_part}-{level}")) + def add_level( reporter: ReportProcessor, - topology_el: _Element, - resources_el: _Element, - level, - target_type, - target_value, - devices, + cib: _Element, + level: str, + target_type: FencingTargetType, + target_value: FencingTargetValue, + devices: StringSequence, cluster_status_nodes: Sequence[StateElement], - force_device=False, - force_node=False, -): + level_id: Optional[str] = None, + force_device: bool = False, + force_node: bool = False, +) -> None: """ Validate and add a new fencing level. Raise LibraryError if not valid. reporter -- report processor - etree topology_el -- etree element to add the level to - etree resources_el -- etree element with resources definitions - int|string level -- level (index) of the new fencing level - constant target_type -- the new fencing level target value type - mixed target_value -- the new fencing level target value - Iterable devices -- list of stonith devices for the new fencing level - Iterable cluster_status_nodes -- list of status of existing cluster nodes - bool force_device -- continue even if a stonith device does not exist - bool force_node -- continue even if a node (target) does not exist + cib -- the whole cib + level -- level (index) of the new fencing level + target_type -- the new fencing level target value type + target_value -- the new fencing level target value + devices -- list of stonith devices for the new fencing level + cluster_status_nodes -- list of status of existing cluster nodes + level_id -- user specified id for the level element + force_device -- continue even if a stonith device does not exist + force_node -- continue even if a node (target) does not exist """ # pylint: disable=too-many-arguments + id_provider = IdProvider(cib) + validate_id_reports: ReportItemList = [] + if level_id is not None: + validate_id(level_id, "fencing level id", reporter=validate_id_reports) reporter.report_list( - _validate_level(level) + (id_provider.book_ids(level_id) if level_id else []) + + validate_id_reports + + _validate_level(level) + _validate_target( cluster_status_nodes, target_type, target_value, force_node ) - + _validate_devices(resources_el, devices, force_device) + + _validate_devices(cib, devices, force_device) ) if reporter.has_errors: raise LibraryError() + topology_el = get_fencing_topology(cib) reporter.report_list( _validate_level_target_devices_does_not_exist( topology_el, level, target_type, target_value, devices @@ -86,14 +132,22 @@ def add_level( if reporter.has_errors: raise LibraryError() _append_level_element( - topology_el, int(level), target_type, target_value, devices + topology_el, + level, + target_type, + target_value, + devices, + ( + level_id + or _generate_level_id(id_provider, level, target_type, target_value) + ), ) -def remove_all_levels(topology_el): +def remove_all_levels(topology_el: _Element) -> None: """ Remove all fencing levels. - etree topology_el -- etree element to remove the levels from + topology_el -- etree element to remove the levels from """ # Do not ever remove a fencing-topology element, even if it is empty. There # may be ACLs set in pacemaker which allow "write" for fencing-level @@ -103,16 +157,16 @@ def remove_all_levels(topology_el): # message. # https://bugzilla.redhat.com/show_bug.cgi?id=1642514 for level_el in topology_el.findall(TAG_FENCING_LEVEL): - level_el.getparent().remove(level_el) + # Parent is guaranteed by CIB schema + cast(_Element, level_el.getparent()).remove(level_el) def remove_levels_by_params( topology_el: _Element, - level=None, - # TODO create a special type, so that it cannot accept any string - target_type: Optional[str] = None, - target_value=None, - devices: Optional[StringCollection] = None, + level: Optional[str] = None, + target_type: Optional[FencingTargetType] = None, + target_value: Optional[FencingTargetValue] = None, + devices: Optional[StringSequence] = None, # TODO remove, deprecated backward compatibility layer ignore_if_missing: bool = False, # TODO remove, deprecated backward compatibility layer @@ -122,9 +176,9 @@ def remove_levels_by_params( Remove specified fencing level(s) topology_el -- etree element to remove the levels from - int|string level -- level (index) of the fencing level to remove + level -- level (index) of the fencing level to remove target_type -- the removed fencing level target value type - mixed target_value -- the removed fencing level target value + target_value -- the removed fencing level target value devices -- list of stonith devices of the removed fencing level ignore_if_missing -- when True, do not report if level not found """ @@ -157,7 +211,7 @@ def remove_levels_by_params( report_list.append( ReportItem.error( reports.messages.CibFencingLevelDoesNotExist( - level, + level or "", target_type, target_value, sorted(devices) if devices else [], @@ -167,7 +221,8 @@ def remove_levels_by_params( if has_errors(report_list): return report_list for el in level_el_list: - el.getparent().remove(el) + # Parent guaranteed by CIB schema + cast(_Element, el.getparent()).remove(el) return report_list @@ -209,7 +264,8 @@ def has_any_devices(level_el: _Element) -> bool: return multivalue_attr_has_any_values(level_el, _DEVICES_ATTRIBUTE) -def export(topology_el): +# DEPRECATED, use fencing_topology_el_to_dto +def export(topology_el: _Element) -> list[dict[str, Any]]: """ Export all fencing levels. @@ -220,7 +276,8 @@ def export(topology_el): """ export_levels = [] for level_el in topology_el.iterfind(TAG_FENCING_LEVEL): - target_type = target_value = None + target_type: Optional[FencingTargetType] = None + target_value: Optional[FencingTargetValue] = None if "target" in level_el.attrib: target_type = TARGET_TYPE_NODE target_value = level_el.get("target") @@ -230,8 +287,8 @@ def export(topology_el): elif "target-attribute" in level_el.attrib: target_type = TARGET_TYPE_ATTRIBUTE target_value = ( - level_el.get("target-attribute"), - level_el.get("target-value"), + str(level_el.attrib["target-attribute"]), + str(level_el.attrib["target-value"]), ) if target_type and target_value: export_levels.append( @@ -239,27 +296,90 @@ def export(topology_el): "target_type": target_type, "target_value": target_value, "level": level_el.get("index"), - "devices": level_el.get(_DEVICES_ATTRIBUTE).split(","), + "devices": str(level_el.attrib[_DEVICES_ATTRIBUTE]).split( + "," + ), } ) return export_levels +def _fencing_level_dto_factory( + dto_type: Type[FencingLevelDto], + level_el_attrib: _Attrib, +) -> FencingLevelDto: + if dto_type is CibFencingLevelRegexDto: + target_args = {"target_pattern": str(level_el_attrib["target-pattern"])} + elif dto_type is CibFencingLevelAttributeDto: + target_args = { + "target_attribute": str(level_el_attrib["target-attribute"]), + "target_value": str(level_el_attrib["target-value"]), + } + else: + target_args = {"target": str(level_el_attrib["target"])} + + return dto_type( + id=str(level_el_attrib["id"]), + index=int(level_el_attrib["index"]), + devices=[ + device.strip() + for device in str(level_el_attrib["devices"]).split(",") + ], + **target_args, + ) + + +def fencing_topology_el_to_dto( + fencing_topology_el: _Element, +) -> CibFencingTopologyDto: + level_el_list = _find_all_level_elements(fencing_topology_el) + target_node_list = [] + target_regex_list = [] + target_attr_list = [] + for level_el in level_el_list: + if "target" in level_el.attrib: + target_node_list.append( + _fencing_level_dto_factory( + CibFencingLevelNodeDto, + level_el.attrib, + ) + ) + if "target-pattern" in level_el.attrib: + target_regex_list.append( + _fencing_level_dto_factory( + CibFencingLevelRegexDto, + level_el.attrib, + ) + ) + if "target-attribute" in level_el.attrib: + target_attr_list.append( + _fencing_level_dto_factory( + CibFencingLevelAttributeDto, + level_el.attrib, + ) + ) + + return CibFencingTopologyDto( + target_node=target_node_list, + target_regex=target_regex_list, + target_attribute=target_attr_list, + ) + + def verify( - topology_el: _Element, - resources_el: _Element, + cib: _Element, cluster_status_nodes: Sequence[StateElement], ) -> ReportItemList: """ Check if all cluster nodes and stonith devices used in fencing levels exist. - topology_el -- fencing levels to check - resources_el -- resources definitions + cib -- the whole cib cluster_status_nodes -- list of status of existing cluster nodes """ report_list: ReportItemList = [] - used_nodes: Set[str] = set() - used_devices: Set[str] = set() + used_nodes: set[str] = set() + used_devices: set[str] = set() + topology_el = get_fencing_topology(cib) for level_el in topology_el.iterfind(TAG_FENCING_LEVEL): used_devices.update( @@ -270,9 +390,7 @@ def verify( if used_devices: report_list.extend( - _validate_devices( - resources_el, sorted(used_devices), allow_force=False - ) + _validate_devices(cib, sorted(used_devices), allow_force=False) ) for node in sorted(used_nodes): @@ -284,12 +402,12 @@ def verify( return report_list -def _validate_level(level) -> ReportItemList: +def _validate_level(level: str) -> ReportItemList: report_list: ReportItemList = [] if not is_integer(level, 1, 9): report_list.append( ReportItem.error( - reports.messages.InvalidOptionValue("level", level, "1..9") + reports.messages.InvalidOptionValue("level", str(level), "1..9") ) ) return report_list @@ -297,16 +415,16 @@ def _validate_level(level) -> ReportItemList: def _validate_target( cluster_status_nodes: Sequence[StateElement], - target_type, - target_value, - force_node=False, + target_type: FencingTargetType, + target_value: FencingTargetValue, + force_node: bool = False, ) -> ReportItemList: return _validate_target_typewise(target_type) + _validate_target_valuewise( cluster_status_nodes, target_type, target_value, force_node ) -def _validate_target_typewise(target_type) -> ReportItemList: +def _validate_target_typewise(target_type: FencingTargetType) -> ReportItemList: report_list: ReportItemList = [] if target_type not in [ TARGET_TYPE_NODE, @@ -326,10 +444,10 @@ def _validate_target_typewise(target_type) -> ReportItemList: def _validate_target_valuewise( cluster_status_nodes: Sequence[StateElement], - target_type, - target_value, - force_node=False, - allow_force=True, + target_type: FencingTargetType, + target_value: FencingTargetValue, + force_node: bool = False, + allow_force: bool = True, ) -> ReportItemList: report_list: ReportItemList = [] if target_type == TARGET_TYPE_NODE: @@ -353,14 +471,20 @@ def _validate_target_valuewise( else report_codes.FORCE ), ), - message=reports.messages.NodeNotFound(target_value), + message=reports.messages.NodeNotFound( + # This is a str based on target_type + str(target_value) + ), ) ) return report_list def _validate_devices( - resources_el: _Element, devices, force_device=False, allow_force=True + cib: _Element, + devices: StringSequence, + force_device: bool = False, + allow_force: bool = True, ) -> ReportItemList: report_list: ReportItemList = [] if not devices: @@ -378,8 +502,10 @@ def _validate_devices( report_list.extend(validate_id_report_list) if has_errors(validate_id_report_list): continue - # TODO use the new finding function - if not is_stonith_resource(resources_el, dev): + try: + if not is_stonith(get_element_by_id(cib, dev)): + invalid_devices.append(dev) + except ElementNotFound: invalid_devices.append(dev) if invalid_devices: report_list.append( @@ -405,54 +531,72 @@ def _validate_devices( def _validate_level_target_devices_does_not_exist( - tree, level, target_type, target_value, devices + tree: _Element, + level: str, + target_type: FencingTargetType, + target_value: FencingTargetValue, + devices: StringSequence, ) -> ReportItemList: report_list: ReportItemList = [] if _find_level_elements(tree, level, target_type, target_value, devices): report_list.append( ReportItem.error( reports.messages.CibFencingLevelAlreadyExists( - level, target_type, target_value, devices + level, target_type, target_value, list(devices) ) ) ) return report_list -def _append_level_element(tree, level, target_type, target_value, devices): +def _append_level_element( + tree: _Element, + level: str, + target_type: FencingTargetType, + target_value: FencingTargetValue, + devices: StringSequence, + level_id: str, +) -> _Element: level_el = etree.SubElement( - tree, TAG_FENCING_LEVEL, index=str(level), devices=",".join(devices) + tree, + TAG_FENCING_LEVEL, + index=level, + devices=",".join(devices), + id=level_id, ) if target_type == TARGET_TYPE_NODE: - level_el.set("target", target_value) - id_part = target_value + level_el.set("target", str(target_value)) elif target_type == TARGET_TYPE_REGEXP: - level_el.set("target-pattern", target_value) - id_part = target_value + level_el.set("target-pattern", str(target_value)) elif target_type == TARGET_TYPE_ATTRIBUTE: level_el.set("target-attribute", target_value[0]) level_el.set("target-value", target_value[1]) - id_part = target_value[0] - level_el.set( - "id", - find_unique_id(tree, sanitize_id(f"fl-{id_part}-{level}")), - ) return level_el +def _find_all_level_elements(tree: _Element) -> list[_Element]: + return tree.findall(TAG_FENCING_LEVEL) + + def _find_level_elements( - tree, level=None, target_type=None, target_value=None, devices=None -): - xpath_vars = {} + tree: _Element, + level: Optional[str] = None, + target_type: Optional[FencingTargetType] = None, + target_value: Optional[FencingTargetValue] = None, + devices: Optional[StringSequence] = None, +) -> list[_Element]: + xpath_vars: dict[str, str] = {} xpath_target = "" if target_type and target_value: if target_type == TARGET_TYPE_NODE: xpath_target = "@target=$var_target" - xpath_vars["var_target"] = target_value + # The value of xpath_target determines that this is a string + xpath_vars["var_target"] = str(target_value) elif target_type == TARGET_TYPE_REGEXP: xpath_target = "@target-pattern=$var_target_pattern" - xpath_vars["var_target_pattern"] = target_value + # The value of xpath_target determines that this is a string + xpath_vars["var_target_pattern"] = str(target_value) elif target_type == TARGET_TYPE_ATTRIBUTE: xpath_target = ( "@target-attribute=$var_target_attribute " @@ -474,7 +618,8 @@ def _find_level_elements( return tree.xpath( f"{TAG_FENCING_LEVEL}[{xpath_attrs}]", var_devices=(",".join(devices) if devices else ""), - var_level=level, - **xpath_vars, + var_level=level or "", + **xpath_vars, # type: ignore ) - return tree.findall(TAG_FENCING_LEVEL) + + return _find_all_level_elements(tree) diff --git a/pcs/lib/cib/resource/stonith.py b/pcs/lib/cib/resource/stonith.py index 2b8621f6f..0285fe78a 100644 --- a/pcs/lib/cib/resource/stonith.py +++ b/pcs/lib/cib/resource/stonith.py @@ -36,18 +36,6 @@ ) -# TODO replace by the new finding function -def is_stonith_resource(resources_el, name): - return ( - len( - resources_el.xpath( - "primitive[@id=$id and @class='stonith']", id=name - ) - ) - > 0 - ) - - def is_stonith(resource_el: _Element): return ( resource_el.tag == TAG_RESOURCE_PRIMITIVE diff --git a/pcs/lib/cib/tools.py b/pcs/lib/cib/tools.py index de3593def..1838b7e94 100644 --- a/pcs/lib/cib/tools.py +++ b/pcs/lib/cib/tools.py @@ -447,7 +447,7 @@ def get_crm_config(tree: _Element) -> _Element: return sections.get(tree, sections.CRM_CONFIG) -def get_fencing_topology(tree): +def get_fencing_topology(tree: _Element) -> _Element: """ Return the 'fencing-topology' element from the tree tree -- cib etree node diff --git a/pcs/lib/commands/cluster.py b/pcs/lib/commands/cluster.py index 6a72a3dd7..83868b74a 100644 --- a/pcs/lib/commands/cluster.py +++ b/pcs/lib/commands/cluster.py @@ -45,10 +45,6 @@ from pcs.lib.cib import fencing_topology from pcs.lib.cib.resource.guest_node import find_node_list as get_guest_nodes from pcs.lib.cib.resource.remote_node import find_node_list as get_remote_nodes -from pcs.lib.cib.tools import ( - get_fencing_topology, - get_resources, -) from pcs.lib.communication import cluster from pcs.lib.communication.corosync import ( CheckCorosyncOffline, @@ -179,8 +175,7 @@ def verify(env: LibraryEnvironment, verbose=False): cib = get_cib(cib_xml) env.report_processor.report_list( fencing_topology.verify( - get_fencing_topology(cib), - get_resources(cib), + cib, ClusterState(env.get_cluster_state()).node_section.nodes, ) ) diff --git a/pcs/lib/commands/fencing_topology.py b/pcs/lib/commands/fencing_topology.py index ff339d4f8..19924bc12 100644 --- a/pcs/lib/commands/fencing_topology.py +++ b/pcs/lib/commands/fencing_topology.py @@ -1,13 +1,18 @@ -from typing import Optional +from typing import ( + Any, + Optional, +) from pcs.common import reports as report -from pcs.common.fencing_topology import TARGET_TYPE_NODE -from pcs.common.types import StringCollection -from pcs.lib.cib import fencing_topology as cib_fencing_topology -from pcs.lib.cib.tools import ( - get_fencing_topology, - get_resources, +from pcs.common.fencing_topology import ( + TARGET_TYPE_NODE, + FencingTargetType, + FencingTargetValue, ) +from pcs.common.pacemaker.fencing_topology import CibFencingTopologyDto +from pcs.common.types import StringSequence +from pcs.lib.cib import fencing_topology as cib_fencing_topology +from pcs.lib.cib.tools import get_fencing_topology from pcs.lib.env import LibraryEnvironment from pcs.lib.errors import LibraryError from pcs.lib.pacemaker.state import ClusterState @@ -15,34 +20,36 @@ def add_level( lib_env: LibraryEnvironment, - level, - target_type, - target_value, - devices, - force_device=False, - force_node=False, -): + level: str, + target_type: FencingTargetType, + target_value: FencingTargetValue, + devices: StringSequence, + level_id: Optional[str] = None, + force_device: bool = False, + force_node: bool = False, +) -> None: """ Validate and add a new fencing level - LibraryEnvironment lib_env -- environment - int|string level -- level (index) of the new fencing level - constant target_type -- the new fencing level target value type - mixed target_value -- the new fencing level target value - Iterable devices -- list of stonith devices for the new fencing level - bool force_device -- continue even if a stonith device does not exist - bool force_node -- continue even if a node (target) does not exist + lib_env -- environment + level -- level (index) of the new fencing level + target_type -- the new fencing level target value type + target_value -- the new fencing level target value + devices -- list of stonith devices for the new fencing level + level_id -- user specified id for the level element + force_device -- continue even if a stonith device does not exist + force_node -- continue even if a node (target) does not exist """ cib = lib_env.get_cib() cib_fencing_topology.add_level( lib_env.report_processor, - get_fencing_topology(cib), - get_resources(cib), + cib, level, target_type, target_value, devices, ClusterState(lib_env.get_cluster_state()).node_section.nodes, + level_id, force_device, force_node, ) @@ -51,23 +58,36 @@ def add_level( lib_env.push_cib() -def get_config(lib_env: LibraryEnvironment): +# DEPRECATED, use get_config_dto +def get_config(lib_env: LibraryEnvironment) -> list[dict[str, Any]]: """ Get fencing levels configuration. Return a list of levels where each level is a dict with keys: target_type, - target_value. level and devices. Devices is a list of stonith device ids. + target_value, level and devices. Devices is a list of stonith device ids. - LibraryEnvironment lib_env -- environment + lib_env -- environment """ cib = lib_env.get_cib() return cib_fencing_topology.export(get_fencing_topology(cib)) -def remove_all_levels(lib_env: LibraryEnvironment): +def get_config_dto(env: LibraryEnvironment) -> CibFencingTopologyDto: + """ + Get fencing level configuration. + + env -- library environment + """ + return cib_fencing_topology.fencing_topology_el_to_dto( + get_fencing_topology(env.get_cib()) + ) + + +def remove_all_levels(lib_env: LibraryEnvironment) -> None: """ Remove all fencing levels - LibraryEnvironment lib_env -- environment + + lib_env -- environment """ cib_fencing_topology.remove_all_levels( get_fencing_topology(lib_env.get_cib()) @@ -77,23 +97,22 @@ def remove_all_levels(lib_env: LibraryEnvironment): def remove_levels_by_params( lib_env: LibraryEnvironment, - level=None, - # TODO create a special type, so that it cannot accept any string - target_type: Optional[str] = None, - target_value=None, - devices: Optional[StringCollection] = None, + level: Optional[str] = None, + target_type: Optional[FencingTargetType] = None, + target_value: Optional[FencingTargetValue] = None, + devices: Optional[StringSequence] = None, # TODO remove, deprecated backward compatibility layer ignore_if_missing: bool = False, # TODO remove, deprecated backward compatibility layer target_may_be_a_device: bool = False, -): +) -> None: """ Remove specified fencing level(s). - LibraryEnvironment lib_env -- environment - int|string level -- level (index) of the fencing level to remove + lib_env -- environment + level -- level (index) of the fencing level to remove target_type -- the removed fencing level target value type - mixed target_value -- the removed fencing level target value + target_value -- the removed fencing level target value devices -- list of stonith devices of the removed fencing level ignore_if_missing -- when True, do not report if level not found target_may_be_a_device -- enables backward compatibility mode for old CLI @@ -151,7 +170,7 @@ def remove_levels_by_params( level, None, None, - target_and_devices, + target_and_devices, # type: ignore ignore_if_missing, validate_device_ids=(not target_may_be_a_device), ) @@ -165,17 +184,16 @@ def remove_levels_by_params( raise LibraryError() -def verify(lib_env: LibraryEnvironment): +def verify(lib_env: LibraryEnvironment) -> None: """ Check if all cluster nodes and stonith devices used in fencing levels exist - LibraryEnvironment lib_env -- environment + lib_env -- environment """ cib = lib_env.get_cib() lib_env.report_processor.report_list( cib_fencing_topology.verify( - get_fencing_topology(cib), - get_resources(cib), + cib, ClusterState(lib_env.get_cluster_state()).node_section.nodes, ) ) diff --git a/pcs/pcs.8.in b/pcs/pcs.8.in index 6a390f375..924461ed7 100644 --- a/pcs/pcs.8.in +++ b/pcs/pcs.8.in @@ -704,6 +704,8 @@ Show status of all currently configured stonith devices. If \fB\-\-hide\-inactiv .TP config [@OUTPUT_FORMAT_SYNTAX_DOC@] []... Show options of all currently configured stonith devices or if stonith device ids are specified show the options for the specified stonith device ids. @OUTPUT_FORMAT_DESC_DOC@ + +Note: The 'json' output format does not include fencing levels. Use 'pcs stonith level config \-\-output\-format=json' to get fencing levels. .TP list [filter] [\fB\-\-nodesc\fR] Show list of all available stonith agents (if filter is provided then only stonith agents matching the filter will be shown). If \fB\-\-nodesc\fR is used then descriptions of stonith agents are not printed. @@ -937,11 +939,11 @@ Allow the cluster to use the stonith devices. If \fB\-\-wait\fR is specified, pc disable ... [\fB\-\-wait[=n]\fR] Attempt to stop the stonith devices if they are running and disallow the cluster to use them. If \fB\-\-wait\fR is specified, pcs will wait up to 'n' seconds for the stonith devices to stop and then return 0 if the stonith devices are stopped or 1 if the stonith devices have not stopped. If 'n' is not specified it defaults to 60 minutes. .TP -level [config] -Lists all of the fencing levels currently configured. +level [config] [@OUTPUT_FORMAT_SYNTAX_DOC@] +Lists all of the fencing levels currently configured. @OUTPUT_FORMAT_DESC_DOC@ .TP -level add [stonith id]... -Add the fencing level for the specified target with the list of stonith devices to attempt for that target at that level. Fence levels are attempted in numerical order (starting with 1). If a level succeeds (meaning all devices are successfully fenced in that level) then no other levels are tried, and the target is considered fenced. Target may be a node name or % or node%, a node name regular expression regexp% or a node attribute value attrib%=. +level add [...] [id=] +Add the fencing level for the specified target with the list of stonith devices to attempt for that target at that level. Fence levels are attempted in numerical order (starting with 1). If a level succeeds (meaning all devices are successfully fenced in that level) then no other levels are tried, and the target is considered fenced. Target may be a node name or % or node%, a node name regular expression regexp% or a node attribute value attrib%=. Id for the fencing level will be generated if not specified by the id option. .TP level delete [target ] [stonith ...] Removes the fence level for the level, target and/or devices specified. If no target or devices are specified then the fence level is removed. Target may be a node name or % or node%, a node name regular expression regexp% or a node attribute value attrib%=. diff --git a/pcs/resource.py b/pcs/resource.py index cacab4d27..7d9f0ff69 100644 --- a/pcs/resource.py +++ b/pcs/resource.py @@ -52,12 +52,9 @@ warn, ) from pcs.cli.resource.output import ( - ResourcesConfigurationFacade, operation_defaults_to_cmd, resource_agent_metadata_to_text, resource_defaults_to_cmd, - resources_to_cmd, - resources_to_text, ) from pcs.cli.resource.parse_args import ( parse_bundle_create_options, @@ -76,7 +73,6 @@ from pcs.common.interface import dto from pcs.common.pacemaker.defaults import CibDefaultsDto from pcs.common.pacemaker.resource.list import ( - CibResourcesDto, get_all_resources_ids, get_stonith_resources_ids, ) @@ -3175,45 +3171,3 @@ def resource_bundle_update_cmd( force_options=modifiers.get("--force"), wait=modifiers.get("--wait"), ) - - -def config(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: - config_common(lib, argv, modifiers, stonith=False) - - -def config_common( - lib: Any, argv: Argv, modifiers: InputModifiers, stonith: bool -) -> None: - """ - Options: - * -f - CIB file - * --output-format - supported formats: text, cmd, json - """ - modifiers.ensure_only_supported("-f", output_format_supported=True) - resources_facade = ( - ResourcesConfigurationFacade.from_resources_dto( - lib.resource.get_configured_resources() - ) - .filter_stonith(stonith) - .filter_resources(argv) - ) - output_format = modifiers.get_output_format() - if output_format == OUTPUT_FORMAT_VALUE_CMD: - output = ";\n".join( - " \\\n".join(cmd) for cmd in resources_to_cmd(resources_facade) - ) - elif output_format == OUTPUT_FORMAT_VALUE_JSON: - output = json.dumps( - dto.to_dict( - CibResourcesDto( - primitives=resources_facade.primitives, - clones=resources_facade.clones, - groups=resources_facade.groups, - bundles=resources_facade.bundles, - ) - ) - ) - else: - output = "\n".join(smart_wrap_text(resources_to_text(resources_facade))) - if output: - print(output) diff --git a/pcs/stonith.py b/pcs/stonith.py index 79d360a93..c36b3c7a6 100644 --- a/pcs/stonith.py +++ b/pcs/stonith.py @@ -22,17 +22,16 @@ deprecation_warning, error, print_to_stderr, - warn, ) from pcs.cli.resource.output import resource_agent_metadata_to_text from pcs.cli.resource.parse_args import ( parse_primitive as parse_primitive_resource, ) +from pcs.cli.stonith.levels.output import stonith_level_config_to_text from pcs.common import reports from pcs.common.fencing_topology import ( TARGET_TYPE_ATTRIBUTE, TARGET_TYPE_NODE, - TARGET_TYPE_REGEXP, ) from pcs.common.pacemaker.resource.list import ( get_all_resources_ids, @@ -61,10 +60,10 @@ def stonith_status_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: def _print_stonith_levels(lib: Any) -> None: - levels = stonith_level_config_to_str(lib.fencing_topology.get_config()) - if levels: + lines = stonith_level_config_to_text(lib.fencing_topology.get_config_dto()) + if lines: print("\nFencing Levels:") - print("\n".join(indent(levels, 1))) + print("\n".join(indent(lines))) def stonith_list_available( @@ -317,14 +316,24 @@ def stonith_level_add_cmd( modifiers.ensure_only_supported("-f", "--force") if len(argv) < 3: raise CmdLineInputError() + # KeyValueParser supports only key value arguments, so we will only filter + # out those by looking for "=" to also include mistyped or missing keys + kvpairs = [arg for arg in argv[2:] if "=" in arg] + for kvpair in kvpairs: + argv.remove(kvpair) + parser = KeyValueParser(kvpairs) + parser.check_allowed_keys(["id"]) + level_id = parser.get_unique().get("id") target_type, target_value = _stonith_level_parse_node(argv[1]) stonith_devices = _stonith_level_normalize_devices(argv[2:]) _check_is_stonith(lib, stonith_devices) + lib.fencing_topology.add_level( argv[0], target_type, target_value, stonith_devices, + level_id=level_id, force_device=modifiers.get("--force"), force_node=modifiers.get("--force"), ) @@ -404,68 +413,6 @@ def stonith_level_clear_cmd( raise LibraryError() -def stonith_level_config_to_str(config): - """ - Commandline option: no options - """ - config_data = {} - for level in config: - if level["target_type"] not in config_data: - config_data[level["target_type"]] = {} - if level["target_value"] not in config_data[level["target_type"]]: - config_data[level["target_type"]][level["target_value"]] = [] - config_data[level["target_type"]][level["target_value"]].append(level) - - lines = [] - for target_type in [ - TARGET_TYPE_NODE, - TARGET_TYPE_REGEXP, - TARGET_TYPE_ATTRIBUTE, - ]: - if not target_type in config_data: - continue - for target_value in sorted(config_data[target_type].keys()): - lines.append( - "Target{0}: {1}".format( - " (regexp)" if target_type == TARGET_TYPE_REGEXP else "", - ( - "=".join(target_value) - if target_type == TARGET_TYPE_ATTRIBUTE - else target_value - ), - ) - ) - level_lines = [] - for target_level in sorted( - config_data[target_type][target_value], - key=lambda level: level["level"], - ): - level_lines.append( - "Level {level} - {devices}".format( - level=target_level["level"], - devices=",".join(target_level["devices"]), - ) - ) - lines.extend(indent(level_lines)) - return lines - - -def stonith_level_config_cmd( - lib: Any, argv: Argv, modifiers: InputModifiers -) -> None: - """ - Options: - * -f - CIB file - """ - modifiers.ensure_only_supported("-f") - if argv: - raise CmdLineInputError() - lines = stonith_level_config_to_str(lib.fencing_topology.get_config()) - # do not print \n when lines are empty - if lines: - print("\n".join(lines)) - - def stonith_level_remove_cmd( lib: Any, argv: Argv, modifiers: InputModifiers ) -> None: @@ -1116,17 +1063,3 @@ def meta_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: raise CmdLineInputError() _check_is_stonith(lib, [argv[0]], "pcs resource meta") resource.resource_meta(argv, modifiers) - - -def config_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: - is_text_format = ( - modifiers.get_output_format() == parse_args.OUTPUT_FORMAT_VALUE_TEXT - ) - if not is_text_format: - warn( - f"Only '{parse_args.OUTPUT_FORMAT_VALUE_TEXT}' output format is " - "supported for stonith levels" - ) - resource.config_common(lib, argv, modifiers, stonith=True) - if is_text_format: - _print_stonith_levels(lib) diff --git a/pcs/usage.py b/pcs/usage.py index e8d0d1a03..55ff2dddc 100644 --- a/pcs/usage.py +++ b/pcs/usage.py @@ -1,5 +1,8 @@ import re -from typing import Callable +from typing import ( + Callable, + Final, +) from pcs import settings from pcs.cli.common.output import format_wrap @@ -744,8 +747,11 @@ def _resource_config_syntax(obj: str) -> str: return f"config [{_output_format_syntax()}] [<{obj} id>]..." +_STONITH_DEVICE: Final = "stonith device" + + def _resource_config_desc(obj: str) -> tuple[str, ...]: - return ( + lines = ( f""" Show options of all currently configured {obj}s or if {obj} ids are specified show the options for the specified {obj} ids. @@ -753,6 +759,14 @@ def _resource_config_desc(obj: str) -> tuple[str, ...]: "", _output_format_desc(), ) + if obj == _STONITH_DEVICE: + lines += ( # type: ignore + "", + "Note: The 'json' output format does not include fencing levels. " + "Use 'pcs stonith level config --output-format=json' to get " + "fencing levels.", + ) + return lines _STONITH_DELETE_SYNTAX = "..." @@ -2022,10 +2036,12 @@ def stonith(args: Argv) -> str: stonith devices are stopped or 1 if the stonith devices have not stopped. If 'n' is not specified it defaults to 60 minutes. - level [config] + level [config] [{output_format_syntax}] Lists all of the fencing levels currently configured. - level add [stonith id]... +{output_format_desc} + + level add [...] [id=] Add the fencing level for the specified target with the list of stonith devices to attempt for that target at that level. Fence levels are attempted in numerical order (starting with 1). If a level succeeds @@ -2033,7 +2049,8 @@ def stonith(args: Argv) -> str: other levels are tried, and the target is considered fenced. Target may be a node name or % or node%, a node name regular expression regexp% - or a node attribute value attrib%=. + or a node attribute value attrib%=. Id for the fencing + level will be generated if not specified by the id option. level delete [target ] [stonith ...] Removes the fence level for the level, target and/or devices specified. @@ -2158,7 +2175,7 @@ def stonith(args: Argv) -> str: is available on the local system. """.format( config_syntax=_format_syntax(_resource_config_syntax("stonith")), - config_desc=_format_desc(_resource_config_desc("stonith device")), + config_desc=_format_desc(_resource_config_desc(_STONITH_DEVICE)), delete_syntax=_format_syntax(f"{_DELETE_CMD} {_STONITH_DELETE_SYNTAX}"), remove_syntax=_format_syntax(f"{_REMOVE_CMD} {_STONITH_DELETE_SYNTAX}"), delete_desc=_format_desc(_STONITH_DELETE_DESC), @@ -2234,7 +2251,7 @@ def stonith(args: Argv) -> str: _RESOURCE_OP_ADD_SYNTAX.format(obj="stonith"), ) ), - op_add_desc=_format_desc(_resource_op_add_desc_fn("stonith device")), + op_add_desc=_format_desc(_resource_op_add_desc_fn(_STONITH_DEVICE)), op_remove_syntax=_format_syntax( "{} {}".format( _RESOURCE_OP_REMOVE_CMD, @@ -2262,7 +2279,7 @@ def stonith(args: Argv) -> str: ) ), meta_desc=_format_desc( - _resource_meta_desc_fn(obj="stonith device", parent_cmd="stonith") + _resource_meta_desc_fn(obj=_STONITH_DEVICE, parent_cmd="stonith") ), defaults_config_syntax=_format_syntax( f"{_RESOURCE_DEFAULTS_CONFIG_CMD} {_RESOURCE_DEFAULTS_CONFIG_SYNTAX}" @@ -2316,6 +2333,8 @@ def stonith(args: Argv) -> str: ) ), update_desc=_format_desc(_resource_update_desc_fn(is_stonith=True)), + output_format_syntax=_output_format_syntax(cmd=True), + output_format_desc=_format_desc([_output_format_desc()]), ) return sub_usage(args, output) diff --git a/pcs_test/Makefile.am b/pcs_test/Makefile.am index 2bbe3514a..2e303da3f 100644 --- a/pcs_test/Makefile.am +++ b/pcs_test/Makefile.am @@ -16,6 +16,7 @@ EXTRA_DIST = \ resources/cib-empty-with3nodes.xml \ resources/cib-empty-withnodes.xml \ resources/cib-empty.xml \ + resources/cib-fencing-levels.xml \ resources/cib-largefile.xml \ resources/cib-large.xml \ resources/cib-property.xml \ @@ -114,6 +115,9 @@ EXTRA_DIST = \ tier0/cli/test_rule.py \ tier0/cli/test_status.py \ tier0/cli/test_stonith.py \ + tier0/cli/stonith/__init__.py \ + tier0/cli/stonith/levels/__init__.py \ + tier0/cli/stonith/levels/test_output.py \ tier0/common/__init__.py \ tier0/common/interface/__init__.py \ tier0/common/interface/test_dto.py \ @@ -396,6 +400,8 @@ EXTRA_DIST = \ tier1/resource/test_config.py \ tier1/resource/test_remove.py \ tier1/stonith/__init__.py \ + tier1/stonith/levels/__init__.py \ + tier1/stonith/levels/test_config.py \ tier1/stonith/test_config.py \ tier1/stonith/test_remove.py \ tier1/test_booth.py \ diff --git a/pcs_test/resources/cib-fencing-levels.xml b/pcs_test/resources/cib-fencing-levels.xml new file mode 100644 index 000000000..ec4f77975 --- /dev/null +++ b/pcs_test/resources/cib-fencing-levels.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pcs_test/tier0/cli/stonith/__init__.py b/pcs_test/tier0/cli/stonith/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pcs_test/tier0/cli/stonith/levels/__init__.py b/pcs_test/tier0/cli/stonith/levels/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pcs_test/tier0/cli/stonith/levels/test_output.py b/pcs_test/tier0/cli/stonith/levels/test_output.py new file mode 100644 index 000000000..fe5c0f0b6 --- /dev/null +++ b/pcs_test/tier0/cli/stonith/levels/test_output.py @@ -0,0 +1,120 @@ +from unittest import TestCase + +from pcs.cli.stonith.levels import output +from pcs.common.pacemaker.fencing_topology import ( + CibFencingLevelAttributeDto, + CibFencingLevelNodeDto, + CibFencingLevelRegexDto, + CibFencingTopologyDto, +) + +FIXTURE_TARGET_NODE_DTO_LIST = [ + CibFencingLevelNodeDto("fl1", "node1", 1, ["d1"]), + CibFencingLevelNodeDto("fl2", "node1", 2, ["d2"]), + CibFencingLevelNodeDto("fl3", "node2", 3, ["d3", "d4"]), +] +FIXTURE_REGEX_DTO_LIST = [ + CibFencingLevelRegexDto("fl1", "node.*", 1, ["d1"]), + CibFencingLevelRegexDto("fl2", "node.*", 2, ["d2"]), + CibFencingLevelRegexDto("fl3", ".*node", 3, ["d3", "d4"]), +] +FIXTURE_ATTRIBUTE_DTO_LIST = [ + CibFencingLevelAttributeDto("fl1", "A", "B", 1, ["d1"]), + CibFencingLevelAttributeDto("fl2", "A", "B", 2, ["d2"]), + CibFencingLevelAttributeDto("fl3", "A", "X", 2, ["d2"]), + CibFencingLevelAttributeDto("fl4", "B", "B", 3, ["d3", "d4"]), +] + + +class LevelsToOutputMixin: + def test_empty(self): + command_output = self._call_command(CibFencingTopologyDto([], [], [])) + self.assertEqual(command_output, []) + + def test_nodes(self): + command_output = self._call_command( + CibFencingTopologyDto(FIXTURE_TARGET_NODE_DTO_LIST, [], []) + ) + self.assertEqual(command_output, self.NODE_OUTPUT) + + def test_regex(self): + command_output = self._call_command( + CibFencingTopologyDto([], FIXTURE_REGEX_DTO_LIST, []) + ) + self.assertEqual(command_output, self.REGEX_OUTPUT) + + def test_attributes(self): + command_output = self._call_command( + CibFencingTopologyDto([], [], FIXTURE_ATTRIBUTE_DTO_LIST) + ) + self.assertEqual(command_output, self.ATTRIBUTE_OUTPUT) + + def test_combination(self): + command_output = self._call_command( + CibFencingTopologyDto( + FIXTURE_TARGET_NODE_DTO_LIST, + FIXTURE_REGEX_DTO_LIST, + FIXTURE_ATTRIBUTE_DTO_LIST, + ) + ) + self.assertEqual( + command_output, + self.NODE_OUTPUT + self.REGEX_OUTPUT + self.ATTRIBUTE_OUTPUT, + ) + + +class LevelsToText(TestCase, LevelsToOutputMixin): + NODE_OUTPUT = [ + "Target (node): node1", + " Level 1: d1", + " Level 2: d2", + "Target (node): node2", + " Level 3: d3 d4", + ] + + REGEX_OUTPUT = [ + "Target (regexp): .*node", + " Level 3: d3 d4", + "Target (regexp): node.*", + " Level 1: d1", + " Level 2: d2", + ] + + ATTRIBUTE_OUTPUT = [ + "Target (attribute): A=B", + " Level 1: d1", + " Level 2: d2", + "Target (attribute): B=B", + " Level 3: d3 d4", + "Target (attribute): A=X", + " Level 2: d2", + ] + + def _call_command(self, dto): + # pylint: disable=no-self-use + return output.stonith_level_config_to_text(dto) + + +class LevelsToCmd(TestCase, LevelsToOutputMixin): + NODE_OUTPUT = [ + "pcs stonith level add --force -- 1 node1 d1 id=fl1", + "pcs stonith level add --force -- 2 node1 d2 id=fl2", + "pcs stonith level add --force -- 3 node2 d3 d4 id=fl3", + ] + + REGEX_OUTPUT = [ + "pcs stonith level add --force -- 1 regexp%node.* d1 id=fl1", + "pcs stonith level add --force -- 2 regexp%node.* d2 id=fl2", + "pcs stonith level add --force -- 3 regexp%.*node d3 d4 id=fl3", + ] + + ATTRIBUTE_OUTPUT = [ + "pcs stonith level add --force -- 1 attrib%A=B d1 id=fl1", + "pcs stonith level add --force -- 2 attrib%A=B d2 id=fl2", + "pcs stonith level add --force -- 2 attrib%A=X d2 id=fl3", + "pcs stonith level add --force -- 3 attrib%B=B d3 d4 id=fl4", + ] + + def _call_command(self, dto): + # pylint: disable=no-self-use + return output.stonith_level_config_to_cmd(dto) diff --git a/pcs_test/tier0/lib/cib/test_fencing_topology.py b/pcs_test/tier0/lib/cib/test_fencing_topology.py index bf9e60164..377ac226c 100644 --- a/pcs_test/tier0/lib/cib/test_fencing_topology.py +++ b/pcs_test/tier0/lib/cib/test_fencing_topology.py @@ -12,6 +12,12 @@ TARGET_TYPE_NODE, TARGET_TYPE_REGEXP, ) +from pcs.common.pacemaker.fencing_topology import ( + CibFencingLevelAttributeDto, + CibFencingLevelNodeDto, + CibFencingLevelRegexDto, + CibFencingTopologyDto, +) from pcs.common.reports import ReportItemSeverity as severity from pcs.common.reports import codes as report_codes from pcs.common.reports.item import ReportItem @@ -54,6 +60,7 @@ def get_cib(): return etree.fromstring( """ + ") - topology = etree.fromstring("") - - report_list = lib.verify(topology, resources, self.get_status()) - + self.cib.find("configuration/fencing-topology").clear() + report_list = lib.verify(self.cib, self.get_status()) assert_report_item_list_equal(report_list, []) @mock.patch.object( @@ -761,18 +853,16 @@ def test_empty(self): rc("pcmk_api_rng/api-result.rng"), ) def test_success(self): - resources = etree.fromstring("") + resources = self.cib.find("configuration/resources") for name in ["d1", "d2", "d3", "d4", "d5", "dR", "dR-special"]: self.fixture_resource(resources, name) - report_list = lib.verify(self.tree, resources, self.get_status()) + report_list = lib.verify(self.cib, self.get_status()) assert_report_item_list_equal(report_list, []) def test_failures(self): - resources = etree.fromstring("") - - report_list = lib.verify(self.tree, resources, []) + report_list = lib.verify(self.cib, []) report = [ ( @@ -831,7 +921,7 @@ def test_invalid(self): [ fixture.error( report_codes.INVALID_OPTION_VALUE, - option_value=level, + option_value=str(level), option_name="level", allowed_values="1..9", cannot_be_empty=False, @@ -1123,7 +1213,7 @@ def setUp(self): def test_node_name(self): lib._append_level_element( - self.tree, 1, TARGET_TYPE_NODE, "node1", ["d1"] + self.tree, "1", TARGET_TYPE_NODE, "node1", ["d1"], "fl-node1-1" ) assert_xml_equal( """ @@ -1139,7 +1229,12 @@ def test_node_name(self): def test_node_pattern(self): lib._append_level_element( - self.tree, "2", TARGET_TYPE_REGEXP, r"node-\d+", ["d1", "d2"] + self.tree, + "2", + TARGET_TYPE_REGEXP, + r"node-\d+", + ["d1", "d2"], + "fl-node-d-2", ) assert_xml_equal( """ @@ -1156,10 +1251,11 @@ def test_node_pattern(self): def test_node_attribute(self): lib._append_level_element( self.tree, - 3, + "3", TARGET_TYPE_ATTRIBUTE, ("name%@x", "val%@x"), ["d1"], + "fl-namex-3", ) assert_xml_equal( """ @@ -1280,3 +1376,82 @@ def test_combination(self): ), ["fl4"], ) + + +class FencingTopologyElToDto(TestCase, CibMixin): + def setUp(self): + self.cib = self.get_cib() + self.tree = self.cib.find("configuration/fencing-topology") + + def test_get_empty_dto(self): + fencing_topology = etree.fromstring("") + self.assertEqual( + lib.fencing_topology_el_to_dto(fencing_topology), + CibFencingTopologyDto( + target_node=[], target_regex=[], target_attribute=[] + ), + ) + + def test_get_dto(self): + self.assertEqual( + lib.fencing_topology_el_to_dto(self.tree), + CibFencingTopologyDto( + target_node=[ + CibFencingLevelNodeDto( + id="fl1", target="nodeA", index=1, devices=["d1", "d2"] + ), + CibFencingLevelNodeDto( + id="fl2", target="nodeA", index=2, devices=["d3"] + ), + CibFencingLevelNodeDto( + id="fl3", target="nodeB", index=1, devices=["d2", "d1"] + ), + CibFencingLevelNodeDto( + id="fl4", target="nodeB", index=2, devices=["d3"] + ), + ], + target_regex=[ + CibFencingLevelRegexDto( + id="fl5", + target_pattern="node\\d+", + index=1, + devices=["d3", "d4"], + ), + CibFencingLevelRegexDto( + id="fl6", + target_pattern="node\\d+", + index=2, + devices=["d1"], + ), + CibFencingLevelRegexDto( + id="fl9", + target_pattern="node-R.*", + index=3, + devices=["dR"], + ), + ], + target_attribute=[ + CibFencingLevelAttributeDto( + id="fl7", + target_attribute="fencing", + target_value="improved", + index=3, + devices=["d3", "d4"], + ), + CibFencingLevelAttributeDto( + id="fl8", + target_attribute="fencing", + target_value="improved", + index=4, + devices=["d5"], + ), + CibFencingLevelAttributeDto( + id="fl10", + target_attribute="fencing", + target_value="remote-special", + index=4, + devices=["dR-special"], + ), + ], + ), + ) diff --git a/pcs_test/tier0/lib/commands/test_fencing_topology.py b/pcs_test/tier0/lib/commands/test_fencing_topology.py index a34e7b0e2..e3001f9fe 100644 --- a/pcs_test/tier0/lib/commands/test_fencing_topology.py +++ b/pcs_test/tier0/lib/commands/test_fencing_topology.py @@ -10,6 +10,10 @@ TARGET_TYPE_NODE, TARGET_TYPE_REGEXP, ) +from pcs.common.pacemaker.fencing_topology import ( + CibFencingLevelNodeDto, + CibFencingTopologyDto, +) from pcs.common.reports import codes as report_codes from pcs.lib.commands import fencing_topology as lib from pcs.lib.env import LibraryEnvironment @@ -29,8 +33,6 @@ @patch_command("cib_fencing_topology.add_level") -@patch_command("get_resources") -@patch_command("get_fencing_topology") @patch_env("push_cib") @patch_command("ClusterState") @patch_env("get_cluster_state") @@ -41,8 +43,6 @@ def prepare_mocks( mock_get_cib, mock_status_dom, mock_status, - mock_get_topology, - mock_get_resources, ): # pylint: disable=no-self-use mock_get_cib.return_value = "mocked cib" @@ -50,22 +50,16 @@ def prepare_mocks( mock_status.return_value = mock.MagicMock( node_section=mock.MagicMock(nodes="nodes") ) - mock_get_topology.return_value = "topology el" - mock_get_resources.return_value = "resources_el" def assert_mocks( self, mock_status_dom, mock_status, - mock_get_topology, - mock_get_resources, mock_push_cib, ): # pylint: disable=no-self-use mock_status_dom.assert_called_once_with() mock_status.assert_called_once_with("mock get_cluster_status_dom") - mock_get_topology.assert_called_once_with("mocked cib") - mock_get_resources.assert_called_once_with("mocked cib") mock_push_cib.assert_called_once_with() def test_success( @@ -74,38 +68,34 @@ def test_success( mock_status_dom, mock_status, mock_push_cib, - mock_get_topology, - mock_get_resources, mock_add_level, ): self.prepare_mocks( mock_get_cib, mock_status_dom, mock_status, - mock_get_topology, - mock_get_resources, ) lib_env = create_lib_env() - lib.add_level( lib_env, "level", "target type", "target value", "devices", + "level id", "force device", "force node", ) mock_add_level.assert_called_once_with( lib_env.report_processor, - "topology el", - "resources_el", + "mocked cib", "level", "target type", "target value", "devices", "nodes", + "level id", "force device", "force node", ) @@ -113,8 +103,6 @@ def test_success( self.assert_mocks( mock_status_dom, mock_status, - mock_get_topology, - mock_get_resources, mock_push_cib, ) @@ -124,16 +112,12 @@ def test_target_attribute_updates_cib( mock_status_dom, mock_status, mock_push_cib, - mock_get_topology, - mock_get_resources, mock_add_level, ): self.prepare_mocks( mock_get_cib, mock_status_dom, mock_status, - mock_get_topology, - mock_get_resources, ) lib_env = create_lib_env() @@ -143,19 +127,20 @@ def test_target_attribute_updates_cib( TARGET_TYPE_ATTRIBUTE, "target value", "devices", + "level id", "force device", "force node", ) mock_add_level.assert_called_once_with( lib_env.report_processor, - "topology el", - "resources_el", + "mocked cib", "level", TARGET_TYPE_ATTRIBUTE, "target value", "devices", "nodes", + "level id", "force device", "force node", ) @@ -163,8 +148,6 @@ def test_target_attribute_updates_cib( self.assert_mocks( mock_status_dom, mock_status, - mock_get_topology, - mock_get_resources, mock_push_cib, ) @@ -174,16 +157,12 @@ def test_target_regexp_updates_cib( mock_status_dom, mock_status, mock_push_cib, - mock_get_topology, - mock_get_resources, mock_add_level, ): self.prepare_mocks( mock_get_cib, mock_status_dom, mock_status, - mock_get_topology, - mock_get_resources, ) lib_env = create_lib_env() @@ -193,19 +172,20 @@ def test_target_regexp_updates_cib( TARGET_TYPE_REGEXP, "target value", "devices", + "level id", "force device", "force node", ) mock_add_level.assert_called_once_with( lib_env.report_processor, - "topology el", - "resources_el", + "mocked cib", "level", TARGET_TYPE_REGEXP, "target value", "devices", "nodes", + "level id", "force device", "force node", ) @@ -213,8 +193,6 @@ def test_target_regexp_updates_cib( self.assert_mocks( mock_status_dom, mock_status, - mock_get_topology, - mock_get_resources, mock_push_cib, ) @@ -454,8 +432,6 @@ def test_devices_target_node_missing_guessing_disabled(self): @patch_command("cib_fencing_topology.verify") -@patch_command("get_resources") -@patch_command("get_fencing_topology") @patch_env("push_cib") @patch_command("ClusterState") @patch_env("get_cluster_state") @@ -466,8 +442,6 @@ def test_success( mock_status_dom, mock_status, mock_push_cib, - mock_get_topology, - mock_get_resources, mock_verify, ): # pylint: disable=no-self-use @@ -475,17 +449,51 @@ def test_success( mock_status.return_value = mock.MagicMock( node_section=mock.MagicMock(nodes="nodes") ) - mock_get_topology.return_value = "topology el" - mock_get_resources.return_value = "resources_el" lib_env = create_lib_env() lib.verify(lib_env) - mock_verify.assert_called_once_with( - "topology el", "resources_el", "nodes" - ) + mock_verify.assert_called_once_with("mocked cib", "nodes") mock_status_dom.assert_called_once_with() mock_status.assert_called_once_with("mock get_cluster_status_dom") - mock_get_topology.assert_called_once_with("mocked cib") - mock_get_resources.assert_called_once_with("mocked cib") mock_push_cib.assert_not_called() + + +class GetConfigDto(TestCase): + def setUp(self): + self.env_assist, self.config = get_env_tools(self) + + def test_success_no_levels(self): + self.config.runner.cib.load() + self.assertEqual( + lib.get_config_dto(self.env_assist.get_env()), + CibFencingTopologyDto( + target_node=[], target_regex=[], target_attribute=[] + ), + ) + + def test_success(self): + self.config.runner.cib.load( + optional_in_conf=""" + + + + """ + ) + self.assertEqual( + lib.get_config_dto(self.env_assist.get_env()), + CibFencingTopologyDto( + target_node=[ + CibFencingLevelNodeDto( + id="fl1", + target="node1", + index=1, + devices=["dev1", "dev2"], + ) + ], + target_regex=[], + target_attribute=[], + ), + ) diff --git a/pcs_test/tier1/legacy/test_stonith.py b/pcs_test/tier1/legacy/test_stonith.py index e62f001db..8daf073a6 100644 --- a/pcs_test/tier1/legacy/test_stonith.py +++ b/pcs_test/tier1/legacy/test_stonith.py @@ -1473,19 +1473,19 @@ def fixture_cib_config(): cib_content = cib_file.read() config = dedent( """\ - Target: rh7-1 - Level 1 - F1 - Level 2 - F2 - Target: rh7-2 - Level 1 - F2 - Level 2 - F1 + Target (node): rh7-1 + Level 1: F1 + Level 2: F2 + Target (node): rh7-2 + Level 1: F2 + Level 2: F1 Target (regexp): rh7-\\d - Level 3 - F2,F1 - Level 4 - F3 - Target: fencewith=levels1 - Level 5 - F3,F2 - Target: fencewith=levels2 - Level 6 - F3,F1 + Level 3: F2 F1 + Level 4: F3 + Target (attribute): fencewith=levels1 + Level 5: F3 F2 + Target (attribute): fencewith=levels2 + Level 6: F3 F1 """ ) config_lines = config.splitlines() @@ -1606,8 +1606,8 @@ def test_add_level_leading_zero(self): "stonith level".split(), dedent( """\ - Target: rh7-1 - Level 2 - F1 + Target (node): rh7-1 + Level 2: F1 """ ), ) @@ -1619,8 +1619,8 @@ def test_add_node(self): "stonith level".split(), dedent( """\ - Target: rh7-1 - Level 1 - F1 + Target (node): rh7-1 + Level 1: F1 """ ), ) @@ -1636,8 +1636,8 @@ def test_add_node(self): "stonith level".split(), dedent( """\ - Target: rh7-1 - Level 1 - F1 + Target (node): rh7-1 + Level 1: F1 """ ), ) @@ -1650,7 +1650,7 @@ def test_add_node_pattern(self): dedent( """\ Target (regexp): rh7-\\d - Level 1 - F1 + Level 1: F1 """ ), ) @@ -1667,7 +1667,7 @@ def test_add_node_pattern(self): dedent( """\ Target (regexp): rh7-\\d - Level 1 - F1 + Level 1: F1 """ ), ) @@ -1681,8 +1681,8 @@ def test_add_node_attribute(self): "stonith level".split(), dedent( """\ - Target: fencewith=levels - Level 1 - F1 + Target (attribute): fencewith=levels + Level 1: F1 """ ), ) @@ -1698,8 +1698,8 @@ def test_add_node_attribute(self): "stonith level".split(), dedent( """\ - Target: fencewith=levels - Level 1 - F1 + Target (attribute): fencewith=levels + Level 1: F1 """ ), ) @@ -1712,8 +1712,8 @@ def test_add_more_devices(self): "stonith level".split(), dedent( """\ - Target: rh7-1 - Level 1 - F1,F2 + Target (node): rh7-1 + Level 1: F1 F2 """ ), ) @@ -1735,8 +1735,8 @@ def test_add_more_devices_old_syntax(self): "stonith level".split(), dedent( """\ - Target: rh7-1 - Level 1 - F1,F2 + Target (node): rh7-1 + Level 1: F1 F2 """ ), ) @@ -1753,9 +1753,9 @@ def test_add_more_devices_old_syntax(self): "stonith level".split(), dedent( """\ - Target: rh7-1 - Level 1 - F1,F2 - Level 2 - F1,F2,F3 + Target (node): rh7-1 + Level 1: F1 F2 + Level 2: F1 F2 F3 """ ), ) @@ -1772,10 +1772,10 @@ def test_add_more_devices_old_syntax(self): "stonith level".split(), dedent( """\ - Target: rh7-1 - Level 1 - F1,F2 - Level 2 - F1,F2,F3 - Level 3 - F1,F2,F3 + Target (node): rh7-1 + Level 1: F1 F2 + Level 2: F1 F2 F3 + Level 3: F1 F2 F3 """ ), ) @@ -1799,8 +1799,8 @@ def test_nonexistent_node(self): "stonith level".split(), dedent( """\ - Target: rh7-X - Level 1 - F1 + Target (node): rh7-X + Level 1: F1 """ ), ) @@ -1821,8 +1821,8 @@ def test_nonexistent_device(self): "stonith level".split(), dedent( """\ - Target: rh7-1 - Level 1 - F1 + Target (node): rh7-1 + Level 1: F1 """ ), ) @@ -1844,8 +1844,8 @@ def test_nonexistent_devices(self): "stonith level".split(), dedent( """\ - Target: rh7-1 - Level 1 - F1,F2,F3 + Target (node): rh7-1 + Level 1: F1 F2 F3 """ ), ) diff --git a/pcs_test/tier1/stonith/levels/__init__.py b/pcs_test/tier1/stonith/levels/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pcs_test/tier1/stonith/levels/test_config.py b/pcs_test/tier1/stonith/levels/test_config.py new file mode 100644 index 000000000..0cd5a1357 --- /dev/null +++ b/pcs_test/tier1/stonith/levels/test_config.py @@ -0,0 +1,177 @@ +import json +import shlex +from unittest import TestCase + +from pcs.common.interface import dto +from pcs.common.pacemaker.fencing_topology import ( + CibFencingLevelAttributeDto, + CibFencingLevelNodeDto, + CibFencingLevelRegexDto, + CibFencingTopologyDto, +) + +from pcs_test.tools.assertions import AssertPcsMixin +from pcs_test.tools.misc import ( + get_test_resource, + get_tmp_file, + write_file_to_tmpfile, +) +from pcs_test.tools.pcs_runner import PcsRunner + + +class LevelsConfigJson(TestCase): + def setUp(self): + self.maxDiff = None + + def test_empty(self): + pcs_runner = PcsRunner(cib_file=get_test_resource("cib-empty.xml")) + stdout, stderr, retval = pcs_runner.run( + ["stonith", "level", "config", "--output-format=json"] + ) + expected = CibFencingTopologyDto( + target_node=[], target_regex=[], target_attribute=[] + ) + self.assertEqual( + json.loads(stdout), json.loads(json.dumps(dto.to_dict(expected))) + ) + self.assertEqual(stderr, "") + self.assertEqual(retval, 0) + + def test_success(self): + pcs_runner = PcsRunner( + cib_file=get_test_resource("cib-fencing-levels.xml") + ) + + stdout, stderr, retval = pcs_runner.run( + ["stonith", "level", "config", "--output-format=json"] + ) + expected = CibFencingTopologyDto( + [ + CibFencingLevelNodeDto( + id="fl-rh-1-1", target="rh-1", index=1, devices=["S1"] + ), + CibFencingLevelNodeDto( + id="fl-rh-1-2", target="rh-1", index=2, devices=["S2"] + ), + CibFencingLevelNodeDto( + id="fl-rh-2-1", target="rh-2", index=1, devices=["S3", "S4"] + ), + ], + [ + CibFencingLevelRegexDto( + id="fl-rh-.-3", + target_pattern="rh-.*", + index=3, + devices=["S4"], + ) + ], + [ + CibFencingLevelAttributeDto( + id="fl-foo-4", + target_attribute="foo", + target_value="bar", + index=4, + devices=["S1", "S2", "S3"], + ) + ], + ) + self.assertEqual( + json.loads(stdout), json.loads(json.dumps(dto.to_dict(expected))) + ) + self.assertEqual(stderr, "") + self.assertEqual(retval, 0) + + +class LevelsConfigCmd(TestCase): + def setUp(self): + self.old_cib = get_tmp_file("tier1_stonith_level_cmd_old") + write_file_to_tmpfile( + get_test_resource("cib-fencing-levels.xml"), self.old_cib + ) + self.new_cib = get_tmp_file("tier1_stonith_level_cmd_new") + write_file_to_tmpfile(get_test_resource("cib-empty.xml"), self.new_cib) + self.old_pcs_runner = PcsRunner(self.old_cib.name) + self.new_pcs_runner = PcsRunner(self.new_cib.name) + self.maxDiff = None + + def tearDown(self): + self.old_cib.close() + self.new_cib.close() + + def _get_as_json(self, runner): + stdout, stderr, retval = runner.run( + ["stonith", "level", "config", "--output-format=json"] + ) + self.assertEqual(stderr, "") + self.assertEqual(retval, 0) + return json.loads(stdout) + + def test_success(self): + stdout, stderr, retval = self.old_pcs_runner.run( + ["stonith", "level", "config", "--output-format=cmd"] + ) + self.assertEqual(retval, 0) + commands = [ + shlex.split(command)[1:] + for command in stdout.replace("\\n", "").strip().split(";\n") + ] + + for cmd in commands: + stdout, stderr, retval = self.new_pcs_runner.run(cmd) + self.assertEqual( + retval, + 0, + ( + f"Command {cmd} exited with {retval}\nstdout:\n{stdout}\n" + f"stderr:\n{stderr}" + ), + ) + self.assertEqual( + self._get_as_json(self.new_pcs_runner), + self._get_as_json(self.old_pcs_runner), + ) + + +class LevelsConfigText(AssertPcsMixin, TestCase): + FIXTURE_OUTPUT = ( + "Target (node): rh-1\n" + " Level 1: S1\n" + " Level 2: S2\n" + "Target (node): rh-2\n" + " Level 1: S3 S4\n" + "Target (regexp): rh-.*\n" + " Level 3: S4\n" + "Target (attribute): foo=bar\n" + " Level 4: S1 S2 S3\n" + ) + + def setUp(self): + self.maxDiff = None + self.pcs_runner = PcsRunner( + cib_file=get_test_resource("cib-fencing-levels.xml") + ) + + def test_empty(self): + pcs_runner = PcsRunner(cib_file=get_test_resource("cib-empty.xml")) + stdout, stderr, retval = pcs_runner.run( + ["stonith", "level", "config", "--output-format=text"] + ) + self.assertEqual(stdout, "") + self.assertEqual(stderr, "") + self.assertEqual(retval, 0) + + def test_success(self): + self.assert_pcs_success( + ["stonith", "level", "config", "--output-format=text"], + stdout_full=self.FIXTURE_OUTPUT, + ) + + def test_success_no_option(self): + self.assert_pcs_success( + ["stonith", "level", "config"], stdout_full=self.FIXTURE_OUTPUT + ) + + def test_success_no_config(self): + self.assert_pcs_success( + ["stonith", "level"], stdout_full=self.FIXTURE_OUTPUT + ) diff --git a/pcs_test/tier1/stonith/test_config.py b/pcs_test/tier1/stonith/test_config.py index f4ffe9fb5..8347df7f4 100644 --- a/pcs_test/tier1/stonith/test_config.py +++ b/pcs_test/tier1/stonith/test_config.py @@ -1,4 +1,5 @@ import json +import shlex from unittest import TestCase from pcs.common.interface.dto import to_dict @@ -6,11 +7,23 @@ from pcs_test.tier1.resource.test_config import ResourceConfigCmdMixin from pcs_test.tools import resources_dto -from pcs_test.tools.misc import get_test_resource +from pcs_test.tools.assertions import AssertPcsMixin +from pcs_test.tools.misc import ( + get_test_resource, + get_tmp_file, + write_file_to_tmpfile, +) from pcs_test.tools.pcs_runner import PcsRunner +FIXTURE_JSON_WARNING = ( + "Warning: Fencing levels are not included because this command " + "could only export stonith configuration previously. This cannot " + "be changed to avoid breaking existing tooling. To export fencing " + "levels, run 'pcs stonith level config --output-format=json'\n" +) -class StonithConfigJson(TestCase): + +class StonithConfigJson(AssertPcsMixin, TestCase): def setUp(self): self.pcs_runner = PcsRunner( cib_file=get_test_resource("cib-resources.xml"), @@ -21,10 +34,7 @@ def test_all(self): stdout, stderr, retval = self.pcs_runner.run( ["stonith", "config", "--output-format=json"], ) - self.assertEqual( - stderr, - "Warning: Only 'text' output format is supported for stonith levels\n", - ) + self.assertEqual(stderr, FIXTURE_JSON_WARNING) self.assertEqual(retval, 0) expected = CibResourcesDto( primitives=[ @@ -41,10 +51,7 @@ def test_get_specified(self): stdout, stderr, retval = self.pcs_runner.run( ["stonith", "config", "--output-format=json", "S1"], ) - self.assertEqual( - stderr, - "Warning: Only 'text' output format is supported for stonith levels\n", - ) + self.assertEqual(stderr, FIXTURE_JSON_WARNING) self.assertEqual(retval, 0) expected = CibResourcesDto( primitives=[ @@ -61,3 +68,125 @@ class StonithConfigCmd(ResourceConfigCmdMixin, TestCase): @staticmethod def _get_tmp_file_name(): return "tier1_stonith_test_config_cib.xml" + + +class StonithConfigWithLevelsCmd(TestCase): + def setUp(self): + self.old_cib = get_tmp_file("tier1_stonith_cmd_old") + write_file_to_tmpfile( + get_test_resource("cib-fencing-levels.xml"), self.old_cib + ) + self.new_cib = get_tmp_file("tier1_stonith_cmd_new") + write_file_to_tmpfile(get_test_resource("cib-empty.xml"), self.new_cib) + self.old_pcs_runner = PcsRunner(self.old_cib.name) + self.new_pcs_runner = PcsRunner(self.new_cib.name) + self.maxDiff = None + + def tearDown(self): + self.old_cib.close() + self.new_cib.close() + + def _get_stonith_resources_json(self, runner): + stdout, stderr, retval = runner.run( + ["stonith", "config", "--output-format=json"] + ) + self.assertEqual(stderr, FIXTURE_JSON_WARNING) + self.assertEqual(retval, 0) + return json.loads(stdout) + + def _get_stonith_level_json(self, runner): + stdout, stderr, retval = runner.run( + ["stonith", "level", "config", "--output-format=json"] + ) + self.assertEqual(stderr, "") + self.assertEqual(retval, 0) + return json.loads(stdout) + + def test_success(self): + stdout, stderr, retval = self.old_pcs_runner.run( + ["stonith", "config", "--output-format=cmd"] + ) + self.assertEqual(retval, 0) + commands = [ + shlex.split(command)[1:] + for command in stdout.replace("\\\n", "").strip().split(";\n") + ] + + for cmd in commands: + stdout, stderr, retval = self.new_pcs_runner.run(cmd) + self.assertEqual( + retval, + 0, + ( + f"Command {cmd} exited with {retval}\nstdout:\n{stdout}\n" + f"stderr:\n{stderr}" + ), + ) + self.assertEqual( + self._get_stonith_resources_json(self.new_pcs_runner), + self._get_stonith_resources_json(self.old_pcs_runner), + ) + self.assertEqual( + self._get_stonith_level_json(self.new_pcs_runner), + self._get_stonith_level_json(self.old_pcs_runner), + ) + + +class StonithConfigText(AssertPcsMixin, TestCase): + FIXTURE_S1 = ( + "Resource: S1 (class=stonith type=fence_pcsmock_minimal)\n" + " Operations:\n" + " monitor: S1-monitor-interval-60s\n" + " interval=60s\n" + ) + FIXTURE_OTHER_RESOURCES = ( + "Resource: S2 (class=stonith type=fence_pcsmock_minimal)\n" + " Operations:\n" + " monitor: S2-monitor-interval-60s\n" + " interval=60s\n" + "Resource: S3 (class=stonith type=fence_pcsmock_minimal)\n" + " Operations:\n" + " monitor: S3-monitor-interval-60s\n" + " interval=60s\n" + "Resource: S4 (class=stonith type=fence_pcsmock_minimal)\n" + " Operations:\n" + " monitor: S4-monitor-interval-60s\n" + " interval=60s\n" + ) + FIXTURE_FENCING_LEVELS = ( + "\n" + "Fencing Levels:\n" + " Target (node): rh-1\n" + " Level 1: S1\n" + " Level 2: S2\n" + " Target (node): rh-2\n" + " Level 1: S3 S4\n" + " Target (regexp): rh-.*\n" + " Level 3: S4\n" + " Target (attribute): foo=bar\n" + " Level 4: S1 S2 S3\n" + ) + FIXTURE_ALL = FIXTURE_S1 + FIXTURE_OTHER_RESOURCES + FIXTURE_FENCING_LEVELS + + def setUp(self): + self.maxDiff = None + self.pcs_runner = PcsRunner( + cib_file=get_test_resource("cib-fencing-levels.xml") + ) + + def test_success(self): + self.assert_pcs_success( + ["stonith", "config", "--output-format=text"], + stdout_full=self.FIXTURE_ALL, + ) + + def test_success_no_option(self): + self.assert_pcs_success( + ["stonith", "config"], stdout_full=self.FIXTURE_ALL + ) + + def test_success_filtered(self): + self.assert_pcs_success( + ["stonith", "config", "S1", "--output-format=text"], + stdout_full=self.FIXTURE_S1 + self.FIXTURE_FENCING_LEVELS, + ) diff --git a/pcs_test/tier1/test_status.py b/pcs_test/tier1/test_status.py index 491f36eb9..d24b52421 100644 --- a/pcs_test/tier1/test_status.py +++ b/pcs_test/tier1/test_status.py @@ -563,12 +563,12 @@ class StonithStatus(ResourceStonithStatusBase, TestCase): * fence-kdump (stonith:fence_pcsmock_minimal): Stopped Fencing Levels: - Target: rh-1 - Level 1 - fence-kdump - Level 2 - fence-rh-1 - Target: rh-2 - Level 1 - fence-kdump - Level 2 - fence-rh-2 + Target (node): rh-1 + Level 1: fence-kdump + Level 2: fence-rh-1 + Target (node): rh-2 + Level 1: fence-kdump + Level 2: fence-rh-2 """ ) active_resources_output = outdent( @@ -576,12 +576,12 @@ class StonithStatus(ResourceStonithStatusBase, TestCase): * fence-rh-1 (stonith:fence_pcsmock_minimal): Started rh-1 Fencing Levels: - Target: rh-1 - Level 1 - fence-kdump - Level 2 - fence-rh-1 - Target: rh-2 - Level 1 - fence-kdump - Level 2 - fence-rh-2 + Target (node): rh-1 + Level 1: fence-kdump + Level 2: fence-rh-1 + Target (node): rh-2 + Level 1: fence-kdump + Level 2: fence-rh-2 """ ) active_resources_output_node = outdent( diff --git a/pcs_test/tier1/test_tag.py b/pcs_test/tier1/test_tag.py index e45c4f0c6..f64df93a4 100644 --- a/pcs_test/tier1/test_tag.py +++ b/pcs_test/tier1/test_tag.py @@ -468,12 +468,12 @@ class PcsConfigTagsTest(TestTagMixin, TestCase): expected_fencing_levels = outdent( """ Fencing Levels: - Target: rh-1 - Level 1 - fence-kdump - Level 2 - fence-rh-1 - Target: rh-2 - Level 1 - fence-kdump - Level 2 - fence-rh-2 + Target (node): rh-1 + Level 1: fence-kdump + Level 2: fence-rh-1 + Target (node): rh-2 + Level 1: fence-kdump + Level 2: fence-rh-2 """ ) expected_tags = outdent( diff --git a/pcsd/capabilities.xml.in b/pcsd/capabilities.xml.in index 6b6139d45..d38119c87 100644 --- a/pcsd/capabilities.xml.in +++ b/pcsd/capabilities.xml.in @@ -2296,6 +2296,24 @@ /api/v1/fencing-topology-add-level/v1 + + + Add a new stonith level with an ability to set its id. + + pcs commands: stonith level add + API v2: fencing_topology.add_level + + + + + Display configured stonith levels in various formats. + + pcs commands: + stonith level config --output-format=text|json|cmd + stonith config --output-format=text|cmd + API v2: fencing_topology.get_config_dto + + Support specifying a target by a node attribute. diff --git a/typos.toml b/typos.toml index ef74964d7..73b8ec667 100644 --- a/typos.toml +++ b/typos.toml @@ -25,6 +25,7 @@ muttualy = "mutually" operatin = "operation" oudside = "outside" pacakamer = "pacemaker" +pacemkaer = "pacemaker" pririty = "priority" proprties = "properties" quourm = "quorum" From bc29b4d6ea533b3120dd1e57e90a1ba1cbfae31d Mon Sep 17 00:00:00 2001 From: Ivan Devat Date: Thu, 7 Nov 2024 15:54:12 +0100 Subject: [PATCH 049/227] concentrate webui location knowledge --- configure.ac | 4 ++ pcs/daemon/env.py | 61 +++++++++++++++++-------------- pcs/daemon/run.py | 13 +++---- pcs/settings.py.in | 2 + pcs_test/tier0/daemon/test_env.py | 26 ++++++------- 5 files changed, 58 insertions(+), 48 deletions(-) diff --git a/configure.ac b/configure.ac index 71bbd1b3a..fd8f97912 100644 --- a/configure.ac +++ b/configure.ac @@ -471,6 +471,10 @@ AC_SUBST([BOOTH_ENABLE_AUTHFILE_SET_ENABLED], [$(if test "x$booth_enable_authfil AC_SUBST([BOOTH_ENABLE_AUTHFILE_UNSET_ENABLED], [$(if test "x$booth_enable_authfile_unset" = "xyes"; then echo "True"; else echo "False"; fi)]) AC_SUBST([BOOTH_ENABLE_AUTHFILE_SET_CAPABILITY], [$(test "x$booth_enable_authfile_set" != "xyes"; echo "$?")]) AC_SUBST([BOOTH_ENABLE_AUTHFILE_UNSET_CAPABILITY], [$(test "x$booth_enable_authfile_unset" != "xyes"; echo "$?")]) +PCSD_PUBLIC_DIR="$LIB_DIR/pcsd/public" +AC_SUBST([PCSD_PUBLIC_DIR]) +PCSD_WEBUI_DIR="$PCSD_PUBLIC_DIR/ui" +AC_SUBST([PCSD_WEBUI_DIR]) OUTPUT_FORMAT_SYNTAX_DOC="\fB\-\-output\-format\fR text|cmd|json" OUTPUT_FORMAT_DESC_DOC="There are 3 formats of output available: 'cmd', 'json' and 'text', default is 'text'. Format 'text' is a human friendly output. Format 'cmd' prints pcs commands which can be used to recreate the same configuration. Format 'json' is a machine oriented output of the configuration." diff --git a/pcs/daemon/env.py b/pcs/daemon/env.py index 3d08edab5..03a288c41 100644 --- a/pcs/daemon/env.py +++ b/pcs/daemon/env.py @@ -1,13 +1,7 @@ +import os.path import ssl from collections import namedtuple from functools import lru_cache -from os.path import ( - abspath, - dirname, -) -from os.path import exists as path_exists -from os.path import join as join_path -from os.path import realpath from pcs import settings from pcs.common.validate import ( @@ -16,9 +10,11 @@ ) # Relative location instead of system location is used for development purposes. -PCSD_LOCAL_DIR = realpath(dirname(abspath(__file__)) + "/../../pcsd") - -PCSD_STATIC_FILES_DIR_NAME = "public" +LOCAL_PUBLIC_DIR = os.path.realpath( + os.path.dirname(os.path.abspath(__file__)) + "/../../pcsd/static" +) +LOCAL_WEBUI_DIR = os.path.join(LOCAL_PUBLIC_DIR, "ui") +WEBUI_FALLBACK_FILE = "ui_instructions.html" PCSD_PORT = "PCSD_PORT" PCSD_SSL_CIPHERS = "PCSD_SSL_CIPHERS" @@ -29,7 +25,8 @@ PCSD_DISABLE_GUI = "PCSD_DISABLE_GUI" PCSD_SESSION_LIFETIME = "PCSD_SESSION_LIFETIME" PCSD_DEV = "PCSD_DEV" -PCSD_STATIC_FILES_DIR = "PCSD_STATIC_FILES_DIR" +WEBUI_DIR = "WEBUI_DIR" +WEBUI_FALLBACK = "WEBUI_FALLBACK" PCSD_WORKER_COUNT = "PCSD_WORKER_COUNT" PCSD_WORKER_RESET_LIMIT = "PCSD_WORKER_RESET_LIMIT" PCSD_MAX_WORKER_COUNT = "PCSD_MAX_WORKER_COUNT" @@ -50,7 +47,8 @@ PCSD_DEBUG, PCSD_DISABLE_GUI, PCSD_SESSION_LIFETIME, - PCSD_STATIC_FILES_DIR, + WEBUI_DIR, + WEBUI_FALLBACK, PCSD_DEV, PCSD_WORKER_COUNT, PCSD_WORKER_RESET_LIMIT, @@ -67,6 +65,7 @@ def prepare_env(environ, logger=None): loader = EnvLoader(environ) + loader.check_webui() env = Env( loader.port(), loader.ssl_ciphers(), @@ -76,7 +75,8 @@ def prepare_env(environ, logger=None): loader.pcsd_debug(), loader.pcsd_disable_gui(), loader.session_lifetime(), - loader.pcsd_static_files_dir(), + loader.webui_dir(), + loader.webui_fallback(), loader.pcsd_dev(), loader.pcsd_worker_count(), loader.pcsd_worker_reset_limit(), @@ -120,6 +120,7 @@ def str_to_ssl_options(ssl_options_string, reports): class EnvLoader: + # pylint: disable=too-many-public-methods def __init__(self, environ): self.environ = environ self.errors = [] @@ -190,11 +191,25 @@ def session_lifetime(self): def pcsd_debug(self): return self.__has_true_in_environ(PCSD_DEBUG) - def pcsd_static_files_dir(self): - return self.__in_pcsd_path( - PCSD_STATIC_FILES_DIR_NAME, - "Directory with web UI assets", - existence_required=not self.pcsd_disable_gui(), + def check_webui(self): + if not self.pcsd_disable_gui() and not ( + os.path.exists(self.webui_dir()) + or os.path.exists(self.webui_fallback()) + ): + self.errors.append( + f"Webui assets directory '{self.webui_dir()}'" + + f" or falback html '{self.webui_fallback()}' does not exist" + ) + + @lru_cache(maxsize=5) + def webui_dir(self): + return LOCAL_WEBUI_DIR if self.pcsd_dev() else settings.pcsd_webui_dir + + @lru_cache(maxsize=5) + def webui_fallback(self): + return os.path.join( + LOCAL_PUBLIC_DIR if self.pcsd_dev() else settings.pcsd_public_dir, + WEBUI_FALLBACK_FILE, ) @lru_cache(maxsize=5) @@ -270,15 +285,5 @@ def pcsd_task_deletion_timeout(self) -> int: PCSD_TASK_DELETION_TIMEOUT, settings.task_deletion_timeout_seconds ) - def __in_pcsd_path(self, path, description="", existence_required=True): - pcsd_dir = ( - PCSD_LOCAL_DIR if self.pcsd_dev() else settings.pcsd_exec_location - ) - - in_pcsd_path = join_path(pcsd_dir, path) - if existence_required and not path_exists(in_pcsd_path): - self.errors.append(f"{description} '{in_pcsd_path}' does not exist") - return in_pcsd_path - def __has_true_in_environ(self, environ_key): return self.environ.get(environ_key, "").lower() == "true" diff --git a/pcs/daemon/run.py b/pcs/daemon/run.py index cecce750f..55b3cf77f 100644 --- a/pcs/daemon/run.py +++ b/pcs/daemon/run.py @@ -89,7 +89,8 @@ def configure_app( session_storage: session.Storage, ruby_pcsd_wrapper: ruby_pcsd.Wrapper, sync_config_lock: Lock, - public_dir: str, + webui_dir: str, + webui_fallback: str, pcsd_capabilities: Iterable[capabilities.Capability], disable_gui: bool = False, debug: bool = False, @@ -123,11 +124,8 @@ def make_app(https_server_manage: HttpsServerManage): [(r"/(ui)?", RedirectHandler, dict(url="/ui/"))] + ui.get_routes( url_prefix="/ui/", - app_dir=os.path.join(public_dir, "ui"), - fallback_page_path=os.path.join( - public_dir, - "ui_instructions.html", - ), + app_dir=webui_dir, + fallback_page_path=webui_fallback, session_storage=session_storage, auth_provider=auth_provider, ) @@ -220,7 +218,8 @@ def main(argv=None) -> None: session.Storage(env.PCSD_SESSION_LIFETIME), ruby_pcsd_wrapper, sync_config_lock, - env.PCSD_STATIC_FILES_DIR, + env.WEBUI_DIR, + env.WEBUI_FALLBACK, pcsd_capabilities, disable_gui=env.PCSD_DISABLE_GUI, debug=env.PCSD_DEV, diff --git a/pcs/settings.py.in b/pcs/settings.py.in index eb079a909..400c1deda 100644 --- a/pcs/settings.py.in +++ b/pcs/settings.py.in @@ -20,6 +20,8 @@ pcs_data_dir = "@LIB_DIR@/pcs/data/" # pcsd pcsd_exec_location = "@LIB_DIR@/pcsd" +pcsd_public_dir = "@PCSD_PUBLIC_DIR@" +pcsd_webui_dir = "@PCSD_WEBUI_DIR@" pcs_capabilities = os.path.join(pcsd_exec_location, "capabilities.xml") # Set pcsd_gem_path to None if there are no bundled ruby gems and the path does # not exists. diff --git a/pcs_test/tier0/daemon/test_env.py b/pcs_test/tier0/daemon/test_env.py index 3d6c62f56..40f0c402e 100644 --- a/pcs_test/tier0/daemon/test_env.py +++ b/pcs_test/tier0/daemon/test_env.py @@ -1,5 +1,4 @@ -from functools import partial -from os.path import join as join_path +import os.path from ssl import OP_NO_SSLv2 from unittest import ( TestCase, @@ -12,6 +11,10 @@ from pcs_test.tools.misc import create_setup_patch_mixin +def webui_fallback(public_dir): + return os.path.join(public_dir, env.WEBUI_FALLBACK_FILE) + + class Logger: def __init__(self): self.errors = [] @@ -27,14 +30,13 @@ def warning(self, warning): class Prepare(TestCase, create_setup_patch_mixin(env)): # pylint: disable=too-many-public-methods def setUp(self): - self.path_exists = self.setup_patch("path_exists", return_value=True) + self.path_exists = self.setup_patch("os.path.exists", return_value=True) self.logger = Logger() self.maxDiff = None def assert_environ_produces_modified_pcsd_env( self, environ=None, specific_env_values=None, errors=None, warnings=None ): - pcsd_dir = partial(join_path, settings.pcsd_exec_location) default_env_values = { env.PCSD_PORT: settings.pcsd_default_port, env.PCSD_SSL_CIPHERS: settings.default_ssl_ciphers, @@ -46,7 +48,8 @@ def assert_environ_produces_modified_pcsd_env( env.PCSD_DEBUG: False, env.PCSD_DISABLE_GUI: False, env.PCSD_SESSION_LIFETIME: settings.gui_session_lifetime_seconds, - env.PCSD_STATIC_FILES_DIR: pcsd_dir(env.PCSD_STATIC_FILES_DIR_NAME), + env.WEBUI_DIR: settings.pcsd_webui_dir, + env.WEBUI_FALLBACK: webui_fallback(settings.pcsd_public_dir), env.PCSD_DEV: False, env.PCSD_WORKER_COUNT: settings.pcsd_worker_count, env.PCSD_WORKER_RESET_LIMIT: settings.pcsd_worker_reset_limit, @@ -74,7 +77,6 @@ def test_nothing_in_environ(self): self.assert_environ_produces_modified_pcsd_env() def test_many_valid_environment_changes(self): - pcsd_dir = partial(join_path, env.PCSD_LOCAL_DIR) session_lifetime = 10 environ = { env.PCSD_PORT: "1234", @@ -106,9 +108,8 @@ def test_many_valid_environment_changes(self): env.PCSD_DEBUG: True, env.PCSD_DISABLE_GUI: True, env.PCSD_SESSION_LIFETIME: session_lifetime, - env.PCSD_STATIC_FILES_DIR: pcsd_dir( - env.PCSD_STATIC_FILES_DIR_NAME - ), + env.WEBUI_DIR: env.LOCAL_WEBUI_DIR, + env.WEBUI_FALLBACK: webui_fallback(env.LOCAL_PUBLIC_DIR), env.PCSD_DEV: True, env.PCSD_WORKER_COUNT: 1, env.PCSD_WORKER_RESET_LIMIT: 2, @@ -178,13 +179,12 @@ def test_no_debug_explicitly(self): def test_errors_on_missing_paths(self): self.path_exists.return_value = False - pcsd_dir = partial(join_path, settings.pcsd_exec_location) self.assert_environ_produces_modified_pcsd_env( specific_env_values={"has_errors": True}, errors=[ - "Directory with web UI assets" - f" '{pcsd_dir(env.PCSD_STATIC_FILES_DIR_NAME)}'" - " does not exist", + f"Webui assets directory '{settings.pcsd_webui_dir}' or" + + f" falback html '{webui_fallback(settings.pcsd_public_dir)}'" + + " does not exist", ], ) From c0ea111bdec24cb5adf5ee41aa87ff7c3798ef8f Mon Sep 17 00:00:00 2001 From: Ivan Devat Date: Fri, 31 May 2024 16:02:34 +0200 Subject: [PATCH 050/227] add pkg-config with info for webui install --- .gitignore | 1 + CHANGELOG.md | 1 + Makefile.am | 4 ++++ configure.ac | 3 +++ pcs.pc.in | 6 ++++++ pcs/settings.py.in | 2 +- rpm/pcs.spec.in | 1 + 7 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 pcs.pc.in diff --git a/.gitignore b/.gitignore index 6b39468d6..4ae9443e7 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ pcs/snmp/pcs_snmp_agent.service /pcsd/public/ui /Gemfile* /scripts/pcsd.sh +pcs.pc pcs-* .mypy_cache/ requirements.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index f97b28fea..31a2357b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Lib command `cib.remove_elements` can now remove resources - Support for exporting stonith levels in `json` and `cmd` formats in commands `pcs stonith config` and `pcs stonith level config` commands ([RHEL-16232]) +- Pkg-config with info for webui is now provided. ### Changed - Commands `pcs resource delete | remove` and `pcs stonith delete | remove` diff --git a/Makefile.am b/Makefile.am index 0cadd9312..c371e2f2e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -9,6 +9,7 @@ EXTRA_DIST = \ make/release.mk \ MANIFEST.in \ mypy.ini \ + pcs.pc.in \ pylintrc \ pyproject.toml \ rpm/pcs.spec.in \ @@ -180,6 +181,9 @@ uninstall-local: dist_doc_DATA = README.md CHANGELOG.md +pkgconfigdir = $(LIB_DIR)/pkgconfig +pkgconfig_DATA = pcs.pc + # testing if CONCISE_TESTS diff --git a/configure.ac b/configure.ac index fd8f97912..a7718d6d6 100644 --- a/configure.ac +++ b/configure.ac @@ -475,6 +475,8 @@ PCSD_PUBLIC_DIR="$LIB_DIR/pcsd/public" AC_SUBST([PCSD_PUBLIC_DIR]) PCSD_WEBUI_DIR="$PCSD_PUBLIC_DIR/ui" AC_SUBST([PCSD_WEBUI_DIR]) +PCSD_UNIX_SOCKET="$LOCALSTATEDIR/run/pcsd.socket" +AC_SUBST([PCSD_UNIX_SOCKET]) OUTPUT_FORMAT_SYNTAX_DOC="\fB\-\-output\-format\fR text|cmd|json" OUTPUT_FORMAT_DESC_DOC="There are 3 formats of output available: 'cmd', 'json' and 'text', default is 'text'. Format 'text' is a human friendly output. Format 'cmd' prints pcs commands which can be used to recreate the same configuration. Format 'json' is a machine oriented output of the configuration." @@ -603,6 +605,7 @@ UTC_DATE=$($UTC_DATE_AT$SOURCE_EPOCH +'%F') AC_SUBST([UTC_DATE]) AC_CONFIG_FILES([Makefile + pcs.pc setup.py setup.cfg data/Makefile diff --git a/pcs.pc.in b/pcs.pc.in new file mode 100644 index 000000000..1a4af31f5 --- /dev/null +++ b/pcs.pc.in @@ -0,0 +1,6 @@ +webui_dir=@PCSD_WEBUI_DIR@ +pcsd_unix_socket=@PCSD_UNIX_SOCKET@ + +Name: pcs +Description: Pacemaker/Corosync Configuration System +Version: @VERSION@ diff --git a/pcs/settings.py.in b/pcs/settings.py.in index 400c1deda..48a21fa99 100644 --- a/pcs/settings.py.in +++ b/pcs/settings.py.in @@ -26,7 +26,7 @@ pcs_capabilities = os.path.join(pcsd_exec_location, "capabilities.xml") # Set pcsd_gem_path to None if there are no bundled ruby gems and the path does # not exists. pcsd_gem_path = "@GEM_HOME@" or None -pcsd_unix_socket = "@LOCALSTATEDIR@/run/pcsd.socket" +pcsd_unix_socket = "@PCSD_UNIX_SOCKET@" pcsd_ruby_socket = "@LOCALSTATEDIR@/run/pcsd-ruby.socket" pcsd_log_location = "@LOCALSTATEDIR@/log/pcsd/pcsd.log" pcsd_default_port = 2224 diff --git a/rpm/pcs.spec.in b/rpm/pcs.spec.in index 4e748e262..35d8aa7a5 100644 --- a/rpm/pcs.spec.in +++ b/rpm/pcs.spec.in @@ -315,6 +315,7 @@ sed -i -e 's#^@#%#g' dynamic_files/pcs-snmp %{_sbindir}/pcs %{python3_sitelib}/* %{_libdir}/pcs/* +%{_libdir}/pkgconfig/pcs.pc %exclude %{_libdir}/pcs/pcs_snmp_agent %exclude %{_libdir}/pcs/%{pcs_bundled_dir}/packages/pyagentx* From 0d90d77e88a032d63071da5a5d46303899f1e8cb Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Tue, 2 Jul 2024 13:32:45 +0200 Subject: [PATCH 051/227] Specify the meaning of zero timeout --- CHANGELOG.md | 4 +++- pcs/pcs.8.in | 2 +- pcs/usage.py | 3 ++- pcs_test/tier0/cli/test_status.py | 19 +++++++++++++++++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31a2357b3..cb92a36f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ resources` unless `--all` is specified ([RHEL-46293]) - Displaying status of local and remote cluster sites in `pcs dr status` command. ([RHEL-61738]) +- Specify the meaning of zero value timeout in `pcs status wait` ([RHEL-46303]) ### Deprecated - Using `pcs resource delete | remove` to delete resources representing remote @@ -39,14 +40,15 @@ rules in a location constraint is among the [features planned to be removed in pacemaker 3] +[RHEL-16232]: https://issues.redhat.com/browse/RHEL-16232 [RHEL-46284]: https://issues.redhat.com/browse/RHEL-46284 [RHEL-46286]: https://issues.redhat.com/browse/RHEL-46286 [RHEL-46293]: https://issues.redhat.com/browse/RHEL-46293 +[RHEL-46303]: https://issues.redhat.com/browse/RHEL-46303 [RHEL-55441]: https://issues.redhat.com/browse/RHEL-55441 [RHEL-61738]: https://issues.redhat.com/browse/RHEL-61738 [RHEL-61901]: https://issues.redhat.com/browse/RHEL-61901 [features planned to be removed in pacemaker 3]: https://projects.clusterlabs.org/w/projects/pacemaker/pacemaker_3.0_changes/pacemaker_3.0_configuration_changes/ -[RHEL-16232]: https://issues.redhat.com/browse/RHEL-16232 ## [0.11.8] - 2024-07-09 diff --git a/pcs/pcs.8.in b/pcs/pcs.8.in index 924461ed7..0ef09ef44 100644 --- a/pcs/pcs.8.in +++ b/pcs/pcs.8.in @@ -1384,7 +1384,7 @@ xml View xml version of status (output from crm_mon \fB\-r\fR \fB\-1\fR \fB\-X\fR). .TP wait [] -Wait for the cluster to settle into stable state. Timeout can be specified as bare number which describes number of seconds or number with unit (s or sec for seconds, m or min for minutes, h or hr for hours). If 'timeout' is not specified it defaults to 60 minutes. +Wait for the cluster to settle into stable state. Timeout can be specified as bare number which describes number of seconds or number with unit (s or sec for seconds, m or min for minutes, h or hr for hours). If 'timeout' is not specified or set to zero, it defaults to 60 minutes. .br Example: pcs status wait 30min .TP diff --git a/pcs/usage.py b/pcs/usage.py index 55ff2dddc..f47e559c3 100644 --- a/pcs/usage.py +++ b/pcs/usage.py @@ -2774,7 +2774,8 @@ def status(args=Argv) -> str: Wait for the cluster to settle into stable state. Timeout can be specified as bare number which describes number of seconds or number with unit (s or sec for seconds, m or min for minutes, h or hr for - hours). If 'timeout' is not specified it defaults to 60 minutes. + hours). If 'timeout' is not specified or set to zero, it defaults to + 60 minutes. Example: pcs status wait 30min query resource exists [--quiet] diff --git a/pcs_test/tier0/cli/test_status.py b/pcs_test/tier0/cli/test_status.py index c5b7b80b7..42cbb4bf7 100644 --- a/pcs_test/tier0/cli/test_status.py +++ b/pcs_test/tier0/cli/test_status.py @@ -5,6 +5,7 @@ from pcs import status from pcs.cli.common.errors import CmdLineInputError +from pcs.cli.status import command from pcs_test.tools.misc import dict_to_modifiers @@ -34,3 +35,21 @@ def test_argv(self, mock_print): self._call_cmd(["x"]) self.assertIsNone(cm.exception.message) mock_print.assert_not_called() + + +class WaitForPcmkIdle(TestCase): + def setUp(self): + self.lib = mock.Mock(spec_set=["cluster"]) + self.lib.cluster = mock.Mock(spec_set=["wait_for_pcmk_idle"]) + self.lib_command = self.lib.cluster.wait_for_pcmk_idle + + def _call_cmd(self, argv) -> None: + command.wait_for_pcmk_idle(self.lib, argv, dict_to_modifiers({})) + + def test_no_timeout(self): + self._call_cmd([]) + self.lib_command.assert_called_once_with(None) + + def test_timeout(self): + self._call_cmd(["30min"]) + self.lib_command.assert_called_once_with("30min") From a75107527bf1d837c9732c7b3ee5438fcb1e5b29 Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Tue, 19 Nov 2024 09:47:56 +0100 Subject: [PATCH 052/227] add booth ticket commands --- CHANGELOG.md | 4 + mypy.ini | 7 + pcs/Makefile.am | 1 + pcs/cli/booth/command.py | 32 ++ pcs/cli/common/lib_wrapper.py | 15 +- pcs/cli/routing/booth.py | 5 +- pcs/common/reports/codes.py | 3 + pcs/common/reports/messages.py | 67 +++- .../async_tasks/worker/command_mapping.py | 13 + pcs/lib/booth/cib.py | 12 + pcs/lib/booth/resource.py | 4 +- pcs/lib/commands/booth.py | 309 ++++++++++++++---- pcs/lib/pacemaker/live.py | 49 +++ pcs/pcs.8.in | 16 +- pcs/usage.py | 27 +- pcs_test/Makefile.am | 1 + pcs_test/tier0/cli/test_booth.py | 78 +++++ .../tier0/common/reports/test_messages.py | 32 ++ pcs_test/tier0/lib/booth/test_cib.py | 30 ++ pcs_test/tier0/lib/commands/test_booth.py | 225 +++++++++++++ pcs_test/tier1/test_booth.py | 34 ++ .../tools/command_env/config_runner_pcmk.py | 64 ++++ pcsd/capabilities.xml.in | 18 + 23 files changed, 957 insertions(+), 89 deletions(-) create mode 100644 pcs/lib/booth/cib.py create mode 100644 pcs_test/tier0/lib/booth/test_cib.py diff --git a/CHANGELOG.md b/CHANGELOG.md index cb92a36f8..fb17f1e81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ - Support for exporting stonith levels in `json` and `cmd` formats in commands `pcs stonith config` and `pcs stonith level config` commands ([RHEL-16232]) - Pkg-config with info for webui is now provided. +- Commands `pcs booth ticket standby`, `pcs booth ticket unstandby`, and + `pcs booth ticket cleanup` which allow for managing the state of the ticket + ([RHEL-69040]) ### Changed - Commands `pcs resource delete | remove` and `pcs stonith delete | remove` @@ -48,6 +51,7 @@ [RHEL-55441]: https://issues.redhat.com/browse/RHEL-55441 [RHEL-61738]: https://issues.redhat.com/browse/RHEL-61738 [RHEL-61901]: https://issues.redhat.com/browse/RHEL-61901 +[RHEL-69040]: https://issues.redhat.com/browse/RHEL-69040 [features planned to be removed in pacemaker 3]: https://projects.clusterlabs.org/w/projects/pacemaker/pacemaker_3.0_changes/pacemaker_3.0_configuration_changes/ diff --git a/mypy.ini b/mypy.ini index 403323571..e20e204b9 100644 --- a/mypy.ini +++ b/mypy.ini @@ -80,6 +80,10 @@ disallow_untyped_calls = True disallow_untyped_defs = False disallow_untyped_calls = False +[mypy-pcs.lib.booth.cib] +disallow_untyped_defs = True +disallow_untyped_calls = True + [mypy-pcs.lib.cluster_property] disallow_untyped_defs = True disallow_untyped_calls = True @@ -128,6 +132,9 @@ disallow_untyped_calls = True [mypy-pcs.lib.cib.tag] disallow_untyped_defs = True +[mypy-pcs.lib.commands.booth] +disallow_untyped_defs = True + [mypy-pcs.lib.commands.cib] disallow_untyped_defs = True diff --git a/pcs/Makefile.am b/pcs/Makefile.am index 1a5a27d6d..861624a49 100644 --- a/pcs/Makefile.am +++ b/pcs/Makefile.am @@ -243,6 +243,7 @@ EXTRA_DIST = \ lib/auth/provider.py \ lib/auth/tools.py \ lib/auth/types.py \ + lib/booth/cib.py \ lib/booth/config_facade.py \ lib/booth/config_files.py \ lib/booth/config_parser.py \ diff --git a/pcs/cli/booth/command.py b/pcs/cli/booth/command.py index db2db5803..3acb3fe6e 100644 --- a/pcs/cli/booth/command.py +++ b/pcs/cli/booth/command.py @@ -192,6 +192,38 @@ def ticket_grant(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: ) +def ticket_cleanup(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: + """ + Options: None + """ + modifiers.ensure_only_supported() + if len(arg_list) != 1: + raise CmdLineInputError() + lib.booth.ticket_cleanup(arg_list[0]) + + +def ticket_unstandby( + lib: Any, arg_list: Argv, modifiers: InputModifiers +) -> None: + """ + Options: None + """ + modifiers.ensure_only_supported() + if len(arg_list) != 1: + raise CmdLineInputError() + lib.booth.ticket_unstandby(arg_list[0]) + + +def ticket_standby(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: + """ + Options: None + """ + modifiers.ensure_only_supported() + if len(arg_list) != 1: + raise CmdLineInputError() + lib.booth.ticket_standby(arg_list[0]) + + def create_in_cluster( lib: Any, arg_list: Argv, modifiers: InputModifiers ) -> None: diff --git a/pcs/cli/common/lib_wrapper.py b/pcs/cli/common/lib_wrapper.py index dc590562a..9a6622e79 100644 --- a/pcs/cli/common/lib_wrapper.py +++ b/pcs/cli/common/lib_wrapper.py @@ -144,23 +144,26 @@ def load_module(env, middleware_factory, name): if name == "booth": bindings = { - "config_setup": booth.config_setup, "config_destroy": booth.config_destroy, + "config_setup": booth.config_setup, + "config_sync": booth.config_sync, "config_text": booth.config_text, "config_ticket_add": booth.config_ticket_add, "config_ticket_remove": booth.config_ticket_remove, "create_in_cluster": booth.create_in_cluster, + "disable_booth": booth.disable_booth, + "enable_booth": booth.enable_booth, + "get_status": booth.get_status, + "pull_config": booth.pull_config, "remove_from_cluster": booth.remove_from_cluster, "restart": booth.restart, - "config_sync": booth.config_sync, - "enable_booth": booth.enable_booth, - "disable_booth": booth.disable_booth, "start_booth": booth.start_booth, "stop_booth": booth.stop_booth, - "pull_config": booth.pull_config, - "get_status": booth.get_status, + "ticket_cleanup": booth.ticket_cleanup, "ticket_grant": booth.ticket_grant, "ticket_revoke": booth.ticket_revoke, + "ticket_standby": booth.ticket_standby, + "ticket_unstandby": booth.ticket_unstandby, } if settings.booth_enable_authfile_set_enabled: bindings["config_set_enable_authfile"] = ( diff --git a/pcs/cli/routing/booth.py b/pcs/cli/routing/booth.py index ec1dd9d9b..c2aff5231 100644 --- a/pcs/cli/routing/booth.py +++ b/pcs/cli/routing/booth.py @@ -14,10 +14,13 @@ { "help": lambda lib, argv, modifiers: print(usage.booth(["ticket"])), "add": command.config_ticket_add, + "cleanup": command.ticket_cleanup, "delete": command.config_ticket_remove, - "remove": command.config_ticket_remove, "grant": command.ticket_grant, + "remove": command.config_ticket_remove, "revoke": command.ticket_revoke, + "standby": command.ticket_standby, + "unstandby": command.ticket_unstandby, }, ["booth", "ticket"], ), diff --git a/pcs/common/reports/codes.py b/pcs/common/reports/codes.py index 0d448f0ab..3f0e669b5 100644 --- a/pcs/common/reports/codes.py +++ b/pcs/common/reports/codes.py @@ -72,9 +72,12 @@ BOOTH_NOT_EXISTS_IN_CIB = M("BOOTH_NOT_EXISTS_IN_CIB") BOOTH_PATH_NOT_EXISTS = M("BOOTH_PATH_NOT_EXISTS") BOOTH_PEERS_STATUS_ERROR = M("BOOTH_PEERS_STATUS_ERROR") +BOOTH_TICKET_CHANGING_STATE = M("BOOTH_TICKET_CHANGING_STATE") +BOOTH_TICKET_CLEANUP = M("BOOTH_TICKET_CLEANUP") BOOTH_TICKET_DOES_NOT_EXIST = M("BOOTH_TICKET_DOES_NOT_EXIST") BOOTH_TICKET_DUPLICATE = M("BOOTH_TICKET_DUPLICATE") BOOTH_TICKET_NAME_INVALID = M("BOOTH_TICKET_NAME_INVALID") +BOOTH_TICKET_NOT_IN_CIB = M("BOOTH_TICKET_NOT_IN_CIB") BOOTH_TICKET_OPERATION_FAILED = M("BOOTH_TICKET_OPERATION_FAILED") BOOTH_TICKET_STATUS_ERROR = M("BOOTH_TICKET_STATUS_ERROR") BOOTH_UNSUPPORTED_FILE_LOCATION = M("BOOTH_UNSUPPORTED_FILE_LOCATION") diff --git a/pcs/common/reports/messages.py b/pcs/common/reports/messages.py index 48971a979..0809c91d1 100644 --- a/pcs/common/reports/messages.py +++ b/pcs/common/reports/messages.py @@ -9,6 +9,7 @@ Any, Dict, List, + Literal, Mapping, Optional, Tuple, @@ -6994,6 +6995,22 @@ def message(self) -> str: return f"booth ticket name '{self.ticket_name}' does not exist" +@dataclass(frozen=True) +class BoothTicketNotInCib(ReportItemMessage): + """ + Expected ticket is not in CIB + + ticket_name -- name of the ticket + """ + + ticket_name: str + _code = codes.BOOTH_TICKET_NOT_IN_CIB + + @property + def message(self) -> str: + return f"Unable to find ticket '{self.ticket_name}' in CIB" + + @dataclass(frozen=True) class BoothAlreadyInCib(ReportItemMessage): """ @@ -7257,8 +7274,9 @@ def message(self) -> str: @dataclass(frozen=True) class BoothTicketOperationFailed(ReportItemMessage): """ - Pcs uses external booth tools for some ticket_name operations. For example - grand and revoke. But the external command failed. + Pcs uses external tools for some ticket_name operations. For example + booth tools are used for grant and revoke, or pacemaker tools are used for + standby and cleanup. But the external command failed. operation -- determine what was intended perform with ticket_name reason -- error description from external booth command @@ -7268,18 +7286,57 @@ class BoothTicketOperationFailed(ReportItemMessage): operation: str reason: str - site_ip: str + site_ip: Optional[str] ticket_name: str _code = codes.BOOTH_TICKET_OPERATION_FAILED @property def message(self) -> str: return ( - f"unable to {self.operation} booth ticket '{self.ticket_name}'" - f" for site '{self.site_ip}', reason: {self.reason}" + "unable to {operation} booth ticket '{ticket_name}'{site}, " + "reason: {reason}" + ).format( + operation=self.operation, + ticket_name=self.ticket_name, + reason=self.reason, + site=format_optional(self.site_ip, template=" for site '{}'"), ) +@dataclass(frozen=True) +class BoothTicketChangingState(ReportItemMessage): + """ + The state of the ticket is changing + + ticket_name -- name of the ticket + state -- new state of the ticket + """ + + ticket_name: str + state: Literal["active", "standby"] + _code = codes.BOOTH_TICKET_CHANGING_STATE + + @property + def message(self) -> str: + return f"Changing state of ticket '{self.ticket_name}' to {self.state}" + + +@dataclass(frozen=True) +class BoothTicketCleanup(ReportItemMessage): + """ + The booth ticket is going to be removed from CIB + + ticket_name -- name of the ticket + """ + + ticket_name: str + _code = codes.BOOTH_TICKET_CLEANUP + + @property + def message(self) -> str: + return f"Cleaning up ticket '{self.ticket_name}' from CIB" + + # TODO: remove, use ADD_REMOVE reports @dataclass(frozen=True) class TagAddRemoveIdsDuplication(ReportItemMessage): diff --git a/pcs/daemon/async_tasks/worker/command_mapping.py b/pcs/daemon/async_tasks/worker/command_mapping.py index 31a78f831..4dcbbd347 100644 --- a/pcs/daemon/async_tasks/worker/command_mapping.py +++ b/pcs/daemon/async_tasks/worker/command_mapping.py @@ -8,6 +8,7 @@ from pcs.lib.commands import ( # services, acl, alert, + booth, cib, cib_options, cluster, @@ -108,6 +109,18 @@ class _Cmd: cmd=alert.update_recipient, required_permission=p.WRITE, ), + "booth.ticket_cleanup": _Cmd( + cmd=booth.ticket_cleanup, + required_permission=p.WRITE, + ), + "booth.ticket_standby": _Cmd( + cmd=booth.ticket_standby, + required_permission=p.WRITE, + ), + "booth.ticket_unstandby": _Cmd( + cmd=booth.ticket_unstandby, + required_permission=p.WRITE, + ), "cluster.add_nodes": _Cmd( cmd=cluster.add_nodes, required_permission=p.FULL, diff --git a/pcs/lib/booth/cib.py b/pcs/lib/booth/cib.py new file mode 100644 index 000000000..3e3903bf9 --- /dev/null +++ b/pcs/lib/booth/cib.py @@ -0,0 +1,12 @@ +from typing import cast + +from lxml.etree import _Element + + +def get_ticket_names(cib: _Element) -> list[str]: + """ + Return names of all tickets present in CIB + + cib -- element representing the CIB + """ + return cast(list[str], cib.xpath("status/tickets/ticket_state/@id")) diff --git a/pcs/lib/booth/resource.py b/pcs/lib/booth/resource.py index e5609c335..6daff50d9 100644 --- a/pcs/lib/booth/resource.py +++ b/pcs/lib/booth/resource.py @@ -71,9 +71,9 @@ def find_for_config( def find_bound_ip( resources_section: _Element, booth_config_file_path: str -) -> list[_Element]: +) -> list[str]: return cast( - list[_Element], + list[str], resources_section.xpath( """ .//group[ diff --git a/pcs/lib/commands/booth.py b/pcs/lib/commands/booth.py index 54f3f3bdb..2ca90c289 100644 --- a/pcs/lib/commands/booth.py +++ b/pcs/lib/commands/booth.py @@ -3,6 +3,7 @@ from functools import partial from typing import ( Collection, + Mapping, Optional, cast, ) @@ -26,6 +27,7 @@ ) from pcs.common.services.errors import ManageServiceError from pcs.common.str_tools import join_multilines +from pcs.common.types import StringSequence from pcs.lib import ( tools, validate, @@ -37,6 +39,7 @@ resource, status, ) +from pcs.lib.booth.cib import get_ticket_names as get_cib_ticket_names from pcs.lib.booth.env import BoothEnv from pcs.lib.cib.remove_elements import ( ElementsToRemove, @@ -59,6 +62,7 @@ from pcs.lib.communication.tools import run_and_raise from pcs.lib.env import LibraryEnvironment from pcs.lib.errors import LibraryError +from pcs.lib.external import CommandRunner from pcs.lib.file.instance import FileInstance from pcs.lib.file.raw_file import ( GhostFile, @@ -71,6 +75,9 @@ has_cib_xml, resource_restart, ) +from pcs.lib.pacemaker.live import ticket_cleanup as live_ticket_cleanup +from pcs.lib.pacemaker.live import ticket_standby as live_ticket_standby +from pcs.lib.pacemaker.live import ticket_unstandby as live_ticket_unstandby from pcs.lib.resource_agent import ( ResourceAgentError, ResourceAgentFacade, @@ -88,19 +95,19 @@ def config_setup( env: LibraryEnvironment, - site_list, - arbitrator_list, - instance_name=None, - overwrite_existing=False, -): + site_list: StringSequence, + arbitrator_list: StringSequence, + instance_name: Optional[str] = None, + overwrite_existing: bool = False, +) -> None: """ create booth configuration env - list site_list -- site addresses of multisite - list arbitrator_list -- arbitrator addresses of multisite - string instance_name -- booth instance name - bool overwrite_existing -- allow overwriting existing files + site_list -- site addresses of multisite + arbitrator_list -- arbitrator addresses of multisite + instance_name -- booth instance name + overwrite_existing -- allow overwriting existing files """ instance_name = instance_name or constants.DEFAULT_INSTANCE_NAME report_processor = env.report_processor @@ -294,7 +301,7 @@ def config_destroy( # TODO: remove once settings booth_enable_autfile_(set|unset)_enabled are removed def _config_set_enable_authfile( - env: LibraryEnvironment, value: bool, instance_name=None + env: LibraryEnvironment, value: bool, instance_name: Optional[str] = None ) -> None: report_processor = env.report_processor booth_env = env.get_booth_env(instance_name) @@ -319,24 +326,28 @@ def _config_set_enable_authfile( def config_set_enable_authfile( - env: LibraryEnvironment, instance_name=None + env: LibraryEnvironment, instance_name: Optional[str] = None ) -> None: _config_set_enable_authfile(env, True, instance_name=instance_name) def config_unset_enable_authfile( - env: LibraryEnvironment, instance_name=None + env: LibraryEnvironment, instance_name: Optional[str] = None ) -> None: _config_set_enable_authfile(env, False, instance_name=instance_name) -def config_text(env: LibraryEnvironment, instance_name=None, node_name=None): +def config_text( + env: LibraryEnvironment, + instance_name: Optional[str] = None, + node_name: Optional[str] = None, +) -> str: """ get configuration in raw format env - string instance_name -- booth instance name - string node_name -- get the config from specified node or local host if None + instance_name -- booth instance name + node_name -- get the config from specified node or local host if None """ report_processor = env.report_processor booth_env = env.get_booth_env(instance_name) @@ -357,7 +368,7 @@ def config_text(env: LibraryEnvironment, instance_name=None, node_name=None): com_cmd = BoothGetConfig(env.report_processor, instance_name) com_cmd.set_targets( - [env.get_node_target_factory().get_target_from_hostname(node_name)] + [env.get_node_target_factory().get_target_from_hostname(str(node_name))] ) remote_data = run_and_raise(env.get_node_communicator(), com_cmd)[0][1] try: @@ -366,25 +377,27 @@ def config_text(env: LibraryEnvironment, instance_name=None, node_name=None): return remote_data["config"]["data"].encode("utf-8") except KeyError as e: raise LibraryError( - ReportItem.error(reports.messages.InvalidResponseFormat(node_name)) + ReportItem.error( + reports.messages.InvalidResponseFormat(str(node_name)) + ) ) from e def config_ticket_add( env: LibraryEnvironment, - ticket_name, - options, - instance_name=None, - allow_unknown_options=False, -): + ticket_name: str, + options: validate.TypeOptionMap, + instance_name: Optional[str] = None, + allow_unknown_options: bool = False, +) -> None: """ add a ticket to booth configuration env - string ticket_name -- the name of the ticket to be created - dict options -- options for the ticket - string instance_name -- booth instance name - bool allow_unknown_options -- allow using options unknown to pcs + ticket_name -- the name of the ticket to be created + options -- options for the ticket + instance_name -- booth instance name + allow_unknown_options -- allow using options unknown to pcs """ report_processor = env.report_processor booth_env = env.get_booth_env(instance_name) @@ -419,15 +432,15 @@ def config_ticket_add( def config_ticket_remove( env: LibraryEnvironment, - ticket_name, - instance_name=None, -): + ticket_name: str, + instance_name: Optional[str] = None, +) -> None: """ remove a ticket from booth configuration env - string ticket_name -- the name of the ticket to be removed - string instance_name -- booth instance name + ticket_name -- the name of the ticket to be removed + instance_name -- booth instance name """ report_processor = env.report_processor booth_env = env.get_booth_env(instance_name) @@ -455,7 +468,7 @@ def create_in_cluster( ip: str, instance_name: Optional[str] = None, allow_absent_resource_agent: bool = False, -): +) -> None: """ Create group with ip resource and booth resource @@ -541,7 +554,7 @@ def remove_from_cluster( env: LibraryEnvironment, instance_name: Optional[str] = None, force_flags: Collection[reports.types.ForceCode] = (), -): +) -> None: """ Remove group with ip resource and booth resource @@ -614,17 +627,17 @@ def restart( def ticket_grant( env: LibraryEnvironment, - ticket_name, - site_ip=None, - instance_name=None, -): + ticket_name: str, + site_ip: Optional[str] = None, + instance_name: Optional[str] = None, +) -> None: """ Grant a ticket to the site specified by site_ip env - string ticket_name -- the name of the ticket to be granted - string site_ip -- IP of the site to grant the ticket to, None for local - string instance_name -- booth instance name + ticket_name -- the name of the ticket to be granted + site_ip -- IP of the site to grant the ticket to, None for local + instance_name -- booth instance name """ return _ticket_operation( "grant", @@ -637,17 +650,17 @@ def ticket_grant( def ticket_revoke( env: LibraryEnvironment, - ticket_name, - site_ip=None, - instance_name=None, -): + ticket_name: str, + site_ip: Optional[str] = None, + instance_name: Optional[str] = None, +) -> None: """ Revoke a ticket from the site specified by site_ip env - string ticket_name -- the name of the ticket to be revoked - string site_ip -- IP of the site to revoke the ticket from, None for local - string instance_name -- booth instance name + ticket_name -- the name of the ticket to be revoked + site_ip -- IP of the site to revoke the ticket from, None for local + instance_name -- booth instance name """ return _ticket_operation( "revoke", @@ -659,8 +672,12 @@ def ticket_revoke( def _ticket_operation( - operation, env: LibraryEnvironment, ticket_name, site_ip, instance_name -): + operation: str, + env: LibraryEnvironment, + ticket_name: str, + site_ip: Optional[str], + instance_name: Optional[str], +) -> None: booth_env = env.get_booth_env(instance_name) _ensure_live_env(env, booth_env) @@ -699,16 +716,149 @@ def _ticket_operation( ) +def ticket_cleanup(env: LibraryEnvironment, ticket_name: str) -> None: + """ + Remove specified booth ticket from CIB on local site + + ticket_name -- name of the ticket to remove + """ + _ensure_live_cib(env) + + report_processor = env.report_processor + + if report_processor.report_list( + _validate_ticket_in_cib(env.get_cib(), ticket_name) + ).has_errors: + raise LibraryError() + + cmd_runner = env.cmd_runner() + + # standby the ticket first, so the node is not fenced if ticket-loss + # policy is set to 'fence' in the ticket constraint + report_processor.report_list(_ticket_standby(cmd_runner, ticket_name)) + if report_processor.has_errors: + raise LibraryError() + + report_processor.report( + reports.ReportItem.info( + reports.messages.BoothTicketCleanup(ticket_name) + ) + ) + stdout, stderr, retval = live_ticket_cleanup(cmd_runner, ticket_name) + if retval != 0: + report_processor.report( + reports.ReportItem.error( + reports.messages.BoothTicketOperationFailed( + "cleanup", + join_multilines([stderr, stdout]), + None, + ticket_name, + ) + ) + ) + + if report_processor.has_errors: + raise LibraryError() + + +def ticket_standby(env: LibraryEnvironment, ticket_name: str) -> None: + """ + Change state of the ticket to standby + + ticket_name -- name of the ticket + """ + _ensure_live_cib(env) + if env.report_processor.report_list( + _validate_ticket_in_cib(env.get_cib(), ticket_name) + ).has_errors: + raise LibraryError() + + if env.report_processor.report_list( + _ticket_standby(env.cmd_runner(), ticket_name) + ).has_errors: + raise LibraryError() + + +def _ticket_standby( + cmd_runner: CommandRunner, ticket: str +) -> reports.ReportItemList: + report_list = [ + reports.ReportItem.info( + reports.messages.BoothTicketChangingState(ticket, "standby") + ) + ] + + stdout, stderr, retval = live_ticket_standby(cmd_runner, ticket) + if retval != 0: + report_list.append( + reports.ReportItem.error( + reports.messages.BoothTicketOperationFailed( + "standby", join_multilines([stderr, stdout]), None, ticket + ) + ) + ) + + return report_list + + +def ticket_unstandby(env: LibraryEnvironment, ticket_name: str) -> None: + """ + Change state of the ticket to active + + ticket_name -- name of the ticket + """ + _ensure_live_cib(env) + if env.report_processor.report_list( + _validate_ticket_in_cib(env.get_cib(), ticket_name) + ).has_errors: + raise LibraryError() + + env.report_processor.report( + reports.ReportItem.info( + reports.messages.BoothTicketChangingState(ticket_name, "active") + ) + ) + stdout, stderr, retval = live_ticket_unstandby( + env.cmd_runner(), ticket_name + ) + if retval != 0: + env.report_processor.report( + reports.ReportItem.error( + reports.messages.BoothTicketOperationFailed( + "unstandby", + join_multilines([stderr, stdout]), + None, + ticket_name, + ) + ) + ) + if env.report_processor.has_errors: + raise LibraryError() + + +def _validate_ticket_in_cib( + cib: _Element, ticket_name: str +) -> reports.ReportItemList: + if ticket_name not in set(get_cib_ticket_names(cib)): + return [ + reports.ReportItem.error( + reports.messages.BoothTicketNotInCib(ticket_name) + ) + ] + + return [] + + def config_sync( env: LibraryEnvironment, - instance_name=None, - skip_offline_nodes=False, -): + instance_name: Optional[str] = None, + skip_offline_nodes: bool = False, +) -> None: """ Send specified local booth configuration to all nodes in the local cluster. env - string instance_name -- booth instance name + instance_name -- booth instance name skip_offline_nodes -- if True offline nodes will be skipped """ report_processor = env.report_processor @@ -771,12 +921,14 @@ def config_sync( run_and_raise(env.get_node_communicator(), com_cmd) -def enable_booth(env: LibraryEnvironment, instance_name=None): +def enable_booth( + env: LibraryEnvironment, instance_name: Optional[str] = None +) -> None: """ Enable specified instance of booth service, systemd systems supported only. env - string instance_name -- booth instance name + instance_name -- booth instance name """ ensure_is_systemd(env.service_manager) booth_env = env.get_booth_env(instance_name) @@ -798,12 +950,14 @@ def enable_booth(env: LibraryEnvironment, instance_name=None): ) -def disable_booth(env: LibraryEnvironment, instance_name=None): +def disable_booth( + env: LibraryEnvironment, instance_name: Optional[str] = None +) -> None: """ Disable specified instance of booth service, systemd systems supported only. env - string instance_name -- booth instance name + instance_name -- booth instance name """ ensure_is_systemd(env.service_manager) booth_env = env.get_booth_env(instance_name) @@ -825,14 +979,16 @@ def disable_booth(env: LibraryEnvironment, instance_name=None): ) -def start_booth(env: LibraryEnvironment, instance_name=None): +def start_booth( + env: LibraryEnvironment, instance_name: Optional[str] = None +) -> None: """ Start specified instance of booth service, systemd systems supported only. On non-systemd systems it can be run like this: BOOTH_CONF_FILE= /etc/initd/booth-arbitrator env - string instance_name -- booth instance name + instance_name -- booth instance name """ ensure_is_systemd(env.service_manager) booth_env = env.get_booth_env(instance_name) @@ -854,12 +1010,14 @@ def start_booth(env: LibraryEnvironment, instance_name=None): ) -def stop_booth(env: LibraryEnvironment, instance_name=None): +def stop_booth( + env: LibraryEnvironment, instance_name: Optional[str] = None +) -> None: """ Stop specified instance of booth service, systemd systems supported only. env - string instance_name -- booth instance name + instance_name -- booth instance name """ ensure_is_systemd(env.service_manager) booth_env = env.get_booth_env(instance_name) @@ -881,14 +1039,16 @@ def stop_booth(env: LibraryEnvironment, instance_name=None): ) -def pull_config(env: LibraryEnvironment, node_name, instance_name=None): +def pull_config( + env: LibraryEnvironment, node_name: str, instance_name: Optional[str] = None +) -> None: """ Get config from specified node and save it on local system. It will rewrite existing files. env - string node_name -- name of the node from which the config should be fetched - string instance_name -- booth instance name + node_name -- name of the node from which the config should be fetched + instance_name -- booth instance name """ report_processor = env.report_processor booth_env = env.get_booth_env(instance_name) @@ -949,12 +1109,14 @@ def pull_config(env: LibraryEnvironment, node_name, instance_name=None): raise LibraryError() -def get_status(env: LibraryEnvironment, instance_name=None): +def get_status( + env: LibraryEnvironment, instance_name: Optional[str] = None +) -> Mapping[str, str]: """ get booth status info env - string instance_name -- booth instance name + instance_name -- booth instance name """ booth_env = env.get_booth_env(instance_name) _ensure_live_env(env, booth_env) @@ -1006,7 +1168,7 @@ def _find_resource_elements_for_operation( return booth_element_list -def _ensure_live_booth_env(booth_env): +def _ensure_live_booth_env(booth_env: BoothEnv) -> None: if booth_env.ghost_file_codes: raise LibraryError( ReportItem.error( @@ -1017,7 +1179,18 @@ def _ensure_live_booth_env(booth_env): ) -def _ensure_live_env(env: LibraryEnvironment, booth_env: BoothEnv): +def _ensure_live_cib(env: LibraryEnvironment) -> None: + if not env.is_cib_live: + env.report_processor.report( + ReportItem.error( + reports.messages.LiveEnvironmentRequired([file_type_codes.CIB]) + ) + ) + if env.report_processor.has_errors: + raise LibraryError() + + +def _ensure_live_env(env: LibraryEnvironment, booth_env: BoothEnv) -> None: not_live = ( booth_env.ghost_file_codes + diff --git a/pcs/lib/pacemaker/live.py b/pcs/lib/pacemaker/live.py index 707970d8f..bd271eb5a 100644 --- a/pcs/lib/pacemaker/live.py +++ b/pcs/lib/pacemaker/live.py @@ -792,6 +792,55 @@ def _run_fence_history_command( return stdout.strip() +### tickets + + +def ticket_standby( + cmd_runner: CommandRunner, ticket_name: str +) -> tuple[str, str, int]: + """ + Change state of the ticket to standby + + ticket_name -- name of the ticket + """ + return cmd_runner.run( + [settings.crm_ticket_exec, "--standby", "--ticket", ticket_name] + ) + + +def ticket_cleanup( + cmd_runner: CommandRunner, ticket_name: str +) -> tuple[str, str, int]: + """ + Delete all state of the ticket from the CIB. + + ticket_name -- name of the ticket + """ + return cmd_runner.run( + [ + settings.crm_ticket_exec, + "--cleanup", + "--force", + "--ticket", + ticket_name, + ] + ) + + +def ticket_unstandby( + cmd_runner: CommandRunner, ticket_name: str +) -> tuple[str, str, int]: + """ + Change state of the ticket to active + + ticket_name -- name of the ticket + """ + + return cmd_runner.run( + [settings.crm_ticket_exec, "--activate", "--ticket", ticket_name] + ) + + ### tools diff --git a/pcs/pcs.8.in b/pcs/pcs.8.in index 0ef09ef44..fb9e1ef85 100644 --- a/pcs/pcs.8.in +++ b/pcs/pcs.8.in @@ -1300,13 +1300,22 @@ destroy Remove booth configuration files. .TP ticket add [= ...] -Add new ticket to the current configuration. Ticket options are specified in booth manpage. +Add new ticket to the local site configuration. Ticket options are specified in booth manpage. .TP ticket delete -Remove the specified ticket from the current configuration. +Remove the specified ticket from the local site configuration. The ticket remains loaded in the current CIB and should be cleaned up using the "pcs ticket cleanup" command. .TP ticket remove -Remove the specified ticket from the current configuration. +Remove the specified ticket from the local site configuration. The ticket remains loaded in the current CIB and should be cleaned up using the "pcs ticket cleanup" command. +.TP +ticket cleanup +Remove specified ticket from CIB at the local site. +.TP +ticket standby +Tell the cluster on the local site that this ticket is standby. The dependent resources will be stopped or demoted gracefully without triggering loss\-policies. +.TP +ticket unstandby +Tell the cluster on the local site that this ticket is no longer standby. .TP config [] Show booth configuration from the specified node or from the current node if node not specified. @@ -1336,6 +1345,7 @@ Print current status of booth on the local node. .TP pull Pull booth configuration from the specified node. +Pull booth configuration from the specified node. After pulling the configuration, the booth should be restarted. In case any tickets were removed, the "pcs ticket cleanup" command should be used to remove any leftover tickets still loaded in the CIB at the current site. .TP sync [\fB\-\-skip\-offline\fR] Send booth configuration from the local node to all nodes in the cluster. diff --git a/pcs/usage.py b/pcs/usage.py index f47e559c3..26453039e 100644 --- a/pcs/usage.py +++ b/pcs/usage.py @@ -3223,14 +3223,30 @@ def booth(args: Argv) -> str: Remove booth configuration files. ticket add [= ...] - Add new ticket to the current configuration. Ticket options are + Add new ticket to the local site configuration. Ticket options are specified in booth manpage. ticket delete - Remove the specified ticket from the current configuration. + Remove the specified ticket from the local site configuration. The + ticket remains loaded in the current CIB and should be cleaned up + using the "pcs ticket cleanup" command. ticket remove - Remove the specified ticket from the current configuration. + Remove the specified ticket from the local site configuration. The + ticket remains loaded in the current CIB and should be cleaned up using + the "pcs ticket cleanup" command. + + ticket cleanup + Remove specified ticket from CIB at the local site. + + ticket standby + Tell the cluster on the local site that this ticket is standby. The + dependent resources will be stopped or demoted gracefully without + triggering loss-policies. + + ticket unstandby + Tell the cluster on the local site that this ticket is no longer + standby. config [] Show booth configuration from the specified node or from the current @@ -3272,7 +3288,10 @@ def booth(args: Argv) -> str: Print current status of booth on the local node. pull - Pull booth configuration from the specified node. + Pull booth configuration from the specified node. After pulling the + configuration, the booth should be restarted. In case any tickets were + removed, the "pcs ticket cleanup" command should be used to remove any + leftover tickets still loaded in the CIB at the current site. sync [--skip-offline] Send booth configuration from the local node to all nodes diff --git a/pcs_test/Makefile.am b/pcs_test/Makefile.am index 2e303da3f..cd9d32ebc 100644 --- a/pcs_test/Makefile.am +++ b/pcs_test/Makefile.am @@ -170,6 +170,7 @@ EXTRA_DIST = \ tier0/lib/auth/config/test_parser.py \ tier0/lib/auth/test_provider.py \ tier0/lib/booth/__init__.py \ + tier0/lib/booth/test_cib.py \ tier0/lib/booth/test_config_facade.py \ tier0/lib/booth/test_config_files.py \ tier0/lib/booth/test_config_parser.py \ diff --git a/pcs_test/tier0/cli/test_booth.py b/pcs_test/tier0/cli/test_booth.py index 1b9e2dc59..d917508d6 100644 --- a/pcs_test/tier0/cli/test_booth.py +++ b/pcs_test/tier0/cli/test_booth.py @@ -462,3 +462,81 @@ def test_lib_call_full(self): self.lib.booth.get_status.assert_called_once_with( instance_name="my_booth", ) + + +class TicketCleanup(TestCase): + def setUp(self): + self.lib = mock.Mock(spec_set=["booth"]) + self.lib.booth = mock.Mock(spec_set=["ticket_cleanup"]) + + def test_no_args(self): + with self.assertRaises(CmdLineInputError) as cm: + booth_cmd.ticket_unstandby(self.lib, [], dict_to_modifiers({})) + self.assertIsNone(cm.exception.message) + self.lib.booth.ticket_cleanup.assert_not_called() + + def test_too_many_args(self): + with self.assertRaises(CmdLineInputError) as cm: + booth_cmd.ticket_unstandby( + self.lib, ["a", "b"], dict_to_modifiers({}) + ) + self.assertIsNone(cm.exception.message) + self.lib.booth.ticket_cleanup.assert_not_called() + + def test_with_ticket_minimal(self): + booth_cmd.ticket_cleanup( + self.lib, + ["ticketA"], + dict_to_modifiers({}), + ) + self.lib.booth.ticket_cleanup.assert_called_once_with("ticketA") + + +class TicketUnstandby(TestCase): + def setUp(self): + self.lib = mock.Mock(spec_set=["booth"]) + self.lib.booth = mock.Mock(spec_set=["ticket_unstandby"]) + + def test_no_args(self): + with self.assertRaises(CmdLineInputError) as cm: + booth_cmd.ticket_unstandby(self.lib, [], dict_to_modifiers({})) + self.assertIsNone(cm.exception.message) + self.lib.booth.ticket_unstandby.assert_not_called() + + def test_too_many_args(self): + with self.assertRaises(CmdLineInputError) as cm: + booth_cmd.ticket_unstandby( + self.lib, ["a", "b"], dict_to_modifiers({}) + ) + self.assertIsNone(cm.exception.message) + self.lib.booth.ticket_unstandby.assert_not_called() + + def test_call_minimal(self): + booth_cmd.ticket_unstandby( + self.lib, ["my_ticket"], dict_to_modifiers({}) + ) + self.lib.booth.ticket_unstandby.assert_called_once_with("my_ticket") + + +class TicketStandby(TestCase): + def setUp(self): + self.lib = mock.Mock(spec_set=["booth"]) + self.lib.booth = mock.Mock(spec_set=["ticket_standby"]) + + def test_no_args(self): + with self.assertRaises(CmdLineInputError) as cm: + booth_cmd.ticket_standby(self.lib, [], dict_to_modifiers({})) + self.assertIsNone(cm.exception.message) + self.lib.booth.ticket_standby.assert_not_called() + + def test_too_many_args(self): + with self.assertRaises(CmdLineInputError) as cm: + booth_cmd.ticket_standby( + self.lib, ["a", "b"], dict_to_modifiers({}) + ) + self.assertIsNone(cm.exception.message) + self.lib.booth.ticket_standby.assert_not_called() + + def test_call_minimal(self): + booth_cmd.ticket_standby(self.lib, ["my_ticket"], dict_to_modifiers({})) + self.lib.booth.ticket_standby.assert_called_once_with("my_ticket") diff --git a/pcs_test/tier0/common/reports/test_messages.py b/pcs_test/tier0/common/reports/test_messages.py index e76d171fb..e9f47786d 100644 --- a/pcs_test/tier0/common/reports/test_messages.py +++ b/pcs_test/tier0/common/reports/test_messages.py @@ -5057,6 +5057,14 @@ def test_success(self): ) +class BoothTicketNotInCib(NameBuildTest): + def test_success(self): + self.assert_message_from_report( + "Unable to find ticket 'name' in CIB", + reports.BoothTicketNotInCib("name"), + ) + + class BoothAlreadyInCib(NameBuildTest): def test_success(self): self.assert_message_from_report( @@ -5309,6 +5317,30 @@ def test_success(self): ), ) + def test_no_site_ip(self): + self.assert_message_from_report( + ("unable to operation booth ticket 'ticket_name', reason: reason"), + reports.BoothTicketOperationFailed( + "operation", "reason", None, "ticket_name" + ), + ) + + +class BoothTicketChangingState(NameBuildTest): + def test_success(self): + self.assert_message_from_report( + "Changing state of ticket 'name' to standby", + reports.BoothTicketChangingState("name", "standby"), + ) + + +class BoothTicketCleanup(NameBuildTest): + def test_success(self): + self.assert_message_from_report( + "Cleaning up ticket 'name' from CIB", + reports.BoothTicketCleanup("name"), + ) + # TODO: remove, use ADD_REMOVE reports class TagAddRemoveIdsDuplication(NameBuildTest): diff --git a/pcs_test/tier0/lib/booth/test_cib.py b/pcs_test/tier0/lib/booth/test_cib.py new file mode 100644 index 000000000..b643467e5 --- /dev/null +++ b/pcs_test/tier0/lib/booth/test_cib.py @@ -0,0 +1,30 @@ +from unittest import TestCase + +from lxml import etree + +from pcs.lib.booth import cib as cib_commands + + +class GetTicketNames(TestCase): + def setUp(self): + self.cib = etree.fromstring( + """ + + + + + + + + + + + + """ + ) + + def test_success(self): + self.assertEqual( + ["T1", "T2", "T3", "T-self-managed"], + cib_commands.get_ticket_names(self.cib), + ) diff --git a/pcs_test/tier0/lib/commands/test_booth.py b/pcs_test/tier0/lib/commands/test_booth.py index 7ff878959..58e27d878 100644 --- a/pcs_test/tier0/lib/commands/test_booth.py +++ b/pcs_test/tier0/lib/commands/test_booth.py @@ -3803,3 +3803,228 @@ def test_read_file_failure(self): ) ] ) + + +class CrmTicketOperationTest: + CIB_STATUS = """ + + + + + + + + + """ + + def setUp(self): + self.env_assist, self.config = get_env_tools(self) + self.config.runner.cib.load(status=self.CIB_STATUS) + + def _call_cmd(self, ticket_name): + raise NotImplementedError() + + def fixture_crm_call(self, ticket_name): + raise NotImplementedError() + + def fixture_reports(self, ticket_name): + raise NotImplementedError() + + def test_default_instance(self): + ticket = "T2" + self.fixture_crm_call(ticket) + self._call_cmd(ticket) + self.env_assist.assert_reports(self.fixture_reports(ticket)) + + def test_custom_instance(self): + ticket = "T3" + self.fixture_crm_call(ticket) + + self._call_cmd(ticket) + self.env_assist.assert_reports(self.fixture_reports(ticket)) + + def test_missing_ticket(self): + ticket = "T4" + self.env_assist.assert_raise_library_error( + lambda: self._call_cmd(ticket) + ) + self.env_assist.assert_reports( + [ + fixture.error( + reports.codes.BOOTH_TICKET_NOT_IN_CIB, ticket_name=ticket + ) + ] + ) + + +class TicketCleanup(CrmTicketOperationTest, TestCase): + def setUp(self): + self.env_assist, self.config = get_env_tools(self) + self.config.runner.cib.load(status=self.CIB_STATUS) + + def fixture_crm_call(self, ticket_name): + self.config.runner.pcmk.ticket_standby(ticket_name) + self.config.runner.pcmk.ticket_cleanup(ticket_name) + + def fixture_reports(self, ticket_name): + return [ + fixture.info( + reports.codes.BOOTH_TICKET_CHANGING_STATE, + ticket_name=ticket_name, + state="standby", + ), + fixture.info( + reports.codes.BOOTH_TICKET_CLEANUP, ticket_name=ticket_name + ), + ] + + def _call_cmd(self, ticket_name): + commands.ticket_cleanup(self.env_assist.get_env(), ticket_name) + + def test_standby_fails(self): + self.config.runner.pcmk.ticket_standby( + "T2", returncode=1, stderr="some error" + ) + + self.env_assist.assert_raise_library_error( + lambda: commands.ticket_cleanup(self.env_assist.get_env(), "T2") + ) + self.env_assist.assert_reports( + [ + fixture.info( + reports.codes.BOOTH_TICKET_CHANGING_STATE, + ticket_name="T2", + state="standby", + ), + fixture.error( + reports.codes.BOOTH_TICKET_OPERATION_FAILED, + operation="standby", + reason="some error", + site_ip=None, + ticket_name="T2", + ), + ] + ) + + def test_cleanup_fails(self): + self.config.runner.pcmk.ticket_standby("T2") + self.config.runner.pcmk.ticket_cleanup( + "T2", returncode=1, stderr="some error" + ) + + self.env_assist.assert_raise_library_error( + lambda: commands.ticket_cleanup(self.env_assist.get_env(), "T2") + ) + self.env_assist.assert_reports( + [ + fixture.info( + reports.codes.BOOTH_TICKET_CHANGING_STATE, + ticket_name="T2", + state="standby", + ), + fixture.info( + reports.codes.BOOTH_TICKET_CLEANUP, ticket_name="T2" + ), + fixture.error( + reports.codes.BOOTH_TICKET_OPERATION_FAILED, + operation="cleanup", + reason="some error", + site_ip=None, + ticket_name="T2", + ), + ] + ) + + +class TicketUnstandby(CrmTicketOperationTest, TestCase): + def setUp(self): + self.env_assist, self.config = get_env_tools(self) + self.config.runner.cib.load(status=self.CIB_STATUS) + + def fixture_crm_call(self, ticket_name): + self.config.runner.pcmk.ticket_unstandby(ticket_name) + + def fixture_reports(self, ticket_name): + return [ + fixture.info( + reports.codes.BOOTH_TICKET_CHANGING_STATE, + ticket_name=ticket_name, + state="active", + ) + ] + + def _call_cmd(self, ticket_name): + commands.ticket_unstandby(self.env_assist.get_env(), ticket_name) + + def test_unstandby_fails(self): + ticket = "T2" + self.config.runner.pcmk.ticket_unstandby( + ticket, returncode=1, stderr="some error" + ) + + self.env_assist.assert_raise_library_error( + lambda: commands.ticket_unstandby(self.env_assist.get_env(), ticket) + ) + self.env_assist.assert_reports( + [ + fixture.info( + reports.codes.BOOTH_TICKET_CHANGING_STATE, + ticket_name=ticket, + state="active", + ), + fixture.error( + reports.codes.BOOTH_TICKET_OPERATION_FAILED, + operation="unstandby", + reason="some error", + site_ip=None, + ticket_name=ticket, + ), + ] + ) + + +class TicketStandby(CrmTicketOperationTest, TestCase): + def setUp(self): + self.env_assist, self.config = get_env_tools(self) + self.config.runner.cib.load(status=self.CIB_STATUS) + + def fixture_crm_call(self, ticket_name): + self.config.runner.pcmk.ticket_standby(ticket_name) + + def fixture_reports(self, ticket_name): + return [ + fixture.info( + reports.codes.BOOTH_TICKET_CHANGING_STATE, + ticket_name=ticket_name, + state="standby", + ) + ] + + def _call_cmd(self, ticket_name): + commands.ticket_standby(self.env_assist.get_env(), ticket_name) + + def test_standby_fails(self): + ticket = "T2" + self.config.runner.pcmk.ticket_standby( + ticket, returncode=1, stderr="some error" + ) + + self.env_assist.assert_raise_library_error( + lambda: commands.ticket_standby(self.env_assist.get_env(), ticket) + ) + self.env_assist.assert_reports( + [ + fixture.info( + reports.codes.BOOTH_TICKET_CHANGING_STATE, + ticket_name=ticket, + state="standby", + ), + fixture.error( + reports.codes.BOOTH_TICKET_OPERATION_FAILED, + operation="standby", + reason="some error", + site_ip=None, + ticket_name=ticket, + ), + ] + ) diff --git a/pcs_test/tier1/test_booth.py b/pcs_test/tier1/test_booth.py index 71000d3f5..29ec2b3d4 100644 --- a/pcs_test/tier1/test_booth.py +++ b/pcs_test/tier1/test_booth.py @@ -699,3 +699,37 @@ def test_disabled(self): ), config_file.read(), ) + + +class BoothTicketOperationBase(BoothMixinNoFiles): + command = "" + + def test_not_enough_args(self): + self.assert_pcs_fail( + ["booth", "ticket", self.command], + stderr_start=( + "\nUsage: pcs booth \n" + f" ticket {self.command} \n" + ), + ) + + def test_too_many_args(self): + self.assert_pcs_fail( + ["booth", "ticket", self.command, "a", "b"], + stderr_start=( + "\nUsage: pcs booth \n" + f" ticket {self.command} \n" + ), + ) + + +class TicketCleanup(BoothTicketOperationBase, TestCase): + command = "cleanup" + + +class TicketUnstandby(BoothTicketOperationBase, TestCase): + command = "unstandby" + + +class TicketStandby(BoothTicketOperationBase, TestCase): + command = "standby" diff --git a/pcs_test/tools/command_env/config_runner_pcmk.py b/pcs_test/tools/command_env/config_runner_pcmk.py index 83f134403..6faa7e233 100644 --- a/pcs_test/tools/command_env/config_runner_pcmk.py +++ b/pcs_test/tools/command_env/config_runner_pcmk.py @@ -1154,3 +1154,67 @@ def stonith_agent_self_validation( RunnerCall(cmd, stdout=stdout, returncode=returncode, env=env), instead=instead, ) + + def ticket_unstandby( + self, + ticket_name: str, + stderr="", + returncode=0, + name="runner.pcmk.ticket_unstandby", + ) -> None: + self.__calls.place( + name, + RunnerCall( + [ + settings.crm_ticket_exec, + "--activate", + "--ticket", + ticket_name, + ], + stderr=stderr, + returncode=returncode, + ), + ) + + def ticket_standby( + self, + ticket_name: str, + stderr="", + returncode=0, + name="runner.pcmk.ticket_standby", + ) -> None: + self.__calls.place( + name, + RunnerCall( + [ + settings.crm_ticket_exec, + "--standby", + "--ticket", + ticket_name, + ], + stderr=stderr, + returncode=returncode, + ), + ) + + def ticket_cleanup( + self, + ticket_name: str, + stderr="", + returncode=0, + name="runner.pcmk.ticket_cleanup", + ) -> None: + self.__calls.place( + name, + RunnerCall( + [ + settings.crm_ticket_exec, + "--cleanup", + "--force", + "--ticket", + ticket_name, + ], + stderr=stderr, + returncode=returncode, + ), + ) diff --git a/pcsd/capabilities.xml.in b/pcsd/capabilities.xml.in index d38119c87..43805dbae 100644 --- a/pcsd/capabilities.xml.in +++ b/pcsd/capabilities.xml.in @@ -65,6 +65,24 @@ pcs commands: booth clean-enable-authfile + + + Remove specified booth ticket from CIB. + + pcs commands: booth ticket cleanup + API v2: booth.ticket_cleanup + + + + + Change the state of the booth ticket to standby and back to active. + + pcs commands: booth ticket ( standby | unstandby ) + API v2: + booth.ticket_standby + booth.ticket_unstandby + + From 3e072e2460365cb26fd9fe9fce100ad8af17b38d Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Thu, 28 Nov 2024 10:34:11 +0100 Subject: [PATCH 053/227] stop testing on fedora fedora no longer provides relevant packages (i.e. pacemaker < 3) --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b63e03574..fa1d098bb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,7 +6,7 @@ default: .parallel: parallel: matrix: - - BASE_IMAGE_NAME: ["PcsRhel9CurrentRelease", "PcsRhel9Next", "PcsFedoraCurrentRelease"] + - BASE_IMAGE_NAME: ["PcsRhel9CurrentRelease", "PcsRhel9Next"] stages: - stage1 From 31ca9897f619536ffd60a58147352c6be4652e27 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Wed, 27 Nov 2024 17:35:08 +0100 Subject: [PATCH 054/227] remove dead code --- pcs/common/tools.py | 20 -------------------- pcs_test/tier0/common/test_tools.py | 21 --------------------- 2 files changed, 41 deletions(-) diff --git a/pcs/common/tools.py b/pcs/common/tools.py index 5ffe7af1f..919f8b340 100644 --- a/pcs/common/tools.py +++ b/pcs/common/tools.py @@ -1,15 +1,10 @@ -import threading import uuid from dataclasses import ( astuple, dataclass, ) from typing import ( - Any, - Callable, Generator, - Iterable, - Mapping, MutableSet, Optional, TypeVar, @@ -43,21 +38,6 @@ def get_unique_uuid(already_used: StringCollection) -> str: return candidate -def run_parallel( - worker: Callable[..., Any], - data_list: tuple[Iterable[Any], Mapping[str, Any]], -) -> None: - thread_list = [] - for args, kwargs in data_list: - thread = threading.Thread(target=worker, args=args, kwargs=kwargs) - thread.daemon = True - thread_list.append(thread) - thread.start() - - for thread in thread_list: - thread.join() - - def format_os_error(e: OSError) -> str: return f"{e.strerror}: '{e.filename}'" if e.filename else e.strerror diff --git a/pcs_test/tier0/common/test_tools.py b/pcs_test/tier0/common/test_tools.py index 0dfedad53..816466431 100644 --- a/pcs_test/tier0/common/test_tools.py +++ b/pcs_test/tier0/common/test_tools.py @@ -1,29 +1,8 @@ -import time from unittest import TestCase from pcs.common import tools -class RunParallelTestCase(TestCase): - def test_run_all(self): - data_list = [([i], {}) for i in range(5)] - out_list = [] - tools.run_parallel(out_list.append, data_list) - self.assertEqual(sorted(out_list), list(range(5))) - - def test_parallelism(self): - timeout = 5 - data_list = [[[i + 1], {}] for i in range(timeout)] - start_time = time.time() - # this should last for least timeout seconds, but less than sum of all - # times - tools.run_parallel(time.sleep, data_list) - finish_time = time.time() - elapsed_time = finish_time - start_time - self.assertTrue(elapsed_time > timeout) - self.assertTrue(elapsed_time < sum(i + 1 for i in range(timeout))) - - class VersionTest(TestCase): # pylint: disable=invalid-name def assert_asterisk(self, expected, major, minor=None, revision=None): From 912e58e3c5a4a71180763d46a263a1a830445bbf Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Wed, 6 Nov 2024 16:42:29 +0100 Subject: [PATCH 055/227] update mypy to 1.13.0 --- dev_requirements.txt | 2 +- pcs/common/interface/dto.py | 23 +++++++++++++++-------- pcs/lib/cib/rule/expression_part.py | 7 +++---- pcs/lib/cib/rule/parser.py | 2 +- pcs/lib/cib/rule/validator.py | 13 +++++++------ 5 files changed, 27 insertions(+), 20 deletions(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index 0578bfcee..6ba5658dd 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,7 +1,7 @@ lxml-stubs pylint==3.1.0 astroid==3.1.0 -mypy==1.9.0 +mypy==1.13.0 black==24.3.0 isort types-cryptography diff --git a/pcs/common/interface/dto.py b/pcs/common/interface/dto.py index 108f00a3d..60b187988 100644 --- a/pcs/common/interface/dto.py +++ b/pcs/common/interface/dto.py @@ -10,7 +10,6 @@ Dict, Iterable, NewType, - Type, TypeVar, Union, ) @@ -58,7 +57,10 @@ def meta(name: str) -> Dict[str, str]: return metadata -def _is_compatible_type(_type: Type, arg_index: int) -> bool: +# _type is Any, since based on static code analysis it can be either of +# type[Any], str, None - depending on the step in dataclass instance +# initialization +def _is_compatible_type(_type: Any, arg_index: int) -> bool: return ( hasattr(_type, "__args__") and len(_type.__args__) >= arg_index @@ -67,7 +69,7 @@ def _is_compatible_type(_type: Type, arg_index: int) -> bool: def _convert_dict( - klass: Type[DataTransferObject], obj_dict: DtoPayload + klass: type[DataTransferObject], obj_dict: DtoPayload ) -> DtoPayload: new_dict = {} for _field in fields(klass): @@ -76,7 +78,10 @@ def _convert_dict( value = _convert_dict(_field.type, value) # type: ignore elif isinstance(value, list) and _is_compatible_type(_field.type, 0): value = [ - _convert_dict(_field.type.__args__[0], item) for item in value + # ignore _field.type may not have __args__ + # this is prevented by _is_compatible_type + _convert_dict(_field.type.__args__[0], item) # type: ignore + for item in value ] elif isinstance(value, dict) and _is_compatible_type(_field.type, 1): value = { @@ -98,7 +103,7 @@ def to_dict(obj: DataTransferObject) -> DtoPayload: DTOTYPE = TypeVar("DTOTYPE", bound=DataTransferObject) -def _convert_payload(klass: Type[DTOTYPE], data: DtoPayload) -> DtoPayload: +def _convert_payload(klass: type[DTOTYPE], data: DtoPayload) -> DtoPayload: try: new_dict = dict(data) except ValueError as e: @@ -112,7 +117,9 @@ def _convert_payload(klass: Type[DTOTYPE], data: DtoPayload) -> DtoPayload: value = _convert_payload(_field.type, value) # type: ignore elif isinstance(value, list) and _is_compatible_type(_field.type, 0): value = [ - _convert_payload(_field.type.__args__[0], item) + # ignore _field.type may not have __args__ + # this is prevented by _is_compatible_type + _convert_payload(_field.type.__args__[0], item) # type: ignore for item in value ] elif isinstance(value, dict) and _is_compatible_type(_field.type, 1): @@ -128,7 +135,7 @@ def _convert_payload(klass: Type[DTOTYPE], data: DtoPayload) -> DtoPayload: def from_dict( - cls: Type[DTOTYPE], data: DtoPayload, strict: bool = False + cls: type[DTOTYPE], data: DtoPayload, strict: bool = False ) -> DTOTYPE: return dacite.from_dict( data_class=cls, @@ -161,5 +168,5 @@ def to_dto(self) -> Any: class ImplementsFromDto: @classmethod - def from_dto(cls: Type[T], dto_obj: Any) -> T: + def from_dto(cls: type[T], dto_obj: Any) -> T: raise NotImplementedError() diff --git a/pcs/lib/cib/rule/expression_part.py b/pcs/lib/cib/rule/expression_part.py index 843310dc3..33d3709bb 100644 --- a/pcs/lib/cib/rule/expression_part.py +++ b/pcs/lib/cib/rule/expression_part.py @@ -7,7 +7,6 @@ NewType, Optional, Sequence, - Tuple, ) BoolOperator = NewType("BoolOperator", str) @@ -65,9 +64,9 @@ class DateInRangeExpr(RuleExprPart): Represents a 'date in range' expression """ - date_start: str + date_start: Optional[str] date_end: Optional[str] - duration_parts: Optional[Sequence[Tuple[str, str]]] + duration_parts: Optional[Sequence[tuple[str, str]]] @dataclass(frozen=True) @@ -76,7 +75,7 @@ class DatespecExpr(RuleExprPart): Represents a date-spec expression """ - date_parts: Sequence[Tuple[str, str]] + date_parts: Sequence[tuple[str, str]] @dataclass(frozen=True) diff --git a/pcs/lib/cib/rule/parser.py b/pcs/lib/cib/rule/parser.py index 212ba4a16..5e61019bb 100644 --- a/pcs/lib/cib/rule/parser.py +++ b/pcs/lib/cib/rule/parser.py @@ -186,7 +186,7 @@ def __build_date_inrange_expr( def __build_datespec_expr(parse_result: pyparsing.ParseResults) -> RuleExprPart: # Those attrs are defined by setResultsName in datespec_expr grammar rule return DatespecExpr( - parse_result.datespec.as_list() if parse_result.datespec else None + parse_result.datespec.as_list() if parse_result.datespec else () ) diff --git a/pcs/lib/cib/rule/validator.py b/pcs/lib/cib/rule/validator.py index 3edf8a113..1835d6469 100644 --- a/pcs/lib/cib/rule/validator.py +++ b/pcs/lib/cib/rule/validator.py @@ -3,7 +3,6 @@ from typing import ( List, Set, - cast, ) from dateutil import parser as dateutil_parser @@ -131,17 +130,19 @@ def _validate_date_inrange_expr( ) ) if ( - start_date is not None + # start and end dates have been specified + expr.date_start is not None + and expr.date_end is not None + # start and end dates are valid dates + and start_date is not None and end_date is not None + # start happens later than end and start_date >= end_date ): report_list.append( reports.item.ReportItem.error( message=reports.messages.RuleExpressionSinceGreaterThanUntil( - expr.date_start, - # If end_date is not None, then expr.date_end is not - # None, but mypy does not see it. - cast(str, expr.date_end), + expr.date_start, expr.date_end ), ) ) From ea05a77e34106b8d299f50fdd83607029f91afd1 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Fri, 22 Nov 2024 13:52:43 +0100 Subject: [PATCH 056/227] move pylint configuration from pylintrc to pyproject.toml --- Makefile.am | 3 +-- pylintrc | 43 --------------------------------------- pyproject.toml | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 45 deletions(-) delete mode 100644 pylintrc diff --git a/Makefile.am b/Makefile.am index c371e2f2e..038844579 100644 --- a/Makefile.am +++ b/Makefile.am @@ -10,7 +10,6 @@ EXTRA_DIST = \ MANIFEST.in \ mypy.ini \ pcs.pc.in \ - pylintrc \ pyproject.toml \ rpm/pcs.spec.in \ scripts/pcsd.sh.in \ @@ -200,7 +199,7 @@ else pylint_options = endif export PYTHONPATH=${abs_top_builddir}/${PCS_BUNDLED_DIR_LOCAL}/packages && \ - $(TIME) $(PYTHON) -m pylint --rcfile pylintrc --persistent=n --reports=n --score=n --disable similarities ${pylint_options} ${PCS_PYTHON_PACKAGES} + $(TIME) $(PYTHON) -m pylint --rcfile pyproject.toml ${pylint_options} ${PCS_PYTHON_PACKAGES} endif isort_check: pyproject.toml diff --git a/pylintrc b/pylintrc deleted file mode 100644 index fe92e4800..000000000 --- a/pylintrc +++ /dev/null @@ -1,43 +0,0 @@ -# Required version of pylint: see requirements.txt -# -# To install linters and their dependencies, run: -# $ make python_static_code_analysis_reqirements -# -# This project should not contain any issues reported by any linter when using -# this command to run linters on the whole project: -# $ make black_check python_static_code_analysis - -[MASTER] -extension-pkg-whitelist=lxml.etree,pycurl -load-plugins=pylint.extensions.no_self_use - -[MESSAGES CONTROL] -# consider-using-f-string - not critical, plus we use str.format() for readability -# fixme - TODO is used to mark e.g. deprecations which are to be resolved in next pcs major version -# line-too-long - handled by black -# missing-docstring - we dont require pointless docstring to be present -# trailing-whitespace - handled by black -# use-dict-literal - lot of dict() in code to be replaced, not worth the effort now -# wrong-import-order - handled by isort -disable=consider-using-f-string, fixme, line-too-long, missing-docstring, unspecified-encoding, use-dict-literal, wrong-import-order -# Everything in module context is a constant, but our naming convention allows -# constants to have the same name format as variables -const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))|([a-z_][a-z0-9_]*)$ - -[DESIGN] -max-module-lines=1500 -max-args=8 -max-parents=10 -min-public-methods=0 - -[BASIC] -good-names=e, i, op, ip, el, maxDiff, cm, ok, T, dr, setUp, tearDown - -[VARIABLES] -# A regular expression matching the name of dummy variables (i.e. expectedly -# not used). -dummy-variables-rgx=_$|dummy - -[FORMAT] -# Maximum number of characters on a single line. -max-line-length=80 diff --git a/pyproject.toml b/pyproject.toml index 537d8c5ad..87f12490f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,3 +14,58 @@ sections = ['FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'TESTS', 'LOCALFOLDE known_first_party = ["pcs"] known_tests = ["pcs_test"] supported_extensions = ["py", "py.in"] + +[tool.pylint.main] +disable = [ + # not critical, plus we use str.format() for readability + "consider-using-f-string", + # TODO is used to mark e.g. deprecations which are to be resolved in next pcs + # major version + "fixme", + # handled by black + "line-too-long", + # we dont require pointless docstring to be present + "missing-docstring", + "similarities", + "unspecified-encoding", + # lot of dict() in code to be replaced, not worth the effort now + "use-dict-literal", + # handled by isort + "wrong-import-order", +] +extension-pkg-allow-list = ["lxml.etree", "pycurl"] +load-plugins = ["pylint.extensions.no_self_use"] +persistent = false +reports = false +score = false + +[tool.pylint.basic] +# Everything in module context is a constant, but our naming convention allows +# constants to have the same name format as variables +const-rgx = "(([A-Z_][A-Z0-9_]*)|(__.*__))|([a-z_][a-z0-9_]*)$" +good-names = [ + "e", + "i", + "op", + "ip", + "el", + "maxDiff", + "cm", + "ok", + "T", + "dr", + "setUp", + "tearDown", +] + +[tool.pylint.design] +max-args = 8 +max-parents = 10 +min-public-methods = 0 + +[tool.pylint.format] +max-module-lines = 1500 +max-line-length = 80 + +[tool.pylint.variables] +dummy-variables-rgx = "_$|dummy" From 9a58d20fdf9de51d145c014cb57ac71f727b01dc Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Wed, 20 Nov 2024 16:36:31 +0100 Subject: [PATCH 057/227] update pylint to 3.3.2 * fix possibly-used-before-assignment * set max-positional-arguments=8 * disable too-many-positional-arguments --- dev_requirements.txt | 4 +- pcs/cli/booth/env.py | 12 ++++- pcs/constraint.py | 7 +-- pcs/daemon/async_tasks/worker/logging.py | 3 +- pcs/daemon/run.py | 1 + pcs/lib/cib/fencing_topology.py | 1 + pcs/lib/cib/resource/bundle.py | 3 ++ pcs/lib/cib/resource/primitive.py | 3 ++ pcs/lib/cib/resource/remote_node.py | 1 + pcs/lib/commands/cluster.py | 5 ++ pcs/lib/commands/remote_node.py | 1 + pcs/lib/commands/resource.py | 7 +++ pcs/lib/commands/sbd.py | 1 + pcs/lib/commands/stonith.py | 2 + pcs/lib/corosync/config_validators.py | 1 + pcs/lib/env.py | 1 + pcs/lib/validate.py | 1 + pcs/utils.py | 1 + pcs_test/tier0/common/test_resource_status.py | 6 ++- .../tier0/lib/cib/test_fencing_topology.py | 1 + .../tier0/lib/commands/cluster/test_setup.py | 11 ++-- .../commands/resource/test_bundle_update.py | 4 +- .../commands/resource/test_resource_create.py | 2 + .../resource/test_resource_enable_disable.py | 1 + .../resource/test_resource_manage_unmanage.py | 1 + pcs_test/tier0/lib/commands/test_status.py | 1 + .../test_stonith_update_scsi_devices.py | 2 + .../lib/resource_agent/test_pcs_transform.py | 1 + pcs_test/tier0/lib/test_cluster_property.py | 1 + pcs_test/tier1/cib_resource/test_create.py | 1 + pcs_test/tier1/test_tag.py | 1 + pcs_test/tools/assertions.py | 2 + pcs_test/tools/cib.py | 2 + pcs_test/tools/color_text_runner/result.py | 1 + pcs_test/tools/command_env/config_env.py | 3 ++ .../tools/command_env/config_http_booth.py | 1 + .../tools/command_env/config_http_files.py | 1 + .../tools/command_env/config_http_status.py | 1 + pcs_test/tools/command_env/config_raw_file.py | 2 + pcs_test/tools/command_env/config_runner.py | 1 + .../tools/command_env/config_runner_booth.py | 2 + .../tools/command_env/config_runner_cib.py | 1 + .../tools/command_env/config_runner_pcmk.py | 51 ++++++++++++------- .../command_env/mock_node_communicator.py | 2 + pcs_test/tools/command_env/tools.py | 1 + pyproject.toml | 1 + 46 files changed, 128 insertions(+), 32 deletions(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index 6ba5658dd..b2e94c8bb 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,6 +1,6 @@ lxml-stubs -pylint==3.1.0 -astroid==3.1.0 +pylint==3.3.2 +astroid==3.3.5 mypy==1.13.0 black==24.3.0 isort diff --git a/pcs/cli/booth/env.py b/pcs/cli/booth/env.py index 16edb73a2..1f9520d3a 100644 --- a/pcs/cli/booth/env.py +++ b/pcs/cli/booth/env.py @@ -22,6 +22,8 @@ def middleware_config(config_path, key_path): ) is_mocked_environment = config_path and key_path + config_file = None + key_file = None if is_mocked_environment: config_file = pcs_file.RawFile( metadata.for_file_type(file_type_codes.BOOTH_CONFIG, config_path) @@ -32,8 +34,14 @@ def middleware_config(config_path, key_path): def create_booth_env(): try: - config_data = config_file.read() if config_file.exists() else None - key_data = key_file.read() if key_file.exists() else None + config_data = ( + config_file.read() + if config_file and config_file.exists() + else None + ) + key_data = ( + key_file.read() if key_file and key_file.exists() else None + ) # TODO write custom error handling, do not use pcs.lib specific code # and LibraryError except pcs_file.RawFileError as e: diff --git a/pcs/constraint.py b/pcs/constraint.py index 65ef6c5f3..6c87a054f 100644 --- a/pcs/constraint.py +++ b/pcs/constraint.py @@ -782,6 +782,7 @@ def location_config_cmd( """ modifiers.ensure_only_supported("-f", "--output-format", "--full", "--all") filter_type: Optional[str] = None + filter_items: parse_args.Argv = [] if argv: filter_type, *filter_items = argv allowed_types = ("resources", "nodes") @@ -899,6 +900,7 @@ def location_prefer(lib, argv, modifiers): raise CmdLineInputError() skip_node_check = False + existing_nodes: list[str] = [] if modifiers.is_specified("-f") or modifiers.get("--force"): skip_node_check = True warn(LOCATION_NODE_VALIDATION_SKIP_MSG) @@ -1295,12 +1297,11 @@ def constraint_rm( c_id = argv.pop(0) elementFound = False - + dom = None + use_cibadmin = False if not constraintsElement: (dom, constraintsElement) = getCurrentConstraints(passed_dom) use_cibadmin = True - else: - use_cibadmin = False for co in constraintsElement.childNodes[:]: if co.nodeType != xml.dom.Node.ELEMENT_NODE: diff --git a/pcs/daemon/async_tasks/worker/logging.py b/pcs/daemon/async_tasks/worker/logging.py index 1319d6512..61d2f3d6b 100644 --- a/pcs/daemon/async_tasks/worker/logging.py +++ b/pcs/daemon/async_tasks/worker/logging.py @@ -7,7 +7,6 @@ class Logger(logging.Logger): - # pylint: disable=too-many-arguments def makeRecord( # type: ignore self, name, @@ -21,6 +20,8 @@ def makeRecord( # type: ignore extra=None, sinfo=None, ) -> logging.LogRecord: + # pylint: disable=too-many-arguments + # pylint: disable=too-many-positional-arguments pid = os.getpid() return super().makeRecord( name, diff --git a/pcs/daemon/run.py b/pcs/daemon/run.py index 55b3cf77f..5518ace97 100644 --- a/pcs/daemon/run.py +++ b/pcs/daemon/run.py @@ -92,6 +92,7 @@ def configure_app( webui_dir: str, webui_fallback: str, pcsd_capabilities: Iterable[capabilities.Capability], + *, disable_gui: bool = False, debug: bool = False, ): diff --git a/pcs/lib/cib/fencing_topology.py b/pcs/lib/cib/fencing_topology.py index 55e347a0f..51c97b7ec 100644 --- a/pcs/lib/cib/fencing_topology.py +++ b/pcs/lib/cib/fencing_topology.py @@ -108,6 +108,7 @@ def add_level( force_node -- continue even if a node (target) does not exist """ # pylint: disable=too-many-arguments + # pylint: disable=too-many-positional-arguments id_provider = IdProvider(cib) validate_id_reports: ReportItemList = [] if level_id is not None: diff --git a/pcs/lib/cib/resource/bundle.py b/pcs/lib/cib/resource/bundle.py index 6905bf0fd..728317d53 100644 --- a/pcs/lib/cib/resource/bundle.py +++ b/pcs/lib/cib/resource/bundle.py @@ -235,6 +235,7 @@ def append_new( meta_attributes, ): # pylint: disable=too-many-arguments + # pylint: disable=too-many-positional-arguments """ Create new bundle and add it to the CIB @@ -351,6 +352,7 @@ def validate_update( force_options=False, ): # pylint: disable=too-many-arguments + # pylint: disable=too-many-positional-arguments """ Validate modifying an existing bundle, return list of report items @@ -395,6 +397,7 @@ def update( meta_attributes, ): # pylint: disable=too-many-arguments + # pylint: disable=too-many-positional-arguments """ Modify an existing bundle (does not touch encapsulated resources) diff --git a/pcs/lib/cib/resource/primitive.py b/pcs/lib/cib/resource/primitive.py index 851dd9b91..82a6348da 100644 --- a/pcs/lib/cib/resource/primitive.py +++ b/pcs/lib/cib/resource/primitive.py @@ -141,6 +141,7 @@ def create( ): # pylint: disable=too-many-arguments # pylint: disable=too-many-locals + # pylint: disable=too-many-positional-arguments """ Prepare all parts of primitive resource and append it into cib. @@ -250,11 +251,13 @@ def append_new( standard, provider, agent_type, + *, instance_attributes=None, meta_attributes=None, operation_list=None, ): # pylint:disable=too-many-arguments + # pylint: disable=too-many-positional-arguments """ Append a new primitive element to the resources_section. diff --git a/pcs/lib/cib/resource/remote_node.py b/pcs/lib/cib/resource/remote_node.py index 00af55091..fb8719c1e 100644 --- a/pcs/lib/cib/resource/remote_node.py +++ b/pcs/lib/cib/resource/remote_node.py @@ -219,6 +219,7 @@ def create( use_default_operations: bool = True, ): # pylint: disable=too-many-arguments + # pylint: disable=too-many-positional-arguments """ Prepare all parts of remote resource and append it into the cib. diff --git a/pcs/lib/commands/cluster.py b/pcs/lib/commands/cluster.py index 83868b74a..42b4ba9e2 100644 --- a/pcs/lib/commands/cluster.py +++ b/pcs/lib/commands/cluster.py @@ -202,6 +202,7 @@ def setup( force_flags: Collection[reports.types.ForceCode] = (), ): # pylint: disable=too-many-arguments + # pylint: disable=too-many-positional-arguments # pylint: disable=too-many-locals # pylint: disable=too-many-statements """ @@ -493,6 +494,7 @@ def setup_local( """ # pylint: disable=too-many-arguments # pylint: disable=too-many-locals + # pylint: disable=too-many-positional-arguments force = report_codes.FORCE in force_flags transport_type = transport_type or "knet" @@ -582,6 +584,7 @@ def _validate_create_corosync_conf( force: bool, ) -> reports.ReportItemList: # pylint: disable=too-many-arguments + # pylint: disable=too-many-positional-arguments # Get IP version for node addresses validation. Defaults taken from man # corosync.conf @@ -642,6 +645,7 @@ def _create_corosync_conf( no_cluster_uuid: bool, ) -> config_facade.ConfigFacade: # pylint: disable=too-many-arguments + # pylint: disable=too-many-positional-arguments corosync_conf = config_facade.ConfigFacade.create( cluster_name, nodes, transport_type ) @@ -1611,6 +1615,7 @@ def defaulter(node): if "name" not in node: return [] address_for_use = None + address_source = None target = targets_dict.get(node["name"]) if target: address_for_use = target.first_addr diff --git a/pcs/lib/commands/remote_node.py b/pcs/lib/commands/remote_node.py index f7fa040ba..5fbbab9ba 100644 --- a/pcs/lib/commands/remote_node.py +++ b/pcs/lib/commands/remote_node.py @@ -261,6 +261,7 @@ def node_add_remote( operations: Iterable[Mapping[str, str]], meta_attributes: Mapping[str, str], instance_attributes: Mapping[str, str], + *, skip_offline_nodes: bool = False, allow_incomplete_distribution: bool = False, allow_pacemaker_remote_service_fail: bool = False, diff --git a/pcs/lib/commands/resource.py b/pcs/lib/commands/resource.py index 9b30a04ec..719f8acd3 100644 --- a/pcs/lib/commands/resource.py +++ b/pcs/lib/commands/resource.py @@ -367,6 +367,7 @@ def create( operation_list: List[Mapping[str, str]], meta_attributes: Mapping[str, str], instance_attributes: Mapping[str, str], + *, allow_absent_agent: bool = False, allow_invalid_operation: bool = False, allow_invalid_instance_attributes: bool = False, @@ -474,6 +475,7 @@ def create_as_clone( meta_attributes: Mapping[str, str], instance_attributes: Mapping[str, str], clone_meta_options: Mapping[str, str], + *, clone_id: Optional[str] = None, allow_absent_agent: bool = False, allow_invalid_operation: bool = False, @@ -623,6 +625,7 @@ def create_in_group( operation_list: List[Mapping[str, str]], meta_attributes: Mapping[str, str], instance_attributes: Mapping[str, str], + *, allow_absent_agent: bool = False, allow_invalid_operation: bool = False, allow_invalid_instance_attributes: bool = False, @@ -769,6 +772,7 @@ def create_into_bundle( meta_attributes: Mapping[str, str], instance_attributes: Mapping[str, str], bundle_id: str, + *, allow_absent_agent: bool = False, allow_invalid_operation: bool = False, allow_invalid_instance_attributes: bool = False, @@ -883,6 +887,7 @@ def bundle_create( env, bundle_id, container_type, + *, container_options=None, network_options=None, port_map=None, @@ -961,6 +966,7 @@ def bundle_create( def bundle_reset( env, bundle_id, + *, container_options=None, network_options=None, port_map=None, @@ -1045,6 +1051,7 @@ def bundle_reset( def bundle_update( env, bundle_id, + *, container_options=None, network_options=None, port_map_add=None, diff --git a/pcs/lib/commands/sbd.py b/pcs/lib/commands/sbd.py index 6a197a7ce..346c200f5 100644 --- a/pcs/lib/commands/sbd.py +++ b/pcs/lib/commands/sbd.py @@ -149,6 +149,7 @@ def enable_sbd( sbd_options, default_device_list=None, node_device_dict=None, + *, allow_unknown_opts=False, ignore_offline_nodes=False, no_watchdog_validation=False, diff --git a/pcs/lib/commands/stonith.py b/pcs/lib/commands/stonith.py index d0799b8ea..096779685 100644 --- a/pcs/lib/commands/stonith.py +++ b/pcs/lib/commands/stonith.py @@ -109,6 +109,7 @@ def create( operations: Collection[Mapping[str, str]], meta_attributes: Mapping[str, str], instance_attributes: Mapping[str, str], + *, allow_absent_agent: bool = False, allow_invalid_operation: bool = False, allow_invalid_instance_attributes: bool = False, @@ -192,6 +193,7 @@ def create_in_group( operations: Collection[Mapping[str, str]], meta_attributes: Mapping[str, str], instance_attributes: Mapping[str, str], + *, allow_absent_agent: bool = False, allow_invalid_operation: bool = False, allow_invalid_instance_attributes: bool = False, diff --git a/pcs/lib/corosync/config_validators.py b/pcs/lib/corosync/config_validators.py index 3b3f8aa24..c28a08e15 100644 --- a/pcs/lib/corosync/config_validators.py +++ b/pcs/lib/corosync/config_validators.py @@ -1155,6 +1155,7 @@ def update_link( # pylint: disable=too-many-arguments # pylint: disable=too-many-branches # pylint: disable=too-many-locals + # pylint: disable=too-many-positional-arguments """ Validate changing an existing link diff --git a/pcs/lib/env.py b/pcs/lib/env.py index abf2c2b6d..85bcf8065 100644 --- a/pcs/lib/env.py +++ b/pcs/lib/env.py @@ -105,6 +105,7 @@ def __init__( request_timeout: Optional[int] = None, ): # pylint: disable=too-many-arguments + # pylint: disable=too-many-positional-arguments self._logger = logger self._report_processor = report_processor self._user_login = user_login diff --git a/pcs/lib/validate.py b/pcs/lib/validate.py index 12babd226..c44a2ab17 100644 --- a/pcs/lib/validate.py +++ b/pcs/lib/validate.py @@ -1162,6 +1162,7 @@ def __init__( container_type: reports.types.AddRemoveContainerType, item_type: reports.types.AddRemoveItemType, container_id: str, + *, adjacent_item_id: Optional[str] = None, container_can_be_empty: bool = False, severity: Optional[ReportItemSeverity] = None, diff --git a/pcs/utils.py b/pcs/utils.py index e2393680c..f32abb8c2 100644 --- a/pcs/utils.py +++ b/pcs/utils.py @@ -2246,6 +2246,7 @@ def tar_add_file_data( tarball, data, name, + *, mode=None, uid=None, gid=None, diff --git a/pcs_test/tier0/common/test_resource_status.py b/pcs_test/tier0/common/test_resource_status.py index c38a72e66..58f4f78cf 100644 --- a/pcs_test/tier0/common/test_resource_status.py +++ b/pcs_test/tier0/common/test_resource_status.py @@ -40,6 +40,7 @@ def fixture_primitive_dto( resource_id: str, instance_id: Optional[str], + *, resource_agent: str = "ocf:pacemaker:Dummy", role: PcmkStatusRoleType = PCMK_STATUS_ROLE_STARTED, target_role: Optional[PcmkRoleType] = None, @@ -96,6 +97,7 @@ def fixture_group_dto( def fixture_clone_dto( resource_id: str, + *, multi_state: bool = False, unique: bool = False, maintenance: bool = False, @@ -302,7 +304,9 @@ def test_combination(self): def fixture_facade() -> ResourcesStatusFacade: - stonith = fixture_primitive_dto("stonith", None, "stonith:fence_xvm") + stonith = fixture_primitive_dto( + "stonith", None, resource_agent="stonith:fence_xvm" + ) primitive = fixture_primitive_dto("primitive", None) primitive_stopped = fixture_primitive_dto( "primitive_stopped", diff --git a/pcs_test/tier0/lib/cib/test_fencing_topology.py b/pcs_test/tier0/lib/cib/test_fencing_topology.py index 377ac226c..4cb438d05 100644 --- a/pcs_test/tier0/lib/cib/test_fencing_topology.py +++ b/pcs_test/tier0/lib/cib/test_fencing_topology.py @@ -181,6 +181,7 @@ def assert_called_invalid( mock_val_dupl, mock_append, mock_id_provider, + *, dupl_called=True, report_list=None, ): diff --git a/pcs_test/tier0/lib/commands/cluster/test_setup.py b/pcs_test/tier0/lib/commands/cluster/test_setup.py index 941467288..175575ba7 100644 --- a/pcs_test/tier0/lib/commands/cluster/test_setup.py +++ b/pcs_test/tier0/lib/commands/cluster/test_setup.py @@ -123,6 +123,7 @@ def options_fixture(options, template=OPTION_TEMPLATE): def corosync_conf_fixture( node_addrs, + *, transport_type="knet", link_list=None, links_numbers=None, @@ -751,7 +752,7 @@ def setUp(self): ) .http.files.remove_files(self.node_list, pcsd_settings=True) .http.files.put_files( - self.node_list, + node_labels=self.node_list, pcmk_authkey=RANDOM_KEY, corosync_authkey=RANDOM_KEY, ) @@ -763,7 +764,7 @@ def test_two_node(self): quorum_options=dict(two_node="1"), ) self.config.http.files.put_files( - self.node_list, + node_labels=self.node_list, corosync_conf=corosync_conf, name="distribute_corosync_conf", ) @@ -782,7 +783,7 @@ def test_auto_tie_breaker(self): quorum_options=dict(auto_tie_breaker="1"), ) self.config.http.files.put_files( - self.node_list, + node_labels=self.node_list, corosync_conf=corosync_conf, name="distribute_corosync_conf", ) @@ -2281,12 +2282,12 @@ def setUp(self): .http.host.update_known_hosts(NODE_LIST, to_add_hosts=NODE_LIST) .http.files.remove_files(NODE_LIST, pcsd_settings=True) .http.files.put_files( - NODE_LIST, + node_labels=NODE_LIST, pcmk_authkey=RANDOM_KEY, corosync_authkey=RANDOM_KEY, ) .http.files.put_files( - NODE_LIST, + node_labels=NODE_LIST, corosync_conf=corosync_conf_fixture( {node: [node] for node in NODE_LIST} ), diff --git a/pcs_test/tier0/lib/commands/resource/test_bundle_update.py b/pcs_test/tier0/lib/commands/resource/test_bundle_update.py index 97c077aa5..72fc00c0c 100644 --- a/pcs_test/tier0/lib/commands/resource/test_bundle_update.py +++ b/pcs_test/tier0/lib/commands/resource/test_bundle_update.py @@ -24,7 +24,9 @@ def simple_bundle_update(env, wait=TIMEOUT): - return resource.bundle_update(env, "B1", {"image": "new:image"}, wait=wait) + return resource.bundle_update( + env, "B1", container_options={"image": "new:image"}, wait=wait + ) def fixture_resources_minimal(container_type="docker"): diff --git a/pcs_test/tier0/lib/commands/resource/test_resource_create.py b/pcs_test/tier0/lib/commands/resource/test_resource_create.py index 69322413c..eff6fe03e 100644 --- a/pcs_test/tier0/lib/commands/resource/test_resource_create.py +++ b/pcs_test/tier0/lib/commands/resource/test_resource_create.py @@ -23,6 +23,7 @@ def create( env, + *, wait=False, disabled=False, meta_attributes=None, @@ -75,6 +76,7 @@ def create_group( def create_clone( env, + *, wait=TIMEOUT, disabled=False, meta_attributes=None, diff --git a/pcs_test/tier0/lib/commands/resource/test_resource_enable_disable.py b/pcs_test/tier0/lib/commands/resource/test_resource_enable_disable.py index 03d0fdfff..51c71d94a 100644 --- a/pcs_test/tier0/lib/commands/resource/test_resource_enable_disable.py +++ b/pcs_test/tier0/lib/commands/resource/test_resource_enable_disable.py @@ -377,6 +377,7 @@ def get_fixture_master_cib( def get_fixture_clone_group_cib( + *, clone_disabled=False, clone_meta=False, group_disabled=False, diff --git a/pcs_test/tier0/lib/commands/resource/test_resource_manage_unmanage.py b/pcs_test/tier0/lib/commands/resource/test_resource_manage_unmanage.py index adc7da8b8..d540799f2 100644 --- a/pcs_test/tier0/lib/commands/resource/test_resource_manage_unmanage.py +++ b/pcs_test/tier0/lib/commands/resource/test_resource_manage_unmanage.py @@ -382,6 +382,7 @@ def get_fixture_clone_cib( def get_fixture_clone_group_cib( + *, clone_unmanaged=False, clone_meta=False, group_unmanaged=False, diff --git a/pcs_test/tier0/lib/commands/test_status.py b/pcs_test/tier0/lib/commands/test_status.py index f837dd5bd..45fb5f056 100644 --- a/pcs_test/tier0/lib/commands/test_status.py +++ b/pcs_test/tier0/lib/commands/test_status.py @@ -144,6 +144,7 @@ def _fixture_config_live_remote_minimal(self): def _fixture_config_local_daemons( self, + *, corosync_enabled=True, corosync_active=True, pacemaker_enabled=True, diff --git a/pcs_test/tier0/lib/commands/test_stonith_update_scsi_devices.py b/pcs_test/tier0/lib/commands/test_stonith_update_scsi_devices.py index f51b93222..31e9ac69e 100644 --- a/pcs_test/tier0/lib/commands/test_stonith_update_scsi_devices.py +++ b/pcs_test/tier0/lib/commands/test_stonith_update_scsi_devices.py @@ -384,6 +384,7 @@ def setUp(self): def config_cib( self, + *, devices_before=DEVICES_1, devices_updated=DEVICES_2, resource_ops=DEFAULT_OPS, @@ -465,6 +466,7 @@ def config_cib( def assert_command_success( self, + *, devices_before=DEVICES_1, devices_updated=DEVICES_2, devices_add=None, diff --git a/pcs_test/tier0/lib/resource_agent/test_pcs_transform.py b/pcs_test/tier0/lib/resource_agent/test_pcs_transform.py index 36f62cf68..e3214a867 100644 --- a/pcs_test/tier0/lib/resource_agent/test_pcs_transform.py +++ b/pcs_test/tier0/lib/resource_agent/test_pcs_transform.py @@ -95,6 +95,7 @@ def test_return_none(self): @mock.patch(f"{ra_pkg}._metadata_action_translate_role") class OcfUnifiedToPcs(TestCase): # pylint: disable=too-many-arguments + # pylint: disable=too-many-positional-arguments @staticmethod def _fixture_metadata(name): return ra.ResourceAgentMetadata( diff --git a/pcs_test/tier0/lib/test_cluster_property.py b/pcs_test/tier0/lib/test_cluster_property.py index 89fe25f60..29383c0c0 100644 --- a/pcs_test/tier0/lib/test_cluster_property.py +++ b/pcs_test/tier0/lib/test_cluster_property.py @@ -240,6 +240,7 @@ def assert_validate_set( configured_properties, new_properties, expected_report_list, + *, sbd_enabled=False, sbd_devices=False, force=False, diff --git a/pcs_test/tier1/cib_resource/test_create.py b/pcs_test/tier1/cib_resource/test_create.py index 93b8dbe45..01c8ead8a 100644 --- a/pcs_test/tier1/cib_resource/test_create.py +++ b/pcs_test/tier1/cib_resource/test_create.py @@ -923,6 +923,7 @@ def setUp(self): @staticmethod def fixture_options( + *, allow_absent_agent=False, allow_invalid_instance_attributes=False, allow_invalid_operation=False, diff --git a/pcs_test/tier1/test_tag.py b/pcs_test/tier1/test_tag.py index f64df93a4..73533aa1f 100644 --- a/pcs_test/tier1/test_tag.py +++ b/pcs_test/tier1/test_tag.py @@ -511,6 +511,7 @@ def setUp(self): def fixture_expected_config( self, + *, constraints=empty_constraints, pacemaker_nodes=empty_pacemaker_nodes, resources=empty_resources, diff --git a/pcs_test/tools/assertions.py b/pcs_test/tools/assertions.py index 99d575a21..e48bfabae 100644 --- a/pcs_test/tools/assertions.py +++ b/pcs_test/tools/assertions.py @@ -68,6 +68,7 @@ def assert_pcs_success( despace=False, ): # pylint: disable=too-many-arguments + # pylint: disable=too-many-positional-arguments # It is common that successful commands don't print anything, so we # default stdout and stderr to an empty string if not specified # otherwise. @@ -144,6 +145,7 @@ def assert_pcs_fail_regardless_of_force( def assert_pcs_result( self, command, + *, stdout_full=None, stdout_start=None, stdout_regexp=None, diff --git a/pcs_test/tools/cib.py b/pcs_test/tools/cib.py index c09d085af..131dd15e4 100644 --- a/pcs_test/tools/cib.py +++ b/pcs_test/tools/cib.py @@ -56,6 +56,7 @@ def assert_effect_single( self, command, expected_xml, + *, stdout_full=None, stdout_start=None, stdout_regexp=None, @@ -79,6 +80,7 @@ def assert_effect( self, alternative_cmds, expected_xml, + *, stdout_full=None, stdout_start=None, stdout_regexp=None, diff --git a/pcs_test/tools/color_text_runner/result.py b/pcs_test/tools/color_text_runner/result.py index f0eabfb5b..ed6688a83 100644 --- a/pcs_test/tools/color_text_runner/result.py +++ b/pcs_test/tools/color_text_runner/result.py @@ -125,6 +125,7 @@ def __chooseWriter(self): # pylint: disable=invalid-name def get_text_test_result_class( + *, slash_last_fail_in_overview=False, traditional_verbose=False, traceback_highlight=False, diff --git a/pcs_test/tools/command_env/config_env.py b/pcs_test/tools/command_env/config_env.py index 538954751..b421c0b21 100644 --- a/pcs_test/tools/command_env/config_env.py +++ b/pcs_test/tools/command_env/config_env.py @@ -82,6 +82,7 @@ def known_hosts_getter(self): def push_cib( self, + *, modifiers=None, name="env.push_cib", load_key="runner.cib.load", @@ -120,6 +121,7 @@ def push_cib( def push_cib_custom( self, + *, name="env.push_cib_custom", custom_cib=None, wait=-1, @@ -139,6 +141,7 @@ def push_cib_custom( def push_corosync_conf( self, + *, name="env.push_corosync_conf", corosync_conf_text="", skip_offline_targets=False, diff --git a/pcs_test/tools/command_env/config_http_booth.py b/pcs_test/tools/command_env/config_http_booth.py index 4f893c548..80a59c3a2 100644 --- a/pcs_test/tools/command_env/config_http_booth.py +++ b/pcs_test/tools/command_env/config_http_booth.py @@ -77,6 +77,7 @@ def get_config( def save_files( self, files_data, + *, saved=(), existing=(), failed=(), diff --git a/pcs_test/tools/command_env/config_http_files.py b/pcs_test/tools/command_env/config_http_files.py index d868dc77a..7cda5a5b4 100644 --- a/pcs_test/tools/command_env/config_http_files.py +++ b/pcs_test/tools/command_env/config_http_files.py @@ -12,6 +12,7 @@ def __init__(self, calls): def put_files( self, + *, node_labels=None, pcmk_authkey=None, corosync_authkey=None, diff --git a/pcs_test/tools/command_env/config_http_status.py b/pcs_test/tools/command_env/config_http_status.py index 2d0e5935e..16472f206 100644 --- a/pcs_test/tools/command_env/config_http_status.py +++ b/pcs_test/tools/command_env/config_http_status.py @@ -11,6 +11,7 @@ def __init__(self, calls): def get_full_cluster_status_plaintext( self, + *, node_labels=None, communication_list=None, name="http.status.get_full_cluster_status_plaintext", diff --git a/pcs_test/tools/command_env/config_raw_file.py b/pcs_test/tools/command_env/config_raw_file.py index b03f1b593..ce972297f 100644 --- a/pcs_test/tools/command_env/config_raw_file.py +++ b/pcs_test/tools/command_env/config_raw_file.py @@ -70,6 +70,7 @@ def write( file_type_code, path, file_data, + *, can_overwrite=False, already_exists=False, exception_msg=None, @@ -109,6 +110,7 @@ def remove( self, file_type_code, path, + *, fail_if_file_not_found=True, exception_msg=None, file_not_found_exception=False, diff --git a/pcs_test/tools/command_env/config_runner.py b/pcs_test/tools/command_env/config_runner.py index 2a8b502fa..1a6161588 100644 --- a/pcs_test/tools/command_env/config_runner.py +++ b/pcs_test/tools/command_env/config_runner.py @@ -21,6 +21,7 @@ def __init__(self, call_collection, wrap_helper): def place( self, command, + *, name="", stdout="", stderr="", diff --git a/pcs_test/tools/command_env/config_runner_booth.py b/pcs_test/tools/command_env/config_runner_booth.py index 0a2cd91c7..a0f001e4c 100644 --- a/pcs_test/tools/command_env/config_runner_booth.py +++ b/pcs_test/tools/command_env/config_runner_booth.py @@ -125,6 +125,7 @@ def ticket_grant( self, ticket_name, site_ip, + *, stdout="", stderr="", returncode=0, @@ -163,6 +164,7 @@ def ticket_revoke( self, ticket_name, site_ip, + *, stdout="", stderr="", returncode=0, diff --git a/pcs_test/tools/command_env/config_runner_cib.py b/pcs_test/tools/command_env/config_runner_cib.py index 991811392..16273b2b0 100644 --- a/pcs_test/tools/command_env/config_runner_cib.py +++ b/pcs_test/tools/command_env/config_runner_cib.py @@ -16,6 +16,7 @@ def __init__(self, calls): def load( self, + *, modifiers=None, name="runner.cib.load", filename=None, diff --git a/pcs_test/tools/command_env/config_runner_pcmk.py b/pcs_test/tools/command_env/config_runner_pcmk.py index 6faa7e233..62508845d 100644 --- a/pcs_test/tools/command_env/config_runner_pcmk.py +++ b/pcs_test/tools/command_env/config_runner_pcmk.py @@ -159,6 +159,7 @@ def fence_history_update( def load_state( self, + *, name="runner.pcmk.load_state", filename="crm_mon.minimal.xml", resources=None, @@ -217,6 +218,7 @@ def load_state( def load_state_plaintext( self, + *, name="runner.pcmk.load_state_plaintext", inactive=True, verbose=False, @@ -355,6 +357,7 @@ def list_agents_for_standard_and_provider( def load_agent( self, + *, name="runner.pcmk.load_agent", agent_name="ocf:heartbeat:Dummy", agent_filename=None, @@ -376,23 +379,12 @@ def load_agent( placed dict env -- CommandRunner environment variables """ - if env: - env = dict(env) - else: - env = {} - env["PATH"] = ":".join( - [ - settings.fence_agent_execs, - "/bin", - "/usr/bin", - ] - ) - - if agent_filename: - agent_metadata_filename = agent_filename - elif agent_name in AGENT_FILENAME_MAP: - agent_metadata_filename = AGENT_FILENAME_MAP[agent_name] - elif not stdout and not agent_is_missing: + if ( + not agent_is_missing + and not stdout + and not agent_filename + and agent_name not in AGENT_FILENAME_MAP + ): raise AssertionError( ( "Filename with metadata of agent '{0}' not specified.\n" @@ -407,6 +399,18 @@ def load_agent( ).format(agent_name, os.path.realpath(__file__), rc("")) ) + if env: + env = dict(env) + else: + env = {} + env["PATH"] = ":".join( + [ + settings.fence_agent_execs, + "/bin", + "/usr/bin", + ] + ) + if agent_is_missing: if stderr is None: stderr = ( @@ -429,6 +433,10 @@ def load_agent( return if not stdout: + if agent_filename: + agent_metadata_filename = agent_filename + else: + agent_metadata_filename = AGENT_FILENAME_MAP[agent_name] with open(rc(agent_metadata_filename)) as a_file: stdout = a_file.read() self.__calls.place( @@ -568,6 +576,7 @@ def resource_restart( def resource_cleanup( self, + *, name="runner.pcmk.cleanup", instead=None, before=None, @@ -614,6 +623,7 @@ def resource_cleanup( def resource_refresh( self, + *, name="runner.pcmk.refresh", instead=None, before=None, @@ -660,6 +670,7 @@ def resource_refresh( def resource_move( self, + *, name="runner.pcmk.resource_move", instead=None, before=None, @@ -698,6 +709,7 @@ def resource_move( def resource_ban( self, + *, name="runner.pcmk.resource_ban", instead=None, before=None, @@ -734,6 +746,7 @@ def resource_ban( def resource_clear( self, + *, name="runner.pcmk.resource_clear", instead=None, before=None, @@ -774,6 +787,7 @@ def _resource_move_ban_clear( self, name, action, + *, instead=None, before=None, resource=None, @@ -972,6 +986,7 @@ def simulate_cib( self, new_cib_filepath, transitions_filepath, + *, cib_xml=None, cib_modifiers=None, cib_load_name="runner.cib.load", @@ -1064,6 +1079,7 @@ def resource_agent_self_validation( standard="ocf", provider="heartbeat", agent_type="Dummy", + *, returncode=0, output=None, stdout="", @@ -1117,6 +1133,7 @@ def stonith_agent_self_validation( self, attributes, agent, + *, returncode=0, output=None, stdout="", diff --git a/pcs_test/tools/command_env/mock_node_communicator.py b/pcs_test/tools/command_env/mock_node_communicator.py index 40c88e2a3..47890cf79 100644 --- a/pcs_test/tools/command_env/mock_node_communicator.py +++ b/pcs_test/tools/command_env/mock_node_communicator.py @@ -123,6 +123,7 @@ def _communication_to_response( raw_data, ): # pylint: disable=too-many-arguments + # pylint: disable=too-many-positional-arguments return Response( MockCurlSimple( info={pycurl.RESPONSE_CODE: response_code}, @@ -148,6 +149,7 @@ def _communication_to_response( def create_communication( communication_list, + *, action="", param_list=None, response_code=200, diff --git a/pcs_test/tools/command_env/tools.py b/pcs_test/tools/command_env/tools.py index 8b5cadb26..23670f22a 100644 --- a/pcs_test/tools/command_env/tools.py +++ b/pcs_test/tools/command_env/tools.py @@ -8,6 +8,7 @@ def get_env_tools( test_case, + *, base_cib_filename=CIB_FILENAME, default_wait_timeout=DEFAULT_WAIT_TIMEOUT, default_wait_error_returncode=WAIT_TIMEOUT_EXPIRED_RETURNCODE, diff --git a/pyproject.toml b/pyproject.toml index 87f12490f..fcfd1cae2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,7 @@ good-names = [ [tool.pylint.design] max-args = 8 max-parents = 10 +max-positional-arguments = 8 min-public-methods = 0 [tool.pylint.format] From 3113625c07e9284983e95da84c2f32e71600d7ec Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Thu, 21 Nov 2024 15:41:24 +0100 Subject: [PATCH 058/227] update black to 24.10.0 --- dev_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index b2e94c8bb..438e68c8b 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -2,7 +2,7 @@ lxml-stubs pylint==3.3.2 astroid==3.3.5 mypy==1.13.0 -black==24.3.0 +black==24.10.0 isort types-cryptography types-dataclasses From aacb9447cdb307a4b7b0eaf9b0e5f32fd4f13027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Posp=C3=AD=C5=A1il?= Date: Fri, 10 Jan 2025 15:49:38 +0100 Subject: [PATCH 059/227] Bumped to 0.11.9 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb17f1e81..15b0dd587 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log -## [Unreleased] +## [0.11.9] - 2025-01-10 ### Added - Support for output formats `json` and `cmd` to `pcs tag config` command From e4f7888a568a0451360c8ba98e41b592b06d5699 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Thu, 9 Jan 2025 14:19:39 +0100 Subject: [PATCH 060/227] fix mypy info note --- pcs/constraint.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pcs/constraint.py b/pcs/constraint.py index 6c87a054f..64edc7e69 100644 --- a/pcs/constraint.py +++ b/pcs/constraint.py @@ -876,7 +876,9 @@ def _verify_score(score): ) -def location_prefer(lib, argv, modifiers): +def location_prefer( + lib: Any, argv: list[str], modifiers: parse_args.InputModifiers +) -> None: """ Options: * --force - allow unknown options, allow constraint for any resource type From 8aceff61a73755d3603a27f9686ace0e899d3e2d Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Mon, 25 Nov 2024 11:41:24 +0100 Subject: [PATCH 061/227] add ruff python linter and formatter --- Makefile.am | 26 ++++++++++++++++++++++++++ configure.ac | 2 +- dev_requirements.txt | 1 + pyproject.toml | 41 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index 038844579..45ac116ff 100644 --- a/Makefile.am +++ b/Makefile.am @@ -202,6 +202,32 @@ endif $(TIME) $(PYTHON) -m pylint --rcfile pyproject.toml ${pylint_options} ${PCS_PYTHON_PACKAGES} endif +ruff_format_check: pyproject.toml +if DEV_TESTS + $(TIME) ruff --config pyproject.toml format --check ${PCS_PYTHON_PACKAGES} +endif + +ruff_format: pyproject.toml +if DEV_TESTS + $(TIME) ruff --config pyproject.toml check --select I --fix ${PCS_PYTHON_PACKAGES} + $(TIME) ruff --config pyproject.toml format ${PCS_PYTHON_PACKAGES} +endif + +ruff_isort_check: pyproject.toml +if DEV_TESTS + $(TIME) ruff --config pyproject.toml check --select I ${PCS_PYTHON_PACKAGES} +endif + +ruff_isort: pyproject.toml +if DEV_TESTS + $(TIME) ruff --config pyproject.toml check --select I --fix ${PCS_PYTHON_PACKAGES} +endif + +ruff_lint: pyproject.toml +if DEV_TESTS + $(TIME) ruff --config pyproject.toml check ${PCS_PYTHON_PACKAGES} +endif + isort_check: pyproject.toml if DEV_TESTS export PYTHONPATH=${abs_top_builddir}/${PCS_BUNDLED_DIR_LOCAL}/packages && \ diff --git a/configure.ac b/configure.ac index a7718d6d6..607c15176 100644 --- a/configure.ac +++ b/configure.ac @@ -129,7 +129,7 @@ fi # configure options section AC_ARG_ENABLE([dev-tests], - [AS_HELP_STRING([--enable-dev-tests], [Enable extra developers tests (black, isort, mypy, pylint) (default: no)])], + [AS_HELP_STRING([--enable-dev-tests], [Enable extra developers tests (black, isort, mypy, pylint, ruff) (default: no)])], [dev_tests="yes"]) AM_CONDITIONAL([DEV_TESTS], [test "x$dev_tests" = "xyes"]) diff --git a/dev_requirements.txt b/dev_requirements.txt index 438e68c8b..29718086d 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -4,6 +4,7 @@ astroid==3.3.5 mypy==1.13.0 black==24.10.0 isort +ruff==0.8.0 types-cryptography types-dataclasses # later versions remove type annotations from a few functions causing diff --git a/pyproject.toml b/pyproject.toml index fcfd1cae2..b0f428213 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,3 +70,44 @@ max-line-length = 80 [tool.pylint.variables] dummy-variables-rgx = "_$|dummy" + +[tool.ruff] +# ruff settings docs: https://docs.astral.sh/ruff/settings/ +line-length = 80 +target-version = "py39" + +[tool.ruff.lint] +# ruff rules docs: https://docs.astral.sh/ruff/rules/ +# pylint rules in ruff: # https://github.com/astral-sh/ruff/issues/970 +select = [ + "PL", # pylint convention, error, refactoring, warning +] +# ruff does not respect pylint ignore directives +# https://github.com/astral-sh/ruff/issues/1203 +ignore = [ + "PLR2004", # magic-value-comparison (99) + "PLR0913", # too-many-arguments (54) + "PLR0912", # too-many-branches (49) + "PLR0915", # too-many-statements (31) + "PLW2901", # redefined-loop-name (22) + "PLR5501", # collapsible-else-if (14) + "PLW0603", # global-statement (9) + "PLR0911", # too-many-return-statements (6) + "PLW1509", # subprocess-popen-preexec-fn (3) + "PLW0602", # global-variable-not-assigned (1) +] + +[tool.ruff.lint.isort] +# known deviations from isort: +# https://docs.astral.sh/ruff/faq/#how-does-ruffs-import-sorting-compare-to-isort +# https://github.com/astral-sh/ruff/issues/1381 +# https://github.com/astral-sh/ruff/issues/2104 +known-first-party = ["pcs"] +section-order = ["future", "standard-library", "third-party", "first-party", "tests", "local-folder"] + +[tool.ruff.lint.isort.sections] +"tests" = ["pcs_test"] + +[tool.ruff.lint.pylint] +max-args = 8 +max-positional-args = 8 From 0878eee9845af5cc4094233725d9ac76ca264cf0 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Wed, 8 Jan 2025 15:02:35 +0100 Subject: [PATCH 062/227] ruff: enable ruff linter `I` (isort) and re-format with ruff isort * https://docs.astral.sh/ruff/rules/#isort-i * deviations from isort: * https://github.com/astral-sh/ruff/issues/1381 * https://github.com/astral-sh/ruff/issues/2104 --- pcs/cluster.py | 3 +-- pcs/daemon/app/common.py | 2 +- pcs/daemon/run.py | 4 +--- pcs/lib/cib/fencing_topology.py | 2 +- pcs/lib/commands/cluster.py | 5 ++--- pcs/lib/commands/cluster_property.py | 2 +- pcs/lib/file/raw_file.py | 4 ++-- pcs/utils.py | 6 ++---- pcs_test/tier1/legacy/test_alert.py | 4 ++-- pcs_test/tier1/legacy/test_cluster.py | 4 ++-- pcs_test/tier1/legacy/test_constraints.py | 4 ++-- pcs_test/tier1/legacy/test_stonith.py | 4 ++-- pcs_test/tier1/test_cluster_pcmk_remote.py | 3 +-- pcs_test/tier1/test_quorum.py | 4 ++-- pyproject.toml | 1 + 15 files changed, 23 insertions(+), 29 deletions(-) diff --git a/pcs/cluster.py b/pcs/cluster.py index 070287c1f..7cd7835b7 100644 --- a/pcs/cluster.py +++ b/pcs/cluster.py @@ -68,9 +68,8 @@ from pcs.lib import sbd as lib_sbd from pcs.lib.commands.remote_node import _destroy_pcmk_remote_env from pcs.lib.communication.nodes import CheckAuth -from pcs.lib.communication.tools import RunRemotelyBase +from pcs.lib.communication.tools import RunRemotelyBase, run_and_raise from pcs.lib.communication.tools import run as run_com_cmd -from pcs.lib.communication.tools import run_and_raise from pcs.lib.corosync import qdevice_net from pcs.lib.corosync.live import ( QuorumStatusException, diff --git a/pcs/daemon/app/common.py b/pcs/daemon/app/common.py index 7dfc2c000..f2b6284a6 100644 --- a/pcs/daemon/app/common.py +++ b/pcs/daemon/app/common.py @@ -8,9 +8,9 @@ from tornado.web import ( Finish, HTTPError, + RequestHandler, ) from tornado.web import RedirectHandler as TornadoRedirectHandler -from tornado.web import RequestHandler RoutesType = Iterable[ tuple[str, Type[RequestHandler], Optional[dict[str, Any]]] diff --git a/pcs/daemon/run.py b/pcs/daemon/run.py index 5518ace97..d9ec28e0e 100644 --- a/pcs/daemon/run.py +++ b/pcs/daemon/run.py @@ -31,13 +31,11 @@ api_v1, api_v2, auth, -) -from pcs.daemon.app import capabilities as capabilities_app -from pcs.daemon.app import ( sinatra_remote, sinatra_ui, ui, ) +from pcs.daemon.app import capabilities as capabilities_app from pcs.daemon.app.common import ( Http404Handler, RedirectHandler, diff --git a/pcs/lib/cib/fencing_topology.py b/pcs/lib/cib/fencing_topology.py index 51c97b7ec..b521364c7 100644 --- a/pcs/lib/cib/fencing_topology.py +++ b/pcs/lib/cib/fencing_topology.py @@ -32,9 +32,9 @@ ReportItemList, ReportItemSeverity, ReportProcessor, + has_errors, ) from pcs.common.reports import codes as report_codes -from pcs.common.reports import has_errors from pcs.common.reports.item import ReportItem from pcs.common.types import StringSequence from pcs.common.validate import is_integer diff --git a/pcs/lib/commands/cluster.py b/pcs/lib/commands/cluster.py index 42b4ba9e2..82738dc3d 100644 --- a/pcs/lib/commands/cluster.py +++ b/pcs/lib/commands/cluster.py @@ -69,16 +69,15 @@ EnableSbdService, SetSbdConfig, ) -from pcs.lib.communication.tools import AllSameDataMixin +from pcs.lib.communication.tools import AllSameDataMixin, run_and_raise from pcs.lib.communication.tools import run as run_com -from pcs.lib.communication.tools import run_and_raise from pcs.lib.corosync import ( config_facade, config_parser, config_validators, + qdevice_net, ) from pcs.lib.corosync import constants as corosync_constants -from pcs.lib.corosync import qdevice_net from pcs.lib.env import ( LibraryEnvironment, WaitType, diff --git a/pcs/lib/commands/cluster_property.py b/pcs/lib/commands/cluster_property.py index a25858cc4..000dacfb1 100644 --- a/pcs/lib/commands/cluster_property.py +++ b/pcs/lib/commands/cluster_property.py @@ -24,9 +24,9 @@ ResourceAgentError, ResourceAgentFacade, ResourceAgentMetadata, + resource_agent_error_to_report_item, ) from pcs.lib.resource_agent import const as ra_const -from pcs.lib.resource_agent import resource_agent_error_to_report_item from pcs.lib.resource_agent.facade import ResourceAgentFacadeFactory diff --git a/pcs/lib/file/raw_file.py b/pcs/lib/file/raw_file.py index 55707abd7..10b420080 100644 --- a/pcs/lib/file/raw_file.py +++ b/pcs/lib/file/raw_file.py @@ -13,12 +13,12 @@ # places # pylint: disable=unused-import from pcs.common import reports -from pcs.common.file import FileMetadata -from pcs.common.file import RawFile as RealFile from pcs.common.file import ( + FileMetadata, RawFileError, RawFileInterface, ) +from pcs.common.file import RawFile as RealFile # TODO add logging (logger / debug reports ?) diff --git a/pcs/utils.py b/pcs/utils.py index f32abb8c2..4bd0ca1d0 100644 --- a/pcs/utils.py +++ b/pcs/utils.py @@ -44,12 +44,10 @@ timeout_to_seconds_legacy, ) from pcs.cli.file import metadata as cli_file_metadata -from pcs.cli.reports import ReportProcessorToConsole +from pcs.cli.reports import ReportProcessorToConsole, process_library_reports from pcs.cli.reports import output as reports_output -from pcs.cli.reports import process_library_reports -from pcs.common import const +from pcs.common import const, file_type_codes from pcs.common import file as pcs_file -from pcs.common import file_type_codes from pcs.common import pacemaker as common_pacemaker from pcs.common import pcs_pycurl as pycurl from pcs.common.host import PcsKnownHost diff --git a/pcs_test/tier1/legacy/test_alert.py b/pcs_test/tier1/legacy/test_alert.py index 825d03cf7..acb1a8f88 100644 --- a/pcs_test/tier1/legacy/test_alert.py +++ b/pcs_test/tier1/legacy/test_alert.py @@ -1,13 +1,13 @@ import unittest from pcs_test.tools.assertions import AssertPcsMixin -from pcs_test.tools.misc import ParametrizedTestMetaClass -from pcs_test.tools.misc import get_test_resource as rc from pcs_test.tools.misc import ( + ParametrizedTestMetaClass, get_tmp_file, outdent, write_file_to_tmpfile, ) +from pcs_test.tools.misc import get_test_resource as rc from pcs_test.tools.pcs_runner import PcsRunner empty_cib = rc("cib-empty.xml") diff --git a/pcs_test/tier1/legacy/test_cluster.py b/pcs_test/tier1/legacy/test_cluster.py index ebb9b6cb1..9051fd0f0 100644 --- a/pcs_test/tier1/legacy/test_cluster.py +++ b/pcs_test/tier1/legacy/test_cluster.py @@ -4,9 +4,8 @@ from unittest import TestCase from pcs_test.tools.assertions import AssertPcsMixin -from pcs_test.tools.misc import compare_version -from pcs_test.tools.misc import get_test_resource as rc from pcs_test.tools.misc import ( + compare_version, get_tmp_dir, get_tmp_file, outdent, @@ -14,6 +13,7 @@ skip_unless_root, write_file_to_tmpfile, ) +from pcs_test.tools.misc import get_test_resource as rc from pcs_test.tools.pcs_runner import ( PcsRunner, pcs, diff --git a/pcs_test/tier1/legacy/test_constraints.py b/pcs_test/tier1/legacy/test_constraints.py index d3dcdf95f..273a62084 100644 --- a/pcs_test/tier1/legacy/test_constraints.py +++ b/pcs_test/tier1/legacy/test_constraints.py @@ -25,14 +25,14 @@ wrap_element_by_master, wrap_element_by_master_file, ) -from pcs_test.tools.misc import ParametrizedTestMetaClass -from pcs_test.tools.misc import get_test_resource as rc from pcs_test.tools.misc import ( + ParametrizedTestMetaClass, get_tmp_file, outdent, skip_unless_crm_rule, write_file_to_tmpfile, ) +from pcs_test.tools.misc import get_test_resource as rc from pcs_test.tools.pcs_runner import ( PcsRunner, pcs, diff --git a/pcs_test/tier1/legacy/test_stonith.py b/pcs_test/tier1/legacy/test_stonith.py index 8daf073a6..96724dfd8 100644 --- a/pcs_test/tier1/legacy/test_stonith.py +++ b/pcs_test/tier1/legacy/test_stonith.py @@ -10,9 +10,8 @@ from pcs_test.tools.assertions import AssertPcsMixin from pcs_test.tools.bin_mock import get_mock_settings from pcs_test.tools.fixture_cib import CachedCibFixture -from pcs_test.tools.misc import ParametrizedTestMetaClass -from pcs_test.tools.misc import get_test_resource as rc from pcs_test.tools.misc import ( + ParametrizedTestMetaClass, get_tmp_file, is_minimum_pacemaker_version, outdent, @@ -20,6 +19,7 @@ write_data_to_tmpfile, write_file_to_tmpfile, ) +from pcs_test.tools.misc import get_test_resource as rc from pcs_test.tools.pcs_runner import PcsRunner PCMK_2_0_3_PLUS = is_minimum_pacemaker_version(2, 0, 3) diff --git a/pcs_test/tier1/test_cluster_pcmk_remote.py b/pcs_test/tier1/test_cluster_pcmk_remote.py index 8979e8df5..2241d1ec6 100644 --- a/pcs_test/tier1/test_cluster_pcmk_remote.py +++ b/pcs_test/tier1/test_cluster_pcmk_remote.py @@ -2,9 +2,8 @@ from pcs_test.tier1.cib_resource.common import ResourceTest from pcs_test.tools.bin_mock import get_mock_settings -from pcs_test.tools.misc import ParametrizedTestMetaClass +from pcs_test.tools.misc import ParametrizedTestMetaClass, write_data_to_tmpfile from pcs_test.tools.misc import get_test_resource as rc -from pcs_test.tools.misc import write_data_to_tmpfile ERRORS_HAVE_OCCURRED = ( "Error: Errors have occurred, therefore pcs is unable to continue\n" diff --git a/pcs_test/tier1/test_quorum.py b/pcs_test/tier1/test_quorum.py index d697f07f4..012d79f3d 100644 --- a/pcs_test/tier1/test_quorum.py +++ b/pcs_test/tier1/test_quorum.py @@ -2,12 +2,12 @@ from unittest import TestCase from pcs_test.tools.assertions import AssertPcsMixin -from pcs_test.tools.misc import ParametrizedTestMetaClass -from pcs_test.tools.misc import get_test_resource as rc from pcs_test.tools.misc import ( + ParametrizedTestMetaClass, get_tmp_file, write_file_to_tmpfile, ) +from pcs_test.tools.misc import get_test_resource as rc from pcs_test.tools.pcs_runner import PcsRunner coro_conf = rc("corosync.conf") diff --git a/pyproject.toml b/pyproject.toml index b0f428213..8030d0f1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,6 +80,7 @@ target-version = "py39" # ruff rules docs: https://docs.astral.sh/ruff/rules/ # pylint rules in ruff: # https://github.com/astral-sh/ruff/issues/970 select = [ + "I", "PL", # pylint convention, error, refactoring, warning ] # ruff does not respect pylint ignore directives From 38d64f232c192f5fb652cc8320f0db7dd94e0b90 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Wed, 8 Jan 2025 17:16:07 +0100 Subject: [PATCH 063/227] ruff: re-format with ruff deviations from black: * https://docs.astral.sh/ruff/formatter/black/#call-chain-calls-break-differently-in-some-cases * https://docs.astral.sh/ruff/formatter/black/#own-line-comments-on-expressions-dont-cause-the-expression-to-expand * https://docs.astral.sh/ruff/formatter/black/#trailing-commas-are-inserted-when-expanding-a-function-definition-with-a-single-argument * https://github.com/astral-sh/ruff/issues/11820 * https://github.com/astral-sh/ruff/issues/14143 * https://github.com/astral-sh/ruff/issues/8598 --- Makefile.am | 2 +- pcs/cli/common/tools.py | 2 +- pcs/cli/resource/output.py | 4 ++- pcs/common/interface/dto.py | 10 +++---- pcs/common/pacemaker/constraint/all.py | 4 +-- pcs/common/reports/messages.py | 8 ++---- pcs/common/resource_status.py | 2 +- pcs/common/str_tools.py | 4 +-- pcs/lib/cib/constraint/ticket.py | 3 +-- pcs/lib/cib/resource/remote_node.py | 4 +-- pcs/lib/cib/resource/stonith.py | 4 +-- pcs/lib/commands/resource.py | 4 +-- pcs/lib/corosync/config_validators.py | 6 ++--- pcs/lib/pacemaker/state.py | 4 +-- pcs/lib/resource_agent/ocf_transform.py | 2 +- pcs/lib/validate.py | 4 +-- pcs_test/suite.py | 2 +- .../cli/cluster_property/test_command.py | 4 +-- .../tier0/common/test_tools_xml_fromstring.py | 8 ++---- pcs_test/tier0/daemon/async_tasks/helpers.py | 8 +++--- pcs_test/tier0/lib/auth/test_provider.py | 16 +++--------- pcs_test/tier0/lib/booth/test_resource.py | 12 +++------ .../lib/cib/resource/test_validations.py | 4 +-- pcs_test/tier0/lib/cib/test_acl.py | 12 +++------ .../remote_node/test_node_add_guest.py | 8 ++---- .../remote_node/test_node_remove_guest.py | 8 ++---- .../remote_node/test_node_remove_remote.py | 12 +++------ .../lib/commands/resource/bundle_common.py | 16 +++--------- .../commands/resource/test_bundle_update.py | 24 +++++------------ .../resource/test_resource_move_autoclean.py | 24 +++++------------ pcs_test/tier0/lib/commands/test_quorum.py | 26 ++++++++----------- pcs_test/tier0/lib/commands/test_status.py | 4 +-- pcs_test/tier0/lib/commands/test_stonith.py | 4 +-- pcs_test/tier0/lib/commands/test_ticket.py | 4 +-- .../corosync/test_config_validators_links.py | 18 ++++++------- .../tier1/cib_resource/test_clone_unclone.py | 8 ++---- .../tier1/cib_resource/test_group_ungroup.py | 4 +-- .../cib_resource/test_manage_unmanage.py | 4 +-- pcs_test/tier1/legacy/test_resource.py | 4 +-- pcs_test/tier1/test_booth.py | 16 +++--------- pcs_test/tier1/test_status.py | 4 +-- 41 files changed, 100 insertions(+), 221 deletions(-) diff --git a/Makefile.am b/Makefile.am index 45ac116ff..a8b2d097d 100644 --- a/Makefile.am +++ b/Makefile.am @@ -350,7 +350,7 @@ test-tree-clean: fi find ${abs_top_builddir} -type d -name __pycache__ -exec rm -rf {} \; 2>/dev/null || : -check-local: check-local-deps test-tree-prep typos_check pylint isort_check black_check mypy tests_tier0 tests_tier1 pcsd-tests test-tree-clean +check-local: check-local-deps test-tree-prep typos_check pylint ruff_isort_check ruff_format_check mypy tests_tier0 tests_tier1 pcsd-tests test-tree-clean clean-local: test-tree-clean $(PYTHON) setup.py clean diff --git a/pcs/cli/common/tools.py b/pcs/cli/common/tools.py index f06539e17..6811f6a78 100644 --- a/pcs/cli/common/tools.py +++ b/pcs/cli/common/tools.py @@ -5,7 +5,7 @@ def timeout_to_seconds_legacy( - timeout: Union[int, str] + timeout: Union[int, str], ) -> Union[int, str, None]: """ Transform pacemaker style timeout to number of seconds. If timeout is not diff --git a/pcs/cli/resource/output.py b/pcs/cli/resource/output.py index 1df2b0e20..e2064a408 100644 --- a/pcs/cli/resource/output.py +++ b/pcs/cli/resource/output.py @@ -286,7 +286,9 @@ def get_primitive_dto( def get_group_dto(self, obj_id: str) -> Optional[CibResourceGroupDto]: return self._groups_map.get(obj_id) - def _get_any_resource_dto(self, obj_id: str) -> Optional[ + def _get_any_resource_dto( + self, obj_id: str + ) -> Optional[ Union[ CibResourcePrimitiveDto, CibResourceGroupDto, diff --git a/pcs/common/interface/dto.py b/pcs/common/interface/dto.py index 60b187988..25748716d 100644 --- a/pcs/common/interface/dto.py +++ b/pcs/common/interface/dto.py @@ -80,14 +80,12 @@ def _convert_dict( value = [ # ignore _field.type may not have __args__ # this is prevented by _is_compatible_type - _convert_dict(_field.type.__args__[0], item) # type: ignore + _convert_dict(_field.type.__args__[0], item) # type: ignore[union-attr] for item in value ] elif isinstance(value, dict) and _is_compatible_type(_field.type, 1): value = { - item_key: _convert_dict( - _field.type.__args__[1], item_val # type: ignore - ) + item_key: _convert_dict(_field.type.__args__[1], item_val) # type: ignore[union-attr,arg-type] for item_key, item_val in value.items() } elif isinstance(value, Enum): @@ -124,9 +122,7 @@ def _convert_payload(klass: type[DTOTYPE], data: DtoPayload) -> DtoPayload: ] elif isinstance(value, dict) and _is_compatible_type(_field.type, 1): value = { - item_key: _convert_payload( - _field.type.__args__[1], item_val # type: ignore - ) + item_key: _convert_payload(_field.type.__args__[1], item_val) # type: ignore[union-attr,arg-type] for item_key, item_val in value.items() } del new_dict[new_name] diff --git a/pcs/common/pacemaker/constraint/all.py b/pcs/common/pacemaker/constraint/all.py index 769f904c8..f8225eeb5 100644 --- a/pcs/common/pacemaker/constraint/all.py +++ b/pcs/common/pacemaker/constraint/all.py @@ -50,7 +50,7 @@ def _get_constraint_ids( CibConstraintTicketDto, CibConstraintTicketSetDto, ] - ] + ], ) -> list[str]: return [ constraint_dto.attributes.constraint_id @@ -64,7 +64,7 @@ def _get_location_rule_ids( CibConstraintLocationDto, CibConstraintLocationSetDto, ] - ] + ], ) -> list[str]: return [ rule_dto.id diff --git a/pcs/common/reports/messages.py b/pcs/common/reports/messages.py index 0809c91d1..7c0b66dac 100644 --- a/pcs/common/reports/messages.py +++ b/pcs/common/reports/messages.py @@ -1611,9 +1611,7 @@ class ParseErrorCorosyncConfMissingSectionNameBeforeOpeningBrace( Corosync config cannot be parsed due to a section name missing before { """ - _code = ( - codes.PARSE_ERROR_COROSYNC_CONF_MISSING_SECTION_NAME_BEFORE_OPENING_BRACE - ) + _code = codes.PARSE_ERROR_COROSYNC_CONF_MISSING_SECTION_NAME_BEFORE_OPENING_BRACE @property def message(self) -> str: @@ -1643,9 +1641,7 @@ class ParseErrorCorosyncConfExtraCharactersBeforeOrAfterClosingBrace( Corosync config cannot be parsed due to extra characters before or after } """ - _code = ( - codes.PARSE_ERROR_COROSYNC_CONF_EXTRA_CHARACTERS_BEFORE_OR_AFTER_CLOSING_BRACE - ) + _code = codes.PARSE_ERROR_COROSYNC_CONF_EXTRA_CHARACTERS_BEFORE_OR_AFTER_CLOSING_BRACE @property def message(self) -> str: diff --git a/pcs/common/resource_status.py b/pcs/common/resource_status.py index fea91a903..25fc1348c 100644 --- a/pcs/common/resource_status.py +++ b/pcs/common/resource_status.py @@ -1088,7 +1088,7 @@ def _is_orphaned(resource: Union[PrimitiveStatusDto, GroupStatusDto]) -> bool: def _filter_clone_orphans( - instance_list: Sequence[Union[PrimitiveStatusDto, GroupStatusDto]] + instance_list: Sequence[Union[PrimitiveStatusDto, GroupStatusDto]], ) -> list[Union[PrimitiveStatusDto, GroupStatusDto]]: return [ instance for instance in instance_list if not _is_orphaned(instance) diff --git a/pcs/common/str_tools.py b/pcs/common/str_tools.py index 2afbe4f10..4b5554fa3 100644 --- a/pcs/common/str_tools.py +++ b/pcs/common/str_tools.py @@ -99,7 +99,7 @@ def format_name_value_list(item_list: Sequence[tuple[str, str]]) -> list[str]: # For now, tuple[str, str, str] is sufficient. Feel free to change it if # needed, e.g. when values can be integers. def format_name_value_id_list( - item_list: Sequence[tuple[str, str, str]] + item_list: Sequence[tuple[str, str, str]], ) -> list[str]: """ Turn 3-tuples to 'name=value (id: id))' strings with standard quoting @@ -119,7 +119,7 @@ def pairs_to_text(pairs: Sequence[tuple[str, str]]) -> list[str]: def format_name_value_default_list( - item_list: Sequence[tuple[str, str, bool]] + item_list: Sequence[tuple[str, str, bool]], ) -> list[str]: """ Turn 3-tuples to 'name=value' or 'name=value (default)' strings with diff --git a/pcs/lib/cib/constraint/ticket.py b/pcs/lib/cib/constraint/ticket.py index a60a170cf..6a7eaddd8 100644 --- a/pcs/lib/cib/constraint/ticket.py +++ b/pcs/lib/cib/constraint/ticket.py @@ -154,8 +154,7 @@ def prepare_options_plain( validate.NamesIn( # rsc and rsc-ticket are passed as parameters not as items in the # options dict - (set(ATTRIB) | set(ATTRIB_PLAIN) | {"id"}) - - {"rsc", "ticket"} + (set(ATTRIB) | set(ATTRIB_PLAIN) | {"id"}) - {"rsc", "ticket"} ).validate(options) ) diff --git a/pcs/lib/cib/resource/remote_node.py b/pcs/lib/cib/resource/remote_node.py index fb8719c1e..621590a3d 100644 --- a/pcs/lib/cib/resource/remote_node.py +++ b/pcs/lib/cib/resource/remote_node.py @@ -25,9 +25,7 @@ _IS_REMOTE_AGENT_XPATH_SNIPPET = """ @class="{0}" and @provider="{1}" and @type="{2}" -""".format( - AGENT_NAME.standard, AGENT_NAME.provider, AGENT_NAME.type -) +""".format(AGENT_NAME.standard, AGENT_NAME.provider, AGENT_NAME.type) _HAS_SERVER_XPATH_SNIPPET = """ instance_attributes/nvpair[ diff --git a/pcs/lib/cib/resource/stonith.py b/pcs/lib/cib/resource/stonith.py index 0285fe78a..1ce95cbd8 100644 --- a/pcs/lib/cib/resource/stonith.py +++ b/pcs/lib/cib/resource/stonith.py @@ -244,9 +244,7 @@ def _get_lrm_rsc_op_elements( ./status/node_state[@uname=$node_name] /lrm/lrm_resources/lrm_resource[@id=$resource_id] /lrm_rsc_op[@operation=$op_name{interval}] - """.format( - interval=" and @interval=$interval" if interval else "" - ), + """.format(interval=" and @interval=$interval" if interval else ""), node_name=node_name, resource_id=resource_id, op_name=op_name, diff --git a/pcs/lib/commands/resource.py b/pcs/lib/commands/resource.py index 719f8acd3..d0e752ce3 100644 --- a/pcs/lib/commands/resource.py +++ b/pcs/lib/commands/resource.py @@ -2041,7 +2041,7 @@ def ban(env, resource_id, node=None, master=False, lifetime=None, wait=False): def _resource_running_on_nodes( - resource_state: Dict[str, List[str]] + resource_state: Dict[str, List[str]], ) -> FrozenSet[str]: if resource_state: return frozenset( @@ -2473,7 +2473,7 @@ def _find_resources_expand_tags( def get_required_cib_version_for_primitive( - op_list: Iterable[Mapping[str, str]] + op_list: Iterable[Mapping[str, str]], ) -> Optional[Version]: for op in op_list: if op.get("on-fail", "") == "demote": diff --git a/pcs/lib/corosync/config_validators.py b/pcs/lib/corosync/config_validators.py index c28a08e15..980dcf5ea 100644 --- a/pcs/lib/corosync/config_validators.py +++ b/pcs/lib/corosync/config_validators.py @@ -912,9 +912,7 @@ def _update_link_options_knet( ) ).validate(new_options) + validate.ValidatorAll( _get_link_options_validators_knet_relations() - ).validate( - after_update - ) + ).validate(after_update) def add_link( @@ -2039,7 +2037,7 @@ def _get_qdevice_generic_options_validators( def _split_heuristics_exec_options( - options: Mapping[str, str] + options: Mapping[str, str], ) -> tuple[dict[str, str], dict[str, str]]: options_exec = {} options_nonexec = {} diff --git a/pcs/lib/pacemaker/state.py b/pcs/lib/pacemaker/state.py index 10f29af4e..a72c45db4 100644 --- a/pcs/lib/pacemaker/state.py +++ b/pcs/lib/pacemaker/state.py @@ -266,9 +266,7 @@ def is_resource_managed(cluster_state, resource_id): .//resource[{predicate_id}] | .//group[{predicate_id}]/resource - """.format( - predicate_id=_id_xpath_predicate - ), + """.format(predicate_id=_id_xpath_predicate), id=resource_id, ) if primitive_list: diff --git a/pcs/lib/resource_agent/ocf_transform.py b/pcs/lib/resource_agent/ocf_transform.py index 8c8a85f6b..b0fb3c196 100644 --- a/pcs/lib/resource_agent/ocf_transform.py +++ b/pcs/lib/resource_agent/ocf_transform.py @@ -23,7 +23,7 @@ def ocf_version_to_ocf_unified( - metadata: Union[ResourceAgentMetadataOcf1_0, ResourceAgentMetadataOcf1_1] + metadata: Union[ResourceAgentMetadataOcf1_0, ResourceAgentMetadataOcf1_1], ) -> ResourceAgentMetadata: """ Transform specific version OCF metadata to a universal format diff --git a/pcs/lib/validate.py b/pcs/lib/validate.py index c44a2ab17..6264ded49 100644 --- a/pcs/lib/validate.py +++ b/pcs/lib/validate.py @@ -122,7 +122,7 @@ def values_to_pairs( def pairs_to_values( - option_dict: Mapping[TypeOptionName, Union[TypeOptionValue, ValuePair]] + option_dict: Mapping[TypeOptionName, Union[TypeOptionValue, ValuePair]], ) -> TypeOptionRawMap: """ Take a dict which has OptionValuePairs as its values and return dict with @@ -142,7 +142,7 @@ def pairs_to_values( def option_value_normalization( normalization_map: Mapping[ TypeOptionName, Callable[[TypeOptionValue], TypeOptionValue] - ] + ], ) -> TypeNormalizeFunc: """ Return function that takes key and value and return the normalized form. diff --git a/pcs_test/suite.py b/pcs_test/suite.py index 118f8e008..a6d6e2e9a 100644 --- a/pcs_test/suite.py +++ b/pcs_test/suite.py @@ -40,7 +40,7 @@ def prepare_test_name(test_name): def tests_from_suite( - test_candidate: Union[unittest.TestCase, unittest.TestSuite] + test_candidate: Union[unittest.TestCase, unittest.TestSuite], ) -> list[str]: if isinstance(test_candidate, unittest.TestCase): return [test_candidate.id()] diff --git a/pcs_test/tier0/cli/cluster_property/test_command.py b/pcs_test/tier0/cli/cluster_property/test_command.py index d98f1bbf7..e19cea032 100644 --- a/pcs_test/tier0/cli/cluster_property/test_command.py +++ b/pcs_test/tier0/cli/cluster_property/test_command.py @@ -582,9 +582,7 @@ def setUp(self): self.cluster_property = mock.MagicMock( spec_set=["get_cluster_properties_definition_legacy"] ) - self.cluster_property.get_cluster_properties_definition_legacy.return_value = ( - {} - ) + self.cluster_property.get_cluster_properties_definition_legacy.return_value = {} self.lib.cluster_property = self.cluster_property def _call_cmd(self, argv, modifiers=None): diff --git a/pcs_test/tier0/common/test_tools_xml_fromstring.py b/pcs_test/tier0/common/test_tools_xml_fromstring.py index 38f0b4b1d..51133f4b8 100644 --- a/pcs_test/tier0/common/test_tools_xml_fromstring.py +++ b/pcs_test/tier0/common/test_tools_xml_fromstring.py @@ -42,9 +42,7 @@ def test_large_xml(self): /> - """.format( - i - ) + """.format(i) for i in range(20000) ] ), @@ -94,9 +92,7 @@ def test_large_xml(self): op-digest="3b2ba04195253e454b50aa4a340af042" /> - """.format( - "{0}-{1}".format(i, j) - ) + """.format("{0}-{1}".format(i, j)) for j in range(98) ] ), diff --git a/pcs_test/tier0/daemon/async_tasks/helpers.py b/pcs_test/tier0/daemon/async_tasks/helpers.py index e3fca2266..afe602bbf 100644 --- a/pcs_test/tier0/daemon/async_tasks/helpers.py +++ b/pcs_test/tier0/daemon/async_tasks/helpers.py @@ -82,11 +82,9 @@ def prepare_scheduler(self): self.logging_queue, # mp.Queue(), ] - self.mp_pool_mock = ( - mock.patch("multiprocessing.Pool", spec=mp.Pool) - .start() - .return_value - ) = mock.Mock() + self.mp_pool_mock = mock.patch( + "multiprocessing.Pool", spec=mp.Pool + ).start().return_value = mock.Mock() # This might be needed when logger is called by get_logger, but is it? self.scheduler = scheduler.Scheduler( scheduler.SchedulerConfig( diff --git a/pcs_test/tier0/lib/auth/test_provider.py b/pcs_test/tier0/lib/auth/test_provider.py index e9fec010e..2114832e6 100644 --- a/pcs_test/tier0/lib/auth/test_provider.py +++ b/pcs_test/tier0/lib/auth/test_provider.py @@ -118,9 +118,7 @@ def test_empty_file(self): data = b"" new_data = b"new data" io_buffer = BytesIO(data) - self.file_instance_mock.raw_file.update.return_value.__enter__.return_value = ( - io_buffer - ) + self.file_instance_mock.raw_file.update.return_value.__enter__.return_value = io_buffer self.file_instance_mock.facade_to_raw.return_value = new_data with self.provider._update_facade() as empty_facade: self.assertEqual(tuple(), empty_facade.config) @@ -152,9 +150,7 @@ def test_write_error(self): _FILE_METADATA, RawFileError.ACTION_UPDATE, reason ) io_buffer = BytesIO(data) - self.file_instance_mock.raw_file.update.return_value.__enter__.return_value = ( - io_buffer - ) + self.file_instance_mock.raw_file.update.return_value.__enter__.return_value = io_buffer self.file_instance_mock.raw_to_facade.return_value = mock_facade self.file_instance_mock.facade_to_raw.return_value = new_data with self.assertRaises(_UpdateFacadeError): @@ -172,9 +168,7 @@ def test_parsing_error(self): data = b"original data" new_data = b"new data" io_buffer = BytesIO(data) - self.file_instance_mock.raw_file.update.return_value.__enter__.return_value = ( - io_buffer - ) + self.file_instance_mock.raw_file.update.return_value.__enter__.return_value = io_buffer self.file_instance_mock.raw_to_facade.side_effect = ( ParserErrorException() ) @@ -195,9 +189,7 @@ def test_success(self): new_data = b"new data" mock_facade = "facade" io_buffer = BytesIO(data) - self.file_instance_mock.raw_file.update.return_value.__enter__.return_value = ( - io_buffer - ) + self.file_instance_mock.raw_file.update.return_value.__enter__.return_value = io_buffer self.file_instance_mock.raw_to_facade.return_value = mock_facade self.file_instance_mock.facade_to_raw.return_value = new_data with self.provider._update_facade() as facade: diff --git a/pcs_test/tier0/lib/booth/test_resource.py b/pcs_test/tier0/lib/booth/test_resource.py index 9c3a0b214..6557a6adb 100644 --- a/pcs_test/tier0/lib/booth/test_resource.py +++ b/pcs_test/tier0/lib/booth/test_resource.py @@ -18,9 +18,7 @@ def fixture_resources_with_booth(booth_config_file_path): - """.format( - booth_config_file_path - ) + """.format(booth_config_file_path) ) @@ -32,9 +30,7 @@ def fixture_booth_element(_id, booth_config_file_path): - """.format( - _id, booth_config_file_path - ) + """.format(_id, booth_config_file_path) ) @@ -50,9 +46,7 @@ def fixture_ip_element(_id, ip=""): /> - """.format( - _id, ip - ) + """.format(_id, ip) ) diff --git a/pcs_test/tier0/lib/cib/resource/test_validations.py b/pcs_test/tier0/lib/cib/resource/test_validations.py index aaf7c76d1..75e4388d2 100644 --- a/pcs_test/tier0/lib/cib/resource/test_validations.py +++ b/pcs_test/tier0/lib/cib/resource/test_validations.py @@ -772,6 +772,4 @@ def test_bundle_resource(self): class ValidateUnmoveUnban(ValidateMoveBanClearMixin, TestCase): validate = staticmethod(validations.validate_unmove_unban) - report_code_bad_master = ( - reports.codes.CANNOT_UNMOVE_UNBAN_RESOURCE_MASTER_RESOURCE_NOT_PROMOTABLE - ) + report_code_bad_master = reports.codes.CANNOT_UNMOVE_UNBAN_RESOURCE_MASTER_RESOURCE_NOT_PROMOTABLE diff --git a/pcs_test/tier0/lib/cib/test_acl.py b/pcs_test/tier0/lib/cib/test_acl.py index b49a2b39f..92847368b 100644 --- a/pcs_test/tier0/lib/cib/test_acl.py +++ b/pcs_test/tier0/lib/cib/test_acl.py @@ -519,9 +519,7 @@ def test_add_for_correct_permissions(self): - """.format( - role_id - ), + """.format(role_id), ) ) @@ -538,9 +536,7 @@ def test_add_role_for_nonexisting_id(self): - """.format( - role_id - ), + """.format(role_id), ) ) @@ -557,9 +553,7 @@ def test_add_role_for_nonexisting_role_id(self): - """.format( - role_id - ), + """.format(role_id), ) ) diff --git a/pcs_test/tier0/lib/commands/remote_node/test_node_add_guest.py b/pcs_test/tier0/lib/commands/remote_node/test_node_add_guest.py index 005ce6cff..c0724d782 100644 --- a/pcs_test/tier0/lib/commands/remote_node/test_node_add_guest.py +++ b/pcs_test/tier0/lib/commands/remote_node/test_node_add_guest.py @@ -59,9 +59,7 @@ def node_add_guest( provider="heartbeat" type="VirtualDomain" /> -""".format( - VIRTUAL_MACHINE_ID -) +""".format(VIRTUAL_MACHINE_ID) FIXTURE_META_ATTRIBUTES = """ @@ -606,9 +604,7 @@ def setUp(self): - """.format( - VIRTUAL_MACHINE_ID - ), + """.format(VIRTUAL_MACHINE_ID), ) self.config.env.set_cib_data( str(cib_xml_man), cib_tempfile=self.tmp_file diff --git a/pcs_test/tier0/lib/commands/remote_node/test_node_remove_guest.py b/pcs_test/tier0/lib/commands/remote_node/test_node_remove_guest.py index d4fc1f9d5..0a4f7e3a4 100644 --- a/pcs_test/tier0/lib/commands/remote_node/test_node_remove_guest.py +++ b/pcs_test/tier0/lib/commands/remote_node/test_node_remove_guest.py @@ -50,9 +50,7 @@ def node_remove_guest(env, node_identifier=REMOTE_HOST, **kwargs): -""".format( - VIRTUAL_MACHINE_ID, REMOTE_HOST, NODE_NAME -) +""".format(VIRTUAL_MACHINE_ID, REMOTE_HOST, NODE_NAME) GUEST_NVPAIR_XPATHS = [ ".//primitive/meta_attributes/nvpair[@name='remote-addr']", @@ -205,9 +203,7 @@ class MultipleResults(TestCase): - """.format( - VIRTUAL_MACHINE_ID, REMOTE_HOST, NODE_NAME, "B-HOST", "B-NAME" - ) + """.format(VIRTUAL_MACHINE_ID, REMOTE_HOST, NODE_NAME, "B-HOST", "B-NAME") def setUp(self): self.env_assist, self.config = get_env_tools(self) diff --git a/pcs_test/tier0/lib/commands/remote_node/test_node_remove_remote.py b/pcs_test/tier0/lib/commands/remote_node/test_node_remove_remote.py index 953110ad4..e00a83841 100644 --- a/pcs_test/tier0/lib/commands/remote_node/test_node_remove_remote.py +++ b/pcs_test/tier0/lib/commands/remote_node/test_node_remove_remote.py @@ -78,9 +78,7 @@ def node_remove_remote(env, *args, node_identifier=REMOTE_HOST, **kwargs): - """.format( - NODE_NAME - ) + """.format(NODE_NAME) } FIXTURE_RESOURCES_STATE_AFTER_MODIFIERS = { @@ -88,9 +86,7 @@ def node_remove_remote(env, *args, node_identifier=REMOTE_HOST, **kwargs): - """.format( - NODE_NAME - ) + """.format(NODE_NAME) } @@ -242,9 +238,7 @@ class MultipleResults(TestCase): - """.format( - NODE_NAME, REMOTE_HOST, "OTHER-REMOTE" - ) + """.format(NODE_NAME, REMOTE_HOST, "OTHER-REMOTE") def setUp(self): self.env_assist, self.config = get_env_tools(self) diff --git a/pcs_test/tier0/lib/commands/resource/bundle_common.py b/pcs_test/tier0/lib/commands/resource/bundle_common.py index 8cea30745..9160e8570 100644 --- a/pcs_test/tier0/lib/commands/resource/bundle_common.py +++ b/pcs_test/tier0/lib/commands/resource/bundle_common.py @@ -1138,9 +1138,7 @@ def fixture_status_running(self): - """.format( - bundle_id=self.bundle_id - ) + """.format(bundle_id=self.bundle_id) @property def fixture_status_not_running(self): @@ -1163,9 +1161,7 @@ def fixture_status_not_running(self): - """.format( - bundle_id=self.bundle_id - ) + """.format(bundle_id=self.bundle_id) @property def fixture_resources_bundle_simple_disabled(self): @@ -1179,9 +1175,7 @@ def fixture_resources_bundle_simple_disabled(self): - """.format( - bundle_id=self.bundle_id, image=self.image - ) + """.format(bundle_id=self.bundle_id, image=self.image) def test_wait_fail(self): wait_error_message = dedent( @@ -1189,9 +1183,7 @@ def test_wait_fail(self): Pending actions: Action 12: {bundle_id}-node2-stop on node2 Error performing operation: Timer expired - """.format( - bundle_id=self.bundle_id - ) + """.format(bundle_id=self.bundle_id) ).strip() self.config.env.push_cib( resources=self.fixture_resources_bundle_simple, diff --git a/pcs_test/tier0/lib/commands/resource/test_bundle_update.py b/pcs_test/tier0/lib/commands/resource/test_bundle_update.py index 72fc00c0c..3a182bfc6 100644 --- a/pcs_test/tier0/lib/commands/resource/test_bundle_update.py +++ b/pcs_test/tier0/lib/commands/resource/test_bundle_update.py @@ -36,9 +36,7 @@ def fixture_resources_minimal(container_type="docker"): <{container_type} image="pcs:test" /> - """.format( - container_type=container_type - ) + """.format(container_type=container_type) class Basics(TestCase): @@ -126,9 +124,7 @@ def fixture_cib_extra_option(self): <{container_type} image="pcs:test" extra="option" /> - """.format( - container_type=self.container_type - ) + """.format(container_type=self.container_type) @property def fixture_cib_masters(self): @@ -138,9 +134,7 @@ def fixture_cib_masters(self): <{container_type} image="pcs:test" masters="2" /> - """.format( - container_type=self.container_type - ) + """.format(container_type=self.container_type) @property def fixture_cib_promoted_max(self): @@ -150,9 +144,7 @@ def fixture_cib_promoted_max(self): <{container_type} image="pcs:test" promoted-max="3" /> - """.format( - container_type=self.container_type - ) + """.format(container_type=self.container_type) fixture_report_deprecated_masters = ( severities.DEPRECATION, @@ -179,9 +171,7 @@ def _test_success(self): /> - """.format( - container_type=self.container_type - ) + """.format(container_type=self.container_type) ).env.push_cib( resources=""" @@ -190,9 +180,7 @@ def _test_success(self): /> - """.format( - container_type=self.container_type - ) + """.format(container_type=self.container_type) ) ) resource.bundle_update( diff --git a/pcs_test/tier0/lib/commands/resource/test_resource_move_autoclean.py b/pcs_test/tier0/lib/commands/resource/test_resource_move_autoclean.py index 9c54186ee..eb749039f 100644 --- a/pcs_test/tier0/lib/commands/resource/test_resource_move_autoclean.py +++ b/pcs_test/tier0/lib/commands/resource/test_resource_move_autoclean.py @@ -203,9 +203,7 @@ def setUp(self): self.simulated_transitions_add_constraint_tmp_file_name = ( "pcmk_simulate_move_transitions" ) - self.cib_apply_diff_remove_constraint_from_simulated_cib_tmp_file_name = ( - "simulated_cib_remove_constraint" - ) + self.cib_apply_diff_remove_constraint_from_simulated_cib_tmp_file_name = "simulated_cib_remove_constraint" self.cib_simulated_apply_diff_removing_constraint = ( '' ) @@ -224,15 +222,11 @@ def setUp(self): self.cib_remove_constraint_diff_applied = ( '' ) - self.pcmk_simulate_remove_constraint_after_push_orig_cib_tmp_file_name = ( - "pcmk_simulate_after_push_input_unmove_cib_after" - ) + self.pcmk_simulate_remove_constraint_after_push_orig_cib_tmp_file_name = "pcmk_simulate_after_push_input_unmove_cib_after" self.simulated_cib_remove_constraint_after_push_tmp_file_name = ( "pcmk_simulate_after_push_unmove_new_cib" ) - self.simulated_transitions_remove_constraint_after_push_tmp_file_name = ( - "pcmk_simulate_after_push_unmove_transitions" - ) + self.simulated_transitions_remove_constraint_after_push_tmp_file_name = "pcmk_simulate_after_push_unmove_transitions" self.cib_diff_add_constraint = "diff_add_constraint" self.cib_diff_remove_constraint = "diff_remove_constraint" @@ -969,9 +963,7 @@ def setUp(self): self.cib_simulate_constraint = ( '' ) - self.cib_apply_diff_remove_constraint_from_simulated_cib_tmp_file_name = ( - "simulated_cib_remove_constraint" - ) + self.cib_apply_diff_remove_constraint_from_simulated_cib_tmp_file_name = "simulated_cib_remove_constraint" self.cib_simulated_apply_diff_removing_constraint = ( '' ) @@ -990,15 +982,11 @@ def setUp(self): self.cib_remove_constraint_diff_applied = ( '' ) - self.pcmk_simulate_remove_constraint_after_push_orig_cib_tmp_file_name = ( - "pcmk_simulate_after_push_input_unmove_cib_after" - ) + self.pcmk_simulate_remove_constraint_after_push_orig_cib_tmp_file_name = "pcmk_simulate_after_push_input_unmove_cib_after" self.simulated_cib_remove_constraint_after_push_tmp_file_name = ( "pcmk_simulate_after_push_unmove_new_cib" ) - self.simulated_transitions_remove_constraint_after_push_tmp_file_name = ( - "pcmk_simulate_after_push_unmove_transitions" - ) + self.simulated_transitions_remove_constraint_after_push_tmp_file_name = "pcmk_simulate_after_push_unmove_transitions" self.config.runner.cib.load( resources=_resources_tag(_rsc_primitive_fixture(self.resource_id)), diff --git a/pcs_test/tier0/lib/commands/test_quorum.py b/pcs_test/tier0/lib/commands/test_quorum.py index 14cbaf14e..ac25dafee 100644 --- a/pcs_test/tier0/lib/commands/test_quorum.py +++ b/pcs_test/tier0/lib/commands/test_quorum.py @@ -1253,7 +1253,7 @@ def test_success_corosync_not_running_not_enabled(self): report_list_success = self.fixture_reports_success() expected_reports = fixture.ReportSequenceBuilder( report_list_success[ - :f"enable_qdevice_done_on_{self.cluster_nodes[0]}" + : f"enable_qdevice_done_on_{self.cluster_nodes[0]}" ] ) for node in self.cluster_nodes: @@ -1299,9 +1299,7 @@ def assert_success_heuristics_no_exec(self, mode, warn): mode: %mode% } } - """.replace( - "%mode%", mode - ) + """.replace("%mode%", mode) ), ) @@ -1734,7 +1732,7 @@ def test_get_ca_cert_error_communication(self): self.env_assist.assert_reports( fixture.ReportSequenceBuilder( self.fixture_reports_success()[ - :f"cert_accepted_by_{self.cluster_nodes[0]}" + : f"cert_accepted_by_{self.cluster_nodes[0]}" ] ).error( reports.codes.NODE_COMMUNICATION_COMMAND_UNSUCCESSFUL, @@ -1768,7 +1766,7 @@ def test_get_ca_cert_error_decode_certificate(self): self.env_assist.assert_reports( fixture.ReportSequenceBuilder( self.fixture_reports_success()[ - :f"cert_accepted_by_{self.cluster_nodes[0]}" + : f"cert_accepted_by_{self.cluster_nodes[0]}" ] ).error( reports.codes.INVALID_RESPONSE_FORMAT, @@ -1808,7 +1806,7 @@ def test_error_client_setup(self): self.env_assist.assert_reports( fixture.ReportSequenceBuilder( self.fixture_reports_success()[ - :f"cert_accepted_by_{self.cluster_nodes[0]}" + : f"cert_accepted_by_{self.cluster_nodes[0]}" ] ).error( reports.codes.NODE_COMMUNICATION_COMMAND_UNSUCCESSFUL, @@ -1850,7 +1848,7 @@ def test_generate_cert_request_error(self): self.env_assist.assert_reports( self.fixture_reports_success()[ - :f"cert_accepted_by_{self.cluster_nodes[0]}" + : f"cert_accepted_by_{self.cluster_nodes[0]}" ] ) @@ -1884,7 +1882,7 @@ def test_sign_certificate_error_communication(self): self.env_assist.assert_reports( fixture.ReportSequenceBuilder( self.fixture_reports_success()[ - :f"cert_accepted_by_{self.cluster_nodes[0]}" + : f"cert_accepted_by_{self.cluster_nodes[0]}" ] ).error( reports.codes.NODE_COMMUNICATION_COMMAND_UNSUCCESSFUL, @@ -1917,7 +1915,7 @@ def test_sign_certificate_error_decode_certificate(self): self.env_assist.assert_reports( fixture.ReportSequenceBuilder( self.fixture_reports_success()[ - :f"cert_accepted_by_{self.cluster_nodes[0]}" + : f"cert_accepted_by_{self.cluster_nodes[0]}" ] ).error( reports.codes.INVALID_RESPONSE_FORMAT, @@ -1958,7 +1956,7 @@ def test_certificate_to_pk12_error(self): self.env_assist.assert_reports( self.fixture_reports_success()[ - :f"cert_accepted_by_{self.cluster_nodes[0]}" + : f"cert_accepted_by_{self.cluster_nodes[0]}" ] ) @@ -1993,7 +1991,7 @@ def test_client_import_cert_error(self): success_reports = self.fixture_reports_success() expected_reports = success_reports[ - :f"cert_accepted_by_{self.cluster_nodes[1]}" + : f"cert_accepted_by_{self.cluster_nodes[1]}" ] expected_reports.append( fixture.error( @@ -3510,9 +3508,7 @@ def assert_success_heuristics_add_no_exec( heuristics { mode: %mode% - """.replace( - "%mode%", mode - ) + """.replace("%mode%", mode) ), ), ) diff --git a/pcs_test/tier0/lib/commands/test_status.py b/pcs_test/tier0/lib/commands/test_status.py index 45fb5f056..30dacf725 100644 --- a/pcs_test/tier0/lib/commands/test_status.py +++ b/pcs_test/tier0/lib/commands/test_status.py @@ -100,9 +100,7 @@ def _fixture_xml_clustername(name): /> - """.format( - name=name - ) + """.format(name=name) def _fixture_config_live_minimal(self): ( diff --git a/pcs_test/tier0/lib/commands/test_stonith.py b/pcs_test/tier0/lib/commands/test_stonith.py index 6019a87e2..15f141604 100644 --- a/pcs_test/tier0/lib/commands/test_stonith.py +++ b/pcs_test/tier0/lib/commands/test_stonith.py @@ -586,9 +586,7 @@ def test_minimal_wait_ok_run_ok(self): - """.format( - id=instance_name, agent=agent_name - ) + """.format(id=instance_name, agent=agent_name) self.config.runner.pcmk.load_agent( agent_name=f"stonith:{agent_name}", diff --git a/pcs_test/tier0/lib/commands/test_ticket.py b/pcs_test/tier0/lib/commands/test_ticket.py index b72fbd481..629887147 100644 --- a/pcs_test/tier0/lib/commands/test_ticket.py +++ b/pcs_test/tier0/lib/commands/test_ticket.py @@ -36,9 +36,7 @@ def test_success_create(self): loss-policy="fence" /> - """.format( - role=const.PCMK_ROLE_PROMOTED_PRIMARY - ) + """.format(role=const.PCMK_ROLE_PROMOTED_PRIMARY) ) ) role = str(const.PCMK_ROLE_PROMOTED_LEGACY).lower() diff --git a/pcs_test/tier0/lib/corosync/test_config_validators_links.py b/pcs_test/tier0/lib/corosync/test_config_validators_links.py index 19b24f439..fce50e3c9 100644 --- a/pcs_test/tier0/lib/corosync/test_config_validators_links.py +++ b/pcs_test/tier0/lib/corosync/test_config_validators_links.py @@ -1208,9 +1208,9 @@ def setUp(self): def test_new_address_already_used(self): pcmk_nodes = [PacemakerNode("node-remote", "addr-remote")] new_addrs = { - self.coro_nodes[1] - .name: self.coro_nodes[0] - .addr_plain_for_link("0"), + self.coro_nodes[1].name: self.coro_nodes[0].addr_plain_for_link( + "0" + ), self.coro_nodes[2].name: pcmk_nodes[0].addr, self.coro_nodes[3].name: "new-addr", } @@ -1322,12 +1322,12 @@ def setUp(self): def test_new_address_already_used(self): pcmk_nodes = [PacemakerNode("node-remote", "addr-remote")] new_addrs = { - self.coro_nodes[0] - .name: self.coro_nodes[3] - .addr_plain_for_link("1"), - self.coro_nodes[1] - .name: self.coro_nodes[1] - .addr_plain_for_link("0"), + self.coro_nodes[0].name: self.coro_nodes[3].addr_plain_for_link( + "1" + ), + self.coro_nodes[1].name: self.coro_nodes[1].addr_plain_for_link( + "0" + ), self.coro_nodes[2].name: pcmk_nodes[0].addr, } patch_getaddrinfo(self, list(new_addrs.values()) + self.existing_addrs) diff --git a/pcs_test/tier1/cib_resource/test_clone_unclone.py b/pcs_test/tier1/cib_resource/test_clone_unclone.py index ce22e8706..9c54e8917 100644 --- a/pcs_test/tier1/cib_resource/test_clone_unclone.py +++ b/pcs_test/tier1/cib_resource/test_clone_unclone.py @@ -133,9 +133,7 @@ def _get_primitive_fixture( {} -""".format( - FIXTURE_DUMMY -) +""".format(FIXTURE_DUMMY) def fixture_resources_xml(*resources_xml_list): @@ -143,9 +141,7 @@ def fixture_resources_xml(*resources_xml_list): {0} - """.format( - "\n".join(resources_xml_list) - ) + """.format("\n".join(resources_xml_list)) def fixture_clone(clone_id, primitive_id, promotable=False): diff --git a/pcs_test/tier1/cib_resource/test_group_ungroup.py b/pcs_test/tier1/cib_resource/test_group_ungroup.py index bcd07d0b7..00dc99c91 100644 --- a/pcs_test/tier1/cib_resource/test_group_ungroup.py +++ b/pcs_test/tier1/cib_resource/test_group_ungroup.py @@ -18,9 +18,7 @@ def fixture_resources_xml(resources_xml_list): {0} - """.format( - "\n".join(resources_xml_list) - ) + """.format("\n".join(resources_xml_list)) def fixture_primitive_xml(primitive_id): diff --git a/pcs_test/tier1/cib_resource/test_manage_unmanage.py b/pcs_test/tier1/cib_resource/test_manage_unmanage.py index 0ce9311ff..f0d56d937 100644 --- a/pcs_test/tier1/cib_resource/test_manage_unmanage.py +++ b/pcs_test/tier1/cib_resource/test_manage_unmanage.py @@ -48,9 +48,7 @@ def fixture_cib_unmanaged_a(add_empty_meta_b=False): - """.format( - empty_meta_b=(empty_meta_b if add_empty_meta_b else "") - ) + """.format(empty_meta_b=(empty_meta_b if add_empty_meta_b else "")) def setUp(self): self.temp_cib = get_tmp_file("tier1_cib_resource_manage_unmanage") diff --git a/pcs_test/tier1/legacy/test_resource.py b/pcs_test/tier1/legacy/test_resource.py index c9640d6e3..b448fcc78 100644 --- a/pcs_test/tier1/legacy/test_resource.py +++ b/pcs_test/tier1/legacy/test_resource.py @@ -114,9 +114,7 @@ def fixture_description(advanced=False): interval=0s timeout=20s migrate_from: interval=0s timeout=20s - """.format( - advanced_params if advanced else "" - ) + """.format(advanced_params if advanced else "") ) def test_success(self): diff --git a/pcs_test/tier1/test_booth.py b/pcs_test/tier1/test_booth.py index 29ec2b3d4..5e3145de7 100644 --- a/pcs_test/tier1/test_booth.py +++ b/pcs_test/tier1/test_booth.py @@ -93,9 +93,7 @@ def test_success_setup_booth_config(self): site = 1.1.1.1 site = 2.2.2.2 arbitrator = 3.3.3.3 - """.format( - self.booth_key_path - ) + """.format(self.booth_key_path) ), config_file.read(), ) @@ -223,9 +221,7 @@ def test_success_add_ticket(self): arbitrator = 3.3.3.3 ticket = "TicketA" expire = 10 - """.format( - self.booth_key_path - ) + """.format(self.booth_key_path) ), config_file.read(), ) @@ -327,9 +323,7 @@ def test_success_remove_ticket(self): site = 2.2.2.2 arbitrator = 3.3.3.3 ticket = "TicketA" - """.format( - self.booth_key_path - ) + """.format(self.booth_key_path) ), config_file.read(), ) @@ -342,9 +336,7 @@ def test_success_remove_ticket(self): site = 1.1.1.1 site = 2.2.2.2 arbitrator = 3.3.3.3 - """.format( - self.booth_key_path - ) + """.format(self.booth_key_path) ), config_file.read(), ) diff --git a/pcs_test/tier1/test_status.py b/pcs_test/tier1/test_status.py index d24b52421..27e24dc35 100644 --- a/pcs_test/tier1/test_status.py +++ b/pcs_test/tier1/test_status.py @@ -723,9 +723,7 @@ def test_success(self): - """.format( - crm_mon=settings.crm_mon_exec - ) + """.format(crm_mon=settings.crm_mon_exec) self.assert_pcs_success( ["status", "xml"], stdout_regexp=re.compile(dedent(xml).strip(), re.MULTILINE), From fb94d927ba7b141f6948ea68cb2eb0a2b1957375 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Fri, 29 Nov 2024 12:06:28 +0100 Subject: [PATCH 064/227] ruff: enable ruff default linters (`E4`, E7`, `E9`, and `F`) --- Makefile.am | 2 +- pcs/app.py | 2 +- pcs/cli/common/parse_args.py | 2 +- pcs/cluster.py | 8 ++++---- pcs/common/pcs_pycurl.py | 2 +- pcs/common/reports/codes.py | 2 +- pcs/common/services/common.py | 2 +- pcs/entry_points/cli.py | 2 +- pcs/entry_points/daemon.py | 2 +- pcs/entry_points/internal.py | 2 +- pcs/entry_points/snmp_agent.py | 2 +- pcs/lib/booth/env.py | 2 +- pcs/lib/cib/acl.py | 4 ++-- pcs/lib/corosync/live.py | 2 +- pcs/lib/file/raw_file.py | 2 +- pcs/lib/tools.py | 2 +- pcs/snmp/updaters/v1.py | 6 ++++-- pcs/stonith.py | 2 +- pcs/usage.py | 4 ++-- pcs/utils.py | 11 ++++------- pcs_test/curl_test.py | 6 +++--- pcs_test/tier0/common/test_file.py | 2 +- pcs_test/tier0/lib/booth/test_env.py | 2 +- .../lib/commands/cluster/test_authkey_corosync.py | 2 +- pcs_test/tier0/lib/commands/test_quorum.py | 12 ++++++------ pcs_test/tools/custom_mock.py | 6 ++++-- pyproject.toml | 10 ++++++++++ 27 files changed, 57 insertions(+), 46 deletions(-) diff --git a/Makefile.am b/Makefile.am index a8b2d097d..5ec454a18 100644 --- a/Makefile.am +++ b/Makefile.am @@ -350,7 +350,7 @@ test-tree-clean: fi find ${abs_top_builddir} -type d -name __pycache__ -exec rm -rf {} \; 2>/dev/null || : -check-local: check-local-deps test-tree-prep typos_check pylint ruff_isort_check ruff_format_check mypy tests_tier0 tests_tier1 pcsd-tests test-tree-clean +check-local: check-local-deps test-tree-prep typos_check pylint ruff_lint ruff_isort_check ruff_format_check mypy tests_tier0 tests_tier1 pcsd-tests test-tree-clean clean-local: test-tree-clean $(PYTHON) setup.py clean diff --git a/pcs/app.py b/pcs/app.py index fac169e86..33fa7e5b1 100644 --- a/pcs/app.py +++ b/pcs/app.py @@ -187,7 +187,7 @@ def main(argv=None): break for opt, val in pcs_options: - if not opt in utils.pcs_options: + if opt not in utils.pcs_options: utils.pcs_options[opt] = val else: # If any options are a list then they've been entered twice which diff --git a/pcs/cli/common/parse_args.py b/pcs/cli/common/parse_args.py index 552bca209..e5e007f9b 100644 --- a/pcs/cli/common/parse_args.py +++ b/pcs/cli/common/parse_args.py @@ -647,7 +647,7 @@ def ensure_not_incompatible( checked -- option incompatible with any of incompatible options incompatible -- set of options incompatible with checked """ - if not checked in self._defined_options: + if checked not in self._defined_options: return disallowed = self._defined_options & set(incompatible) if disallowed: diff --git a/pcs/cluster.py b/pcs/cluster.py index 7cd7835b7..7f96f9377 100644 --- a/pcs/cluster.py +++ b/pcs/cluster.py @@ -1361,7 +1361,7 @@ def cluster_destroy(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: try: utils.disableServices() # pylint: disable=bare-except - except: + except: # noqa: E722 # previously errors were suppressed in here, let's keep it that way # for now pass @@ -1371,7 +1371,7 @@ def cluster_destroy(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: lib_sbd.get_sbd_service_name(service_manager) ) # pylint: disable=bare-except - except: + except: # noqa: E722 # it's not a big deal if sbd disable fails pass @@ -1412,7 +1412,7 @@ def cluster_destroy(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: try: qdevice_net.client_destroy() # pylint: disable=bare-except - except: + except: # noqa: E722 # errors from deleting other files are suppressed as well # we do not want to fail if qdevice was not set up pass @@ -1533,7 +1533,7 @@ def send_local_configs( "Unable to set pcsd configs on {0}".format(node_name) ) # pylint: disable=bare-except - except: + except: # noqa: E722 err_msgs.append("Unable to communicate with pcsd") else: err_msgs.append("Unable to set pcsd configs") diff --git a/pcs/common/pcs_pycurl.py b/pcs/common/pcs_pycurl.py index 01de83120..196cc6a82 100644 --- a/pcs/common/pcs_pycurl.py +++ b/pcs/common/pcs_pycurl.py @@ -2,7 +2,7 @@ # pylint: disable=unused-wildcard-import # pylint: disable=wildcard-import -from pycurl import * +from pycurl import * # noqa: F403 # This package defines constants which are not present in some older versions # of pycurl but pcs needs to use them diff --git a/pcs/common/reports/codes.py b/pcs/common/reports/codes.py index 3f0e669b5..6a7049f23 100644 --- a/pcs/common/reports/codes.py +++ b/pcs/common/reports/codes.py @@ -2,7 +2,7 @@ # pylint: disable=wildcard-import # Wildcard import of deprecated report codes will prevent creation of a new # report with the code of deprecated report -from .deprecated_codes import * +from .deprecated_codes import * # noqa: F403 from .types import ForceCode as F from .types import MessageCode as M diff --git a/pcs/common/services/common.py b/pcs/common/services/common.py index d8a46f7e2..d24062483 100644 --- a/pcs/common/services/common.py +++ b/pcs/common/services/common.py @@ -1,2 +1,2 @@ # pylint: disable=unused-import -from pcs.common.str_tools import join_multilines +from pcs.common.str_tools import join_multilines # noqa: F401 diff --git a/pcs/entry_points/cli.py b/pcs/entry_points/cli.py index 0d1dcad94..757130b5a 100644 --- a/pcs/entry_points/cli.py +++ b/pcs/entry_points/cli.py @@ -5,4 +5,4 @@ add_bundled_packages_to_path() -from pcs.app import main +from pcs.app import main # noqa: E402 diff --git a/pcs/entry_points/daemon.py b/pcs/entry_points/daemon.py index 6287b4471..1341bf450 100644 --- a/pcs/entry_points/daemon.py +++ b/pcs/entry_points/daemon.py @@ -5,4 +5,4 @@ add_bundled_packages_to_path() -from pcs.daemon.run import main +from pcs.daemon.run import main # noqa: E402 diff --git a/pcs/entry_points/internal.py b/pcs/entry_points/internal.py index 4f828d9c1..29ae7e60f 100644 --- a/pcs/entry_points/internal.py +++ b/pcs/entry_points/internal.py @@ -5,4 +5,4 @@ add_bundled_packages_to_path() -from pcs.pcs_internal import main +from pcs.pcs_internal import main # noqa: E402 diff --git a/pcs/entry_points/snmp_agent.py b/pcs/entry_points/snmp_agent.py index f3e460059..520baa82e 100644 --- a/pcs/entry_points/snmp_agent.py +++ b/pcs/entry_points/snmp_agent.py @@ -5,4 +5,4 @@ add_bundled_packages_to_path() -from pcs.snmp.pcs_snmp_agent import main +from pcs.snmp.pcs_snmp_agent import main # noqa: E402 diff --git a/pcs/lib/booth/env.py b/pcs/lib/booth/env.py index 5cb5b479b..a9a429986 100644 --- a/pcs/lib/booth/env.py +++ b/pcs/lib/booth/env.py @@ -68,7 +68,7 @@ def __init__(self, instance_name: Optional[str], booth_files_data): @staticmethod def _init_file_data(booth_files_data, file_key): # ghost file not specified - if not file_key in booth_files_data: + if file_key not in booth_files_data: return dict( ghost_file=False, ghost_data=None, diff --git a/pcs/lib/cib/acl.py b/pcs/lib/cib/acl.py index 83e2ed8b1..076af9215 100644 --- a/pcs/lib/cib/acl.py +++ b/pcs/lib/cib/acl.py @@ -30,7 +30,7 @@ def validate_permissions(tree, permission_info_list): allowed_permissions = ["read", "write", "deny"] allowed_scopes = ["xpath", "id"] for permission, scope_type, scope in permission_info_list: - if not permission in allowed_permissions: + if permission not in allowed_permissions: report_items.append( reports.ReportItem.error( reports.messages.InvalidOptionValue( @@ -39,7 +39,7 @@ def validate_permissions(tree, permission_info_list): ) ) - if not scope_type in allowed_scopes: + if scope_type not in allowed_scopes: report_items.append( reports.ReportItem.error( reports.messages.InvalidOptionValue( diff --git a/pcs/lib/corosync/live.py b/pcs/lib/corosync/live.py index 0295fc266..7409b8f28 100644 --- a/pcs/lib/corosync/live.py +++ b/pcs/lib/corosync/live.py @@ -215,7 +215,7 @@ def _parse_quorum_status(quorum_status: str) -> QuorumStatus: if line == "Membership information": in_node_list = True continue - if not ":" in line: + if ":" not in line: continue parts = [x.strip() for x in line.split(":", 1)] if parts[0] == "Quorate": diff --git a/pcs/lib/file/raw_file.py b/pcs/lib/file/raw_file.py index 10b420080..f73981eb4 100644 --- a/pcs/lib/file/raw_file.py +++ b/pcs/lib/file/raw_file.py @@ -18,7 +18,7 @@ RawFileError, RawFileInterface, ) -from pcs.common.file import RawFile as RealFile +from pcs.common.file import RawFile as RealFile # noqa: F401 # TODO add logging (logger / debug reports ?) diff --git a/pcs/lib/tools.py b/pcs/lib/tools.py index 97137ebd9..a59807cd2 100644 --- a/pcs/lib/tools.py +++ b/pcs/lib/tools.py @@ -53,7 +53,7 @@ def environment_file_to_dict(config: str) -> dict[str, str]: config = config.replace("\\\n", "") data = {} - for line in [l.strip() for l in config.split("\n")]: + for line in [line.strip() for line in config.split("\n")]: if line == "" or line.startswith("#") or line.startswith(";"): continue if "=" not in line: diff --git a/pcs/snmp/updaters/v1.py b/pcs/snmp/updaters/v1.py index 91504053a..2c7b648fa 100644 --- a/pcs/snmp/updaters/v1.py +++ b/pcs/snmp/updaters/v1.py @@ -197,9 +197,11 @@ def _get_primitives(resource): def _get_resource_id_list(resource_list, predicate=None): - # pylint: disable=unnecessary-lambda-assignment if predicate is None: - predicate = lambda _: True + + def predicate(_): + return True + return [resource["id"] for resource in resource_list if predicate(resource)] diff --git a/pcs/stonith.py b/pcs/stonith.py index c36b3c7a6..5b61447a0 100644 --- a/pcs/stonith.py +++ b/pcs/stonith.py @@ -443,7 +443,7 @@ def stonith_level_remove_cmd( "use 'pcs stonith level delete | remove " "[target ] [stonith ...]'." ) - if not parse_args.ARG_TYPE_DELIMITER in argv[1] and "," in argv[1]: + if parse_args.ARG_TYPE_DELIMITER not in argv[1] and "," in argv[1]: deprecation_warning( "Delimiting stonith devices with ',' is deprecated and " "will be removed. Please use a space to delimit stonith " diff --git a/pcs/usage.py b/pcs/usage.py index 26453039e..e1eab9462 100644 --- a/pcs/usage.py +++ b/pcs/usage.py @@ -222,13 +222,13 @@ def generate_tree(usage_txt: str) -> CompletionTree: if re.match(r"^ \w", line): args = line.split() arg = args.pop(0) - if not arg in ret_hash: + if arg not in ret_hash: ret_hash[arg] = {} cur_hash = ret_hash[arg] for arg in args: if arg.startswith("[") or arg.startswith("<"): break - if not arg in cur_hash: + if arg not in cur_hash: cur_hash[arg] = {} cur_hash = cur_hash[arg] return ret_hash diff --git a/pcs/utils.py b/pcs/utils.py index 4bd0ca1d0..2ac1c7f55 100644 --- a/pcs/utils.py +++ b/pcs/utils.py @@ -1678,13 +1678,12 @@ def get_cib_dom(cib_xml=None): Commandline options: * -f - CIB file """ - # pylint: disable=bare-except if cib_xml is None: cib_xml = get_cib() try: dom = parseString(cib_xml) return dom - except: + except xml.parsers.expat.ExpatError: return err("unable to get cib") @@ -1693,13 +1692,12 @@ def get_cib_etree(cib_xml=None): Commandline options: * -f - CIB file """ - # pylint: disable=bare-except if cib_xml is None: cib_xml = get_cib() try: root = ET.fromstring(cib_xml) return root - except: + except xml.etree.ElementTree.ParseError: return err("unable to get cib") @@ -1967,7 +1965,6 @@ def getTerminalSize(fd=1): Commandline options: no options """ - # pylint: disable=bare-except try: # pylint: disable=import-outside-toplevel import fcntl @@ -1977,10 +1974,10 @@ def getTerminalSize(fd=1): hw = struct.unpack( str("hh"), fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234") ) - except: + except OSError: try: hw = (os.environ["LINES"], os.environ["COLUMNS"]) - except: + except KeyError: hw = (25, 80) return hw diff --git a/pcs_test/curl_test.py b/pcs_test/curl_test.py index fd2d0dbb4..628738539 100644 --- a/pcs_test/curl_test.py +++ b/pcs_test/curl_test.py @@ -13,9 +13,9 @@ PACKAGE_DIR = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, PACKAGE_DIR) -from pcs import utils -from pcs.common.host import Destination -from pcs.common.node_communicator import ( +from pcs import utils # noqa: E402 +from pcs.common.host import Destination # noqa: E402 +from pcs.common.node_communicator import ( # noqa: E402 Request, RequestData, RequestTarget, diff --git a/pcs_test/tier0/common/test_file.py b/pcs_test/tier0/common/test_file.py index 5a380a05e..7353d59e4 100644 --- a/pcs_test/tier0/common/test_file.py +++ b/pcs_test/tier0/common/test_file.py @@ -140,7 +140,7 @@ class RawFileWrite(TestCase): def assert_success(self, mock_flock, raw_file, mode, can_overwrite): file_data = "some file data".encode("utf-8") written_data = file_data - if not "b" in mode: + if "b" not in mode: written_data = written_data.decode("utf-8") mock_open = mock.mock_open() with patch_file("open", mock_open): diff --git a/pcs_test/tier0/lib/booth/test_env.py b/pcs_test/tier0/lib/booth/test_env.py index 2c3b2de93..a0390657e 100644 --- a/pcs_test/tier0/lib/booth/test_env.py +++ b/pcs_test/tier0/lib/booth/test_env.py @@ -76,7 +76,7 @@ def test_ghost(self): self.assertTrue(isinstance(my_env.config.raw_file, GhostFile)) self.assertTrue(isinstance(my_env.key.raw_file, GhostFile)) with self.assertRaises(AssertionError) as cm: - dummy_path = my_env.config_path + _ = my_env.config_path self.assertEqual( "Reading config path is supported only in live environment", str(cm.exception), diff --git a/pcs_test/tier0/lib/commands/cluster/test_authkey_corosync.py b/pcs_test/tier0/lib/commands/cluster/test_authkey_corosync.py index 8b1389450..fd30a1f6d 100644 --- a/pcs_test/tier0/lib/commands/cluster/test_authkey_corosync.py +++ b/pcs_test/tier0/lib/commands/cluster/test_authkey_corosync.py @@ -660,7 +660,7 @@ def _get_corosync_nodes_options(node_list, skip_name_idx_list): for index, node in enumerate(node_list): node_option_list = [] node_option_list.append(("ring0_addr", node)) - if not index in skip_name_idx_list: + if index not in skip_name_idx_list: node_option_list.append(("name", node)) node_option_list.append(("nodeid", str(index + 1))) result.append(node_option_list) diff --git a/pcs_test/tier0/lib/commands/test_quorum.py b/pcs_test/tier0/lib/commands/test_quorum.py index ac25dafee..43663f93b 100644 --- a/pcs_test/tier0/lib/commands/test_quorum.py +++ b/pcs_test/tier0/lib/commands/test_quorum.py @@ -1702,7 +1702,7 @@ def test_invalid_model_forced(self): self.env_assist.assert_reports(expected_reports) def test_get_ca_cert_error_communication(self): - dummy_tmp_file_mock_calls = self.fixture_config_success() + self.fixture_config_success() self.config.trim_before( "http.corosync.qdevice_net_get_ca_cert_requests" ) @@ -1744,7 +1744,7 @@ def test_get_ca_cert_error_communication(self): ) def test_get_ca_cert_error_decode_certificate(self): - dummy_tmp_file_mock_calls = self.fixture_config_success() + self.fixture_config_success() self.config.trim_before( "http.corosync.qdevice_net_get_ca_cert_requests" ) @@ -1776,7 +1776,7 @@ def test_get_ca_cert_error_decode_certificate(self): ) def test_error_client_setup(self): - dummy_tmp_file_mock_calls = self.fixture_config_success() + self.fixture_config_success() self.config.trim_before( "http.corosync.qdevice_net_client_setup_requests" ) @@ -1818,7 +1818,7 @@ def test_error_client_setup(self): ) def test_generate_cert_request_error(self): - dummy_tmp_file_mock_calls = self.fixture_config_success() + self.fixture_config_success() self.config.trim_before("runner.corosync.qdevice_generate_cert") self.config.runner.corosync.qdevice_generate_cert( self.cluster_name, @@ -1853,7 +1853,7 @@ def test_generate_cert_request_error(self): ) def test_sign_certificate_error_communication(self): - dummy_tmp_file_mock_calls = self.fixture_config_success() + self.fixture_config_success() self.config.trim_before( "http.corosync.qdevice_net_sign_certificate_requests" ) @@ -1894,7 +1894,7 @@ def test_sign_certificate_error_communication(self): ) def test_sign_certificate_error_decode_certificate(self): - dummy_tmp_file_mock_calls = self.fixture_config_success() + self.fixture_config_success() self.config.trim_before( "http.corosync.qdevice_net_sign_certificate_requests" ) diff --git a/pcs_test/tools/custom_mock.py b/pcs_test/tools/custom_mock.py index 0882234d6..042201496 100644 --- a/pcs_test/tools/custom_mock.py +++ b/pcs_test/tools/custom_mock.py @@ -78,8 +78,10 @@ def __init__( def _assert_file_content_equal(self, name, expected, real): if expected is None and real is None: return - # pylint: disable=unnecessary-lambda-assignment - eq_callback = lambda file1, file2: file1 != file2 + + def eq_callback(file1, file2): + return file1 != file2 + if self._file_content_checker is not None: eq_callback = self._file_content_checker try: diff --git a/pyproject.toml b/pyproject.toml index 8030d0f1d..110f3630f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,6 +80,10 @@ target-version = "py39" # ruff rules docs: https://docs.astral.sh/ruff/rules/ # pylint rules in ruff: # https://github.com/astral-sh/ruff/issues/970 select = [ + "E4", + "E7", + "E9", + "F", "I", "PL", # pylint convention, error, refactoring, warning ] @@ -109,6 +113,12 @@ section-order = ["future", "standard-library", "third-party", "first-party", "te [tool.ruff.lint.isort.sections] "tests" = ["pcs_test"] +[tool.ruff.lint.per-file-ignores] +# Ignore `F401` https://docs.astral.sh/ruff/rules/unused-import/ +"__init__.py" = ["F401"] +"pcs/entry_points/*.py" = ["F401"] +"pcs/lib/cib/rule/compat_pyparsing.py" = ["F401"] + [tool.ruff.lint.pylint] max-args = 8 max-positional-args = 8 From 50deec4e7f3b5e13d43ad3aa7ac24e13d38afaa6 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Thu, 9 Jan 2025 12:00:05 +0100 Subject: [PATCH 065/227] ruff: enable ruff linter `SIM` * https://docs.astral.sh/ruff/rules/#flake8-simplify-sim * disable rule SIM102 collapsible-if * https://docs.astral.sh/ruff/rules/collapsible-if/ --- pcs/cli/common/parse_args.py | 5 +-- pcs/cluster.py | 35 ++++++---------- pcs/common/reports/item.py | 2 +- pcs/common/reports/processor.py | 5 +-- pcs/common/str_tools.py | 5 +-- pcs/constraint.py | 24 +++-------- pcs/lib/cib/constraint/constraint.py | 4 +- pcs/lib/cib/tools.py | 5 +-- pcs/lib/commands/status.py | 9 ++-- pcs/lib/communication/nodes.py | 2 +- pcs/lib/pacemaker/state.py | 6 +-- pcs/lib/pacemaker/values.py | 2 +- pcs/lib/tools.py | 2 +- pcs/lib/xml_tools.py | 2 +- pcs/pcsd.py | 11 +++-- pcs/resource.py | 12 +++--- pcs/rule.py | 10 ++--- pcs/utils.py | 20 ++++----- pcs_test/api_v2_client.py | 5 +-- pcs_test/tier0/common/test_file.py | 41 ++++++++++--------- pcs_test/tier0/common/test_resource_status.py | 36 ++++++++-------- pcs_test/tier0/daemon/app/test_api_v0.py | 2 +- pcs_test/tier0/lib/auth/test_provider.py | 16 +++++--- .../commands/remote_node/fixtures_remove.py | 2 +- .../tools/command_env/config_runner_pcmk.py | 5 +-- pcs_test/tools/custom_mock.py | 5 +-- pcs_test/tools/parallel_test_runner.py | 4 +- pyproject.toml | 2 + 28 files changed, 123 insertions(+), 156 deletions(-) diff --git a/pcs/cli/common/parse_args.py b/pcs/cli/common/parse_args.py index e5e007f9b..ea593382c 100644 --- a/pcs/cli/common/parse_args.py +++ b/pcs/cli/common/parse_args.py @@ -682,10 +682,7 @@ def is_specified(self, option: str) -> bool: return option in self._defined_options def is_specified_any(self, option_list: StringIterable) -> bool: - for option in option_list: - if self.is_specified(option): - return True - return False + return any(self.is_specified(option) for option in option_list) def get( self, option: str, default: ModifierValueType = None diff --git a/pcs/cluster.py b/pcs/cluster.py index 7f96f9377..c3640ba65 100644 --- a/pcs/cluster.py +++ b/pcs/cluster.py @@ -1,4 +1,5 @@ # pylint: disable=too-many-lines +import contextlib import datetime import json import math @@ -1350,30 +1351,23 @@ def cluster_destroy(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: else: print_to_stderr("Shutting down pacemaker/corosync services...") for service in ["pacemaker", "corosync-qdevice", "corosync"]: - try: + # It is safe to ignore error since we want it not to be running + # anyways. + with contextlib.suppress(LibraryError): utils.stop_service(service) - except LibraryError: - # It is safe to ignore error since we want it not to be running - # anyways. - pass print_to_stderr("Killing any remaining services...") kill_local_cluster_services() - try: + # previously errors were suppressed in here, let's keep it that way + # for now + with contextlib.suppress(Exception): utils.disableServices() - # pylint: disable=bare-except - except: # noqa: E722 - # previously errors were suppressed in here, let's keep it that way - # for now - pass - try: + + # it's not a big deal if sbd disable fails + with contextlib.suppress(Exception): service_manager = utils.get_service_manager() service_manager.disable( lib_sbd.get_sbd_service_name(service_manager) ) - # pylint: disable=bare-except - except: # noqa: E722 - # it's not a big deal if sbd disable fails - pass print_to_stderr("Removing all cluster configuration files...") dummy_output, dummy_retval = utils.run( @@ -1409,13 +1403,10 @@ def cluster_destroy(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: ";", ] ) - try: + # errors from deleting other files are suppressed as well we do not + # want to fail if qdevice was not set up + with contextlib.suppress(Exception): qdevice_net.client_destroy() - # pylint: disable=bare-except - except: # noqa: E722 - # errors from deleting other files are suppressed as well - # we do not want to fail if qdevice was not set up - pass def cluster_verify(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: diff --git a/pcs/common/reports/item.py b/pcs/common/reports/item.py index 262845b21..7bb7447a3 100644 --- a/pcs/common/reports/item.py +++ b/pcs/common/reports/item.py @@ -98,7 +98,7 @@ def to_dto(self) -> ReportItemMessageDto: annotations = self.__class__.__annotations__ except AttributeError as e: raise AssertionError() from e - for attr_name in annotations.keys(): + for attr_name in annotations: if attr_name.startswith("_") or attr_name in ("message",): continue attr_val = getattr(self, attr_name) diff --git a/pcs/common/reports/processor.py b/pcs/common/reports/processor.py index 0be230731..24dca4a0d 100644 --- a/pcs/common/reports/processor.py +++ b/pcs/common/reports/processor.py @@ -32,10 +32,7 @@ def _do_report(self, report_item: ReportItem) -> None: def has_errors(report_list: ReportItemList) -> bool: - for report_item in report_list: - if _is_error(report_item): - return True - return False + return any(_is_error(report_item) for report_item in report_list) def _is_error(report_item: ReportItem) -> bool: diff --git a/pcs/common/str_tools.py b/pcs/common/str_tools.py index 4b5554fa3..125966fba 100644 --- a/pcs/common/str_tools.py +++ b/pcs/common/str_tools.py @@ -1,3 +1,4 @@ +import contextlib from collections.abc import Iterable as IterableAbc from collections.abc import Sized from typing import ( @@ -185,10 +186,8 @@ def _is_multiple(what: Union[int, Sized]) -> bool: if isinstance(what, int): retval = abs(what) != 1 elif not isinstance(what, str): - try: + with contextlib.suppress(TypeError): retval = len(what) != 1 - except TypeError: - pass return retval diff --git a/pcs/constraint.py b/pcs/constraint.py index 64edc7e69..a3d831906 100644 --- a/pcs/constraint.py +++ b/pcs/constraint.py @@ -93,10 +93,7 @@ class CrmRuleReturnCode(Enum): def constraint_location_cmd(lib, argv, modifiers): - if not argv: - sub_cmd = "config" - else: - sub_cmd = argv.pop(0) + sub_cmd = "config" if not argv else argv.pop(0) try: if sub_cmd == "add": @@ -121,10 +118,7 @@ def constraint_location_cmd(lib, argv, modifiers): def constraint_order_cmd(lib, argv, modifiers): - if not argv: - sub_cmd = "config" - else: - sub_cmd = argv.pop(0) + sub_cmd = "config" if not argv else argv.pop(0) try: if sub_cmd == "set": @@ -923,18 +917,12 @@ def location_prefer( if not skip_node_check: report_list += _verify_node_name(node, existing_nodes) if len(nodeconf_a) == 1: - if prefer: - score = "INFINITY" - else: - score = "-INFINITY" + score = "INFINITY" if prefer else "-INFINITY" else: score = nodeconf_a[1] _verify_score(score) if not prefer: - if score[0] == "-": - score = score[1:] - else: - score = "-" + score + score = score[1:] if score[0] == "-" else "-" + score parameters_list.append( [ @@ -1051,11 +1039,11 @@ def location_add(lib, argv, modifiers, skip_score_and_node_check=False): rsc_loc.getAttribute("node") == node and ( ( - RESOURCE_TYPE_RESOURCE == rsc_type + rsc_type == RESOURCE_TYPE_RESOURCE and rsc_loc.getAttribute("rsc") == rsc_value ) or ( - RESOURCE_TYPE_REGEXP == rsc_type + rsc_type == RESOURCE_TYPE_REGEXP and rsc_loc.getAttribute("rsc-pattern") == rsc_value ) ) diff --git a/pcs/lib/cib/constraint/constraint.py b/pcs/lib/cib/constraint/constraint.py index eca748ef9..8e1788793 100644 --- a/pcs/lib/cib/constraint/constraint.py +++ b/pcs/lib/cib/constraint/constraint.py @@ -22,9 +22,7 @@ def _validate_attrib_names(attrib_names, options): - invalid_names = [ - name for name in options.keys() if name not in attrib_names - ] + invalid_names = [name for name in options if name not in attrib_names] if invalid_names: raise LibraryError( reports.ReportItem.error( diff --git a/pcs/lib/cib/tools.py b/pcs/lib/cib/tools.py index 1838b7e94..f92d934b8 100644 --- a/pcs/lib/cib/tools.py +++ b/pcs/lib/cib/tools.py @@ -1,3 +1,4 @@ +import contextlib import re from typing import ( List, @@ -627,10 +628,8 @@ def remove_element_by_id(cib: _Element, element_id: str) -> None: """ Remove element with specified id from cib element. """ - try: + with contextlib.suppress(ElementNotFound): remove_one_element(get_element_by_id(cib, element_id)) - except ElementNotFound: - pass def multivalue_attr_contains_value( diff --git a/pcs/lib/commands/status.py b/pcs/lib/commands/status.py index f9ec6160b..72c234567 100644 --- a/pcs/lib/commands/status.py +++ b/pcs/lib/commands/status.py @@ -1,3 +1,4 @@ +import contextlib import os.path from typing import ( Iterable, @@ -155,12 +156,10 @@ def full_cluster_status_plaintext( # get extra info if live if live: service_manager = env.service_manager - try: + with contextlib.suppress(LibraryError): is_sbd_running = service_manager.is_running( get_sbd_service_name(service_manager) ) - except LibraryError: - pass local_services_status = _get_local_services_status(service_manager) if verbose and corosync_conf: node_name_list, node_names_report_list = get_existing_nodes_names( @@ -343,7 +342,7 @@ def _get_local_services_status( ] service_status_list = [] for service, display_always in service_def: - try: + with contextlib.suppress(LibraryError): service_status_list.append( _ServiceStatus( service, @@ -352,8 +351,6 @@ def _get_local_services_status( service_manager.is_running(service), ) ) - except LibraryError: - pass return service_status_list diff --git a/pcs/lib/communication/nodes.py b/pcs/lib/communication/nodes.py index ae296ffcd..4b608a021 100644 --- a/pcs/lib/communication/nodes.py +++ b/pcs/lib/communication/nodes.py @@ -234,7 +234,7 @@ def before(self): self._start_report( [ self._action_key_to_report(key) - for key in self._action_definition.keys() + for key in self._action_definition ], [target.label for target in self._target_list], ) diff --git a/pcs/lib/pacemaker/state.py b/pcs/lib/pacemaker/state.py index a72c45db4..d4ba21783 100644 --- a/pcs/lib/pacemaker/state.py +++ b/pcs/lib/pacemaker/state.py @@ -41,7 +41,7 @@ def __init__(self, owner_name, attrib, required_attrs): self.required_attrs = required_attrs def __getattr__(self, name): - if name in self.required_attrs.keys(): + if name in self.required_attrs: try: attr_specification = self.required_attrs[name] if isinstance(attr_specification, tuple): @@ -67,7 +67,7 @@ def __init__(self, owner_name, dom_part, children, sections): self.sections = sections def __getattr__(self, name): - if name in self.children.keys(): + if name in self.children: element_name, wrapper = self.children[name] return [ wrapper(element) @@ -76,7 +76,7 @@ def __getattr__(self, name): ) ] - if name in self.sections.keys(): + if name in self.sections: element_name, wrapper = self.sections[name] return wrapper( self.dom_part.xpath( diff --git a/pcs/lib/pacemaker/values.py b/pcs/lib/pacemaker/values.py index 77ad1e18c..a289d084d 100644 --- a/pcs/lib/pacemaker/values.py +++ b/pcs/lib/pacemaker/values.py @@ -121,7 +121,7 @@ def validate_id( # see NCName definition # http://www.w3.org/TR/REC-xml-names/#NT-NCName # http://www.w3.org/TR/REC-xml/#NT-Name - description = "id" if not description else description # for mypy + description = description if description else "id" # for mypy if not id_candidate: report_item = ReportItem.error( reports.messages.InvalidIdIsEmpty(description) diff --git a/pcs/lib/tools.py b/pcs/lib/tools.py index a59807cd2..1c2d79bc7 100644 --- a/pcs/lib/tools.py +++ b/pcs/lib/tools.py @@ -138,7 +138,7 @@ def create_tmp_cib( ) -> IO[str]: try: # pylint: disable=consider-using-with - tmp_file = tempfile.NamedTemporaryFile(mode="w+", suffix=".pcs") + tmp_file = tempfile.NamedTemporaryFile(mode="w+", suffix=".pcs") # noqa: SIM115 if data is not None: tmp_file.write(data) tmp_file.flush() diff --git a/pcs/lib/xml_tools.py b/pcs/lib/xml_tools.py index 68ed48297..1506f3bd7 100644 --- a/pcs/lib/xml_tools.py +++ b/pcs/lib/xml_tools.py @@ -222,7 +222,7 @@ def reset_element( keep_attrs = keep_attrs or [] for child in list(element): element.remove(child) - for key in element.attrib.keys(): + for key in element.attrib: if key not in keep_attrs: del element.attrib[key] diff --git a/pcs/pcsd.py b/pcs/pcsd.py index 24a086ff5..3370ffea7 100644 --- a/pcs/pcsd.py +++ b/pcs/pcsd.py @@ -1,3 +1,4 @@ +import contextlib import json import os import sys @@ -68,15 +69,13 @@ def pcsd_certkey_cmd(lib: Any, argv: Argv, modifiers: InputModifiers): ) try: - try: + # If the file doesn't exist, we don't care + with contextlib.suppress(OSError): os.chmod(settings.pcsd_cert_location, 0o600) - except OSError: # If the file doesn't exist, we don't care - pass - try: + # If the file doesn't exist, we don't care + with contextlib.suppress(OSError): os.chmod(settings.pcsd_key_location, 0o600) - except OSError: # If the file doesn't exist, we don't care - pass with os.fdopen( os.open( diff --git a/pcs/resource.py b/pcs/resource.py index 7d9f0ff69..3af78735b 100644 --- a/pcs/resource.py +++ b/pcs/resource.py @@ -1484,7 +1484,7 @@ def resource_operation_remove(res_id: str, argv: Argv) -> None: found_match = False for op in resource_el.getElementsByTagName("op"): temp_properties = [] - for attr_name in op.attributes.keys(): + for attr_name in op.attributes.keys(): # noqa: SIM118, attributes is not a dict if attr_name == "id": continue temp_properties.append( @@ -2289,10 +2289,12 @@ def resource_status( has_resources = True print(line) continue - if not preg.match(line) and not stonith: - has_resources = True - print(line) - elif preg.match(line) and stonith: + if ( + not preg.match(line) + and not stonith + or preg.match(line) + and stonith + ): has_resources = True print(line) diff --git a/pcs/rule.py b/pcs/rule.py index 3e7d210b4..f08602dc9 100644 --- a/pcs/rule.py +++ b/pcs/rule.py @@ -150,7 +150,7 @@ def list_rule(self, rule): return rule_parts def list_expression(self, expression): - if "value" in expression.attributes.keys(): + if "value" in expression.attributes: exp_parts = [ expression.getAttribute("attribute"), expression.getAttribute("operation"), @@ -245,7 +245,7 @@ def string_rule(self, rule): return (" %s " % boolean_op).join(rule_parts) def string_expression(self, expression): - if "value" in expression.attributes.keys(): + if "value" in expression.attributes: exp_parts = [ expression.getAttribute("attribute"), expression.getAttribute("operation"), @@ -318,9 +318,9 @@ def has_node_attr_expr_with_type_integer(rule_tree): if isinstance(rule_tree, SymbolPrefix): return False child = rule_tree.children[1] - if isinstance(child, SymbolType) and child.symbol_id == "integer": - return True - return False + return bool( + isinstance(child, SymbolType) and child.symbol_id == "integer" + ) return False diff --git a/pcs/utils.py b/pcs/utils.py index 2ac1c7f55..f403cdde9 100644 --- a/pcs/utils.py +++ b/pcs/utils.py @@ -1845,12 +1845,10 @@ def get_role(_el, new_roles_supported): for op in operations_el.getElementsByTagName("op"): if op.getAttribute("name") == op_name: - if op_name != "monitor": - existing.append(op) - elif get_role( - op, new_roles_supported - ) == op_role and ocf_check_level == get_operation_ocf_check_level( - op + if ( + op_name != "monitor" + or get_role(op, new_roles_supported) == op_role + and ocf_check_level == get_operation_ocf_check_level(op) ): existing.append(op) return existing @@ -2106,9 +2104,7 @@ def is_score_or_opt(var): """ if is_score(var): return True - if var.find("=") != -1: - return True - return False + return var.find("=") != -1 def is_score(var): @@ -2680,11 +2676,11 @@ def get_middleware_factory(): return middleware.create_middleware_factory( cib=middleware.cib(filename if usefile else None, touch_cib_file), corosync_conf_existing=middleware.corosync_conf_existing( - pcs_options.get("--corosync_conf", None) + pcs_options.get("--corosync_conf") ), booth_conf=pcs.cli.booth.env.middleware_config( - pcs_options.get("--booth-conf", None), - pcs_options.get("--booth-key", None), + pcs_options.get("--booth-conf"), + pcs_options.get("--booth-key"), ), ) diff --git a/pcs_test/api_v2_client.py b/pcs_test/api_v2_client.py index 53209a1e1..7db2b6761 100644 --- a/pcs_test/api_v2_client.py +++ b/pcs_test/api_v2_client.py @@ -205,10 +205,7 @@ def main(): if sys.argv[0] == "--sync": sys.argv.pop(0) run_fn = run_command_synchronously - if len(sys.argv) == 2: - payload = sys.argv[1] - else: - payload = sys.stdin.read() + payload = sys.argv[1] if len(sys.argv) == 2 else sys.stdin.read() token = os.environ.get("PCS_TOKEN", None) if token is None: token = get_token_for_localhost() diff --git a/pcs_test/tier0/common/test_file.py b/pcs_test/tier0/common/test_file.py index 7353d59e4..5afee84b6 100644 --- a/pcs_test/tier0/common/test_file.py +++ b/pcs_test/tier0/common/test_file.py @@ -486,7 +486,7 @@ def test_non_existing_file(self, mock_chown, mock_chmod): with raw_file.update() as io_buffer: try: # pylint: disable=consider-using-with - file_obj = open(file_path) + file_obj = open(file_path) # noqa: SIM115 except OSError: self.fail("Unable to open file") with self.assertRaises(OSError): @@ -503,10 +503,12 @@ def test_non_existing_file(self, mock_chown, mock_chmod): def test_open_error(self, mock_chown, mock_chmod): mock_open = mock.MagicMock() mock_open.side_effect = OSError() - with patch_file("open", mock_open): - with self.assertRaises(RawFileError): - with self.raw_file.update(): - self.fail("should not get here") + with ( + patch_file("open", mock_open), + self.assertRaises(RawFileError), + self.raw_file.update(), + ): + self.fail("should not get here") mock_chmod.assert_not_called() mock_chown.assert_not_called() @@ -516,10 +518,12 @@ def test_read_error(self, mock_flock, mock_chown, mock_chmod): file_mock = mock_open.return_value.__enter__.return_value file_mock.fileno.return_value = self.fileno file_mock.read.side_effect = OSError() - with patch_file("open", mock_open): - with self.assertRaises(RawFileError): - with self.raw_file.update(): - self.fail("should not get here") + with ( + patch_file("open", mock_open), + self.assertRaises(RawFileError), + self.raw_file.update(), + ): + self.fail("should not get here") mock_open.return_value.__enter__.return_value.read.assert_called_once_with() mock_flock.assert_called_once_with(self.fileno, fcntl.LOCK_EX) mock_chmod.assert_not_called() @@ -534,16 +538,15 @@ def test_write_error(self, mock_flock, mock_chown, mock_chmod): file_mock.fileno.return_value = self.fileno file_mock.read.return_value = orig_data file_mock.write.side_effect = OSError() - with patch_file("open", mock_open): - with self.assertRaises(RawFileError): - with self.raw_file.update() as io_buffer: - self.assertEqual( - orig_data.encode("utf-8"), io_buffer.getvalue() - ) - io_buffer.seek(0) - io_buffer.truncate() - io_buffer.write(new_data.encode("utf-8")) - self.fail("should not get here") + with patch_file("open", mock_open), self.assertRaises(RawFileError): + with self.raw_file.update() as io_buffer: + self.assertEqual( + orig_data.encode("utf-8"), io_buffer.getvalue() + ) + io_buffer.seek(0) + io_buffer.truncate() + io_buffer.write(new_data.encode("utf-8")) + self.fail("should not get here") file_mock.read.assert_called_once_with() file_mock.write.assert_called_once_with(new_data) mock_flock.assert_called_once_with(self.fileno, fcntl.LOCK_EX) diff --git a/pcs_test/tier0/common/test_resource_status.py b/pcs_test/tier0/common/test_resource_status.py index 58f4f78cf..27eecc8dc 100644 --- a/pcs_test/tier0/common/test_resource_status.py +++ b/pcs_test/tier0/common/test_resource_status.py @@ -2693,14 +2693,16 @@ def test_bad_members_quantifier(self): resource_list = ["primitive", "clone", "cloned_primitive"] for resource_id in resource_list: - with self.subTest(value=resource_id): - with self.assertRaises(MembersQuantifierUnsupportedException): - facade.is_state( - resource_id, - None, - ResourceState.STARTED, - members_quantifier=MoreChildrenQuantifierType.ALL, - ) + with ( + self.subTest(value=resource_id), + self.assertRaises(MembersQuantifierUnsupportedException), + ): + facade.is_state( + resource_id, + None, + ResourceState.STARTED, + members_quantifier=MoreChildrenQuantifierType.ALL, + ) def test_bad_members_quantifier_bundle(self): facade = fixture_bundle_facade() @@ -2717,14 +2719,16 @@ def test_bad_instances_quantifier(self): resource_list = ["primitive", "group"] for resource_id in resource_list: - with self.subTest(value=resource_id): - with self.assertRaises(InstancesQuantifierUnsupportedException): - facade.is_state( - resource_id, - None, - ResourceState.STARTED, - instances_quantifier=MoreChildrenQuantifierType.ALL, - ) + with ( + self.subTest(value=resource_id), + self.assertRaises(InstancesQuantifierUnsupportedException), + ): + facade.is_state( + resource_id, + None, + ResourceState.STARTED, + instances_quantifier=MoreChildrenQuantifierType.ALL, + ) class TestStateExactValue(TestCase): diff --git a/pcs_test/tier0/daemon/app/test_api_v0.py b/pcs_test/tier0/daemon/app/test_api_v0.py index 80461801a..9d9bcfc7e 100644 --- a/pcs_test/tier0/daemon/app/test_api_v0.py +++ b/pcs_test/tier0/daemon/app/test_api_v0.py @@ -117,7 +117,7 @@ def fetch( response = super().fetch( path, raise_error, - method=("GET" if kwargs.get("body", None) is None else "POST"), + method=("GET" if kwargs.get("body") is None else "POST"), **kwargs, ) self.assert_headers(response.headers) diff --git a/pcs_test/tier0/lib/auth/test_provider.py b/pcs_test/tier0/lib/auth/test_provider.py index 2114832e6..d8c99d140 100644 --- a/pcs_test/tier0/lib/auth/test_provider.py +++ b/pcs_test/tier0/lib/auth/test_provider.py @@ -134,9 +134,11 @@ def test_read_error(self): self.file_instance_mock.raw_file.update.return_value.__enter__.side_effect = RawFileError( _FILE_METADATA, RawFileError.ACTION_UPDATE, reason ) - with self.assertRaises(_UpdateFacadeError): - with self.provider._update_facade(): - self.fail("should not get here") + with ( + self.assertRaises(_UpdateFacadeError), + self.provider._update_facade(), + ): + self.fail("should not get here") self.logger.error.assert_called_once_with( "Unable to update file '%s': %s", _FILE_PATH, reason ) @@ -153,9 +155,11 @@ def test_write_error(self): self.file_instance_mock.raw_file.update.return_value.__enter__.return_value = io_buffer self.file_instance_mock.raw_to_facade.return_value = mock_facade self.file_instance_mock.facade_to_raw.return_value = new_data - with self.assertRaises(_UpdateFacadeError): - with self.provider._update_facade() as facade: - self.assertIs(mock_facade, facade) + with ( + self.assertRaises(_UpdateFacadeError), + self.provider._update_facade() as facade, + ): + self.assertIs(mock_facade, facade) self.logger.error.assert_called_once_with( "Unable to update file '%s': %s", _FILE_PATH, reason ) diff --git a/pcs_test/tier0/lib/commands/remote_node/fixtures_remove.py b/pcs_test/tier0/lib/commands/remote_node/fixtures_remove.py index 325a1299b..8db264ec3 100644 --- a/pcs_test/tier0/lib/commands/remote_node/fixtures_remove.py +++ b/pcs_test/tier0/lib/commands/remote_node/fixtures_remove.py @@ -33,7 +33,7 @@ def destroy_pacemaker_remote( ) if label or dest_list: - if kwargs.get("communication_list", None): + if kwargs.get("communication_list"): raise AssertionError( "Keywords 'label' and 'dest_list' makes no sense with" " 'communication_list != None'" diff --git a/pcs_test/tools/command_env/config_runner_pcmk.py b/pcs_test/tools/command_env/config_runner_pcmk.py index 62508845d..269d310bb 100644 --- a/pcs_test/tools/command_env/config_runner_pcmk.py +++ b/pcs_test/tools/command_env/config_runner_pcmk.py @@ -399,10 +399,7 @@ def load_agent( ).format(agent_name, os.path.realpath(__file__), rc("")) ) - if env: - env = dict(env) - else: - env = {} + env = dict(env) if env else {} env["PATH"] = ":".join( [ settings.fence_agent_execs, diff --git a/pcs_test/tools/custom_mock.py b/pcs_test/tools/custom_mock.py index 042201496..e759148ba 100644 --- a/pcs_test/tools/custom_mock.py +++ b/pcs_test/tools/custom_mock.py @@ -1,3 +1,4 @@ +import contextlib import io import socket from dataclasses import dataclass @@ -229,10 +230,8 @@ def setopt(self, opt, val): self._opts[opt] = val def unsetopt(self, opt): - try: + with contextlib.suppress(KeyError): del self._opts[opt] - except KeyError: - pass def getinfo(self, opt): try: diff --git a/pcs_test/tools/parallel_test_runner.py b/pcs_test/tools/parallel_test_runner.py index 008b5cb09..55521f4c8 100644 --- a/pcs_test/tools/parallel_test_runner.py +++ b/pcs_test/tools/parallel_test_runner.py @@ -97,9 +97,7 @@ def print_summary( ) -> None: # pylint: disable=import-outside-toplevel too-many-branches summary_lines = [] - if self.error_count or self.failure_count or self.skip_count: - summary_lines.append("") - elif vanilla: + if self.error_count or self.failure_count or self.skip_count or vanilla: summary_lines.append("") summary_lines.extend(self.error_reports) diff --git a/pyproject.toml b/pyproject.toml index 110f3630f..6044f3fb2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,6 +86,7 @@ select = [ "F", "I", "PL", # pylint convention, error, refactoring, warning + "SIM", ] # ruff does not respect pylint ignore directives # https://github.com/astral-sh/ruff/issues/1203 @@ -100,6 +101,7 @@ ignore = [ "PLR0911", # too-many-return-statements (6) "PLW1509", # subprocess-popen-preexec-fn (3) "PLW0602", # global-variable-not-assigned (1) + "SIM102", # https://docs.astral.sh/ruff/rules/collapsible-if/ ] [tool.ruff.lint.isort] From 4769762d54917b96d819005de6699f8abb04665f Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Wed, 11 Dec 2024 10:32:13 +0100 Subject: [PATCH 066/227] ruff: enable ruff linter `ASYNC` (flake8-async) * https://docs.astral.sh/ruff/rules/#flake8-async-async --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 6044f3fb2..ec4a405ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,6 +80,7 @@ target-version = "py39" # ruff rules docs: https://docs.astral.sh/ruff/rules/ # pylint rules in ruff: # https://github.com/astral-sh/ruff/issues/970 select = [ + "ASYNC", "E4", "E7", "E9", From 1b3e99a6ed1ca331f61e8df2bf4015d1b9d43d19 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Wed, 11 Dec 2024 14:00:35 +0100 Subject: [PATCH 067/227] ruff: enable ruff linter `B` (flake8-bugbear) * https://docs.astral.sh/ruff/rules/#flake8-bugbear-b --- pcs/acl.py | 2 +- pcs/app.py | 2 +- pcs/config.py | 2 +- pcs/daemon/env.py | 1 + pcs/lib/booth/config_facade.py | 4 ++-- pcs/lib/cib/rule/compat_pyparsing.py | 7 +++++-- pcs/lib/pacemaker/status.py | 3 ++- pcs/lib/resource_agent/error.py | 4 +++- pcs/pcsd.py | 2 +- pcs/utils.py | 2 +- pcs_test/tier0/daemon/test_session.py | 4 ++-- .../tier0/lib/cib/test_remove_elements.py | 20 ++++++++++--------- .../lib/corosync/test_config_facade_quorum.py | 2 +- .../corosync/test_config_facade_transport.py | 12 +++++------ pcs_test/tools/command_env/assistant.py | 4 +--- pcs_test/tools/command_env/config.py | 2 +- pcs_test/tools/misc.py | 2 +- pyproject.toml | 1 + 18 files changed, 42 insertions(+), 34 deletions(-) diff --git a/pcs/acl.py b/pcs/acl.py index 4bfd6cc63..0c4214d0b 100644 --- a/pcs/acl.py +++ b/pcs/acl.py @@ -138,7 +138,7 @@ def argv_to_permission_info_list(argv): ) ) - for permission, scope_type, dummy_scope in permission_info_list: + for permission, scope_type, _ in permission_info_list: if permission not in ["read", "write", "deny"] or scope_type not in [ "xpath", "id", diff --git a/pcs/app.py b/pcs/app.py index 33fa7e5b1..783361845 100644 --- a/pcs/app.py +++ b/pcs/app.py @@ -181,7 +181,7 @@ def main(argv=None): sys.exit(1) full = False - for option, dummy_value in pcs_options: + for option, _ in pcs_options: if option == "--full": full = True break diff --git a/pcs/config.py b/pcs/config.py index bd50c5425..96a4601ef 100644 --- a/pcs/config.py +++ b/pcs/config.py @@ -487,7 +487,7 @@ def config_restore_local(infile_name, infile_obj): if not extract_info: continue path_full = None - if hasattr(extract_info.get("pre_store_call"), "__call__"): + if callable(extract_info.get("pre_store_call")): extract_info["pre_store_call"]() if "rename" in extract_info and extract_info["rename"]: if tmp_dir is None: diff --git a/pcs/daemon/env.py b/pcs/daemon/env.py index 03a288c41..3139a4e06 100644 --- a/pcs/daemon/env.py +++ b/pcs/daemon/env.py @@ -1,3 +1,4 @@ +# ruff: noqa: B019 https://docs.astral.sh/ruff/rules/cached-instance-method/ import os.path import ssl from collections import namedtuple diff --git a/pcs/lib/booth/config_facade.py b/pcs/lib/booth/config_facade.py index af9931cf8..706d1fbdc 100644 --- a/pcs/lib/booth/config_facade.py +++ b/pcs/lib/booth/config_facade.py @@ -54,7 +54,7 @@ def has_ticket(self, ticket_name): string ticket_name -- the name of the ticket """ - for key, value, dummy_details in self._config: + for key, value, _ in self._config: if key == "ticket" and value == ticket_name: return True return False @@ -97,7 +97,7 @@ def set_option(self, key: str, value: str) -> None: self._config.insert(0, ConfigItem(key, value)) def get_option(self, option: str) -> Optional[str]: - for key, value, dummy_details in reversed(self._config): + for key, value, _ in reversed(self._config): if key == option: return value return None diff --git a/pcs/lib/cib/rule/compat_pyparsing.py b/pcs/lib/cib/rule/compat_pyparsing.py index 3947fdb9e..91c97a82f 100644 --- a/pcs/lib/cib/rule/compat_pyparsing.py +++ b/pcs/lib/cib/rule/compat_pyparsing.py @@ -27,6 +27,9 @@ nums, ) +SUPPRESS_LEFT_PARENTHESIS = Suppress("(") +SUPPRESS_RIGHT_PARENTHESIS = Suppress(")") + if pyparsing.__version__.startswith("3."): from pyparsing import ( # pylint: disable=no-name-in-module OpAssoc, @@ -73,8 +76,8 @@ def set_results_name( def infix_notation( # type: ignore base_expr: pyparsing.ParserElement, op_list: list[Any], - lpar: Union[str, pyparsing.ParserElement] = Suppress("("), - rpar: Union[str, pyparsing.ParserElement] = Suppress(")"), + lpar: Union[str, pyparsing.ParserElement] = SUPPRESS_LEFT_PARENTHESIS, + rpar: Union[str, pyparsing.ParserElement] = SUPPRESS_RIGHT_PARENTHESIS, ) -> pyparsing.ParserElement: # pylint: disable=too-many-function-args return pyparsing.infixNotation(base_expr, op_list, lpar, rpar) # type: ignore diff --git a/pcs/lib/pacemaker/status.py b/pcs/lib/pacemaker/status.py index ed5eb0bad..ffa36b8cf 100644 --- a/pcs/lib/pacemaker/status.py +++ b/pcs/lib/pacemaker/status.py @@ -26,6 +26,7 @@ from pcs.common.str_tools import format_list from pcs.lib.pacemaker.values import is_true +_DEFAULT_SEVERITY = reports.ReportItemSeverity.error() _PRIMITIVE_TAG = "resource" _GROUP_TAG = "group" _CLONE_TAG = "clone" @@ -104,7 +105,7 @@ def __init__(self, bundle_id: str, bad_ids: list[str]): def cluster_status_parsing_error_to_report( e: ClusterStatusParsingError, - severity: reports.ReportItemSeverity = reports.ReportItemSeverity.error(), + severity: reports.ReportItemSeverity = _DEFAULT_SEVERITY, ) -> reports.ReportItem: reason = "" if isinstance(e, EmptyResourceIdError): diff --git a/pcs/lib/resource_agent/error.py b/pcs/lib/resource_agent/error.py index 6ae02af95..5d827d423 100644 --- a/pcs/lib/resource_agent/error.py +++ b/pcs/lib/resource_agent/error.py @@ -3,6 +3,8 @@ from . import const +_DEFAULT_SEVERITY = reports.ReportItemSeverity.error() + class ResourceAgentError(Exception): def __init__(self, agent_name: str): @@ -44,7 +46,7 @@ def __init__(self, agent_name: str, ocf_version: str): def resource_agent_error_to_report_item( e: ResourceAgentError, - severity: reports.ReportItemSeverity = reports.ReportItemSeverity.error(), + severity: reports.ReportItemSeverity = _DEFAULT_SEVERITY, is_stonith: bool = False, ) -> reports.ReportItem: """ diff --git a/pcs/pcsd.py b/pcs/pcsd.py index 3370ffea7..c81e6db63 100644 --- a/pcs/pcsd.py +++ b/pcs/pcsd.py @@ -218,7 +218,7 @@ def accept_token_cmd(lib, argv, modifiers): try: pcs_users_config.write_facade(facade, can_overwrite=True) except pcs_file.RawFileError as e: - raise output.error(raw_file_error_report(e).message.message) + raise output.error(raw_file_error_report(e).message.message) from e def _check_nodes(node_list, prefix=""): diff --git a/pcs/utils.py b/pcs/utils.py index f403cdde9..2c868e925 100644 --- a/pcs/utils.py +++ b/pcs/utils.py @@ -2439,7 +2439,7 @@ def dom_update_nvset(dom_element, nvpair_tuples, tag_name, id_candidate): return only_removing = True - for name, value in nvpair_tuples: + for _, value in nvpair_tuples: if value != "": only_removing = False break diff --git a/pcs_test/tier0/daemon/test_session.py b/pcs_test/tier0/daemon/test_session.py index 5c3cb0458..9ebc86bac 100644 --- a/pcs_test/tier0/daemon/test_session.py +++ b/pcs_test/tier0/daemon/test_session.py @@ -37,9 +37,9 @@ def test_session_is_refreshable(self): with self.refresh_test() as session1: session1.refresh() with self.refresh_test() as session1: - session1.username + _ = session1.username with self.refresh_test() as session1: - session1.sid + _ = session1.sid class StorageTest(TestCase, PatchSessionMixin): diff --git a/pcs_test/tier0/lib/cib/test_remove_elements.py b/pcs_test/tier0/lib/cib/test_remove_elements.py index 5b6a7c420..f297fa0c9 100644 --- a/pcs_test/tier0/lib/cib/test_remove_elements.py +++ b/pcs_test/tier0/lib/cib/test_remove_elements.py @@ -22,6 +22,14 @@ from pcs_test.tools.misc import read_test_resource from pcs_test.tools.xml import etree_to_str +EXPECTED_TYPES_FOR_REMOVE = ["constraint", "location rule", "resource"] + +_DEFAULT_DEPENDANT_ELEMENTS = lib.DependantElements({}) +_DEFAULT_ELEMENT_REFERENCES = lib.ElementReferences({}, {}) +_DEFAULT_UNSUPPORTED_ELEMENTS = lib.UnsupportedElements( + {}, EXPECTED_TYPES_FOR_REMOVE +) + def _constraints(*argv): return f"{''.join(argv)}" @@ -56,8 +64,6 @@ def _constraints(*argv): FIXTURE_LOC_CONSTRAINT_WITH_2_RULES, ) -EXPECTED_TYPES_FOR_REMOVE = ["constraint", "location rule", "resource"] - def fixture_primitive_to_disable(cib: _Element) -> list[_Element]: return [cib.find("./configuration/resources/primitive[@id='A']")] @@ -108,14 +114,10 @@ def assert_elements_to_remove( elements_to_remove: lib.ElementsToRemove, ids_to_remove: set[str], resources_to_remove: Optional[list[etree._Element]] = None, - dependant_elements: lib.DependantElements = lib.DependantElements({}), - element_references: lib.ElementReferences = lib.ElementReferences( - {}, {} - ), + dependant_elements: lib.DependantElements = _DEFAULT_DEPENDANT_ELEMENTS, + element_references: lib.ElementReferences = _DEFAULT_ELEMENT_REFERENCES, missing_ids: Optional[set[str]] = None, - unsupported_elements: lib.UnsupportedElements = lib.UnsupportedElements( - {}, EXPECTED_TYPES_FOR_REMOVE - ), + unsupported_elements: lib.UnsupportedElements = _DEFAULT_UNSUPPORTED_ELEMENTS, ): self.assertEqual(elements_to_remove.ids_to_remove, ids_to_remove) self.assertEqual( diff --git a/pcs_test/tier0/lib/corosync/test_config_facade_quorum.py b/pcs_test/tier0/lib/corosync/test_config_facade_quorum.py index d15654902..303711972 100644 --- a/pcs_test/tier0/lib/corosync/test_config_facade_quorum.py +++ b/pcs_test/tier0/lib/corosync/test_config_facade_quorum.py @@ -165,7 +165,7 @@ class SetQuorumOptionsTest(TestCase): def get_two_node(facade): two_node = None for quorum in facade.config.get_sections("quorum"): - for dummy_name, value in quorum.get_attributes("two_node"): + for _, value in quorum.get_attributes("two_node"): two_node = value return two_node diff --git a/pcs_test/tier0/lib/corosync/test_config_facade_transport.py b/pcs_test/tier0/lib/corosync/test_config_facade_transport.py index 51ecfd7e6..9c2580d65 100644 --- a/pcs_test/tier0/lib/corosync/test_config_facade_transport.py +++ b/pcs_test/tier0/lib/corosync/test_config_facade_transport.py @@ -942,17 +942,17 @@ def test_remove_empty_section(self): def test_do_not_remove_option(self): params = self._udp_do_not_modify_params_list - for transport, params in [("udp", params), ("udpu", params)]: + for transport, params_list in [("udp", params), ("udpu", params)]: with self.subTest(transport=transport, params=params): self._assert_set_transport_options( - params, + params_list, self._transport_option_tmplt.format(transport=transport), self._transport_option_tmplt.format(transport=transport), ) def test_modify_option(self): params = self._add_option_params_list - for transport, params in [ + for transport, params_list in [ ("", params), ("knet", params), ("udp", params[0:1]), @@ -960,7 +960,7 @@ def test_modify_option(self): ]: with self.subTest(transport=transport, params=params): self._assert_set_transport_options( - params, + params_list, dedent( f"""\ totem {{{{ @@ -974,10 +974,10 @@ def test_modify_option(self): def test_do_not_modify_option(self): params = self._udp_do_not_modify_params_list - for transport, params in [("udp", params), ("udpu", params)]: + for transport, params_list in [("udp", params), ("udpu", params)]: with self.subTest(transport=transport, params=params): self._assert_set_transport_options( - params, + params_list, self._transport_option_tmplt.format(transport=transport), self._transport_option_tmplt.format(transport=transport), ) diff --git a/pcs_test/tools/command_env/assistant.py b/pcs_test/tools/command_env/assistant.py index af5ea77ca..b92736c2c 100644 --- a/pcs_test/tools/command_env/assistant.py +++ b/pcs_test/tools/command_env/assistant.py @@ -129,9 +129,7 @@ def get_cmd_runner(self, env=None): ) raw_file_mock = get_raw_file_mock(call_queue) - for method_name, dummy_method in inspect.getmembers( - RawFile, inspect.isfunction - ): + for method_name, _ in inspect.getmembers(RawFile, inspect.isfunction): # patch all public methods # inspect.isfunction must be used instead of ismethod because we are # working with a class and not an instance - no method is bound yet so diff --git a/pcs_test/tools/command_env/config.py b/pcs_test/tools/command_env/config.py index 9acba7da2..ef75a865d 100644 --- a/pcs_test/tools/command_env/config.py +++ b/pcs_test/tools/command_env/config.py @@ -91,6 +91,6 @@ def __wrap_helper(self, helper): object helper -- helper for creatig call configuration """ for name, attr in inspect.getmembers(helper.__class__): - if not name.startswith("_") and hasattr(attr, "__call__"): + if not name.startswith("_") and callable(attr): self.__wrap_method(helper, name, attr) return helper diff --git a/pcs_test/tools/misc.py b/pcs_test/tools/misc.py index 28e03411d..c3e4db9b6 100644 --- a/pcs_test/tools/misc.py +++ b/pcs_test/tools/misc.py @@ -49,7 +49,7 @@ class Test3(GeneralTest, metaclass=ParametrizedTestMetaClass): def __init__(cls, classname, bases, class_dict): for attr_name in dir(cls): attr = getattr(cls, attr_name) - if attr_name.startswith("_test") and hasattr(attr, "__call__"): + if attr_name.startswith("_test") and callable(attr): setattr(cls, attr_name[1:], attr) super().__init__(classname, bases, class_dict) diff --git a/pyproject.toml b/pyproject.toml index ec4a405ff..f1feacb9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,6 +81,7 @@ target-version = "py39" # pylint rules in ruff: # https://github.com/astral-sh/ruff/issues/970 select = [ "ASYNC", + "B", "E4", "E7", "E9", From 7933c7192e3684850e536e9918e7aceefadd8534 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Wed, 11 Dec 2024 14:19:54 +0100 Subject: [PATCH 068/227] ruff: enable ruff linter `A` (flake8-builtins) * https://docs.astral.sh/ruff/rules/#flake8-builtins-a --- pcs/cli/constraint/parse_args.py | 2 +- pcs/cli/routing/prop.py | 4 +++- pcs/lib/cib/constraint/resource_set.py | 2 +- pcs/lib/cib/tag.py | 2 +- pcs/lib/pacemaker/status.py | 4 ++-- pcs/rule.py | 2 +- pcs/usage.py | 8 ++++---- pcs_test/tier0/cli/cluster_property/test_output.py | 2 +- pcs_test/tier0/lib/commands/cluster/test_setup.py | 4 ++-- pcs_test/tier0/lib/commands/tag/tag_common.py | 2 +- .../tier0/lib/corosync/test_config_validators_create.py | 8 ++++---- .../tier0/lib/corosync/test_config_validators_nodes.py | 4 ++-- pcs_test/tools/custom_mock.py | 2 +- pyproject.toml | 1 + 14 files changed, 25 insertions(+), 22 deletions(-) diff --git a/pcs/cli/constraint/parse_args.py b/pcs/cli/constraint/parse_args.py index c79f012e3..c23791e77 100644 --- a/pcs/cli/constraint/parse_args.py +++ b/pcs/cli/constraint/parse_args.py @@ -13,7 +13,7 @@ def prepare_resource_sets( ) -> list[dict[str, Union[list[str], dict[str, str]]]]: return [ { - "ids": [id for id in args if "=" not in id], + "ids": [id_ for id_ in args if "=" not in id_], "options": KeyValueParser( [opt for opt in args if "=" in opt] ).get_unique(), diff --git a/pcs/cli/routing/prop.py b/pcs/cli/routing/prop.py index 726b616ea..6f1f669b6 100644 --- a/pcs/cli/routing/prop.py +++ b/pcs/cli/routing/prop.py @@ -4,7 +4,9 @@ property_cmd = create_router( { - "help": lambda _lib, _argv, _modifiers: print(usage.property(_argv)), + "help": lambda _lib, _argv, _modifiers: print( + usage.property_usage(_argv) + ), "set": cluster_property.set_property, "unset": cluster_property.unset_property, # TODO remove, deprecated command diff --git a/pcs/lib/cib/constraint/resource_set.py b/pcs/lib/cib/constraint/resource_set.py index 4fa65d81a..16ae1edde 100644 --- a/pcs/lib/cib/constraint/resource_set.py +++ b/pcs/lib/cib/constraint/resource_set.py @@ -37,7 +37,7 @@ def prepare_set( ).has_errors: raise LibraryError() return { - "ids": [find_valid_id(id) for id in resource_set["ids"]], + "ids": [find_valid_id(id_) for id_ in resource_set["ids"]], "options": resource_set["options"], } diff --git a/pcs/lib/cib/tag.py b/pcs/lib/cib/tag.py index 0bf0fe6f1..461003e29 100644 --- a/pcs/lib/cib/tag.py +++ b/pcs/lib/cib/tag.py @@ -86,7 +86,7 @@ def _validate_add_remove_duplicate_reference_ids( add_or_not_remove -- flag for add/remove action """ duplicate_ids_list = [ - id for id, count in Counter(idref_list).items() if count > 1 + id_ for id_, count in Counter(idref_list).items() if count > 1 ] if duplicate_ids_list: return [ diff --git a/pcs/lib/pacemaker/status.py b/pcs/lib/pacemaker/status.py index ffa36b8cf..2932f5c04 100644 --- a/pcs/lib/pacemaker/status.py +++ b/pcs/lib/pacemaker/status.py @@ -410,8 +410,8 @@ def _replica_to_dto( ] duplicate_ids = [ - id - for id, count in Counter( + id_ + for id_, count in Counter( resource.resource_id for resource in resource_list ).items() if count > 1 diff --git a/pcs/rule.py b/pcs/rule.py index f08602dc9..8dfc5d633 100644 --- a/pcs/rule.py +++ b/pcs/rule.py @@ -619,7 +619,7 @@ class UnexpectedEndOfInput(ParserException): pass -class SyntaxError(ParserException): +class SyntaxError(ParserException): # noqa: A001 # pylint: disable=redefined-builtin pass diff --git a/pcs/usage.py b/pcs/usage.py index e1eab9462..1f181be82 100644 --- a/pcs/usage.py +++ b/pcs/usage.py @@ -84,7 +84,7 @@ def full_usage() -> None: out += strip_extras(resource([])) out += strip_extras(cluster([])) out += strip_extras(stonith([])) - out += strip_extras(property([])) + out += strip_extras(property_usage([])) out += strip_extras(constraint([])) out += strip_extras(node([])) out += strip_extras(acl([])) @@ -187,7 +187,7 @@ def generate_completion_tree_from_usage() -> CompletionTree: tree["resource"] = generate_tree(resource([])) tree["cluster"] = generate_tree(cluster([])) tree["stonith"] = generate_tree(stonith([])) - tree["property"] = generate_tree(property([])) + tree["property"] = generate_tree(property_usage([])) tree["acl"] = generate_tree(acl([])) tree["constraint"] = generate_tree(constraint([])) tree["qdevice"] = generate_tree(qdevice([])) @@ -2339,7 +2339,7 @@ def stonith(args: Argv) -> str: return sub_usage(args, output) -def property(args: Argv) -> str: +def property_usage(args: Argv) -> str: # 'property' is a built-in # pylint: disable=redefined-builtin output = """ @@ -3449,7 +3449,7 @@ def show(main_usage_name: str, rest_usage_names: Argv) -> None: "host": host, "node": node, "pcsd": pcsd, - "property": property, + "property": property_usage, "qdevice": qdevice, "quorum": quorum, "resource": resource, diff --git a/pcs_test/tier0/cli/cluster_property/test_output.py b/pcs_test/tier0/cli/cluster_property/test_output.py index 59d334663..7308d559d 100644 --- a/pcs_test/tier0/cli/cluster_property/test_output.py +++ b/pcs_test/tier0/cli/cluster_property/test_output.py @@ -53,7 +53,7 @@ def fixture_property_metadata( name="property-name", shortdesc=None, longdesc=None, - type="string", + type="string", # noqa: A002 default=None, enum_values=None, advanced=False, diff --git a/pcs_test/tier0/lib/commands/cluster/test_setup.py b/pcs_test/tier0/lib/commands/cluster/test_setup.py index 175575ba7..01ec7ccc8 100644 --- a/pcs_test/tier0/lib/commands/cluster/test_setup.py +++ b/pcs_test/tier0/lib/commands/cluster/test_setup.py @@ -1449,9 +1449,9 @@ def test_too_many_addrs_knet(self): min_count=1, max_count=8, node_name=name, - node_index=id, + node_index=id_, ) - for id, name in enumerate(NODE_LIST, 1) + for id_, name in enumerate(NODE_LIST, 1) ] ) diff --git a/pcs_test/tier0/lib/commands/tag/tag_common.py b/pcs_test/tier0/lib/commands/tag/tag_common.py index 8b2416d13..6086ea39c 100644 --- a/pcs_test/tier0/lib/commands/tag/tag_common.py +++ b/pcs_test/tier0/lib/commands/tag/tag_common.py @@ -12,7 +12,7 @@ def fixture_tags_xml(tag_with_ids): "" + "".join( ''.format(tag_id=tag_id) - + "".join(''.format(id=id) for id in id_list) + + "".join(f'' for id_ in id_list) + "" for tag_id, id_list in tag_with_ids ) diff --git a/pcs_test/tier0/lib/corosync/test_config_validators_create.py b/pcs_test/tier0/lib/corosync/test_config_validators_create.py index f7c02029c..637c45cfa 100644 --- a/pcs_test/tier0/lib/corosync/test_config_validators_create.py +++ b/pcs_test/tier0/lib/corosync/test_config_validators_create.py @@ -318,9 +318,9 @@ def test_node_addrs_missing_udp(self): min_count=1, max_count=1, node_name=name, - node_index=id, + node_index=id_, ) - for id, name in enumerate(["node1", "node2", "node3"], 1) + for id_, name in enumerate(["node1", "node2", "node3"], 1) ], ) @@ -343,9 +343,9 @@ def test_node_addrs_missing_knet(self): min_count=1, max_count=8, node_name=name, - node_index=id, + node_index=id_, ) - for id, name in enumerate(["node1", "node2", "node3"], 1) + for id_, name in enumerate(["node1", "node2", "node3"], 1) ], ) diff --git a/pcs_test/tier0/lib/corosync/test_config_validators_nodes.py b/pcs_test/tier0/lib/corosync/test_config_validators_nodes.py index 4415d3b3f..7ff08a293 100644 --- a/pcs_test/tier0/lib/corosync/test_config_validators_nodes.py +++ b/pcs_test/tier0/lib/corosync/test_config_validators_nodes.py @@ -220,9 +220,9 @@ def test_node_addrs_missing(self): min_count=1, max_count=1, node_name=name, - node_index=id, + node_index=id_, ) - for id, name in enumerate(["node3", "node4", "node5"], 1) + for id_, name in enumerate(["node3", "node4", "node5"], 1) ], ) diff --git a/pcs_test/tools/custom_mock.py b/pcs_test/tools/custom_mock.py index e759148ba..af0127f16 100644 --- a/pcs_test/tools/custom_mock.py +++ b/pcs_test/tools/custom_mock.py @@ -24,7 +24,7 @@ def get_getaddrinfo_mock(resolvable_addr_list): - def socket_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0): + def socket_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0): # noqa: A002 # pylint: disable=redefined-builtin # pylint: disable=unused-argument if host not in resolvable_addr_list: diff --git a/pyproject.toml b/pyproject.toml index f1feacb9f..4357d214b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,6 +80,7 @@ target-version = "py39" # ruff rules docs: https://docs.astral.sh/ruff/rules/ # pylint rules in ruff: # https://github.com/astral-sh/ruff/issues/970 select = [ + "A", "ASYNC", "B", "E4", From 24e70802ab62e354d2f663e24d071a420e2cd6bb Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Wed, 11 Dec 2024 14:26:53 +0100 Subject: [PATCH 069/227] ruff: enable ruff linter `LOG` (flake8-logging) * https://docs.astral.sh/ruff/rules/#flake8-logging-log --- pcs_test/tier0/daemon/app/test_app_spa.py | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pcs_test/tier0/daemon/app/test_app_spa.py b/pcs_test/tier0/daemon/app/test_app_spa.py index dbbe0b774..48409c643 100644 --- a/pcs_test/tier0/daemon/app/test_app_spa.py +++ b/pcs_test/tier0/daemon/app/test_app_spa.py @@ -38,7 +38,7 @@ def get_routes(self): app_dir=self.spa_dir_path, fallback_page_path=self.fallback_path, session_storage=self.session_storage, - auth_provider=AuthProvider(logging.Logger("test logger")), + auth_provider=AuthProvider(logging.getLogger("test logger")), ) diff --git a/pyproject.toml b/pyproject.toml index 4357d214b..9afa7c677 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,6 +88,7 @@ select = [ "E9", "F", "I", + "LOG", "PL", # pylint convention, error, refactoring, warning "SIM", ] From d8878ae61970693382d3c445ea535b9f6751c65e Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Wed, 11 Dec 2024 15:02:23 +0100 Subject: [PATCH 070/227] ruff: enable ruff linter `G` (flake8-logging-format) * https://docs.astral.sh/ruff/rules/#flake8-logging-format-g --- pcs/lib/external.py | 58 ++++++------ pcs/lib/node_communication.py | 8 +- pcs_test/tier0/lib/test_external.py | 88 ++++++++++--------- pcs_test/tier0/lib/test_node_communication.py | 6 +- pyproject.toml | 1 + 5 files changed, 83 insertions(+), 78 deletions(-) diff --git a/pcs/lib/external.py b/pcs/lib/external.py index a84c1c934..e47dafb29 100644 --- a/pcs/lib/external.py +++ b/pcs/lib/external.py @@ -60,31 +60,29 @@ def run( env_vars.update(dict(env_extend) if env_extend else {}) log_args = " ".join([shell_quote(x) for x in args]) - self._logger.debug( - "Running: {args}\nEnvironment:{env_vars}{stdin_string}".format( - args=log_args, - stdin_string=( - "" - if not stdin_string - else ( - "\n--Debug Input Start--\n{0}\n--Debug Input End--" - ).format(stdin_string) - ), - env_vars=( - "" - if not env_vars - else ( - "\n" - + "\n".join( - [ - " {0}={1}".format(key, val) - for key, val in sorted(env_vars.items()) - ] - ) - ) - ), + env = ( + "" + if not env_vars + else ( + "\n" + + "\n".join( + [ + " {0}={1}".format(key, val) + for key, val in sorted(env_vars.items()) + ] + ) ) ) + stdin = ( + "" + if not stdin_string + else ("\n--Debug Input Start--\n{0}\n--Debug Input End--").format( + stdin_string + ) + ) + self._logger.debug( + "Running: %s\nEnvironment:%s%s", log_args, env, stdin + ) self._reporter.report( ReportItem.debug( reports.messages.RunExternalProcessStarted( @@ -131,12 +129,14 @@ def run( self._logger.debug( ( - "Finished running: {args}\nReturn value: {retval}" - + "\n--Debug Stdout Start--\n{out_std}\n--Debug Stdout End--" - + "\n--Debug Stderr Start--\n{out_err}\n--Debug Stderr End--" - ).format( - args=log_args, retval=retval, out_std=out_std, out_err=out_err - ) + "Finished running: %s\nReturn value: %s" + "\n--Debug Stdout Start--\n%s\n--Debug Stdout End--" + "\n--Debug Stderr Start--\n%s\n--Debug Stderr End--" + ), + log_args, + retval, + out_std, + out_err, ) self._reporter.report( ReportItem.debug( diff --git a/pcs/lib/node_communication.py b/pcs/lib/node_communication.py index 0db7be9c6..71bd59588 100644 --- a/pcs/lib/node_communication.py +++ b/pcs/lib/node_communication.py @@ -97,10 +97,12 @@ def _log_debug(self, response): url = response.request.url debug_data = response.debug self._logger.debug( - f"Communication debug info for calling: {url}\n" + "Communication debug info for calling: %s\n" "--Debug Communication Info Start--\n" - f"{debug_data}\n" - "--Debug Communication Info End--" + "%s\n" + "--Debug Communication Info End--", + url, + debug_data, ) self._reporter.report( ReportItem.debug( diff --git a/pcs_test/tier0/lib/test_external.py b/pcs_test/tier0/lib/test_external.py index bc0c9ed6e..b85d74942 100644 --- a/pcs_test/tier0/lib/test_external.py +++ b/pcs_test/tier0/lib/test_external.py @@ -63,24 +63,23 @@ def test_basic(self, mock_popen): }, ) logger_calls = [ - mock.call("Running: {0}\nEnvironment:".format(command_str)), + mock.call("Running: %s\nEnvironment:%s%s", command_str, "", ""), mock.call( outdent( """\ - Finished running: {0} - Return value: {1} + Finished running: %s + Return value: %s --Debug Stdout Start-- - {2} + %s --Debug Stdout End-- --Debug Stderr Start-- - {3} + %s --Debug Stderr End--""" - ).format( - command_str, - expected_retval, - expected_stdout, - expected_stderr, - ) + ), + command_str, + expected_retval, + expected_stdout, + expected_stderr, ), ] self.assertEqual(self.mock_logger.debug.call_count, len(logger_calls)) @@ -152,30 +151,29 @@ def test_env(self, mock_popen): mock.call( outdent( """\ - Running: {0} - Environment: - a=a - b=B - c={1}""" - ).format(command_str, "{C}") + Running: %s + Environment:%s%s""" + ), + command_str, + "\n a=a\n b=B\n c={C}", + "", ), mock.call( outdent( """\ - Finished running: {0} - Return value: {1} + Finished running: %s + Return value: %s --Debug Stdout Start-- - {2} + %s --Debug Stdout End-- --Debug Stderr Start-- - {3} + %s --Debug Stderr End--""" - ).format( - command_str, - expected_retval, - expected_stdout, - expected_stderr, - ) + ), + command_str, + expected_retval, + expected_stdout, + expected_stderr, ), ] self.assertEqual(self.mock_logger.debug.call_count, len(logger_calls)) @@ -236,30 +234,34 @@ def test_stdin(self, mock_popen): mock.call( outdent( """\ - Running: {0} - Environment: + Running: %s + Environment:%s%s""" + ), + command_str, + "", + outdent( + f""" --Debug Input Start-- - {1} + {stdin} --Debug Input End--""" - ).format(command_str, stdin) + ), ), mock.call( outdent( """\ - Finished running: {0} - Return value: {1} + Finished running: %s + Return value: %s --Debug Stdout Start-- - {2} + %s --Debug Stdout End-- --Debug Stderr Start-- - {3} + %s --Debug Stderr End--""" - ).format( - command_str, - expected_retval, - expected_stdout, - expected_stderr, - ) + ), + command_str, + expected_retval, + expected_stdout, + expected_stderr, ), ] self.assertEqual(self.mock_logger.debug.call_count, len(logger_calls)) @@ -321,7 +323,7 @@ def test_popen_error(self, mock_popen): }, ) logger_calls = [ - mock.call("Running: {0}\nEnvironment:".format(command_str)), + mock.call("Running: %s\nEnvironment:%s%s", command_str, "", "") ] self.assertEqual(self.mock_logger.debug.call_count, len(logger_calls)) self.mock_logger.debug.assert_has_calls(logger_calls) @@ -373,7 +375,7 @@ def test_communicate_error(self, mock_popen): }, ) logger_calls = [ - mock.call("Running: {0}\nEnvironment:".format(command_str)), + mock.call("Running: %s\nEnvironment:%s%s", command_str, "", ""), ] self.assertEqual(self.mock_logger.debug.call_count, len(logger_calls)) self.mock_logger.debug.assert_has_calls(logger_calls) diff --git a/pcs_test/tier0/lib/test_node_communication.py b/pcs_test/tier0/lib/test_node_communication.py index dac414b18..110c7fcc5 100644 --- a/pcs_test/tier0/lib/test_node_communication.py +++ b/pcs_test/tier0/lib/test_node_communication.py @@ -404,12 +404,12 @@ def fixture_logger_call_send(url, data): def fixture_logger_call_debug_data(url, data): send_msg = outdent( """\ - Communication debug info for calling: {url} + Communication debug info for calling: %s --Debug Communication Info Start-- - {data} + %s --Debug Communication Info End--""" ) - return mock.call.debug(send_msg.format(url=url, data=data)) + return mock.call.debug(send_msg, url, data) def fixture_logger_call_connected(url, response_code, response_data): diff --git a/pyproject.toml b/pyproject.toml index 9afa7c677..f1c7a3feb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,6 +87,7 @@ select = [ "E7", "E9", "F", + "G", "I", "LOG", "PL", # pylint convention, error, refactoring, warning From a726a489cd596ad177cfe759fee7ac4aeaacec79 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Thu, 9 Jan 2025 14:39:26 +0100 Subject: [PATCH 071/227] ruff: enable ruff linter `C4` (flake8-comprehensions) * https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4 * disable: * C408 https://docs.astral.sh/ruff/rules/unnecessary-collection-call/ --- pcs/alert.py | 8 +- pcs/cli/booth/command.py | 2 +- pcs/cli/common/completion.py | 2 +- pcs/cli/common/lib_wrapper.py | 6 +- pcs/cli/query/resource.py | 4 +- pcs/cli/resource/parse_args.py | 22 ++--- pcs/common/pacemaker/resource/list.py | 4 +- pcs/common/resource_status.py | 8 +- pcs/common/str_tools.py | 2 +- pcs/host.py | 2 +- pcs/lib/cib/remove_elements.py | 4 +- pcs/lib/cib/resource/common.py | 2 +- pcs/lib/cib/resource/operations.py | 12 +-- pcs/lib/commands/resource.py | 4 +- pcs/lib/commands/status.py | 4 +- pcs/lib/pacemaker/status.py | 8 +- pcs/lib/resource_agent/pcs_transform.py | 2 +- pcs/lib/resource_agent/types.py | 2 +- pcs/resource.py | 6 +- pcs_test/suite.py | 12 +-- .../cli/cluster_property/test_command.py | 4 +- pcs_test/tier0/cli/common/test_parse_args.py | 22 ++--- pcs_test/tier0/cli/resource/test_defaults.py | 4 +- .../tier0/common/reports/test_messages.py | 4 +- .../daemon/async_tasks/test_scheduler.py | 4 +- .../lib/commands/resource/bundle_common.py | 14 +-- pcs_test/tier1/legacy/test_resource.py | 32 +++---- pcs_test/tier1/legacy/test_utils.py | 92 +++++++++---------- pcs_test/tools/color_text_runner/format.py | 6 +- pyproject.toml | 2 + 30 files changed, 144 insertions(+), 156 deletions(-) diff --git a/pcs/alert.py b/pcs/alert.py index ab3a1dcdc..f8fd31457 100644 --- a/pcs/alert.py +++ b/pcs/alert.py @@ -22,7 +22,7 @@ def alert_add(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: raise CmdLineInputError() sections = group_by_keywords( - argv, set(["options", "meta"]), implicit_first_keyword="main" + argv, {"options", "meta"}, implicit_first_keyword="main" ) parser = KeyValueParser(sections.get_args_flat("main")) parser.check_allowed_keys(["id", "description", "path"]) @@ -49,7 +49,7 @@ def alert_update(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: alert_id = argv[0] sections = group_by_keywords( - argv[1:], set(["options", "meta"]), implicit_first_keyword="main" + argv[1:], {"options", "meta"}, implicit_first_keyword="main" ) parser = KeyValueParser(sections.get_args_flat("main")) parser.check_allowed_keys(["description", "path"]) @@ -89,7 +89,7 @@ def recipient_add(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: alert_id = argv[0] sections = group_by_keywords( - argv[1:], set(["options", "meta"]), implicit_first_keyword="main" + argv[1:], {"options", "meta"}, implicit_first_keyword="main" ) parser = KeyValueParser(sections.get_args_flat("main")) parser.check_allowed_keys(["description", "id", "value"]) @@ -119,7 +119,7 @@ def recipient_update(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: recipient_id = argv[0] sections = group_by_keywords( - argv[1:], set(["options", "meta"]), implicit_first_keyword="main" + argv[1:], {"options", "meta"}, implicit_first_keyword="main" ) parser = KeyValueParser(sections.get_args_flat("main")) parser.check_allowed_keys(["description", "value"]) diff --git a/pcs/cli/booth/command.py b/pcs/cli/booth/command.py index 3acb3fe6e..6a9f68e89 100644 --- a/pcs/cli/booth/command.py +++ b/pcs/cli/booth/command.py @@ -29,7 +29,7 @@ def config_setup(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: "--booth-key", "--name", ) - peers = group_by_keywords(arg_list, set(["sites", "arbitrators"])) + peers = group_by_keywords(arg_list, {"sites", "arbitrators"}) peers.ensure_unique_keywords() if not peers.has_keyword("sites") or not peers.get_args_flat("sites"): raise CmdLineInputError() diff --git a/pcs/cli/common/completion.py b/pcs/cli/common/completion.py index f42017bf7..db3798558 100644 --- a/pcs/cli/common/completion.py +++ b/pcs/cli/common/completion.py @@ -120,4 +120,4 @@ def _get_subcommands( if subcommand not in subcommand_tree: return [] subcommand_tree = subcommand_tree[subcommand] - return sorted(list(subcommand_tree.keys())) + return sorted(subcommand_tree.keys()) diff --git a/pcs/cli/common/lib_wrapper.py b/pcs/cli/common/lib_wrapper.py index 9a6622e79..d2c18ba7b 100644 --- a/pcs/cli/common/lib_wrapper.py +++ b/pcs/cli/common/lib_wrapper.py @@ -94,10 +94,10 @@ def decorated_run(*args, **kwargs): def bind_all(env, run_with_middleware, dictionary): return wrapper( - dict( - (exposed_fn, bind(env, run_with_middleware, library_fn)) + { + exposed_fn: bind(env, run_with_middleware, library_fn) for exposed_fn, library_fn in dictionary.items() - ) + } ) diff --git a/pcs/cli/query/resource.py b/pcs/cli/query/resource.py index cee13d194..bbba12de3 100644 --- a/pcs/cli/query/resource.py +++ b/pcs/cli/query/resource.py @@ -355,9 +355,7 @@ def is_state(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: sections = group_by_keywords( argv, - set( - ["on-node", "members", "instances"], - ), + {"on-node", "members", "instances"}, implicit_first_keyword="state", ) sections.ensure_unique_keywords() diff --git a/pcs/cli/resource/parse_args.py b/pcs/cli/resource/parse_args.py index 88f2b2852..b07f9fef1 100644 --- a/pcs/cli/resource/parse_args.py +++ b/pcs/cli/resource/parse_args.py @@ -74,7 +74,7 @@ class AddRemoveOptions: def parse_primitive(arg_list: Argv) -> PrimitiveOptions: groups = group_by_keywords( - arg_list, set(["op", "meta"]), implicit_first_keyword="instance" + arg_list, {"op", "meta"}, implicit_first_keyword="instance" ) parts = PrimitiveOptions( @@ -93,7 +93,7 @@ def parse_primitive(arg_list: Argv) -> PrimitiveOptions: def parse_clone(arg_list: Argv, promotable: bool = False) -> CloneOptions: clone_id = None - allowed_keywords = set(["op", "meta"]) + allowed_keywords = {"op", "meta"} if ( arg_list and arg_list[0] not in allowed_keywords @@ -134,14 +134,14 @@ def parse_create_new(arg_list: Argv) -> ComplexResourceOptions: try: top_groups = group_by_keywords( arg_list, - set(["clone", "promotable", "bundle", "group"]), + {"clone", "promotable", "bundle", "group"}, implicit_first_keyword="primitive", ) top_groups.ensure_unique_keywords() primitive_groups = group_by_keywords( top_groups.get_args_flat("primitive"), - set(["op", "meta"]), + {"op", "meta"}, implicit_first_keyword="instance", ) primitive_options = PrimitiveOptions( @@ -163,7 +163,7 @@ def parse_create_new(arg_list: Argv) -> ComplexResourceOptions: if top_groups.has_keyword("group"): group_groups = group_by_keywords( top_groups.get_args_flat("group"), - set(["before", "after", "op", "meta"]), + {"before", "after", "op", "meta"}, implicit_first_keyword="group_id", ) if group_groups.has_keyword("meta"): @@ -203,7 +203,7 @@ def parse_create_new(arg_list: Argv) -> ComplexResourceOptions: continue clone_groups = group_by_keywords( top_groups.get_args_flat(clone_type), - set(["op", "meta"]), + {"op", "meta"}, implicit_first_keyword="options", ) clone_id = None @@ -232,7 +232,7 @@ def parse_create_new(arg_list: Argv) -> ComplexResourceOptions: if top_groups.has_keyword("bundle"): bundle_groups = group_by_keywords( top_groups.get_args_flat("bundle"), - set(["op", "meta"]), + {"op", "meta"}, implicit_first_keyword="options", ) if bundle_groups.has_keyword("meta"): @@ -285,13 +285,13 @@ def parse_create_old( try: top_groups = group_by_keywords( arg_list, - set(["clone", "promotable", "bundle"]), + {"clone", "promotable", "bundle"}, implicit_first_keyword="primitive", ) primitive_groups = group_by_keywords( top_groups.get_args_flat("primitive"), - set(["op", "meta"]), + {"op", "meta"}, implicit_first_keyword="instance", ) primitive_instance_attrs = primitive_groups.get_args_flat("instance") @@ -335,7 +335,7 @@ def parse_create_old( continue clone_groups = group_by_keywords( top_groups.get_args_flat(clone_type), - set(["op", "meta"]), + {"op", "meta"}, implicit_first_keyword="options", ) clone_id = None @@ -375,7 +375,7 @@ def parse_create_old( if top_groups.has_keyword("bundle"): bundle_groups = group_by_keywords( top_groups.get_args_flat("bundle"), - set(["op", "meta"]), + {"op", "meta"}, implicit_first_keyword="options", ) if bundle_groups.has_keyword("meta"): diff --git a/pcs/common/pacemaker/resource/list.py b/pcs/common/pacemaker/resource/list.py index 86c8aed33..124adad24 100644 --- a/pcs/common/pacemaker/resource/list.py +++ b/pcs/common/pacemaker/resource/list.py @@ -30,8 +30,8 @@ def get_all_resources_ids(resources_dto: CibResourcesDto) -> set[str]: def get_stonith_resources_ids(resources_dto: CibResourcesDto) -> set[str]: - return set( + return { primitive.id for primitive in resources_dto.primitives if primitive.agent_name.standard == "stonith" - ) + } diff --git a/pcs/common/resource_status.py b/pcs/common/resource_status.py index 25fc1348c..a46349de1 100644 --- a/pcs/common/resource_status.py +++ b/pcs/common/resource_status.py @@ -743,20 +743,20 @@ def get_members( if isinstance(resource, CloneStatusDto): return list( - set( + { instance.resource_id for instance in resource.instances if not _is_orphaned(instance) - ) + } ) if isinstance(resource, BundleStatusDto): return list( - set( + { replica.member.resource_id for replica in resource.replicas if replica.member is not None - ) + } ) raise ResourceUnexpectedTypeException( diff --git a/pcs/common/str_tools.py b/pcs/common/str_tools.py index 125966fba..89a80b7b5 100644 --- a/pcs/common/str_tools.py +++ b/pcs/common/str_tools.py @@ -244,7 +244,7 @@ def format_plural( def transform(items: list[T], mapping: Mapping[T, str]) -> list[str]: - return list(map(lambda item: mapping.get(item, str(item)), items)) + return [mapping.get(item, str(item)) for item in items] def is_iterable_not_str(value: Union[IterableAbc, str]) -> bool: diff --git a/pcs/host.py b/pcs/host.py index 09621dd4a..a50df6502 100644 --- a/pcs/host.py +++ b/pcs/host.py @@ -23,7 +23,7 @@ def _parse_host_options( host: str, options: Argv ) -> dict[str, Union[str, list[dict[str, Union[None, str, int]]]]]: ADDR_OPT_KEYWORD = "addr" # pylint: disable=invalid-name - supported_options = set([ADDR_OPT_KEYWORD]) + supported_options = {ADDR_OPT_KEYWORD} parsed_options = KeyValueParser(options).get_unique() unknown_options = set(parsed_options.keys()) - supported_options if unknown_options: diff --git a/pcs/lib/cib/remove_elements.py b/pcs/lib/cib/remove_elements.py index 093218dac..8ca9e7095 100644 --- a/pcs/lib/cib/remove_elements.py +++ b/pcs/lib/cib/remove_elements.py @@ -145,9 +145,9 @@ def __init__(self, cib: _Element, ids: StringCollection): self._ids_to_remove = element_ids_to_remove self._dependant_element_ids = self._ids_to_remove - initial_ids self._missing_ids = set(missing_ids) - self._unsupported_ids = set( + self._unsupported_ids = { str(el.attrib["id"]) for el in unsupported_elements - ) + } all_ids = set( chain( diff --git a/pcs/lib/cib/resource/common.py b/pcs/lib/cib/resource/common.py index ce259aa55..5acaf3405 100644 --- a/pcs/lib/cib/resource/common.py +++ b/pcs/lib/cib/resource/common.py @@ -119,7 +119,7 @@ def get_all_inner_resources(resource_el: _Element) -> Set[_Element]: resource_el -- resource element to get its inner resources """ all_inner: Set[_Element] = set() - to_process = set([resource_el]) + to_process = {resource_el} while to_process: new_inner = get_inner_resources(to_process.pop()) to_process.update(set(new_inner) - all_inner) diff --git a/pcs/lib/cib/resource/operations.py b/pcs/lib/cib/resource/operations.py index e44900cb9..7b4cc85a4 100644 --- a/pcs/lib/cib/resource/operations.py +++ b/pcs/lib/cib/resource/operations.py @@ -406,11 +406,11 @@ def append_new_operation(operations_element, id_provider, options): IdProvider id_provider -- elements' ids generator dict options are attributes of operation """ - attribute_map = dict( - (key, value) + attribute_map = { + key: value for key, value in options.items() if key not in OPERATION_NVPAIR_ATTRIBUTES - ) + } if "id" in attribute_map: if does_id_exist(operations_element, attribute_map["id"]): raise LibraryError( @@ -434,11 +434,11 @@ def append_new_operation(operations_element, id_provider, options): "op", attribute_map, ) - nvpair_attribute_map = dict( - (key, value) + nvpair_attribute_map = { + key: value for key, value in options.items() if key in OPERATION_NVPAIR_ATTRIBUTES - ) + } if nvpair_attribute_map: append_new_instance_attributes( diff --git a/pcs/lib/commands/resource.py b/pcs/lib/commands/resource.py index d0e752ce3..2d1b0dfcc 100644 --- a/pcs/lib/commands/resource.py +++ b/pcs/lib/commands/resource.py @@ -1608,10 +1608,10 @@ def group_add( and resource.group.is_group(old_parent) and str(old_parent.attrib["id"]) not in all_resources ): - all_resources[str(old_parent.attrib["id"])] = set( + all_resources[str(old_parent.attrib["id"])] = { str(res.attrib["id"]) for res in resource.common.get_inner_resources(old_parent) - ) + } affected_resources = set(resource_id_list) # Set comparison step to determine if groups will be emptied by move diff --git a/pcs/lib/commands/status.py b/pcs/lib/commands/status.py index 72c234567..c7767ede0 100644 --- a/pcs/lib/commands/status.py +++ b/pcs/lib/commands/status.py @@ -278,7 +278,7 @@ def _move_constraints_warnings( location_constraints, _ = get_all_as_dtos(constraint_el, rule_evaluator) - resource_ids = set( + resource_ids = { constraint_dto.resource_id for constraint_dto in location_constraints if constraint_dto.resource_id @@ -290,7 +290,7 @@ def _move_constraints_warnings( for rule in constraint_dto.attributes.rules ) ) - ) + } if resource_ids: warning_list.append( diff --git a/pcs/lib/pacemaker/status.py b/pcs/lib/pacemaker/status.py index 2932f5c04..4beea62c2 100644 --- a/pcs/lib/pacemaker/status.py +++ b/pcs/lib/pacemaker/status.py @@ -252,14 +252,14 @@ def _clone_to_dto( raise MixedMembersError(clone_id) if primitive_list: - if len(set(res.resource_id for res in primitive_list)) > 1: + if len({res.resource_id for res in primitive_list}) > 1: raise DifferentMemberIdsError(clone_id) if group_list: - group_ids = set(group.resource_id for group in group_list) - children_ids = set( + group_ids = {group.resource_id for group in group_list} + children_ids = { tuple(child.resource_id for child in group.members) for group in group_list - ) + } if len(group_ids) > 1 or len(children_ids) > 1: raise DifferentMemberIdsError(clone_id) diff --git a/pcs/lib/resource_agent/pcs_transform.py b/pcs/lib/resource_agent/pcs_transform.py index 14022c87b..8b045fd16 100644 --- a/pcs/lib/resource_agent/pcs_transform.py +++ b/pcs/lib/resource_agent/pcs_transform.py @@ -231,7 +231,7 @@ def _metadata_make_stonith_port_parameter_not_required( # parameter (defined in fenced metadata). Therefore, we must mark 'port' # and all parameters replacing it as not required. port_related_params = set() - next_iteration_params = set(["port"]) + next_iteration_params = {"port"} while next_iteration_params: current_params = next_iteration_params next_iteration_params = set() diff --git a/pcs/lib/resource_agent/types.py b/pcs/lib/resource_agent/types.py index 68d07b6a4..9b1bd421a 100644 --- a/pcs/lib/resource_agent/types.py +++ b/pcs/lib/resource_agent/types.py @@ -287,7 +287,7 @@ def provides_self_validation(self) -> bool: @property def provides_promotability(self) -> bool: - return set(action.name for action in self.actions) >= { + return {action.name for action in self.actions} >= { "promote", "demote", } diff --git a/pcs/resource.py b/pcs/resource.py index 3af78735b..4214174c9 100644 --- a/pcs/resource.py +++ b/pcs/resource.py @@ -181,7 +181,7 @@ def _defaults_set_create_cmd( modifiers.ensure_only_supported("-f", "--force") groups = group_by_keywords( - argv, set(["meta", "rule"]), implicit_first_keyword="options" + argv, {"meta", "rule"}, implicit_first_keyword="options" ) groups.ensure_unique_keywords() force_flags = set() @@ -377,7 +377,7 @@ def _defaults_set_update_cmd( raise CmdLineInputError() set_id = argv[0] - groups = group_by_keywords(argv[1:], set(["meta"])) + groups = group_by_keywords(argv[1:], {"meta"}) groups.ensure_unique_keywords() lib_command( set_id, KeyValueParser(groups.get_args_flat("meta")).get_unique() @@ -1488,7 +1488,7 @@ def resource_operation_remove(res_id: str, argv: Argv) -> None: if attr_name == "id": continue temp_properties.append( - tuple([attr_name, op.attributes.get(attr_name).nodeValue]) + (attr_name, op.attributes.get(attr_name).nodeValue) ) if remove_all and op.attributes["name"].value == op_name: diff --git a/pcs_test/suite.py b/pcs_test/suite.py index a6d6e2e9a..39bee0b52 100644 --- a/pcs_test/suite.py +++ b/pcs_test/suite.py @@ -88,13 +88,11 @@ def discover_tests( def tier1_fixtures_needed(test_list: list[str]) -> set[str]: - fixture_modules = set( - [ - "pcs_test.tier1.legacy.test_constraints", - "pcs_test.tier1.legacy.test_resource", - "pcs_test.tier1.legacy.test_stonith", - ] - ) + fixture_modules = { + "pcs_test.tier1.legacy.test_constraints", + "pcs_test.tier1.legacy.test_resource", + "pcs_test.tier1.legacy.test_stonith", + } fixtures_needed = set() for test_name in test_list: for module in fixture_modules: diff --git a/pcs_test/tier0/cli/cluster_property/test_command.py b/pcs_test/tier0/cli/cluster_property/test_command.py index e19cea032..d4bfba6fe 100644 --- a/pcs_test/tier0/cli/cluster_property/test_command.py +++ b/pcs_test/tier0/cli/cluster_property/test_command.py @@ -137,7 +137,7 @@ def test_multiple_args(self): def test_multiple_args_with_force(self): self._call_cmd(["a=1", "b=2", "c="], {"force": True}) self.cluster_property.set_properties.assert_called_once_with( - {"a": "1", "b": "2", "c": ""}, set([report_codes.FORCE]) + {"a": "1", "b": "2", "c": ""}, {report_codes.FORCE} ) def test_unsupported_modifier(self): @@ -176,7 +176,7 @@ def test_args(self): def test_args_with_force(self): self._call_cmd(["a=1", "=b", ""], {"force": True}) self.cluster_property.set_properties.assert_called_once_with( - {"a=1": "", "=b": "", "": ""}, set([report_codes.FORCE]) + {"a=1": "", "=b": "", "": ""}, {report_codes.FORCE} ) def test_unsupported_modifier(self): diff --git a/pcs_test/tier0/cli/common/test_parse_args.py b/pcs_test/tier0/cli/common/test_parse_args.py index ce0a98cb2..95d7d2459 100644 --- a/pcs_test/tier0/cli/common/test_parse_args.py +++ b/pcs_test/tier0/cli/common/test_parse_args.py @@ -252,7 +252,7 @@ def test_split_with_implicit_first_keyword(self): self.assertEqual( group_by_keywords( [0, "first", 1, 2, "second", 3], - set(["first", "second"]), + {"first", "second"}, implicit_first_keyword="zero", )._groups, {"zero": [[0]], "first": [[1, 2]], "second": [[3]]}, @@ -262,7 +262,7 @@ def test_split_without_implicit_keyword(self): self.assertEqual( group_by_keywords( ["first", 1, 2, "second", 3], - set(["first", "second"]), + {"first", "second"}, )._groups, {"first": [[1, 2]], "second": [[3]]}, ) @@ -272,13 +272,13 @@ def test_raises_when_args_do_not_start_with_keyword_nor_implicit(self): CmdLineInputError, lambda: group_by_keywords( [0, "first", 1, 2, "second", 3], - set(["first", "second"]), + {"first", "second"}, ), ) def test_no_args(self): self.assertEqual( - group_by_keywords([], set(["first", "second"]))._groups, + group_by_keywords([], {"first", "second"})._groups, {}, ) @@ -286,7 +286,7 @@ def test_no_args_implicit_case(self): self.assertEqual( group_by_keywords( [], - set(["first", "second"]), + {"first", "second"}, implicit_first_keyword="zero", )._groups, {}, @@ -296,7 +296,7 @@ def test_no_args_for_keyword(self): self.assertEqual( group_by_keywords( ["first"], - set(["first", "second"]), + {"first", "second"}, )._groups, {"first": [[]]}, ) @@ -305,7 +305,7 @@ def test_keywords_repeating(self): self.assertEqual( group_by_keywords( ["first", 1, 2, "second", 3, "first", 4], - set(["first", "second"]), + {"first", "second"}, )._groups, {"first": [[1, 2], [4]], "second": [[3]]}, ) @@ -314,7 +314,7 @@ def test_no_args_for_group(self): self.assertEqual( group_by_keywords( ["second", 1, "second", "second", 2, 3], - set(["first", "second"]), + {"first", "second"}, )._groups, {"second": [[1], [], [2, 3]]}, ) @@ -323,7 +323,7 @@ def test_implicit_first_keyword_not_applied_in_the_middle(self): self.assertEqual( group_by_keywords( [1, 2, "first", 3, "zero", 4], - set(["first"]), + {"first"}, implicit_first_keyword="zero", )._groups, {"zero": [[1, 2]], "first": [[3, "zero", 4]]}, @@ -335,7 +335,7 @@ def test_implicit_first_keyword_applied_in_the_middle_when_in_keywords( self.assertEqual( group_by_keywords( [1, 2, "first", 3, "zero", 4], - set(["first", "zero"]), + {"first", "zero"}, implicit_first_keyword="zero", )._groups, {"zero": [[1, 2], [4]], "first": [[3]]}, @@ -347,7 +347,7 @@ def test_implicit_first_keyword_ignored_when_another_keyword_is_first_arg( self.assertEqual( group_by_keywords( ["first", "1", "2", "second", "3"], - set(["first", "second"]), + {"first", "second"}, implicit_first_keyword="zero", )._groups, {"first": [["1", "2"]], "second": [["3"]]}, diff --git a/pcs_test/tier0/cli/resource/test_defaults.py b/pcs_test/tier0/cli/resource/test_defaults.py index 845e80dab..c51ff2a86 100644 --- a/pcs_test/tier0/cli/resource/test_defaults.py +++ b/pcs_test/tier0/cli/resource/test_defaults.py @@ -357,7 +357,7 @@ def test_rule(self): def test_force(self): self._call_cmd([], {"force": True}) self.lib_command.assert_called_once_with( - {}, {}, nvset_rule=None, force_flags=set([report_codes.FORCE]) + {}, {}, nvset_rule=None, force_flags={report_codes.FORCE} ) def test_all(self): @@ -381,7 +381,7 @@ def test_all(self): {"name1": "value1", "name2": "value2"}, {"id": "custom-id", "score": "10"}, nvset_rule="resource dummy or op monitor", - force_flags=set([report_codes.FORCE]), + force_flags={report_codes.FORCE}, ) diff --git a/pcs_test/tier0/common/reports/test_messages.py b/pcs_test/tier0/common/reports/test_messages.py index e9f47786d..4a6ceb6ce 100644 --- a/pcs_test/tier0/common/reports/test_messages.py +++ b/pcs_test/tier0/common/reports/test_messages.py @@ -5834,7 +5834,7 @@ def test_all(self): class AgentSelfValidationResult(NameBuildTest): def test_message(self): - lines = list(f"line #{i}" for i in range(3)) + lines = [f"line #{i}" for i in range(3)] self.assert_message_from_report( "Validation result from agent:\n {}".format("\n ".join(lines)), reports.AgentSelfValidationResult("\n".join(lines)), @@ -5852,7 +5852,7 @@ def test_message(self): class AgentSelfValidationSkippedUpdatedResourceMisconfigured(NameBuildTest): def test_message(self): - lines = list(f"line #{i}" for i in range(3)) + lines = [f"line #{i}" for i in range(3)] self.assert_message_from_report( ( "The resource was misconfigured before the update, therefore " diff --git a/pcs_test/tier0/daemon/async_tasks/test_scheduler.py b/pcs_test/tier0/daemon/async_tasks/test_scheduler.py index c6fd8eef2..c7ea0e857 100644 --- a/pcs_test/tier0/daemon/async_tasks/test_scheduler.py +++ b/pcs_test/tier0/daemon/async_tasks/test_scheduler.py @@ -193,11 +193,11 @@ async def test_normal_run(self): self.assertEqual( 0, len( - list( + [ task for task in self.scheduler._task_register.values() if task.state == TaskState.CREATED - ) + ] ), ) diff --git a/pcs_test/tier0/lib/commands/resource/bundle_common.py b/pcs_test/tier0/lib/commands/resource/bundle_common.py index 9160e8570..c79bc7777 100644 --- a/pcs_test/tier0/lib/commands/resource/bundle_common.py +++ b/pcs_test/tier0/lib/commands/resource/bundle_common.py @@ -274,7 +274,7 @@ def test_unknow_container_option(self): "extra", ], "option_type": "container", - "allowed": sorted(list(GENERIC_CONTAINER_OPTIONS)), + "allowed": sorted(GENERIC_CONTAINER_OPTIONS), "allowed_patterns": [], }, report_codes.FORCE, @@ -313,7 +313,7 @@ def test_unknow_container_option_forced(self): "extra", ], "option_type": "container", - "allowed": sorted(list(GENERIC_CONTAINER_OPTIONS)), + "allowed": sorted(GENERIC_CONTAINER_OPTIONS), "allowed_patterns": [], }, None, @@ -437,7 +437,7 @@ def test_options_forced(self): "extra", ], "option_type": "network", - "allowed": sorted(list(NETWORK_OPTIONS)), + "allowed": sorted(NETWORK_OPTIONS), "allowed_patterns": [], }, None, @@ -628,7 +628,7 @@ def test_forceable_options_errors(self): { "option_names": ["extra"], "option_type": "port-map", - "allowed": sorted(list(PORT_MAP_OPTIONS)), + "allowed": sorted(PORT_MAP_OPTIONS), "allowed_patterns": [], }, report_codes.FORCE, @@ -685,7 +685,7 @@ def test_forceable_options_errors_forced(self): { "option_names": ["extra"], "option_type": "port-map", - "allowed": sorted(list(PORT_MAP_OPTIONS)), + "allowed": sorted(PORT_MAP_OPTIONS), "allowed_patterns": [], }, None, @@ -860,7 +860,7 @@ def test_forceable_options_errors(self): "extra", ], "option_type": "storage-map", - "allowed": sorted(list(STORAGE_MAP_OPTIONS)), + "allowed": sorted(STORAGE_MAP_OPTIONS), "allowed_patterns": [], }, report_codes.FORCE, @@ -912,7 +912,7 @@ def test_forceable_options_errors_forced(self): "extra", ], "option_type": "storage-map", - "allowed": sorted(list(STORAGE_MAP_OPTIONS)), + "allowed": sorted(STORAGE_MAP_OPTIONS), "allowed_patterns": [], }, None, diff --git a/pcs_test/tier1/legacy/test_resource.py b/pcs_test/tier1/legacy/test_resource.py index b448fcc78..06a2a8f58 100644 --- a/pcs_test/tier1/legacy/test_resource.py +++ b/pcs_test/tier1/legacy/test_resource.py @@ -3230,20 +3230,18 @@ def test_relocate_stickiness(self): self.assertEqual(stderr, "") self.assertEqual(returncode, 0) - resources = set( - [ - "D1", - "DG1", - "DG2", - "GR", - "DC", - "DC-clone", - "DGC1", - "DGC2", - "GRC", - "GRC-clone", - ] - ) + resources = { + "D1", + "DG1", + "DG2", + "GR", + "DC", + "DC-clone", + "DGC1", + "DGC2", + "GRC", + "GRC-clone", + } self.assert_pcs_success("resource config".split(), status) cib_in = utils.parseString(cib_original) cib_out, updated_resources = resource.resource_relocate_set_stickiness( @@ -3310,7 +3308,7 @@ def test_relocate_stickiness(self): ), ) - resources = set(["D1", "DG1", "DC", "DGC1"]) + resources = {"D1", "DG1", "DC", "DGC1"} write_data_to_tmpfile(cib_original, self.temp_cib) self.assert_pcs_success("resource config".split(), status) cib_in = utils.parseString(cib_original) @@ -3365,7 +3363,7 @@ def test_relocate_stickiness(self): ), ) - resources = set(["GRC-clone", "GRC", "DGC1", "DGC2"]) + resources = {"GRC-clone", "GRC", "DGC1", "DGC2"} write_data_to_tmpfile(cib_original, self.temp_cib) self.assert_pcs_success("resource config".split(), status) cib_in = utils.parseString(cib_original) @@ -3420,7 +3418,7 @@ def test_relocate_stickiness(self): ), ) - resources = set(["GR", "DG1", "DG2", "DC-clone", "DC"]) + resources = {"GR", "DG1", "DG2", "DC-clone", "DC"} write_data_to_tmpfile(cib_original, self.temp_cib) self.assert_pcs_success("resource config".split(), status) cib_in = utils.parseString(cib_original) diff --git a/pcs_test/tier1/legacy/test_utils.py b/pcs_test/tier1/legacy/test_utils.py index ca4e235d7..f75ceec6a 100644 --- a/pcs_test/tier1/legacy/test_utils.py +++ b/pcs_test/tier1/legacy/test_utils.py @@ -127,42 +127,38 @@ def test_dom_get(method, dom, ok_ids, bad_ids): ) cib_dom = self.get_cib_resources() - all_ids = set( - [ - "none", - "myResource", - "myClone", - "myClonedResource", - "myUniqueClone", - "myUniqueClonedResource", - "myMaster", - "myMasteredResource", - "myGroup", - "myGroupedResource", - "myGroupClone", - "myClonedGroup", - "myClonedGroupedResource", - "myGroupMaster", - "myMasteredGroup", - "myMasteredGroupedResource", - "myBundledResource", - "myBundle", - "myEmptyBundle", - ] - ) + all_ids = { + "none", + "myResource", + "myClone", + "myClonedResource", + "myUniqueClone", + "myUniqueClonedResource", + "myMaster", + "myMasteredResource", + "myGroup", + "myGroupedResource", + "myGroupClone", + "myClonedGroup", + "myClonedGroupedResource", + "myGroupMaster", + "myMasteredGroup", + "myMasteredGroupedResource", + "myBundledResource", + "myBundle", + "myEmptyBundle", + } - resource_ids = set( - [ - "myResource", - "myClonedResource", - "myUniqueClonedResource", - "myGroupedResource", - "myMasteredResource", - "myClonedGroupedResource", - "myMasteredGroupedResource", - "myBundledResource", - ] - ) + resource_ids = { + "myResource", + "myClonedResource", + "myUniqueClonedResource", + "myGroupedResource", + "myMasteredResource", + "myClonedGroupedResource", + "myMasteredGroupedResource", + "myBundledResource", + } test_dom_get( utils.dom_get_resource, cib_dom, @@ -170,13 +166,11 @@ def test_dom_get(method, dom, ok_ids, bad_ids): all_ids - resource_ids, ) - cloned_ids = set( - [ - "myClonedResource", - "myUniqueClonedResource", - "myClonedGroupedResource", - ] - ) + cloned_ids = { + "myClonedResource", + "myUniqueClonedResource", + "myClonedGroupedResource", + } test_dom_get( utils.dom_get_resource_clone, cib_dom, @@ -184,7 +178,7 @@ def test_dom_get(method, dom, ok_ids, bad_ids): all_ids - cloned_ids, ) - mastered_ids = set(["myMasteredResource", "myMasteredGroupedResource"]) + mastered_ids = {"myMasteredResource", "myMasteredGroupedResource"} test_dom_get( utils.dom_get_resource_masterslave, cib_dom, @@ -192,12 +186,12 @@ def test_dom_get(method, dom, ok_ids, bad_ids): all_ids - mastered_ids, ) - group_ids = set(["myGroup", "myClonedGroup", "myMasteredGroup"]) + group_ids = {"myGroup", "myClonedGroup", "myMasteredGroup"} test_dom_get( utils.dom_get_group, cib_dom, group_ids, all_ids - group_ids ) - cloned_group_ids = set(["myClonedGroup"]) + cloned_group_ids = {"myClonedGroup"} test_dom_get( utils.dom_get_group_clone, cib_dom, @@ -205,12 +199,12 @@ def test_dom_get(method, dom, ok_ids, bad_ids): all_ids - cloned_group_ids, ) - clone_ids = set(["myClone", "myUniqueClone", "myGroupClone"]) + clone_ids = {"myClone", "myUniqueClone", "myGroupClone"} test_dom_get( utils.dom_get_clone, cib_dom, clone_ids, all_ids - clone_ids ) - mastered_group_ids = set(["myMasteredGroup"]) + mastered_group_ids = {"myMasteredGroup"} test_dom_get( utils.dom_get_group_masterslave, cib_dom, @@ -218,12 +212,12 @@ def test_dom_get(method, dom, ok_ids, bad_ids): all_ids - mastered_group_ids, ) - master_ids = set(["myMaster", "myGroupMaster"]) + master_ids = {"myMaster", "myGroupMaster"} test_dom_get( utils.dom_get_master, cib_dom, master_ids, all_ids - master_ids ) - bundle_ids = set(["myBundle", "myEmptyBundle"]) + bundle_ids = {"myBundle", "myEmptyBundle"} test_dom_get( utils.dom_get_bundle, cib_dom, bundle_ids, all_ids - bundle_ids ) diff --git a/pcs_test/tools/color_text_runner/format.py b/pcs_test/tools/color_text_runner/format.py index 622f64296..d1c6d885d 100644 --- a/pcs_test/tools/color_text_runner/format.py +++ b/pcs_test/tools/color_text_runner/format.py @@ -91,10 +91,8 @@ def test_method_name(self, test): return self._output.lightgrey("_").join(parts) def error_overview(self, errors, failures, slash_last): - error_names = sorted(set(self.test_name(test) for test, _ in errors)) - failure_names = sorted( - set(self.test_name(test) for test, _ in failures) - ) + error_names = sorted({self.test_name(test) for test, _ in errors}) + failure_names = sorted({self.test_name(test) for test, _ in failures}) overview = [] overview.append( diff --git a/pyproject.toml b/pyproject.toml index f1c7a3feb..41a000e9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,6 +83,7 @@ select = [ "A", "ASYNC", "B", + "C4", "E4", "E7", "E9", @@ -96,6 +97,7 @@ select = [ # ruff does not respect pylint ignore directives # https://github.com/astral-sh/ruff/issues/1203 ignore = [ + "C408", # https://docs.astral.sh/ruff/rules/unnecessary-collection-call/ "PLR2004", # magic-value-comparison (99) "PLR0913", # too-many-arguments (54) "PLR0912", # too-many-branches (49) From 1538ef5b50e877b781d327ebce511e1077776c2f Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Wed, 11 Dec 2024 18:17:05 +0100 Subject: [PATCH 072/227] ruff: enable ruff linter `PIE` (flake8-pie) * https://docs.astral.sh/ruff/rules/#flake8-pie-pie --- pcs/cluster.py | 6 +---- pcs/lib/corosync/live.py | 2 +- pcs/lib/tools.py | 2 +- pcs/usage.py | 2 +- .../commands/cluster/test_authkey_corosync.py | 2 +- .../lib/commands/test_cluster_property.py | 2 +- pcs_test/tier0/lib/commands/test_quorum.py | 24 +++++++++---------- pyproject.toml | 1 + 8 files changed, 19 insertions(+), 22 deletions(-) diff --git a/pcs/cluster.py b/pcs/cluster.py index c3640ba65..c355a9d56 100644 --- a/pcs/cluster.py +++ b/pcs/cluster.py @@ -1470,11 +1470,7 @@ def cluster_report(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: utils.err("cluster is not configured on this node") newoutput = "" for line in output.split("\n"): - if ( - line.startswith("cat:") - or line.startswith("grep") - or line.startswith("tail") - ): + if line.startswith(("cat:", "grep", "tail")): continue if "We will attempt to remove" in line: continue diff --git a/pcs/lib/corosync/live.py b/pcs/lib/corosync/live.py index 7409b8f28..7137936c9 100644 --- a/pcs/lib/corosync/live.py +++ b/pcs/lib/corosync/live.py @@ -189,7 +189,7 @@ def _parse_quorum_status(quorum_status: str) -> QuorumStatus: if not line: continue if in_node_list: - if line.startswith("-") or line.startswith("Nodeid"): + if line.startswith(("-", "Nodeid")): # skip headers continue parts = line.split() diff --git a/pcs/lib/tools.py b/pcs/lib/tools.py index 1c2d79bc7..e945c30ac 100644 --- a/pcs/lib/tools.py +++ b/pcs/lib/tools.py @@ -54,7 +54,7 @@ def environment_file_to_dict(config: str) -> dict[str, str]: data = {} for line in [line.strip() for line in config.split("\n")]: - if line == "" or line.startswith("#") or line.startswith(";"): + if line == "" or line.startswith(("#", ";")): continue if "=" not in line: continue diff --git a/pcs/usage.py b/pcs/usage.py index 1f181be82..6fb3a8a28 100644 --- a/pcs/usage.py +++ b/pcs/usage.py @@ -226,7 +226,7 @@ def generate_tree(usage_txt: str) -> CompletionTree: ret_hash[arg] = {} cur_hash = ret_hash[arg] for arg in args: - if arg.startswith("[") or arg.startswith("<"): + if arg.startswith(("[", "<")): break if arg not in cur_hash: cur_hash[arg] = {} diff --git a/pcs_test/tier0/lib/commands/cluster/test_authkey_corosync.py b/pcs_test/tier0/lib/commands/cluster/test_authkey_corosync.py index fd30a1f6d..bdacb2c28 100644 --- a/pcs_test/tier0/lib/commands/cluster/test_authkey_corosync.py +++ b/pcs_test/tier0/lib/commands/cluster/test_authkey_corosync.py @@ -693,7 +693,7 @@ def test_missing_name_of_a_node(self): def test_missing_name_of_all_nodes(self): self.assert_command( self._get_corosync_nodes_options( - self.existing_nodes, range(0, len(self.existing_nodes)) + self.existing_nodes, range(len(self.existing_nodes)) ) ) diff --git a/pcs_test/tier0/lib/commands/test_cluster_property.py b/pcs_test/tier0/lib/commands/test_cluster_property.py index 02a268661..4e4df1062 100644 --- a/pcs_test/tier0/lib/commands/test_cluster_property.py +++ b/pcs_test/tier0/lib/commands/test_cluster_property.py @@ -220,7 +220,7 @@ def test_set_not_zero_or_empty_forced(self): @mock.patch("pcs.lib.sbd._get_local_sbd_watchdog_timeout", lambda: 10) -@mock.patch("pcs.lib.sbd.get_local_sbd_device_list", lambda: []) +@mock.patch("pcs.lib.sbd.get_local_sbd_device_list", list) class TestSetStonithWatchdogTimeoutSBDIsEnabledWatchdogOnly( StonithWatchdogTimeoutMixin, TestCase ): diff --git a/pcs_test/tier0/lib/commands/test_quorum.py b/pcs_test/tier0/lib/commands/test_quorum.py index 43663f93b..09d93fb05 100644 --- a/pcs_test/tier0/lib/commands/test_quorum.py +++ b/pcs_test/tier0/lib/commands/test_quorum.py @@ -2975,7 +2975,7 @@ def fixture_reports_success(cluster_nodes, atb_enabled=False): ) return report_list - @mock.patch("pcs.lib.sbd.get_local_sbd_device_list", lambda: []) + @mock.patch("pcs.lib.sbd.get_local_sbd_device_list", list) def test_not_live_success(self): dummy_cluster_nodes, original_conf, expected_conf = self.conf_2nodes( # cluster consists of two nodes, two_node must be set @@ -2989,7 +2989,7 @@ def test_not_live_success(self): lib.remove_device(self.env_assist.get_env()) - @mock.patch("pcs.lib.sbd.get_local_sbd_device_list", lambda: []) + @mock.patch("pcs.lib.sbd.get_local_sbd_device_list", list) def test_not_live_error(self): ( self.config.env.set_corosync_conf_data( @@ -3021,7 +3021,7 @@ def test_not_live_doesnt_care_about_node_names(self): lib.remove_device(self.env_assist.get_env()) - @mock.patch("pcs.lib.sbd.get_local_sbd_device_list", lambda: []) + @mock.patch("pcs.lib.sbd.get_local_sbd_device_list", list) def test_fail_if_device_not_set(self): self.config.corosync_conf.load_content( _read_file_rc("corosync-3nodes.conf") @@ -3034,7 +3034,7 @@ def test_fail_if_device_not_set(self): expected_in_processor=False, ) - @mock.patch("pcs.lib.sbd.get_local_sbd_device_list", lambda: []) + @mock.patch("pcs.lib.sbd.get_local_sbd_device_list", list) def test_success_2nodes_no_sbd(self): # cluster consists of two nodes, two_node must be set cluster_nodes, original_conf, expected_conf = self.conf_2nodes( @@ -3047,7 +3047,7 @@ def test_success_2nodes_no_sbd(self): self.fixture_reports_success(cluster_nodes) ) - @mock.patch("pcs.lib.sbd.get_local_sbd_device_list", lambda: []) + @mock.patch("pcs.lib.sbd.get_local_sbd_device_list", list) def test_success_2nodes_sbd_installed_disabled(self): # cluster consists of two nodes, two_node must be set cluster_nodes, original_conf, expected_conf = self.conf_2nodes( @@ -3060,7 +3060,7 @@ def test_success_2nodes_sbd_installed_disabled(self): self.fixture_reports_success(cluster_nodes, atb_enabled=False) ) - @mock.patch("pcs.lib.sbd.get_local_sbd_device_list", lambda: []) + @mock.patch("pcs.lib.sbd.get_local_sbd_device_list", list) def test_success_2nodes_sbd_enabled(self): # cluster consists of two nodes and SBD is in use, so teo_nodes must be # disabled and auto_tie_breaker must be enabled @@ -3093,7 +3093,7 @@ def test_success_2nodes_sbd_enabled_with_devices(self): self.fixture_reports_success(cluster_nodes, atb_enabled=False) ) - @mock.patch("pcs.lib.sbd.get_local_sbd_device_list", lambda: []) + @mock.patch("pcs.lib.sbd.get_local_sbd_device_list", list) def test_success_3nodes(self): # with odd number of nodes it doesn't matter if sbd is used cluster_nodes, original_conf, expected_conf = self.conf_3nodes() @@ -3103,7 +3103,7 @@ def test_success_3nodes(self): self.fixture_reports_success(cluster_nodes) ) - @mock.patch("pcs.lib.sbd.get_local_sbd_device_list", lambda: []) + @mock.patch("pcs.lib.sbd.get_local_sbd_device_list", list) def test_success_3nodes_file(self): # with odd number of nodes it doesn't matter if sbd is used dummy_cluster_nodes, original_conf, expected_conf = self.conf_3nodes() @@ -3115,7 +3115,7 @@ def test_success_3nodes_file(self): lib.remove_device(self.env_assist.get_env()) self.env_assist.assert_reports([]) - @mock.patch("pcs.lib.sbd.get_local_sbd_device_list", lambda: []) + @mock.patch("pcs.lib.sbd.get_local_sbd_device_list", list) def test_success_3nodes_one_node_offline(self): # with odd number of nodes it doesn't matter if sbd is used cluster_nodes, original_conf, expected_conf = self.conf_3nodes() @@ -3257,7 +3257,7 @@ def test_all_node_names_missing(self): ] ) - @mock.patch("pcs.lib.sbd.get_local_sbd_device_list", lambda: []) + @mock.patch("pcs.lib.sbd.get_local_sbd_device_list", list) def test_error_disable_qdevice(self): cluster_nodes, original_conf, dummy_expected_conf = self.conf_3nodes() @@ -3314,7 +3314,7 @@ def test_error_disable_qdevice(self): ] ) - @mock.patch("pcs.lib.sbd.get_local_sbd_device_list", lambda: []) + @mock.patch("pcs.lib.sbd.get_local_sbd_device_list", list) def test_error_stop_qdevice(self): cluster_nodes, original_conf, dummy_expected_conf = self.conf_3nodes() @@ -3392,7 +3392,7 @@ def test_error_stop_qdevice(self): ] ) - @mock.patch("pcs.lib.sbd.get_local_sbd_device_list", lambda: []) + @mock.patch("pcs.lib.sbd.get_local_sbd_device_list", list) def test_error_destroy_qdevice_net(self): cluster_nodes, original_conf, dummy_expected_conf = self.conf_3nodes() diff --git a/pyproject.toml b/pyproject.toml index 41a000e9b..bb9d9eccc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,6 +91,7 @@ select = [ "G", "I", "LOG", + "PIE", "PL", # pylint convention, error, refactoring, warning "SIM", ] From 980fd672469b75c861b0d2b34eaf6e8ddf616ad7 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Wed, 11 Dec 2024 18:26:56 +0100 Subject: [PATCH 073/227] ruff: enable ruff linter `RET` (flake8-return) * https://docs.astral.sh/ruff/rules/#flake8-return-ret --- pcs/cli/resource/parse_args.py | 4 +--- pcs/config.py | 3 +-- pcs/lib/cib/rule/cib_to_str.py | 3 +-- pcs/usage.py | 3 +-- pcs/utils.py | 12 ++++-------- pcs_test/api_v2_client.py | 4 +--- .../tier0/lib/commands/sbd/test_watchdog_list.py | 3 +-- pcs_test/tier1/constraint/test_config.py | 3 +-- pcs_test/tools/color_text_runner/result.py | 6 ++---- pyproject.toml | 1 + 10 files changed, 14 insertions(+), 28 deletions(-) diff --git a/pcs/cli/resource/parse_args.py b/pcs/cli/resource/parse_args.py index b07f9fef1..f8ff0f02a 100644 --- a/pcs/cli/resource/parse_args.py +++ b/pcs/cli/resource/parse_args.py @@ -77,7 +77,7 @@ def parse_primitive(arg_list: Argv) -> PrimitiveOptions: arg_list, {"op", "meta"}, implicit_first_keyword="instance" ) - parts = PrimitiveOptions( + return PrimitiveOptions( instance_attrs=KeyValueParser( groups.get_args_flat("instance") ).get_unique(), @@ -88,8 +88,6 @@ def parse_primitive(arg_list: Argv) -> PrimitiveOptions: ], ) - return parts - def parse_clone(arg_list: Argv, promotable: bool = False) -> CloneOptions: clone_id = None diff --git a/pcs/config.py b/pcs/config.py index 96a4601ef..654566d18 100644 --- a/pcs/config.py +++ b/pcs/config.py @@ -567,7 +567,7 @@ def config_backup_path_list(with_uid_gid=False): pcmk_authkey_attrs = dict(cib_attrs) pcmk_authkey_attrs["mode"] = 0o440 - file_list = { + return { "cib.xml": { "path": os.path.join(settings.cib_dir, "cib.xml"), "required": True, @@ -611,7 +611,6 @@ def config_backup_path_list(with_uid_gid=False): }, }, } - return file_list def _get_uid(user_name): diff --git a/pcs/lib/cib/rule/cib_to_str.py b/pcs/lib/cib/rule/cib_to_str.py index 29b67a8a9..aca890414 100644 --- a/pcs/lib/cib/rule/cib_to_str.py +++ b/pcs/lib/cib/rule/cib_to_str.py @@ -51,10 +51,9 @@ def _date_to_str(date: str) -> str: # remove spaces around separators result = re.sub(RuleToStr._date_separators_re, r"\1", date) # if there are any spaces left, replace the first one with T - result = re.sub(r"\s+", "T", result, count=1) # keep all other spaces in place # the date wouldn't be valid, but there is nothing more we can do - return result + return re.sub(r"\s+", "T", result, count=1) def _rule_to_str(self, rule_el: _Element) -> str: # "and" is a documented pacemaker default diff --git a/pcs/usage.py b/pcs/usage.py index 6fb3a8a28..fcc7fbd9a 100644 --- a/pcs/usage.py +++ b/pcs/usage.py @@ -235,7 +235,7 @@ def generate_tree(usage_txt: str) -> CompletionTree: def main() -> str: - output = """ + return """ Usage: pcs [-f file] [-h] [commands]... Control and configure pacemaker and corosync. @@ -279,7 +279,6 @@ def main() -> str: """ # Advanced usage to possibly add later # --corosync_conf= Specify alternative corosync.conf file - return output def _alias_of(cmd: str) -> str: diff --git a/pcs/utils.py b/pcs/utils.py index 2c868e925..c67467e1d 100644 --- a/pcs/utils.py +++ b/pcs/utils.py @@ -1681,8 +1681,7 @@ def get_cib_dom(cib_xml=None): if cib_xml is None: cib_xml = get_cib() try: - dom = parseString(cib_xml) - return dom + return parseString(cib_xml) except xml.parsers.expat.ExpatError: return err("unable to get cib") @@ -1695,8 +1694,7 @@ def get_cib_etree(cib_xml=None): if cib_xml is None: cib_xml = get_cib() try: - root = ET.fromstring(cib_xml) - return root + return ET.fromstring(cib_xml) except xml.etree.ElementTree.ParseError: return err("unable to get cib") @@ -2344,8 +2342,7 @@ def get_operations_from_transitions(transitions_dom): ) ) operation_list.sort(key=lambda x: x[0]) - op_list = [op[1] for op in operation_list] - return op_list + return [op[1] for op in operation_list] def get_resources_location_from_operations(cib_dom, resources_operations): @@ -2380,12 +2377,11 @@ def get_resources_location_from_operations(cib_dom, resources_operations): locations[long_id]["start_on_node"] = res_op["on_node"] if operation == "promote": locations[long_id]["promote_on_node"] = res_op["on_node"] - locations_clean = { + return { key: val for key, val in locations.items() if "start_on_node" in val or "promote_on_node" in val } - return locations_clean def get_remote_quorumtool_output(node): diff --git a/pcs_test/api_v2_client.py b/pcs_test/api_v2_client.py index 7db2b6761..6028f57e5 100644 --- a/pcs_test/api_v2_client.py +++ b/pcs_test/api_v2_client.py @@ -178,9 +178,7 @@ def perform_command(command_dto: CommandDto, auth_token: str) -> TaskResultDto: task_ident_dto = from_dict(TaskIdentDto, json.loads(response)) task_ident = task_ident_dto.task_ident - task_result_dto = fetch_task_result(task_ident_dto, auth_token) - - return task_result_dto + return fetch_task_result(task_ident_dto, auth_token) def run_command_synchronously( diff --git a/pcs_test/tier0/lib/commands/sbd/test_watchdog_list.py b/pcs_test/tier0/lib/commands/sbd/test_watchdog_list.py index 09b64967e..d7ff98c15 100644 --- a/pcs_test/tier0/lib/commands/sbd/test_watchdog_list.py +++ b/pcs_test/tier0/lib/commands/sbd/test_watchdog_list.py @@ -9,12 +9,11 @@ def _watchdog_fixture(identity, driver, caution=None): - info = dict( + return dict( identity=identity, driver="" if driver is None else driver, caution=caution, ) - return info class GetLocalAvailableWatchdogs(TestCase): diff --git a/pcs_test/tier1/constraint/test_config.py b/pcs_test/tier1/constraint/test_config.py index 85dccafa3..47e5a1b26 100644 --- a/pcs_test/tier1/constraint/test_config.py +++ b/pcs_test/tier1/constraint/test_config.py @@ -171,14 +171,13 @@ def _replace(struct, search_replace): def _get_as_json(self, runner, use_all): data = super()._get_as_json(runner, use_all) - data = self._replace( + return self._replace( data, [ ("2023-01-01 12:00", "2023-01-01T12:00"), ("2023-12-31 12:00", "2023-12-31T12:00"), ], ) - return data def test_commands(self): stdout, stderr, retval = self.pcs_runner_orig.run( diff --git a/pcs_test/tools/color_text_runner/result.py b/pcs_test/tools/color_text_runner/result.py index ed6688a83..a9f6d2386 100644 --- a/pcs_test/tools/color_text_runner/result.py +++ b/pcs_test/tools/color_text_runner/result.py @@ -74,16 +74,14 @@ def get_error_names(self) -> list[str]: return [self._format.test_name(test) for test, _ in self.errors] def get_errors(self) -> list[str]: - line_list = self._format.error_list( + return self._format.error_list( "ERROR", self.errors, self.descriptions, self.traceback_highlight ) - return line_list def get_failures(self) -> list[str]: - line_list = self._format.error_list( + return self._format.error_list( "FAIL", self.failures, self.descriptions, self.traceback_highlight ) - return line_list def get_skips(self) -> dict[str, int]: return { diff --git a/pyproject.toml b/pyproject.toml index bb9d9eccc..70ce71aef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,6 +93,7 @@ select = [ "LOG", "PIE", "PL", # pylint convention, error, refactoring, warning + "RET", "SIM", ] # ruff does not respect pylint ignore directives From fe3707737457ef359e5d76216911af39aa358436 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Wed, 11 Dec 2024 18:46:28 +0100 Subject: [PATCH 074/227] ruff: enable ruff linter `SLF` (flake8-self) * https://docs.astral.sh/ruff/rules/#flake8-self-slf --- pcs/cli/reports/messages.py | 2 +- pcs/common/reports/conversions.py | 2 +- pcs/daemon/async_tasks/scheduler.py | 4 ++-- pcs/lib/cib/resource/relations.py | 11 ++++++----- pcs/lib/corosync/config_parser.py | 4 ++-- pcs/stonith.py | 2 +- pyproject.toml | 3 +++ 7 files changed, 16 insertions(+), 12 deletions(-) diff --git a/pcs/cli/reports/messages.py b/pcs/cli/reports/messages.py index 56e76bec4..1cf6847c4 100644 --- a/pcs/cli/reports/messages.py +++ b/pcs/cli/reports/messages.py @@ -642,7 +642,7 @@ def _create_report_msg_map() -> Dict[str, type]: for report_msg_cls in get_all_subclasses(CliReportMessageCustom): # pylint: disable=protected-access code = ( - get_type_hints(report_msg_cls) + get_type_hints(report_msg_cls) # noqa: SLF001 .get("_obj", item.ReportItemMessage) ._code ) diff --git a/pcs/common/reports/conversions.py b/pcs/common/reports/conversions.py index 4c3f36f3b..206971c66 100644 --- a/pcs/common/reports/conversions.py +++ b/pcs/common/reports/conversions.py @@ -39,7 +39,7 @@ def report_dto_to_item( def _create_report_msg_map() -> Dict[str, type]: result: Dict[str, type] = {} for report_msg_cls in get_all_subclasses(messages.ReportItemMessage): - code = report_msg_cls._code # pylint: disable=protected-access + code = report_msg_cls._code # pylint: disable=protected-access # noqa: SLF001 if code: if code in result: raise AssertionError() diff --git a/pcs/daemon/async_tasks/scheduler.py b/pcs/daemon/async_tasks/scheduler.py index e3645c86a..975578aad 100644 --- a/pcs/daemon/async_tasks/scheduler.py +++ b/pcs/daemon/async_tasks/scheduler.py @@ -209,8 +209,8 @@ def _spawn_new_single_use_worker(self) -> None: group=None, target=mp_worker_init, args=( - self._proc_pool._inqueue, # type: ignore - self._proc_pool._outqueue, # type: ignore + self._proc_pool._inqueue, # type: ignore # noqa: SLF001 + self._proc_pool._outqueue, # type: ignore # noqa: SLF001 worker_init, (self._worker_message_q, self._logging_q), 1, diff --git a/pcs/lib/cib/resource/relations.py b/pcs/lib/cib/resource/relations.py index 5a76e4500..e99ca372c 100644 --- a/pcs/lib/cib/resource/relations.py +++ b/pcs/lib/cib/resource/relations.py @@ -70,10 +70,11 @@ def stop(self) -> None: def add_member(self, member: "ResourceRelationNode") -> None: # pylint: disable=protected-access - if member._parent is not None: + if member._parent is not None: # noqa: SLF001 raise AssertionError( "object {} already has a parent set: {}".format( - repr(member), repr(member._parent) + repr(member), + repr(member._parent), # noqa: SLF001 ) ) # we don't want opposite relations (inner resource vs outer resource) @@ -83,18 +84,18 @@ def add_member(self, member: "ResourceRelationNode") -> None: self != member and member.obj.id not in parents and ( - member._opposite_id not in parents + member._opposite_id not in parents # noqa: SLF001 or len(member.obj.members) > 1 ) ): - member._parent = self + member._parent = self # noqa: SLF001 self._members.append(member) def _get_all_parents(self) -> List[str]: # pylint: disable=protected-access if self._parent is None: return [] - return self._parent._get_all_parents() + [self._parent.obj.id] + return self._parent._get_all_parents() + [self._parent.obj.id] # noqa: SLF001 class ResourceRelationTreeBuilder: diff --git a/pcs/lib/corosync/config_parser.py b/pcs/lib/corosync/config_parser.py index 47a9cd180..b8be50a84 100644 --- a/pcs/lib/corosync/config_parser.py +++ b/pcs/lib/corosync/config_parser.py @@ -128,7 +128,7 @@ def add_section(self, section: "Section") -> "Section": section.parent.del_section(section) # here we are editing obj's _parent attribute of the same class # pylint: disable=protected-access - section._parent = self + section._parent = self # noqa: SLF001 self._section_list.append(section) return self @@ -138,7 +138,7 @@ def del_section(self, section: "Section") -> "Section": # thanks to remove raising a ValueError in that case # here we are editing obj's _parent attribute of the same class # pylint: disable=protected-access - section._parent = None + section._parent = None # noqa: SLF001 return self def __str__(self) -> str: diff --git a/pcs/stonith.py b/pcs/stonith.py index 5b61447a0..b6b037eea 100644 --- a/pcs/stonith.py +++ b/pcs/stonith.py @@ -99,7 +99,7 @@ def stonith_list_available( "{0} - {1}".format( name, # pylint: disable=protected-access - resource._format_desc( + resource._format_desc( # noqa: SLF001 len(name + " - "), shortdesc.replace("\n", " ") ), ) diff --git a/pyproject.toml b/pyproject.toml index 70ce71aef..65c7f1b7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,6 +95,7 @@ select = [ "PL", # pylint convention, error, refactoring, warning "RET", "SIM", + "SLF", ] # ruff does not respect pylint ignore directives # https://github.com/astral-sh/ruff/issues/1203 @@ -129,6 +130,8 @@ section-order = ["future", "standard-library", "third-party", "first-party", "te "__init__.py" = ["F401"] "pcs/entry_points/*.py" = ["F401"] "pcs/lib/cib/rule/compat_pyparsing.py" = ["F401"] +# Ignore `SLF001` https://docs.astral.sh/ruff/rules/private-member-access/ +"pcs_test/**.py" = ["SLF001"] [tool.ruff.lint.pylint] max-args = 8 From c5b4a6eef5d3cacb786beaeec9b906781b6e52d4 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Wed, 11 Dec 2024 20:00:15 +0100 Subject: [PATCH 075/227] ruff: enable ruff linter `SLOT` (flake8-slots) * https://docs.astral.sh/ruff/rules/#flake8-self-slf * https://docs.python.org/3/reference/datamodel.html#slots * https://wiki.python.org/moin/UsingSlots --- pcs/common/pacemaker/types.py | 4 ++++ pcs/daemon/ruby_pcsd.py | 4 ++++ pcs/lib/booth/config_parser.py | 2 ++ pcs/lib/cib/node.py | 2 ++ pcs/lib/node_communication_format.py | 2 ++ pcs/snmp/agentx/types.py | 2 ++ pyproject.toml | 1 + 7 files changed, 17 insertions(+) diff --git a/pcs/common/pacemaker/types.py b/pcs/common/pacemaker/types.py index 4d0076f4d..8a58afd80 100644 --- a/pcs/common/pacemaker/types.py +++ b/pcs/common/pacemaker/types.py @@ -2,23 +2,27 @@ class CibResourceDiscovery(str): + __slots__ = () ALWAYS = cast("CibResourceDiscovery", "always") NEVER = cast("CibResourceDiscovery", "never") EXCLUSIVE = cast("CibResourceDiscovery", "exclusive") class CibResourceSetOrdering(str): + __slots__ = () GROUP = cast("CibResourceSetOrdering", "group") LISTED = cast("CibResourceSetOrdering", "listed") class CibResourceSetOrderType(str): + __slots__ = () OPTIONAL = cast("CibResourceSetOrderType", "Optional") MANDATORY = cast("CibResourceSetOrderType", "Mandatory") SERIALIZE = cast("CibResourceSetOrderType", "Serialize") class CibTicketLossPolicy(str): + __slots__ = () STOP = cast("CibTicketLossPolicy", "stop") DEMOTE = cast("CibTicketLossPolicy", "demote") FENCE = cast("CibTicketLossPolicy", "fence") diff --git a/pcs/daemon/ruby_pcsd.py b/pcs/daemon/ruby_pcsd.py index 1956d2072..e492ae077 100644 --- a/pcs/daemon/ruby_pcsd.py +++ b/pcs/daemon/ruby_pcsd.py @@ -46,6 +46,8 @@ def get_request_id(): class SinatraResult(namedtuple("SinatraResult", "headers, status, body")): + __slots__ = () + @classmethod def from_response(cls, response): return cls(response["headers"], response["status"], response["body"]) @@ -81,6 +83,8 @@ class RubyDaemonRequest( "RubyDaemonRequest", "request_type, path, query, headers, method, body" ) ): + __slots__ = () + def __new__( cls, request_type, diff --git a/pcs/lib/booth/config_parser.py b/pcs/lib/booth/config_parser.py index b122b7c7f..317756dc5 100644 --- a/pcs/lib/booth/config_parser.py +++ b/pcs/lib/booth/config_parser.py @@ -15,6 +15,8 @@ class ConfigItem(namedtuple("ConfigItem", "key value details")): + __slots__ = () + def __new__(cls, key, value, details=None): return super().__new__(cls, key, value, details or []) diff --git a/pcs/lib/cib/node.py b/pcs/lib/cib/node.py index efa244aa7..c8af236b8 100644 --- a/pcs/lib/cib/node.py +++ b/pcs/lib/cib/node.py @@ -21,6 +21,8 @@ class PacemakerNode(namedtuple("PacemakerNode", "name addr")): communication and checking if node name / address is in use. """ + __slots__ = () + def update_node_instance_attrs( cib, id_provider, node_name, attrs, state_nodes=None diff --git a/pcs/lib/node_communication_format.py b/pcs/lib/node_communication_format.py index fc06ad707..d38808c5e 100644 --- a/pcs/lib/node_communication_format.py +++ b/pcs/lib/node_communication_format.py @@ -102,6 +102,8 @@ def service_cmd_format(service, command): class Result(namedtuple("Result", "code message")): """Wrapper over some call results""" + __slots__ = () + def unpack_items_from_response(main_response, main_key, node_label): """ diff --git a/pcs/snmp/agentx/types.py b/pcs/snmp/agentx/types.py index e1600e20b..21e40727c 100644 --- a/pcs/snmp/agentx/types.py +++ b/pcs/snmp/agentx/types.py @@ -37,6 +37,8 @@ class Oid( is ignored. """ + __slots__ = () + def __new__(cls, oid, str_oid, data_type=None, member_list=None): return super(Oid, cls).__new__( cls, oid, str_oid, data_type, member_list diff --git a/pyproject.toml b/pyproject.toml index 65c7f1b7c..4f1020d8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,6 +96,7 @@ select = [ "RET", "SIM", "SLF", + "SLOT", ] # ruff does not respect pylint ignore directives # https://github.com/astral-sh/ruff/issues/1203 From f3043a67ba42c6bb16400f6527250d60c46067b5 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Wed, 11 Dec 2024 20:15:56 +0100 Subject: [PATCH 076/227] ruff: enable ruff linter `TC` (flake8-type-checking) * https://docs.astral.sh/ruff/rules/#flake8-type-checking-tc --- pcs/lib/auth/tools.py | 6 ++++-- pcs/lib/booth/status.py | 9 +++++++-- pcs/lib/commands/alert.py | 6 +++++- pcs/lib/commands/remote_node.py | 7 ++++++- pcs/resource.py | 5 ++++- pcs/utils.py | 5 ++++- pyproject.toml | 1 + 7 files changed, 31 insertions(+), 8 deletions(-) diff --git a/pcs/lib/auth/tools.py b/pcs/lib/auth/tools.py index b40b12d11..0cdade26e 100644 --- a/pcs/lib/auth/tools.py +++ b/pcs/lib/auth/tools.py @@ -1,13 +1,15 @@ import grp import pwd - -from pcs.common.tools import StringCollection +from typing import TYPE_CHECKING from .types import ( AuthUser, DesiredUser, ) +if TYPE_CHECKING: + from pcs.common.tools import StringCollection + class UserGroupsError(Exception): pass diff --git a/pcs/lib/booth/status.py b/pcs/lib/booth/status.py index b495c6dc2..d9aeead04 100644 --- a/pcs/lib/booth/status.py +++ b/pcs/lib/booth/status.py @@ -1,16 +1,21 @@ -from typing import Optional +from typing import ( + TYPE_CHECKING, + Optional, +) from pcs import settings from pcs.common import reports from pcs.common.file import RawFileError from pcs.common.str_tools import join_multilines -from pcs.lib.booth.config_facade import ConfigFacade from pcs.lib.booth.constants import AUTHFILE_FIX_OPTION from pcs.lib.booth.env import BoothEnv from pcs.lib.errors import LibraryError from pcs.lib.file.raw_file import raw_file_error_report from pcs.lib.interface.config import ParserErrorException +if TYPE_CHECKING: + from pcs.lib.booth.config_facade import ConfigFacade + def get_daemon_status(runner, name=None): cmd = [settings.booth_exec, "status"] diff --git a/pcs/lib/commands/alert.py b/pcs/lib/commands/alert.py index a8d9cce97..714c7fd9d 100644 --- a/pcs/lib/commands/alert.py +++ b/pcs/lib/commands/alert.py @@ -1,4 +1,5 @@ -from pcs.common.reports import ReportItemList +from typing import TYPE_CHECKING + from pcs.lib.cib import alert from pcs.lib.cib.nvpair import ( arrange_first_instance_attributes, @@ -11,6 +12,9 @@ from pcs.lib.env import LibraryEnvironment from pcs.lib.errors import LibraryError +if TYPE_CHECKING: + from pcs.common.reports import ReportItemList + def create_alert( lib_env: LibraryEnvironment, diff --git a/pcs/lib/commands/remote_node.py b/pcs/lib/commands/remote_node.py index 5fbbab9ba..b9b0cc10c 100644 --- a/pcs/lib/commands/remote_node.py +++ b/pcs/lib/commands/remote_node.py @@ -1,4 +1,5 @@ from typing import ( + TYPE_CHECKING, Callable, Collection, Iterable, @@ -44,7 +45,6 @@ ) from pcs.lib.communication.tools import run as run_com from pcs.lib.communication.tools import run_and_raise -from pcs.lib.corosync.config_facade import ConfigFacade as CorosyncConfigFacade from pcs.lib.env import ( LibraryEnvironment, WaitType, @@ -62,6 +62,11 @@ ) from pcs.lib.tools import generate_binary_key +if TYPE_CHECKING: + from pcs.lib.corosync.config_facade import ( + ConfigFacade as CorosyncConfigFacade, + ) + def _reports_skip_new_node(new_node_name, reason_type): assert reason_type in {"unreachable", "not_live_cib"} diff --git a/pcs/resource.py b/pcs/resource.py index 4214174c9..2e4f131ec 100644 --- a/pcs/resource.py +++ b/pcs/resource.py @@ -5,6 +5,7 @@ import textwrap from functools import partial from typing import ( + TYPE_CHECKING, Any, Callable, Mapping, @@ -79,7 +80,6 @@ from pcs.common.pacemaker.resource.operations import ( OCF_CHECK_LEVEL_INSTANCE_ATTRIBUTE_NAME, ) -from pcs.common.resource_agent.dto import ResourceAgentNameDto from pcs.common.str_tools import ( format_list, format_list_custom_last_separator, @@ -104,6 +104,9 @@ pacemaker_wait_timeout_status as PACEMAKER_WAIT_TIMEOUT_STATUS, ) +if TYPE_CHECKING: + from pcs.common.resource_agent.dto import ResourceAgentNameDto + RESOURCE_RELOCATE_CONSTRAINT_PREFIX = "pcs-relocate-" diff --git a/pcs/utils.py b/pcs/utils.py index c67467e1d..cf89a6286 100644 --- a/pcs/utils.py +++ b/pcs/utils.py @@ -18,6 +18,7 @@ from io import BytesIO from textwrap import dedent from typing import ( + TYPE_CHECKING, Any, Dict, Optional, @@ -55,7 +56,6 @@ OCF_CHECK_LEVEL_INSTANCE_ATTRIBUTE_NAME, ) from pcs.common.reports import ReportProcessor -from pcs.common.reports.item import ReportItemList from pcs.common.reports.messages import CibUpgradeFailedToMinimalRequiredVersion from pcs.common.services.errors import ManageServiceError from pcs.common.services.interfaces import ServiceManagerInterface @@ -81,6 +81,9 @@ from pcs.lib.services import get_service_manager as _get_service_manager from pcs.lib.services import service_exception_to_report +if TYPE_CHECKING: + from pcs.common.reports.item import ReportItemList + # pylint: disable=invalid-name # pylint: disable=too-many-branches diff --git a/pyproject.toml b/pyproject.toml index 4f1020d8d..6085fa920 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,6 +97,7 @@ select = [ "SIM", "SLF", "SLOT", + "TC", ] # ruff does not respect pylint ignore directives # https://github.com/astral-sh/ruff/issues/1203 From 2b6a8120854e6e6879b3a3c63e514c5f6c10a20d Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Thu, 12 Dec 2024 12:10:24 +0100 Subject: [PATCH 077/227] ruff: enable ruff linter `PERF` (perflint) * https://docs.astral.sh/ruff/rules/#perflint-perf --- pcs/cli/common/printable_tree.py | 3 +- pcs/cli/resource/output.py | 23 +- pcs/cli/resource/relations.py | 13 +- pcs/cli/stonith/levels/output.py | 11 +- pcs/common/reports/messages.py | 8 +- pcs/constraint.py | 24 +- pcs/lib/cib/acl.py | 50 ++- pcs/lib/cib/alert.py | 62 ++-- pcs/lib/cib/nvpair.py | 18 +- pcs/lib/cib/resource/agent.py | 12 +- pcs/lib/cib/resource/primitive.py | 9 +- pcs/lib/commands/cib.py | 29 +- pcs/lib/commands/node.py | 11 +- pcs/lib/corosync/config_facade.py | 33 +- pcs/lib/corosync/config_parser.py | 4 +- pcs/lib/corosync/config_validators.py | 28 +- pcs/lib/resource_agent/list.py | 22 +- pcs/lib/resource_agent/ocf_transform.py | 44 ++- pcs/lib/sbd.py | 16 +- pcs/resource.py | 19 +- pcs/status.py | 5 +- pcs/utils.py | 19 +- pcs_test/tier0/lib/commands/cluster/common.py | 13 +- .../lib/commands/cluster/test_remove_nodes.py | 13 +- pcs_test/tier0/lib/pacemaker/test_live.py | 9 +- pcs_test/tools/constraints_dto.py | 303 +++++++++--------- pyproject.toml | 1 + 27 files changed, 390 insertions(+), 412 deletions(-) diff --git a/pcs/cli/common/printable_tree.py b/pcs/cli/common/printable_tree.py index 3a9525279..ef32b3db9 100644 --- a/pcs/cli/common/printable_tree.py +++ b/pcs/cli/common/printable_tree.py @@ -39,8 +39,7 @@ def tree_to_lines( _indent = "| " if not node.members: _indent = " " - for line in node.detail: - result.append(f"{indent}{_indent}{line}") + result.extend(f"{indent}{_indent}{line}" for line in node.detail) _indent = "| " _title_prefix = "|- " for member in node.members: diff --git a/pcs/cli/resource/output.py b/pcs/cli/resource/output.py index e2064a408..1ccece634 100644 --- a/pcs/cli/resource/output.py +++ b/pcs/cli/resource/output.py @@ -632,11 +632,11 @@ def _resource_bundle_storage_to_text( ) -> List[str]: if not storage_mappings: return [] - output = [] - for storage_mapping in storage_mappings: - output.append( - " ".join(_resource_bundle_storage_mapping_to_str(storage_mapping)) - ) + output = [ + " ".join(_resource_bundle_storage_mapping_to_str(storage_mapping)) + for storage_mapping in storage_mappings + ] + return ["Storage Mapping:"] + indent(output, indent_step=INDENT_STEP) @@ -723,14 +723,13 @@ def _resource_operation_to_cmd( ) -> List[str]: if not operations: return [] - cmd = [] - for op in operations: - cmd.append( - "{name} {options}".format( - name=op.name, - options=pairs_to_cmd(_resource_operation_to_pairs(op)), - ) + cmd = [ + "{name} {options}".format( + name=op.name, + options=pairs_to_cmd(_resource_operation_to_pairs(op)), ) + for op in operations + ] return ["op"] + indent(cmd, indent_step=INDENT_STEP) diff --git a/pcs/cli/resource/relations.py b/pcs/cli/resource/relations.py index 261148b36..218f5a332 100644 --- a/pcs/cli/resource/relations.py +++ b/pcs/cli/resource/relations.py @@ -200,14 +200,13 @@ def _order_metadata_to_str(metadata: Mapping[str, Any]) -> list[str]: def _order_set_metadata_to_str(metadata: Mapping[str, Any]) -> list[str]: - result = [] - for res_set in metadata["sets"]: - result.append( - " set {resources}{options}".format( - resources=" ".join(res_set["members"]), - options=_resource_set_options_to_str(res_set["metadata"]), - ) + result = [ + " set {resources}{options}".format( + resources=" ".join(res_set["members"]), + options=_resource_set_options_to_str(res_set["metadata"]), ) + for res_set in metadata["sets"] + ] return _order_common_metadata_to_str(metadata) + result diff --git a/pcs/cli/stonith/levels/output.py b/pcs/cli/stonith/levels/output.py index 9f5a0cf29..0048434dd 100644 --- a/pcs/cli/stonith/levels/output.py +++ b/pcs/cli/stonith/levels/output.py @@ -87,13 +87,10 @@ def stonith_level_config_to_cmd( fencing_topology: CibFencingTopologyDto, ) -> StringSequence: lines: list[str] = [] - level = None - for level in fencing_topology.target_node: - lines.append( - _get_level_add_cmd( - level.index, level.target, level.devices, level.id - ) - ) + lines.extend( + _get_level_add_cmd(level.index, level.target, level.devices, level.id) + for level in fencing_topology.target_node + ) for level_regex in fencing_topology.target_regex: target = f"regexp%{level_regex.target_pattern}" lines.append( diff --git a/pcs/common/reports/messages.py b/pcs/common/reports/messages.py index 7c0b66dac..fa1db0796 100644 --- a/pcs/common/reports/messages.py +++ b/pcs/common/reports/messages.py @@ -60,9 +60,11 @@ def _stdout_stderr_to_string(stdout: str, stderr: str, prefix: str = "") -> str: new_lines = [prefix] if prefix else [] - for line in stdout.splitlines() + stderr.splitlines(): - if line.strip(): - new_lines.append(line) + new_lines.extend( + line + for line in stdout.splitlines() + stderr.splitlines() + if line.strip() + ) return "\n".join(new_lines) diff --git a/pcs/constraint.py b/pcs/constraint.py index a3d831906..4993d6b5c 100644 --- a/pcs/constraint.py +++ b/pcs/constraint.py @@ -1031,11 +1031,13 @@ def location_add(lib, argv, modifiers, skip_score_and_node_check=False): # Verify current constraint doesn't already exist # If it does we replace it with the new constraint dummy_dom, constraintsElement = getCurrentConstraints(dom) - elementsToRemove = [] # If the id matches, or the rsc & node match, then we replace/remove - for rsc_loc in constraintsElement.getElementsByTagName("rsc_location"): + elementsToRemove = [ + rsc_loc + for rsc_loc in constraintsElement.getElementsByTagName("rsc_location") # pylint: disable=too-many-boolean-expressions - if rsc_loc.getAttribute("id") == constraint_id or ( + if rsc_loc.getAttribute("id") == constraint_id + or ( rsc_loc.getAttribute("node") == node and ( ( @@ -1047,8 +1049,8 @@ def location_add(lib, argv, modifiers, skip_score_and_node_check=False): and rsc_loc.getAttribute("rsc-pattern") == rsc_value ) ) - ): - elementsToRemove.append(rsc_loc) + ) + ] for etr in elementsToRemove: constraintsElement.removeChild(etr) @@ -1192,12 +1194,14 @@ def location_rule_check_duplicates(dom, constraint_el, force): lines = [] for dup in duplicates: lines.append(" Constraint: %s" % dup.getAttribute("id")) - for dup_rule in utils.dom_get_children_by_tag_name(dup, "rule"): - lines.append( - rule_utils.ExportDetailed().get_string( - dup_rule, False, True, indent=" " - ) + lines.extend( + rule_utils.ExportDetailed().get_string( + dup_rule, False, True, indent=" " ) + for dup_rule in utils.dom_get_children_by_tag_name( + dup, "rule" + ) + ) utils.err( "duplicate constraint already exists, use --force to override\n" + "\n".join(lines) diff --git a/pcs/lib/cib/acl.py b/pcs/lib/cib/acl.py index 076af9215..8a982998f 100644 --- a/pcs/lib/cib/acl.py +++ b/pcs/lib/cib/acl.py @@ -350,23 +350,21 @@ def _get_permission_list(role_el): role_el -- acl_role etree element of which permissions would be returned """ - output_list = [] - for permission in role_el.findall("./acl_permission"): - output_list.append( - { - key: permission.get(key) - for key in [ - "id", - "description", - "kind", - "xpath", - "reference", - "object-type", - "attribute", - ] - } - ) - return output_list + return [ + { + key: permission.get(key) + for key in [ + "id", + "description", + "kind", + "xpath", + "reference", + "object-type", + "attribute", + ] + } + for permission in role_el.findall("./acl_permission") + ] def get_target_list(acl_section): @@ -396,17 +394,15 @@ def get_group_list(acl_section): def get_target_like_list(acl_section, tag): - output_list = [] - for target_el in acl_section.xpath( - "./*[local-name()=$tag_name]", tag_name=tag - ): - output_list.append( - { - "id": target_el.get("id"), - "role_list": _get_role_list_of_target(target_el), - } + return [ + { + "id": target_el.get("id"), + "role_list": _get_role_list_of_target(target_el), + } + for target_el in acl_section.xpath( + "./*[local-name()=$tag_name]", tag_name=tag ) - return output_list + ] def _get_role_list_of_target(target): diff --git a/pcs/lib/cib/alert.py b/pcs/lib/cib/alert.py index f4cac0ec9..b18048e80 100644 --- a/pcs/lib/cib/alert.py +++ b/pcs/lib/cib/alert.py @@ -318,22 +318,20 @@ def get_all_recipients(alert): 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 + return [ + { + "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") + ), + } + for recipient in alert.findall("./recipient") + ] def get_all_alerts(tree): @@ -352,20 +350,18 @@ def get_all_alerts(tree): 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 + return [ + { + "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), + } + for alert in get_alerts(tree).findall("./alert") + ] diff --git a/pcs/lib/cib/nvpair.py b/pcs/lib/cib/nvpair.py index 7e96f944f..a5d7e6490 100644 --- a/pcs/lib/cib/nvpair.py +++ b/pcs/lib/cib/nvpair.py @@ -162,16 +162,14 @@ def get_nvset(nvset): 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 + return [ + { + "id": nvpair.get("id"), + "name": nvpair.get("name"), + "value": nvpair.get("value", ""), + } + for nvpair in nvset.findall("./nvpair") + ] @overload diff --git a/pcs/lib/cib/resource/agent.py b/pcs/lib/cib/resource/agent.py index 0560324c0..d03f4051c 100644 --- a/pcs/lib/cib/resource/agent.py +++ b/pcs/lib/cib/resource/agent.py @@ -80,13 +80,11 @@ def get_default_operations( # add necessary actions if they are missing defined_operation_names = frozenset(op.name for op in action_list) - for op_name in _NECESSARY_OPERATIONS: - if op_name not in defined_operation_names: - action_list.append( - ResourceAgentAction( - op_name, None, None, None, None, None, False, False - ) - ) + action_list.extend( + ResourceAgentAction(op_name, None, None, None, None, None, False, False) + for op_name in _NECESSARY_OPERATIONS + if op_name not in defined_operation_names + ) # transform actions to operation definitions return [action_to_operation_dto(action) for action in action_list] diff --git a/pcs/lib/cib/resource/primitive.py b/pcs/lib/cib/resource/primitive.py index 82a6348da..be912f3f6 100644 --- a/pcs/lib/cib/resource/primitive.py +++ b/pcs/lib/cib/resource/primitive.py @@ -171,11 +171,10 @@ def create( if instance_attributes is None: instance_attributes = {} - filtered_raw_operation_list = [] - for op in raw_operation_list: - filtered_raw_operation_list.append( - {name: "" if value is None else value for name, value in op.items()} - ) + filtered_raw_operation_list = [ + {name: "" if value is None else value for name, value in op.items()} + for op in raw_operation_list + ] if does_id_exist(resources_section, resource_id): raise LibraryError( diff --git a/pcs/lib/commands/cib.py b/pcs/lib/commands/cib.py index 2eeb41a8f..ed72b9710 100644 --- a/pcs/lib/commands/cib.py +++ b/pcs/lib/commands/cib.py @@ -146,26 +146,21 @@ def _stop_resources_wait( def _validate_elements_to_remove( element_to_remove: ElementsToRemove, ) -> reports.ReportItemList: - report_list = [] - for missing_id in sorted(element_to_remove.missing_ids): - report_list.append( - reports.ReportItem.error( - reports.messages.IdNotFound(missing_id, []) - ) - ) - + report_list = [ + reports.ReportItem.error(reports.messages.IdNotFound(missing_id, [])) + for missing_id in sorted(element_to_remove.missing_ids) + ] unsupported_elements = element_to_remove.unsupported_elements - for unsupported_id in sorted(unsupported_elements.id_tag_map): - report_list.append( - reports.ReportItem.error( - reports.messages.IdBelongsToUnexpectedType( - unsupported_id, - list(unsupported_elements.supported_element_types), - unsupported_elements.id_tag_map[unsupported_id], - ) + report_list.extend( + reports.ReportItem.error( + reports.messages.IdBelongsToUnexpectedType( + unsupported_id, + list(unsupported_elements.supported_element_types), + unsupported_elements.id_tag_map[unsupported_id], ) ) - + for unsupported_id in sorted(unsupported_elements.id_tag_map) + ) return report_list diff --git a/pcs/lib/commands/node.py b/pcs/lib/commands/node.py index da27fa3a4..9eb5e2cf5 100644 --- a/pcs/lib/commands/node.py +++ b/pcs/lib/commands/node.py @@ -160,12 +160,11 @@ def _set_instance_attrs_node_list( ): with cib_runner_nodes(lib_env, wait) as (cib, dummy_runner, state_nodes): known_nodes = [node.attrs.name for node in state_nodes] - report_list = [] - for node in node_names: - if node not in known_nodes: - report_list.append( - ReportItem.error(reports.messages.NodeNotFound(node)) - ) + report_list = [ + ReportItem.error(reports.messages.NodeNotFound(node)) + for node in node_names + if node not in known_nodes + ] if report_list: raise LibraryError(*report_list) diff --git a/pcs/lib/corosync/config_facade.py b/pcs/lib/corosync/config_facade.py index 6ee8edc73..f25c44681 100644 --- a/pcs/lib/corosync/config_facade.py +++ b/pcs/lib/corosync/config_facade.py @@ -398,18 +398,17 @@ def update_link( del options_without_linknumber["linknumber"] # change options if options_without_linknumber: - target_interface_section_list = [] - for totem_section in self.config.get_sections("totem"): - for interface_section in totem_section.get_sections( - "interface" - ): - if ( - linknumber - == - # if no linknumber is set, corosync treats it as 0 - interface_section.get_attribute_value("linknumber", "0") - ): - target_interface_section_list.append(interface_section) + target_interface_section_list = [ + interface_section + for totem_section in self.config.get_sections("totem") + for interface_section in totem_section.get_sections("interface") + if ( + linknumber + == + # if no linknumber is set, corosync treats it as 0 + interface_section.get_attribute_value("linknumber", "0") + ) + ] self._set_link_options( options_without_linknumber, interface_section_list=target_interface_section_list, @@ -680,9 +679,13 @@ def get_quorum_device_settings( heuristics_options: dict[str, str] = {} for quorum in self.config.get_sections("quorum"): for device in quorum.get_sections("device"): - for name, value in device.get_attributes(): - if name != "model": - generic_options[name] = value + generic_options.update( + { + name: value + for name, value in device.get_attributes() + if name != "model" + } + ) for subsection in device.get_sections(): if subsection.name == "heuristics": heuristics_options.update(subsection.get_attributes()) diff --git a/pcs/lib/corosync/config_parser.py b/pcs/lib/corosync/config_parser.py index b8be50a84..b19d3c561 100644 --- a/pcs/lib/corosync/config_parser.py +++ b/pcs/lib/corosync/config_parser.py @@ -41,9 +41,7 @@ def empty(self) -> bool: return not self._attr_list and not self._section_list def export(self, indent: str = " ") -> str: - lines = [] - for attr in self._attr_list: - lines.append("{0}: {1}".format(*attr)) + lines = ["{0}: {1}".format(*attr) for attr in self._attr_list] if self._attr_list and self._section_list: lines.append("") section_count = len(self._section_list) diff --git a/pcs/lib/corosync/config_validators.py b/pcs/lib/corosync/config_validators.py index 980dcf5ea..492700060 100644 --- a/pcs/lib/corosync/config_validators.py +++ b/pcs/lib/corosync/config_validators.py @@ -622,11 +622,11 @@ def remove_nodes( quorum_device_settings -- model, generic and heuristic qdevice options """ existing_node_names = [node.name for node in existing_nodes] - report_items = [] - for not_found_node in set(nodes_names_to_remove) - set(existing_node_names): - report_items.append( - ReportItem.error(reports.messages.NodeNotFound(not_found_node)) - ) + report_items = [ + ReportItem.error(reports.messages.NodeNotFound(not_found_node)) + for not_found_node in set(nodes_names_to_remove) + - set(existing_node_names) + ] if not set(existing_node_names) - set(nodes_names_to_remove): report_items.append( @@ -637,20 +637,20 @@ def remove_nodes( qdevice_model_options, _, _ = quorum_device_settings tie_breaker_nodeid = qdevice_model_options.get("tie_breaker") if tie_breaker_nodeid not in [None, "lowest", "highest"]: - for node in existing_nodes: + report_items.extend( + ReportItem.error( + reports.messages.NodeUsedAsTieBreaker( + node.name, node.nodeid + ) + ) + for node in existing_nodes if ( node.name in nodes_names_to_remove and # "4" != 4, convert ids to string to detect a match for sure str(node.nodeid) == str(tie_breaker_nodeid) - ): - report_items.append( - ReportItem.error( - reports.messages.NodeUsedAsTieBreaker( - node.name, node.nodeid - ) - ) - ) + ) + ) return report_items diff --git a/pcs/lib/resource_agent/list.py b/pcs/lib/resource_agent/list.py index 4eb449d56..a4e6672af 100644 --- a/pcs/lib/resource_agent/list.py +++ b/pcs/lib/resource_agent/list.py @@ -141,15 +141,13 @@ def _find_all_resource_agents_by_type( type_ -- last part of an agent name """ type_lower = type_.lower() - possible_names = [] - for std_provider in list_resource_agents_standards_and_providers(runner): - for existing_type in list_resource_agents(runner, std_provider): - if type_lower == existing_type.lower(): - possible_names.append( - ResourceAgentName( - std_provider.standard, - std_provider.provider, - existing_type, - ) - ) - return possible_names + return [ + ResourceAgentName( + std_provider.standard, + std_provider.provider, + existing_type, + ) + for std_provider in list_resource_agents_standards_and_providers(runner) + for existing_type in list_resource_agents(runner, std_provider) + if type_lower == existing_type.lower() + ] diff --git a/pcs/lib/resource_agent/ocf_transform.py b/pcs/lib/resource_agent/ocf_transform.py index b0fb3c196..29a9920e6 100644 --- a/pcs/lib/resource_agent/ocf_transform.py +++ b/pcs/lib/resource_agent/ocf_transform.py @@ -116,30 +116,28 @@ def _ocf_1_0_parameter_list_to_ocf_unified( if parameter.obsoletes: deprecated_by_dict[parameter.obsoletes].add(parameter.name) - result = [] - for parameter in parameter_list: - result.append( - ResourceAgentParameter( - name=parameter.name, - shortdesc=parameter.shortdesc, - longdesc=parameter.longdesc, - type=parameter.type, - default=parameter.default, - enum_values=parameter.enum_values, - required=_bool_value(parameter.required), - advanced=False, - deprecated=_bool_value(parameter.deprecated), - deprecated_by=sorted(deprecated_by_dict[parameter.name]), - deprecated_desc=None, - unique_group=( - f"{const.DEFAULT_UNIQUE_GROUP_PREFIX}{parameter.name}" - if _bool_value(parameter.unique) - else None - ), - reloadable=_bool_value(parameter.unique), - ) + return [ + ResourceAgentParameter( + name=parameter.name, + shortdesc=parameter.shortdesc, + longdesc=parameter.longdesc, + type=parameter.type, + default=parameter.default, + enum_values=parameter.enum_values, + required=_bool_value(parameter.required), + advanced=False, + deprecated=_bool_value(parameter.deprecated), + deprecated_by=sorted(deprecated_by_dict[parameter.name]), + deprecated_desc=None, + unique_group=( + f"{const.DEFAULT_UNIQUE_GROUP_PREFIX}{parameter.name}" + if _bool_value(parameter.unique) + else None + ), + reloadable=_bool_value(parameter.unique), ) - return result + for parameter in parameter_list + ] def _ocf_1_1_parameter_list_to_ocf_unified( diff --git a/pcs/lib/sbd.py b/pcs/lib/sbd.py index b6e69d4bc..c77344e32 100644 --- a/pcs/lib/sbd.py +++ b/pcs/lib/sbd.py @@ -198,15 +198,13 @@ def validate_nodes_devices( ) ) ) - for device in device_list: - if not device or not path.isabs(device): - report_item_list.append( - reports.ReportItem.error( - reports.messages.SbdDevicePathNotAbsolute( - device, node_label - ) - ) - ) + report_item_list.extend( + reports.ReportItem.error( + reports.messages.SbdDevicePathNotAbsolute(device, node_label) + ) + for device in device_list + if not device or not path.isabs(device) + ) return report_item_list diff --git a/pcs/resource.py b/pcs/resource.py index 2e4f131ec..d8994c878 100644 --- a/pcs/resource.py +++ b/pcs/resource.py @@ -2078,8 +2078,9 @@ def resource_group_rm(cib_dom, group_name, resource_ids): resources_to_move = [] if all_resources: - for resource in group_match.getElementsByTagName("primitive"): - resources_to_move.append(resource) + resources_to_move.extend( + list(group_match.getElementsByTagName("primitive")) + ) else: for resource_id in resource_ids: resource = utils.dom_get_resource(group_match, resource_id) @@ -2153,8 +2154,10 @@ def resource_group_list( for e in elements: line_parts = [e.getAttribute("id") + ":"] - for resource in e.getElementsByTagName("primitive"): - line_parts.append(resource.getAttribute("id")) + line_parts.extend( + resource.getAttribute("id") + for resource in e.getElementsByTagName("primitive") + ) print(" ".join(line_parts)) @@ -2707,10 +2710,10 @@ def operation_to_string(op_el): if name in ["id", "name"]: continue parts.append(name + "=" + value) - for nvpair in op_el.getElementsByTagName("nvpair"): - parts.append( - nvpair.getAttribute("name") + "=" + nvpair.getAttribute("value") - ) + parts.extend( + f'{nvpair.getAttribute("name")}={nvpair.getAttribute("value")}' + for nvpair in op_el.getElementsByTagName("nvpair") + ) parts.append("(" + op_el.getAttribute("id") + ")") return " ".join(parts) diff --git a/pcs/status.py b/pcs/status.py index 13d56cee2..e4d4b8808 100644 --- a/pcs/status.py +++ b/pcs/status.py @@ -90,10 +90,7 @@ def nodes_status(lib, argv, modifiers): if report_list: process_library_reports(report_list) online_nodes = utils.getCorosyncActiveNodes() - offline_nodes = [] - for node in all_nodes: - if node not in online_nodes: - offline_nodes.append(node) + offline_nodes = [node for node in all_nodes if node not in online_nodes] online_nodes.sort() offline_nodes.sort() diff --git a/pcs/utils.py b/pcs/utils.py index cf89a6286..2479cda39 100644 --- a/pcs/utils.py +++ b/pcs/utils.py @@ -1810,17 +1810,17 @@ def operation_exists(operations_el, op_el): """ Commandline options: no options """ - existing = [] op_name = op_el.getAttribute("name") op_interval = timeout_to_seconds_legacy(op_el.getAttribute("interval")) - for op in operations_el.getElementsByTagName("op"): + return [ + op + for op in operations_el.getElementsByTagName("op") if ( op.getAttribute("name") == op_name and timeout_to_seconds_legacy(op.getAttribute("interval")) == op_interval - ): - existing.append(op) - return existing + ) + ] def operation_exists_by_name(operations_el, op_el): @@ -2407,10 +2407,11 @@ def dom_prepare_child_element(dom_element, tag_name, id_candidate): """ Commandline options: no options """ - child_elements = [] - for child in dom_element.childNodes: - if child.nodeType == child.ELEMENT_NODE and child.tagName == tag_name: - child_elements.append(child) + child_elements = [ + child + for child in dom_element.childNodes + if child.nodeType == child.ELEMENT_NODE and child.tagName == tag_name + ] if not child_elements: dom = dom_element.ownerDocument diff --git a/pcs_test/tier0/lib/commands/cluster/common.py b/pcs_test/tier0/lib/commands/cluster/common.py index 1894e5f0d..7f3d570f3 100644 --- a/pcs_test/tier0/lib/commands/cluster/common.py +++ b/pcs_test/tier0/lib/commands/cluster/common.py @@ -21,16 +21,15 @@ def _corosync_options_fixture(option_list, indent_level=2): def corosync_conf_fixture(node_list=(), quorum_options=(), qdevice_net=False): - nodes = [] - for node in node_list: - nodes.append( - dedent( - """\ + nodes = [ + dedent( + """\ node {{ {options} }} """ - ).format(options=_corosync_options_fixture(node)) - ) + ).format(options=_corosync_options_fixture(node)) + for node in node_list + ] device = "" if qdevice_net: device = outdent( diff --git a/pcs_test/tier0/lib/commands/cluster/test_remove_nodes.py b/pcs_test/tier0/lib/commands/cluster/test_remove_nodes.py index 19d23c8bd..db70e72de 100644 --- a/pcs_test/tier0/lib/commands/cluster/test_remove_nodes.py +++ b/pcs_test/tier0/lib/commands/cluster/test_remove_nodes.py @@ -40,16 +40,15 @@ def _get_two_node(nodes_num): def corosync_conf_fixture( node_list=(), quorum_options=(), qdevice_net=False, qdevice_tie_breaker=None ): - nodes = [] - for node in node_list: - nodes.append( - dedent( - """\ + nodes = [ + dedent( + """\ node {{ {options} }} """ - ).format(options=_corosync_options_fixture(node)) - ) + ).format(options=_corosync_options_fixture(node)) + for node in node_list + ] device = "" if qdevice_net: if qdevice_tie_breaker: diff --git a/pcs_test/tier0/lib/pacemaker/test_live.py b/pcs_test/tier0/lib/pacemaker/test_live.py index bfd8b0969..fab9f8291 100644 --- a/pcs_test/tier0/lib/pacemaker/test_live.py +++ b/pcs_test/tier0/lib/pacemaker/test_live.py @@ -525,11 +525,10 @@ def get_in_out_filtered_stderr(): "another output\n", ), ) - out_stderr = [] - for input_lines in in_stderr: - out_stderr.append( - [line for line in input_lines if "-V" not in line] - ) + out_stderr = [ + [line for line in input_lines if "-V" not in line] + for input_lines in in_stderr + ] return zip(in_stderr, out_stderr) @staticmethod diff --git a/pcs_test/tools/constraints_dto.py b/pcs_test/tools/constraints_dto.py index c1ac74540..44b8fdc12 100644 --- a/pcs_test/tools/constraints_dto.py +++ b/pcs_test/tools/constraints_dto.py @@ -101,165 +101,168 @@ def get_all_constraints( ), ), ] - for loc_const in [ - CibConstraintLocationDto( - resource_id="B2", - resource_pattern=None, - role=None, - attributes=CibConstraintLocationAttributesDto( - constraint_id="loc_constr_with_expired_rule", - score=None, - node=None, - rules=[ - CibRuleExpressionDto( - id="loc_constr_with_expired_rule-rule", - type=CibRuleExpressionType.RULE, - in_effect=rule_eval.get_rule_status( - "loc_constr_with_expired_rule-rule" - ), - options={"score": "500"}, - date_spec=None, - duration=None, - expressions=[ - CibRuleExpressionDto( - id="loc_constr_with_expired_rule-rule-expr", - type=CibRuleExpressionType.DATE_EXPRESSION, - in_effect=rule_eval.get_rule_status( - "loc_constr_with_expired_rule-rule-expr" - ), - options={ - "operation": "lt", - "end": "2000-01-01", - }, - date_spec=None, - duration=None, - expressions=[], - as_string="date lt 2000-01-01", - ) - ], - as_string="date lt 2000-01-01", - ) - ], - lifetime=[], - resource_discovery=None, + location.extend( + loc_const + for loc_const in [ + CibConstraintLocationDto( + resource_id="B2", + resource_pattern=None, + role=None, + attributes=CibConstraintLocationAttributesDto( + constraint_id="loc_constr_with_expired_rule", + score=None, + node=None, + rules=[ + CibRuleExpressionDto( + id="loc_constr_with_expired_rule-rule", + type=CibRuleExpressionType.RULE, + in_effect=rule_eval.get_rule_status( + "loc_constr_with_expired_rule-rule" + ), + options={"score": "500"}, + date_spec=None, + duration=None, + expressions=[ + CibRuleExpressionDto( + id="loc_constr_with_expired_rule-rule-expr", + type=CibRuleExpressionType.DATE_EXPRESSION, + in_effect=rule_eval.get_rule_status( + "loc_constr_with_expired_rule-rule-expr" + ), + options={ + "operation": "lt", + "end": "2000-01-01", + }, + date_spec=None, + duration=None, + expressions=[], + as_string="date lt 2000-01-01", + ) + ], + as_string="date lt 2000-01-01", + ) + ], + lifetime=[], + resource_discovery=None, + ), ), - ), - CibConstraintLocationDto( - resource_id="R6-clone", - resource_pattern=None, - role=None, - attributes=CibConstraintLocationAttributesDto( - constraint_id="loc_constr_with_not_expired_rule", - score=None, - node=None, - rules=[ - CibRuleExpressionDto( - id="loc_constr_with_not_expired_rule-rule", - type=CibRuleExpressionType.RULE, - in_effect=rule_eval.get_rule_status( - "loc_constr_with_not_expired_rule-rule" - ), - options={ - "boolean-op": "and", - "role": "Unpromoted", - "score": "500", - }, - date_spec=None, - duration=None, - expressions=[ - CibRuleExpressionDto( - id="loc_constr_with_not_expired_rule-rule-expr", - type=CibRuleExpressionType.EXPRESSION, - in_effect=rule_eval.get_rule_status( - "loc_constr_with_not_expired_rule-rule-expr" - ), - options={ - "operation": "eq", - "attribute": "#uname", - "value": "node1", - }, - date_spec=None, - duration=None, - expressions=[], - as_string="#uname eq node1", + CibConstraintLocationDto( + resource_id="R6-clone", + resource_pattern=None, + role=None, + attributes=CibConstraintLocationAttributesDto( + constraint_id="loc_constr_with_not_expired_rule", + score=None, + node=None, + rules=[ + CibRuleExpressionDto( + id="loc_constr_with_not_expired_rule-rule", + type=CibRuleExpressionType.RULE, + in_effect=rule_eval.get_rule_status( + "loc_constr_with_not_expired_rule-rule" ), - CibRuleExpressionDto( - id="loc_constr_with_not_expired_rule-rule-expr-1", - type=CibRuleExpressionType.DATE_EXPRESSION, - in_effect=rule_eval.get_rule_status( - "loc_constr_with_not_expired_rule-rule-expr-1" + options={ + "boolean-op": "and", + "role": "Unpromoted", + "score": "500", + }, + date_spec=None, + duration=None, + expressions=[ + CibRuleExpressionDto( + id="loc_constr_with_not_expired_rule-rule-expr", + type=CibRuleExpressionType.EXPRESSION, + in_effect=rule_eval.get_rule_status( + "loc_constr_with_not_expired_rule-rule-expr" + ), + options={ + "operation": "eq", + "attribute": "#uname", + "value": "node1", + }, + date_spec=None, + duration=None, + expressions=[], + as_string="#uname eq node1", ), - options={ - "operation": "gt", - "start": "2000-01-01", - }, - date_spec=None, - duration=None, - expressions=[], - as_string="date gt 2000-01-01", - ), - ], - as_string="#uname eq node1 and date gt 2000-01-01", - ), - CibRuleExpressionDto( - id="loc_constr_with_not_expired_rule-rule-1", - type=CibRuleExpressionType.RULE, - in_effect=rule_eval.get_rule_status( - "loc_constr_with_not_expired_rule-rule-1" - ), - options={ - "boolean-op": "and", - "role": "Promoted", - "score-attribute": "test-attr", - }, - date_spec=None, - duration=None, - expressions=[ - CibRuleExpressionDto( - id="loc_constr_with_not_expired_rule-rule-1-expr", - type=CibRuleExpressionType.DATE_EXPRESSION, - in_effect=rule_eval.get_rule_status( - "loc_constr_with_not_expired_rule-rule-1-expr" + CibRuleExpressionDto( + id="loc_constr_with_not_expired_rule-rule-expr-1", + type=CibRuleExpressionType.DATE_EXPRESSION, + in_effect=rule_eval.get_rule_status( + "loc_constr_with_not_expired_rule-rule-expr-1" + ), + options={ + "operation": "gt", + "start": "2000-01-01", + }, + date_spec=None, + duration=None, + expressions=[], + as_string="date gt 2000-01-01", ), - options={ - "operation": "gt", - "start": "2010-12-31", - }, - date_spec=None, - duration=None, - expressions=[], - as_string="date gt 2010-12-31", + ], + as_string="#uname eq node1 and date gt 2000-01-01", + ), + CibRuleExpressionDto( + id="loc_constr_with_not_expired_rule-rule-1", + type=CibRuleExpressionType.RULE, + in_effect=rule_eval.get_rule_status( + "loc_constr_with_not_expired_rule-rule-1" ), - CibRuleExpressionDto( - id="loc_constr_with_not_expired_rule-rule-1-expr-1", - type=CibRuleExpressionType.EXPRESSION, - in_effect=rule_eval.get_rule_status( - "loc_constr_with_not_expired_rule-rule-1-expr-1" + options={ + "boolean-op": "and", + "role": "Promoted", + "score-attribute": "test-attr", + }, + date_spec=None, + duration=None, + expressions=[ + CibRuleExpressionDto( + id="loc_constr_with_not_expired_rule-rule-1-expr", + type=CibRuleExpressionType.DATE_EXPRESSION, + in_effect=rule_eval.get_rule_status( + "loc_constr_with_not_expired_rule-rule-1-expr" + ), + options={ + "operation": "gt", + "start": "2010-12-31", + }, + date_spec=None, + duration=None, + expressions=[], + as_string="date gt 2010-12-31", ), - options={ - "operation": "eq", - "attribute": "#uname", - "value": "node1", - }, - date_spec=None, - duration=None, - expressions=[], - as_string="#uname eq node1", - ), - ], - as_string="date gt 2010-12-31 and #uname eq node1", - ), - ], - lifetime=[], - resource_discovery=None, + CibRuleExpressionDto( + id="loc_constr_with_not_expired_rule-rule-1-expr-1", + type=CibRuleExpressionType.EXPRESSION, + in_effect=rule_eval.get_rule_status( + "loc_constr_with_not_expired_rule-rule-1-expr-1" + ), + options={ + "operation": "eq", + "attribute": "#uname", + "value": "node1", + }, + date_spec=None, + duration=None, + expressions=[], + as_string="#uname eq node1", + ), + ], + as_string="date gt 2010-12-31 and #uname eq node1", + ), + ], + lifetime=[], + resource_discovery=None, + ), ), - ), - ]: - if include_expired or not any( + ] + if include_expired + or not any( rule.in_effect == CibRuleInEffectStatus.EXPIRED for rule in loc_const.attributes.rules - ): - location.append(loc_const) + ) + ) return CibConstraintsDto( location=location, diff --git a/pyproject.toml b/pyproject.toml index 6085fa920..b1f0355c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,6 +91,7 @@ select = [ "G", "I", "LOG", + "PERF", "PIE", "PL", # pylint convention, error, refactoring, warning "RET", From 8108d64fb8fb3e84056f602e9393e85a167f936c Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Thu, 9 Jan 2025 17:37:11 +0100 Subject: [PATCH 078/227] ruff: disable rule PERF203 try-except-in-loop * in order to be consistend with pcs-0.12 * https://docs.astral.sh/ruff/rules/try-except-in-loop/ --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index b1f0355c8..27854a6fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,6 +104,7 @@ select = [ # https://github.com/astral-sh/ruff/issues/1203 ignore = [ "C408", # https://docs.astral.sh/ruff/rules/unnecessary-collection-call/ + "PERF203", # http://docs.astral.sh/ruff/rules/try-except-in-loop/ "PLR2004", # magic-value-comparison (99) "PLR0913", # too-many-arguments (54) "PLR0912", # too-many-branches (49) From 6dac6db4c1c10732f53ac707733805151f5af1bb Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Thu, 12 Dec 2024 13:02:02 +0100 Subject: [PATCH 079/227] ruff: enable ruff linter `ARG` (flake8-unused-arguments) * https://docs.astral.sh/ruff/rules/#flake8-unused-arguments-arg * disable: * ARG005 https://docs.astral.sh/ruff/rules/unused-lambda-argument/ --- pcs/common/services/drivers/sysvinit_rhel.py | 6 ++++++ pcs/daemon/app/api_v1.py | 1 + pcs/lib/cib/rule/in_effect.py | 1 + pcs/lib/corosync/config_parser.py | 1 + pcs/lib/file/raw_file.py | 1 + pcs/lib/file/toolbox.py | 7 +++++++ pcs/lib/services.py | 3 +++ pcs_test/tier0/daemon/app/fixtures_app.py | 1 + .../lib/corosync/test_config_validators_create.py | 2 +- pcs_test/tools/color_text_runner/writer.py | 12 ++++++++++++ pcs_test/tools/command_env/mock_runner.py | 1 + pcs_test/tools/custom_mock.py | 5 +++-- pcs_test/tools/parallel_test_runner.py | 1 + pyproject.toml | 2 ++ 14 files changed, 41 insertions(+), 3 deletions(-) diff --git a/pcs/common/services/drivers/sysvinit_rhel.py b/pcs/common/services/drivers/sysvinit_rhel.py index 203f7c807..17ec8a219 100644 --- a/pcs/common/services/drivers/sysvinit_rhel.py +++ b/pcs/common/services/drivers/sysvinit_rhel.py @@ -29,21 +29,25 @@ def __init__( self._available_services: List[str] = [] def start(self, service: str, instance: Optional[str] = None) -> None: + del instance result = self._executor.run([self._service_bin, service, "start"]) if result.retval != 0: raise errors.StartServiceError(service, result.joined_output) def stop(self, service: str, instance: Optional[str] = None) -> None: + del instance result = self._executor.run([self._service_bin, service, "stop"]) if result.retval != 0: raise errors.StopServiceError(service, result.joined_output) def enable(self, service: str, instance: Optional[str] = None) -> None: + del instance result = self._executor.run([self._chkconfig_bin, service, "on"]) if result.retval != 0: raise errors.EnableServiceError(service, result.joined_output) def disable(self, service: str, instance: Optional[str] = None) -> None: + del instance if not self.is_installed(service): return result = self._executor.run([self._chkconfig_bin, service, "off"]) @@ -51,9 +55,11 @@ def disable(self, service: str, instance: Optional[str] = None) -> None: raise errors.DisableServiceError(service, result.joined_output) def is_enabled(self, service: str, instance: Optional[str] = None) -> bool: + del instance return self._executor.run([self._chkconfig_bin, service]).retval == 0 def is_running(self, service: str, instance: Optional[str] = None) -> bool: + del instance return ( self._executor.run([self._service_bin, service, "status"]).retval == 0 diff --git a/pcs/daemon/app/api_v1.py b/pcs/daemon/app/api_v1.py index 5babad1da..11d94e375 100644 --- a/pcs/daemon/app/api_v1.py +++ b/pcs/daemon/app/api_v1.py @@ -171,6 +171,7 @@ def send_response( self.finish(json.dumps(to_dict(response))) def write_error(self, status_code: int, **kwargs: Any) -> None: + del status_code response = communication.dto.InternalCommunicationResultDto( status=communication.const.COM_STATUS_EXCEPTION, status_msg=None, diff --git a/pcs/lib/cib/rule/in_effect.py b/pcs/lib/cib/rule/in_effect.py index c70877f8e..4e2f827fc 100644 --- a/pcs/lib/cib/rule/in_effect.py +++ b/pcs/lib/cib/rule/in_effect.py @@ -32,6 +32,7 @@ class RuleInEffectEvalDummy(RuleInEffectEval): """ def get_rule_status(self, rule_id: str) -> CibRuleInEffectStatus: + del rule_id return CibRuleInEffectStatus.UNKNOWN diff --git a/pcs/lib/corosync/config_parser.py b/pcs/lib/corosync/config_parser.py index b19d3c561..799114c27 100644 --- a/pcs/lib/corosync/config_parser.py +++ b/pcs/lib/corosync/config_parser.py @@ -158,6 +158,7 @@ def exception_to_report_list( force_code: Optional[reports.types.ForceCode], is_forced_or_warning: bool, ) -> reports.ReportItemList: + del file_type_code, file_path, force_code, is_forced_or_warning # TODO switch to new exceptions / reports and do not ignore input # arguments of the function return [ diff --git a/pcs/lib/file/raw_file.py b/pcs/lib/file/raw_file.py index f73981eb4..05529aa96 100644 --- a/pcs/lib/file/raw_file.py +++ b/pcs/lib/file/raw_file.py @@ -85,6 +85,7 @@ def read(self) -> bytes: return self.__file_data def write(self, file_data: bytes, can_overwrite: bool = False) -> None: + del can_overwrite self.__file_data = file_data @contextmanager diff --git a/pcs/lib/file/toolbox.py b/pcs/lib/file/toolbox.py index 2fe856c53..09d5d6863 100644 --- a/pcs/lib/file/toolbox.py +++ b/pcs/lib/file/toolbox.py @@ -57,6 +57,13 @@ def exception_to_report_list( force_code: Optional[reports.types.ForceCode], is_forced_or_warning: bool, ) -> reports.ReportItemList: + del ( + exception, + file_type_code, + file_path, + force_code, + is_forced_or_warning, + ) return [] diff --git a/pcs/lib/services.py b/pcs/lib/services.py index fcfb548c4..3a712c624 100644 --- a/pcs/lib/services.py +++ b/pcs/lib/services.py @@ -59,12 +59,15 @@ def kill(self, service: str, instance: Optional[str] = None) -> None: self._warn(service, instance, reports.const.SERVICE_ACTION_KILL) def is_enabled(self, service: str, instance: Optional[str] = None) -> bool: + del service, instance return False def is_running(self, service: str, instance: Optional[str] = None) -> bool: + del service, instance return False def is_installed(self, service: str) -> bool: + del service return True def get_available_services(self) -> List[str]: diff --git a/pcs_test/tier0/daemon/app/fixtures_app.py b/pcs_test/tier0/daemon/app/fixtures_app.py index 698a7a1a6..97aae69f5 100644 --- a/pcs_test/tier0/daemon/app/fixtures_app.py +++ b/pcs_test/tier0/daemon/app/fixtures_app.py @@ -33,6 +33,7 @@ async def run_ruby( http_request=None, payload=None, ): + del http_request, payload if request_type != self.request_type: raise AssertionError( f"Wrong request type: expected '{self.request_type}'" diff --git a/pcs_test/tier0/lib/corosync/test_config_validators_create.py b/pcs_test/tier0/lib/corosync/test_config_validators_create.py index 637c45cfa..1d60cc7e5 100644 --- a/pcs_test/tier0/lib/corosync/test_config_validators_create.py +++ b/pcs_test/tier0/lib/corosync/test_config_validators_create.py @@ -1689,7 +1689,7 @@ def call_function( crypto_options, current_crypto_options=None, ): - # pylint: disable=unused-argument + del current_crypto_options return config_validators.create_transport_knet( generic_options, compression_options, crypto_options ) diff --git a/pcs_test/tools/color_text_runner/writer.py b/pcs_test/tools/color_text_runner/writer.py index 2b84ed8eb..f29f31001 100644 --- a/pcs_test/tools/color_text_runner/writer.py +++ b/pcs_test/tools/color_text_runner/writer.py @@ -51,48 +51,58 @@ def show_fast_info(self, traceback): class DotWriter(Writer): def addSuccess(self, test): + del test self.stream.write(self.format.green(".")) self.stream.flush() def addError(self, test, err, traceback): + del test, err self.stream.write(self.format.red("E")) self.stream.flush() self.show_fast_info(traceback) def addFailure(self, test, err, traceback): + del test, err self.stream.write(self.format.red("F")) self.stream.flush() self.show_fast_info(traceback) def addSkip(self, test, reason): + del test, reason self.stream.write(self.format.blue("s")) self.stream.flush() def addExpectedFailure(self, test, err): + del test, err self.stream.write(self.format.blue("x")) self.stream.flush() def addUnexpectedSuccess(self, test): + del test self.stream.write(self.format.red("u")) self.stream.flush() class StandardVerboseWriter(Writer): def addSuccess(self, test): + del test self.stream.writeln(self.format.green("OK")) self.stream.flush() def addError(self, test, err, traceback): + del test, err self.stream.writeln(self.format.red("ERROR")) self.stream.flush() self.show_fast_info(traceback) def addFailure(self, test, err, traceback): + del test, err self.stream.writeln(self.format.red("FAIL")) self.stream.flush() self.show_fast_info(traceback) def addSkip(self, test, reason): + del test self.stream.writeln(self.format.blue("skipped {0!r}".format(reason))) self.stream.flush() @@ -102,10 +112,12 @@ def startTest(self, test): self.stream.flush() def addExpectedFailure(self, test, err): + del test, err self.stream.writeln(self.format.blue("expected failure")) self.stream.flush() def addUnexpectedSuccess(self, test): + del test self.stream.writeln(self.format.red("unexpected success")) self.stream.flush() diff --git a/pcs_test/tools/command_env/mock_runner.py b/pcs_test/tools/command_env/mock_runner.py index 32e4840b3..e3ab30be3 100644 --- a/pcs_test/tools/command_env/mock_runner.py +++ b/pcs_test/tools/command_env/mock_runner.py @@ -10,6 +10,7 @@ def __init__(self, expected_stdin): self.expected_stdin = expected_stdin def __call__(self, stdin, command, order_num): + del order_num if stdin != self.expected_stdin: raise AssertionError( ( diff --git a/pcs_test/tools/custom_mock.py b/pcs_test/tools/custom_mock.py index af0127f16..973632e0f 100644 --- a/pcs_test/tools/custom_mock.py +++ b/pcs_test/tools/custom_mock.py @@ -26,7 +26,7 @@ def get_getaddrinfo_mock(resolvable_addr_list): def socket_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0): # noqa: A002 # pylint: disable=redefined-builtin - # pylint: disable=unused-argument + del port, family, type, proto, flags if host not in resolvable_addr_list: raise socket.gaierror(1, "") @@ -308,7 +308,8 @@ def assert_no_handle_left(self): ) def select(self, timeout=1): - # pylint: disable=no-self-use, unused-argument + # pylint: disable=no-self-use + del timeout return 0 def perform(self): diff --git a/pcs_test/tools/parallel_test_runner.py b/pcs_test/tools/parallel_test_runner.py index 55521f4c8..d651900dd 100644 --- a/pcs_test/tools/parallel_test_runner.py +++ b/pcs_test/tools/parallel_test_runner.py @@ -63,6 +63,7 @@ def _get_fail_descriptions( return line_list def stopTest(self, test): + del test # super().stopTest(test) if self.stream.seekable(): self.stream.seek(0) diff --git a/pyproject.toml b/pyproject.toml index 27854a6fa..406fb0e06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,6 +81,7 @@ target-version = "py39" # pylint rules in ruff: # https://github.com/astral-sh/ruff/issues/970 select = [ "A", + "ARG", "ASYNC", "B", "C4", @@ -103,6 +104,7 @@ select = [ # ruff does not respect pylint ignore directives # https://github.com/astral-sh/ruff/issues/1203 ignore = [ + "ARG005", # https://docs.astral.sh/ruff/rules/unused-lambda-argument/ "C408", # https://docs.astral.sh/ruff/rules/unnecessary-collection-call/ "PERF203", # http://docs.astral.sh/ruff/rules/try-except-in-loop/ "PLR2004", # magic-value-comparison (99) From 0ef5aef1dd87d1c058d3f911cd6e9afe0f9e8070 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Fri, 13 Dec 2024 12:51:59 +0100 Subject: [PATCH 080/227] ruff: enable previously disabled `PL` (pylint) rules * PLR0911 https://docs.astral.sh/ruff/rules/too-many-return-statements/ * PLR0912 https://docs.astral.sh/ruff/rules/too-many-branches/ * PLR0913 https://docs.astral.sh/ruff/rules/too-many-arguments/ * PLR0915 https://docs.astral.sh/ruff/rules/too-many-statements/ * PLR2004 https://docs.astral.sh/ruff/rules/magic-value-comparison/ * PLR5501 https://docs.astral.sh/ruff/rules/collapsible-else-if/ * PLW0602 https://docs.astral.sh/ruff/rules/global-variable-not-assigned/ * PLW0603 https://docs.astral.sh/ruff/rules/global-statement/ * PLW1509 https://docs.astral.sh/ruff/rules/subprocess-popen-preexec-fn/ * PLW2901 https://docs.astral.sh/ruff/rules/redefined-loop-name/ --- pcs/alert.py | 5 +- pcs/app.py | 7 +- pcs/cli/common/lib_wrapper.py | 2 +- pcs/cli/reports/preprocessor.py | 2 +- pcs/cli/resource/output.py | 2 +- pcs/cli/resource/parse_args.py | 9 ++- pcs/cluster.py | 21 +++--- pcs/common/reports/messages.py | 6 +- pcs/common/services/drivers/sysvinit_rhel.py | 4 +- pcs/common/str_tools.py | 18 ++--- pcs/config.py | 6 +- pcs/constraint.py | 28 ++++---- pcs/daemon/async_tasks/worker/executor.py | 2 +- pcs/daemon/async_tasks/worker/logging.py | 2 +- pcs/daemon/run.py | 2 +- pcs/lib/booth/config_parser.py | 4 +- pcs/lib/cib/fencing_topology.py | 2 +- pcs/lib/cib/remove_elements.py | 4 +- pcs/lib/cib/resource/bundle.py | 6 +- pcs/lib/cib/resource/operations.py | 5 +- pcs/lib/cib/resource/primitive.py | 4 +- pcs/lib/cib/resource/remote_node.py | 2 +- pcs/lib/cib/resource/stonith.py | 5 +- pcs/lib/cib/rule/validator.py | 2 +- pcs/lib/cluster_property.py | 17 +++-- pcs/lib/commands/booth.py | 2 +- pcs/lib/commands/cluster.py | 12 ++-- pcs/lib/commands/remote_node.py | 70 ++++++++----------- pcs/lib/commands/resource.py | 33 +++++---- pcs/lib/commands/sbd.py | 2 +- pcs/lib/commands/status.py | 2 +- pcs/lib/commands/stonith.py | 4 +- pcs/lib/communication/nodes.py | 11 ++- pcs/lib/corosync/config_facade.py | 15 ++-- pcs/lib/corosync/config_validators.py | 6 +- pcs/lib/corosync/live.py | 6 +- pcs/lib/env.py | 2 +- pcs/lib/external.py | 2 +- pcs/lib/file/metadata.py | 2 +- pcs/lib/node_communication.py | 17 +++-- pcs/lib/validate.py | 15 ++-- pcs/resource.py | 45 ++++++------ pcs/rule.py | 2 +- pcs/status.py | 16 ++--- pcs/usage.py | 9 ++- pcs/utils.py | 16 ++--- pcs_test/api_v2_client.py | 8 +-- pcs_test/tier0/common/test_resource_status.py | 4 +- .../tier0/lib/commands/cluster/test_setup.py | 7 +- .../commands/resource/test_resource_create.py | 4 +- .../resource/test_resource_enable_disable.py | 2 +- .../resource/test_resource_manage_unmanage.py | 2 +- pcs_test/tier0/lib/commands/test_status.py | 2 +- .../test_stonith_update_scsi_devices.py | 4 +- .../lib/resource_agent/test_pcs_transform.py | 6 +- pcs_test/tier0/lib/test_cluster_property.py | 13 ++-- pcs_test/tier0/test_host.py | 9 +-- pcs_test/tier1/legacy/test_cluster.py | 2 +- pcs_test/tier1/legacy/test_constraints.py | 16 ++--- pcs_test/tier1/legacy/test_stonith.py | 4 +- pcs_test/tier1/legacy/test_utils.py | 2 +- pcs_test/tools/assertions.py | 2 +- .../tools/bin_mock/pcmk/crm_resource_mock.py | 3 +- .../tools/command_env/config_http_status.py | 2 +- pcs_test/tools/command_env/config_raw_file.py | 2 +- pcs_test/tools/command_env/config_runner.py | 2 +- .../tools/command_env/config_runner_pcmk.py | 16 ++--- .../command_env/mock_node_communicator.py | 4 +- pcs_test/tools/pcs_runner.py | 2 +- pyproject.toml | 11 +-- 70 files changed, 281 insertions(+), 304 deletions(-) diff --git a/pcs/alert.py b/pcs/alert.py index f8fd31457..3c83e3e78 100644 --- a/pcs/alert.py +++ b/pcs/alert.py @@ -154,9 +154,8 @@ def _nvset_to_str(nvset_obj): } output = [] for name, value in sorted(key_val.items()): - if " " in value: - value = f'"{value}"' - output.append(f"{name}={value}") + safe_value = f'"{value}"' if " " in value else value + output.append(f"{name}={safe_value}") return " ".join(output) diff --git a/pcs/app.py b/pcs/app.py index 783361845..02636d10c 100644 --- a/pcs/app.py +++ b/pcs/app.py @@ -118,7 +118,7 @@ def _non_root_run(argv_cmd): filename = "" -def main(argv=None): +def main(argv=None): # noqa: PLR0912, PLR0915 # pylint: disable=global-statement # pylint: disable=too-many-branches # pylint: disable=too-many-locals @@ -133,7 +133,7 @@ def main(argv=None): argv = argv if argv else sys.argv[1:] utils.subprocess_setup() - global filename, usefile + global filename, usefile # noqa: PLW0603 utils.pcs_options = {} # we want to support optional arguments for --wait, so if an argument @@ -145,7 +145,8 @@ def main(argv=None): tempsecs = arg.replace("--wait=", "") if tempsecs: waitsecs = tempsecs - arg = "--wait" + new_argv.append("--wait") + continue new_argv.append(arg) argv = new_argv diff --git a/pcs/cli/common/lib_wrapper.py b/pcs/cli/common/lib_wrapper.py index d2c18ba7b..b0a88a6df 100644 --- a/pcs/cli/common/lib_wrapper.py +++ b/pcs/cli/common/lib_wrapper.py @@ -101,7 +101,7 @@ def bind_all(env, run_with_middleware, dictionary): ) -def load_module(env, middleware_factory, name): +def load_module(env, middleware_factory, name): # noqa: PLR0911, PLR0912 # pylint: disable=too-many-branches # pylint: disable=too-many-return-statements if name == "acl": diff --git a/pcs/cli/reports/preprocessor.py b/pcs/cli/reports/preprocessor.py index 71365d912..18d200a5e 100644 --- a/pcs/cli/reports/preprocessor.py +++ b/pcs/cli/reports/preprocessor.py @@ -25,7 +25,7 @@ def get_duplicate_constraint_exists_preprocessor( ) -> ReportItemPreprocessor: constraints_dto: Optional[CibConstraintsDto] = None - def _report_item_preprocessor( + def _report_item_preprocessor( # noqa: PLR0912 report_item: reports.ReportItem, ) -> Optional[reports.ReportItem]: """ diff --git a/pcs/cli/resource/output.py b/pcs/cli/resource/output.py index 1ccece634..b0ad6ea55 100644 --- a/pcs/cli/resource/output.py +++ b/pcs/cli/resource/output.py @@ -113,7 +113,7 @@ def _resource_operation_to_str( ] + indent(lines, indent_step=INDENT_STEP) -def resource_agent_parameter_metadata_to_text( +def resource_agent_parameter_metadata_to_text( # noqa: PLR0912 parameter: resource_agent.dto.ResourceAgentParameterDto, ) -> list[str]: # pylint: disable=too-many-branches diff --git a/pcs/cli/resource/parse_args.py b/pcs/cli/resource/parse_args.py index f8ff0f02a..41b914ce9 100644 --- a/pcs/cli/resource/parse_args.py +++ b/pcs/cli/resource/parse_args.py @@ -126,7 +126,7 @@ def parse_clone(arg_list: Argv, promotable: bool = False) -> CloneOptions: return CloneOptions(clone_id=clone_id, meta_attrs=meta) -def parse_create_new(arg_list: Argv) -> ComplexResourceOptions: +def parse_create_new(arg_list: Argv) -> ComplexResourceOptions: # noqa: PLR0912 # pylint: disable=too-many-branches # pylint: disable=too-many-locals try: @@ -274,7 +274,7 @@ def parse_create_new(arg_list: Argv) -> ComplexResourceOptions: # deprecated since 0.11.6 -def parse_create_old( +def parse_create_old( # noqa: PLR0912, PLR0915 arg_list: Argv, modifiers: InputModifiers ) -> ComplexResourceOptions: # pylint: disable=too-many-branches @@ -442,9 +442,8 @@ def _parse_bundle_groups(arg_list: Argv) -> ArgsByKeywords: for repeated_section in groups.get_args_groups(keyword): if not repeated_section: raise CmdLineInputError(f"No {keyword} options specified") - else: - if not groups.get_args_flat(keyword): - raise CmdLineInputError(f"No {keyword} options specified") + elif not groups.get_args_flat(keyword): + raise CmdLineInputError(f"No {keyword} options specified") return groups diff --git a/pcs/cluster.py b/pcs/cluster.py index c355a9d56..2e8553a31 100644 --- a/pcs/cluster.py +++ b/pcs/cluster.py @@ -484,7 +484,7 @@ def stop_cluster_all() -> None: stop_cluster_nodes(all_nodes) -def stop_cluster_nodes(nodes: StringCollection) -> None: +def stop_cluster_nodes(nodes: StringCollection) -> None: # noqa: PLR0912 """ Commandline options: * --force - no error when possible quorum loss @@ -792,7 +792,7 @@ def kill_local_cluster_services() -> tuple[str, int]: return utils.run([settings.killall_exec, "-9"] + all_cluster_daemons) -def cluster_push(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: +def cluster_push(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: # noqa: PLR0912, PLR0915 """ Options: * --wait @@ -913,7 +913,7 @@ def cluster_push(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: utils.err("\n".join(msg).strip()) -def cluster_edit(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: +def cluster_edit(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: # noqa: PLR0912 """ Options: * --config - edit configuration section of CIB @@ -971,7 +971,7 @@ def cluster_edit(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: utils.err("$EDITOR environment variable is not set") -def get_cib(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: +def get_cib(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: # noqa: PLR0912 """ Options: * --config show configuration section of CIB @@ -1165,7 +1165,7 @@ def node_remove(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: lib.cluster.remove_nodes(argv, force_flags=force_flags) -def cluster_uidgid( +def cluster_uidgid( # noqa: PLR0912 lib: Any, argv: Argv, modifiers: InputModifiers, silent_list: bool = False ) -> None: """ @@ -1282,7 +1282,7 @@ def cluster_reload(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: # Completely tear down the cluster & remove config files # Code taken from cluster-clean script in pacemaker -def cluster_destroy(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: +def cluster_destroy(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: # noqa: PLR0912 """ Options: * --all - destroy cluster on all cluster nodes => destroy whole cluster @@ -1422,7 +1422,7 @@ def cluster_verify(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: lib.cluster.verify(verbose=modifiers.get("--full")) -def cluster_report(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: +def cluster_report(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: # noqa: PLR0912 """ Options: * --force - overwrite existing file @@ -1480,9 +1480,10 @@ def cluster_report(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: continue if "to diagnose" in line: continue + new_line = line if "--dest" in line: - line = line.replace("--dest", "") - newoutput = newoutput + line + "\n" + new_line = line.replace("--dest", "") + newoutput = newoutput + new_line + "\n" if retval != 0: utils.err(newoutput) print_to_stderr(newoutput) @@ -1527,7 +1528,7 @@ def send_local_configs( return err_msgs -def cluster_auth_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: +def cluster_auth_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: # noqa: PLR0912 """ Options: * --corosync_conf - corosync.conf file diff --git a/pcs/common/reports/messages.py b/pcs/common/reports/messages.py index fa1db0796..3c0eee4ba 100644 --- a/pcs/common/reports/messages.py +++ b/pcs/common/reports/messages.py @@ -80,10 +80,10 @@ def _resource_move_ban_clear_master_resource_not_promotable( def _resource_move_ban_pcmk_success(stdout: str, stderr: str) -> str: new_lines = [] - for line in stdout.splitlines() + stderr.splitlines(): - if not line.strip(): + for output_line in stdout.splitlines() + stderr.splitlines(): + if not output_line.strip(): continue - line = line.replace( + line = output_line.replace( "WARNING: Creating rsc_location constraint", "Warning: Creating location constraint", ) diff --git a/pcs/common/services/drivers/sysvinit_rhel.py b/pcs/common/services/drivers/sysvinit_rhel.py index 17ec8a219..36948cab1 100644 --- a/pcs/common/services/drivers/sysvinit_rhel.py +++ b/pcs/common/services/drivers/sysvinit_rhel.py @@ -79,8 +79,8 @@ def _get_available_services(self) -> List[str]: return [] service_list = [] - for service in result.stdout.splitlines(): - service = service.split(" ", 1)[0] + for service_line in result.stdout.splitlines(): + service = service_line.split(" ", 1)[0] if service: service_list.append(service) return service_list diff --git a/pcs/common/str_tools.py b/pcs/common/str_tools.py index 89a80b7b5..5b2ee3625 100644 --- a/pcs/common/str_tools.py +++ b/pcs/common/str_tools.py @@ -90,9 +90,9 @@ def format_name_value_list(item_list: Sequence[tuple[str, str]]) -> list[str]: Turn 2-tuples to 'name=value' strings with standard quoting """ output = [] - for name, value in item_list: - name = quote(name, "= ") - value = quote(value, "= ") + for raw_name, raw_value in item_list: + name = quote(raw_name, "= ") + value = quote(raw_value, "= ") output.append(f"{name}={value}") return output @@ -106,9 +106,9 @@ def format_name_value_id_list( Turn 3-tuples to 'name=value (id: id))' strings with standard quoting """ output = [] - for name, value, an_id in item_list: - name = quote(name, "= ") - value = quote(value, "= ") + for raw_name, raw_value, an_id in item_list: + name = quote(raw_name, "= ") + value = quote(raw_value, "= ") output.append(f"{name}={value} (id: {an_id})") return output @@ -127,9 +127,9 @@ def format_name_value_default_list( standard quoting """ output = [] - for name, value, is_default in item_list: - name = quote(name, "= ") - value = quote(value, "= ") + for raw_name, raw_value, is_default in item_list: + name = quote(raw_name, "= ") + value = quote(raw_value, "= ") default = " (default)" if is_default else "" output.append(f"{name}={value}{default}") return output diff --git a/pcs/config.py b/pcs/config.py index 654566d18..bf5beb710 100644 --- a/pcs/config.py +++ b/pcs/config.py @@ -109,7 +109,7 @@ def config_show(lib, argv, modifiers): print("\n".join(indent(quorum_lines))) -def _config_show_cib_lines(lib, properties_facade=None): +def _config_show_cib_lines(lib, properties_facade=None): # noqa: PLR0912, PLR0915 """ Commandline options: * -f - CIB file @@ -330,7 +330,7 @@ def config_restore(lib, argv, modifiers): sys.exit(exitcode) -def config_restore_remote(infile_name, infile_obj): +def config_restore_remote(infile_name, infile_obj): # noqa: PLR0912 """ Commandline options: * --request-timeout - timeout for HTTP requests @@ -419,7 +419,7 @@ def config_restore_remote(infile_name, infile_obj): utils.err("unable to restore all nodes\n" + "\n".join(error_list)) -def config_restore_local(infile_name, infile_obj): +def config_restore_local(infile_name, infile_obj): # noqa: PLR0912, PLR0915 """ Commandline options: no options """ diff --git a/pcs/constraint.py b/pcs/constraint.py index 4993d6b5c..89afa679b 100644 --- a/pcs/constraint.py +++ b/pcs/constraint.py @@ -184,7 +184,7 @@ def _validate_resources_not_in_same_group(cib_dom, resource1, resource2): # with [score] [options] # with [score] [options] # with [score] [options] -def colocation_add(lib, argv, modifiers): +def colocation_add(lib, argv, modifiers): # noqa: PLR0912, PLR0915 """ Options: * -f - CIB file @@ -256,14 +256,11 @@ def _validate_and_prepare_role(role): if not argv: raise CmdLineInputError() - if len(argv) == 1: + if len(argv) == 1 or utils.is_score_or_opt(argv[1]): resource2 = argv.pop(0) else: - if utils.is_score_or_opt(argv[1]): - resource2 = argv.pop(0) - else: - role2 = _validate_and_prepare_role(argv.pop(0)) - resource2 = argv.pop(0) + role2 = _validate_and_prepare_role(argv.pop(0)) + resource2 = argv.pop(0) score, nv_pairs = _parse_score_options(argv) @@ -477,7 +474,7 @@ def order_start(lib, argv, modifiers): _order_add(resource1, resource2, order_options, modifiers) -def _order_add(resource1, resource2, options_list, modifiers): +def _order_add(resource1, resource2, options_list, modifiers): # noqa: PLR0912, PLR0915 """ Commandline options: * -f - CIB file @@ -870,8 +867,8 @@ def _verify_score(score): ) -def location_prefer( - lib: Any, argv: list[str], modifiers: parse_args.InputModifiers +def location_prefer( # noqa: PLR0912 + lib: Any, argv: parse_args.Argv, modifiers: parse_args.InputModifiers ) -> None: """ Options: @@ -942,7 +939,12 @@ def location_prefer( location_add(lib, parameters, modifiers, skip_score_and_node_check=True) -def location_add(lib, argv, modifiers, skip_score_and_node_check=False): +def location_add( # noqa: PLR0912, PLR0915 + lib: Any, + argv: parse_args.Argv, + modifiers: parse_args.InputModifiers, + skip_score_and_node_check: bool = False, +) -> None: """ Options: * --force - allow unknown options, allow constraint for any resource type @@ -1069,7 +1071,7 @@ def location_add(lib, argv, modifiers, skip_score_and_node_check=False): utils.replace_cib_configuration(dom) -def location_rule(lib, argv, modifiers): +def location_rule(lib, argv, modifiers): # noqa: PLR0912 """ Options: * -f - CIB file @@ -1261,7 +1263,7 @@ def getCurrentConstraints(passed_dom=None): # If returnStatus is set, then we don't error out, we just print the error # and return false -def constraint_rm( +def constraint_rm( # noqa: PLR0912 lib, argv, modifiers, diff --git a/pcs/daemon/async_tasks/worker/executor.py b/pcs/daemon/async_tasks/worker/executor.py index db2648265..2ff51368d 100644 --- a/pcs/daemon/async_tasks/worker/executor.py +++ b/pcs/daemon/async_tasks/worker/executor.py @@ -69,7 +69,7 @@ def worker_init(message_q: mp.Queue, logging_q: mp.Queue) -> None: logger.info("Worker initialized.") # Let task_executor use worker_com for sending messages to the scheduler - global worker_com + global worker_com # noqa: PLW0603 worker_com = WorkerCommunicator(message_q) def ignore_signals(sig_num, frame): # type: ignore diff --git a/pcs/daemon/async_tasks/worker/logging.py b/pcs/daemon/async_tasks/worker/logging.py index 61d2f3d6b..954eddbf3 100644 --- a/pcs/daemon/async_tasks/worker/logging.py +++ b/pcs/daemon/async_tasks/worker/logging.py @@ -7,7 +7,7 @@ class Logger(logging.Logger): - def makeRecord( # type: ignore + def makeRecord( # type: ignore # noqa: PLR0913 self, name, level, diff --git a/pcs/daemon/run.py b/pcs/daemon/run.py index d9ec28e0e..8cd848511 100644 --- a/pcs/daemon/run.py +++ b/pcs/daemon/run.py @@ -81,7 +81,7 @@ async def config_synchronization(): return config_synchronization -def configure_app( +def configure_app( # noqa: PLR0913 async_scheduler: Scheduler, auth_provider: AuthProvider, session_storage: session.Storage, diff --git a/pcs/lib/booth/config_parser.py b/pcs/lib/booth/config_parser.py index 317756dc5..ea6a587c3 100644 --- a/pcs/lib/booth/config_parser.py +++ b/pcs/lib/booth/config_parser.py @@ -113,8 +113,8 @@ def _parse_to_raw_lines(config_content): line_list = [] invalid_line_list = [] - for line in config_content.splitlines(): - line = line.strip() + for config_line in config_content.splitlines(): + line = config_line.strip() match = _search_with_multiple_re(expression_list, line) if match: line_list.append((match.group("key"), match.group("value"))) diff --git a/pcs/lib/cib/fencing_topology.py b/pcs/lib/cib/fencing_topology.py index b521364c7..8ca01a70b 100644 --- a/pcs/lib/cib/fencing_topology.py +++ b/pcs/lib/cib/fencing_topology.py @@ -81,7 +81,7 @@ def _generate_level_id( return id_provider.allocate_id(sanitize_id(f"fl-{id_part}-{level}")) -def add_level( +def add_level( # noqa: PLR0913 reporter: ReportProcessor, cib: _Element, level: str, diff --git a/pcs/lib/cib/remove_elements.py b/pcs/lib/cib/remove_elements.py index 8ca9e7095..f4339dd3d 100644 --- a/pcs/lib/cib/remove_elements.py +++ b/pcs/lib/cib/remove_elements.py @@ -550,7 +550,9 @@ def _is_last_element(parent_element: _Element, child_tag: str) -> bool: return len(parent_element.findall(f"./{child_tag}")) == 1 -def _is_empty_after_inner_el_removal(parent_el: _Element) -> bool: +def _is_empty_after_inner_el_removal( # noqa: PLR0911 + parent_el: _Element, +) -> bool: # pylint: disable=too-many-return-statements if is_any_clone(parent_el): return True diff --git a/pcs/lib/cib/resource/bundle.py b/pcs/lib/cib/resource/bundle.py index 728317d53..5918a049d 100644 --- a/pcs/lib/cib/resource/bundle.py +++ b/pcs/lib/cib/resource/bundle.py @@ -223,7 +223,7 @@ def validate_new( ) -def append_new( +def append_new( # noqa: PLR0913 parent_element, id_provider, bundle_id, @@ -340,7 +340,7 @@ def _get_report_unsupported_container(bundle_el): ) -def validate_update( +def validate_update( # noqa: PLR0913 id_provider, bundle_el, container_options, @@ -385,7 +385,7 @@ def validate_update( ) -def update( +def update( # noqa: PLR0913 id_provider, bundle_el, container_options, diff --git a/pcs/lib/cib/resource/operations.py b/pcs/lib/cib/resource/operations.py index 7b4cc85a4..68a4ea3d7 100644 --- a/pcs/lib/cib/resource/operations.py +++ b/pcs/lib/cib/resource/operations.py @@ -327,6 +327,7 @@ def uniquify_operations_intervals( new_operations = [] for operation in operation_list: new_interval = get_unique_interval(operation.name, operation.interval) + new_operation = operation if new_interval != operation.interval: report_list.append( ReportItem.warning( @@ -337,8 +338,8 @@ def uniquify_operations_intervals( ) ) ) - operation = dt_replace(operation, interval=new_interval) - new_operations.append(operation) + new_operation = dt_replace(operation, interval=new_interval) + new_operations.append(new_operation) return report_list, new_operations diff --git a/pcs/lib/cib/resource/primitive.py b/pcs/lib/cib/resource/primitive.py index be912f3f6..3a2822276 100644 --- a/pcs/lib/cib/resource/primitive.py +++ b/pcs/lib/cib/resource/primitive.py @@ -121,7 +121,7 @@ def _find_primitives_by_agent( ) -def create( +def create( # noqa: PLR0913 report_processor: reports.ReportProcessor, cmd_runner: CommandRunner, resources_section: _Element, @@ -243,7 +243,7 @@ def create( ) -def append_new( +def append_new( # noqa: PLR0913 resources_section, id_provider, resource_id, diff --git a/pcs/lib/cib/resource/remote_node.py b/pcs/lib/cib/resource/remote_node.py index 621590a3d..b1425ca3e 100644 --- a/pcs/lib/cib/resource/remote_node.py +++ b/pcs/lib/cib/resource/remote_node.py @@ -201,7 +201,7 @@ def _prepare_instance_attributes( return enriched_instance_attributes -def create( +def create( # noqa: PLR0913 report_processor: reports.ReportProcessor, cmd_runner: CommandRunner, resource_agent_facade: ResourceAgentFacade, diff --git a/pcs/lib/cib/resource/stonith.py b/pcs/lib/cib/resource/stonith.py index 1ce95cbd8..c9eef2e02 100644 --- a/pcs/lib/cib/resource/stonith.py +++ b/pcs/lib/cib/resource/stonith.py @@ -345,6 +345,7 @@ def _get_transient_digest_value( """ new_comma_values_list = [] for comma_value in old_value.split(","): + new_comma_value = comma_value if comma_value: try: _id, _type, _ = comma_value.split(":") @@ -357,8 +358,8 @@ def _get_transient_digest_value( ) ) from e if _id == stonith_id and _type == stonith_type: - comma_value = ":".join([stonith_id, stonith_type, digest]) - new_comma_values_list.append(comma_value) + new_comma_value = ":".join([stonith_id, stonith_type, digest]) + new_comma_values_list.append(new_comma_value) return ",".join(new_comma_values_list) diff --git a/pcs/lib/cib/rule/validator.py b/pcs/lib/cib/rule/validator.py index 1835d6469..2816170fa 100644 --- a/pcs/lib/cib/rule/validator.py +++ b/pcs/lib/cib/rule/validator.py @@ -56,7 +56,7 @@ def get_reports(self) -> reports.ReportItemList: ) return report_list - def _call_validate(self, expr: RuleExprPart) -> reports.ReportItemList: + def _call_validate(self, expr: RuleExprPart) -> reports.ReportItemList: # noqa: PLR0911 # pylint: disable=too-many-return-statements if isinstance(expr, BoolExpr): return self._validate_bool_expr(expr) diff --git a/pcs/lib/cluster_property.py b/pcs/lib/cluster_property.py index 24fb44225..deec58d76 100644 --- a/pcs/lib/cluster_property.py +++ b/pcs/lib/cluster_property.py @@ -54,19 +54,18 @@ def _validate_stonith_watchdog_timeout_property( validate.ValuePair(original_value, value), force ) ) - else: - if value not in ["", "0"]: - report_list.append( - reports.ReportItem.error( - reports.messages.StonithWatchdogTimeoutCannotBeSet( - reports.const.SBD_NOT_SET_UP - ), - ) + elif value not in ["", "0"]: + report_list.append( + reports.ReportItem.error( + reports.messages.StonithWatchdogTimeoutCannotBeSet( + reports.const.SBD_NOT_SET_UP + ), ) + ) return report_list -def validate_set_cluster_properties( +def validate_set_cluster_properties( # noqa: PLR0912 runner: CommandRunner, cluster_property_facade_list: Iterable[ResourceAgentFacade], properties_set_id: str, diff --git a/pcs/lib/commands/booth.py b/pcs/lib/commands/booth.py index 2ca90c289..d71c55294 100644 --- a/pcs/lib/commands/booth.py +++ b/pcs/lib/commands/booth.py @@ -166,7 +166,7 @@ def config_setup( raise LibraryError() -def config_destroy( +def config_destroy( # noqa: PLR0912 env: LibraryEnvironment, instance_name: Optional[str] = None, ignore_config_load_problems: bool = False, diff --git a/pcs/lib/commands/cluster.py b/pcs/lib/commands/cluster.py index 82738dc3d..360c74dd6 100644 --- a/pcs/lib/commands/cluster.py +++ b/pcs/lib/commands/cluster.py @@ -182,7 +182,7 @@ def verify(env: LibraryEnvironment, verbose=False): raise LibraryError() -def setup( +def setup( # noqa: PLR0913, PLR0915 env: LibraryEnvironment, cluster_name: str, nodes: Sequence[Mapping[str, Any]], @@ -444,7 +444,7 @@ def setup( ) -def setup_local( +def setup_local( # noqa: PLR0913 env: LibraryEnvironment, cluster_name: str, nodes: Sequence[Mapping[str, Any]], @@ -570,7 +570,7 @@ def setup_local( ) -def _validate_create_corosync_conf( +def _validate_create_corosync_conf( # noqa: PLR0913 cluster_name: str, nodes: Sequence[Mapping[str, Any]], transport_type: str, @@ -631,7 +631,7 @@ def _validate_create_corosync_conf( ) -def _create_corosync_conf( +def _create_corosync_conf( # noqa: PLR0913 cluster_name: str, nodes: Sequence[Mapping[str, Any]], transport_type: str, @@ -834,7 +834,7 @@ def get_corosync_conf_struct(env: LibraryEnvironment) -> CorosyncConfDto: ) from e -def add_nodes( +def add_nodes( # noqa: PLR0912, PLR0915 env: LibraryEnvironment, nodes, wait=False, @@ -1713,7 +1713,7 @@ def _verify_corosync_conf(corosync_conf_facade): ) -def remove_nodes( +def remove_nodes( # noqa: PLR0912, PLR0915 env: LibraryEnvironment, node_list, force_flags: Collection[reports.types.ForceCode] = (), diff --git a/pcs/lib/commands/remote_node.py b/pcs/lib/commands/remote_node.py index b9b0cc10c..6e4a22edb 100644 --- a/pcs/lib/commands/remote_node.py +++ b/pcs/lib/commands/remote_node.py @@ -259,7 +259,7 @@ def _ensure_resource_running(env: LibraryEnvironment, resource_id): raise LibraryError() -def node_add_remote( +def node_add_remote( # noqa: PLR0912, PLR0913, PLR0915 env: LibraryEnvironment, node_name: str, node_addr: Optional[str], @@ -369,27 +369,22 @@ def node_add_remote( ) ) ) - else: - # default node_addr to an address from known-hosts - if node_addr is None: - known_hosts = env.get_known_hosts([node_name]) - if known_hosts: - node_addr = known_hosts[0].dest.addr - node_addr_source = ( - reports.const.DEFAULT_ADDRESS_SOURCE_KNOWN_HOSTS - ) - else: - node_addr = node_name - node_addr_source = ( - reports.const.DEFAULT_ADDRESS_SOURCE_HOST_NAME - ) - report_processor.report( - ReportItem.info( - reports.messages.UsingDefaultAddressForHost( - node_name, node_addr, node_addr_source - ) + # default node_addr to an address from known-hosts + elif node_addr is None: + known_hosts = env.get_known_hosts([node_name]) + if known_hosts: + node_addr = known_hosts[0].dest.addr + node_addr_source = reports.const.DEFAULT_ADDRESS_SOURCE_KNOWN_HOSTS + else: + node_addr = node_name + node_addr_source = reports.const.DEFAULT_ADDRESS_SOURCE_HOST_NAME + report_processor.report( + ReportItem.info( + reports.messages.UsingDefaultAddressForHost( + node_name, node_addr, node_addr_source ) ) + ) # validate inputs report_list = remote_node.validate_create( @@ -467,7 +462,7 @@ def node_add_remote( _ensure_resource_running(env, remote_resource_element.attrib["id"]) -def node_add_guest( +def node_add_guest( # noqa: PLR0912, PLR0915 env: LibraryEnvironment, node_name, resource_id, @@ -552,26 +547,23 @@ def node_add_guest( ) ) ) - else: - # default remote-addr to an address from known-hosts - if "remote-addr" not in options or options["remote-addr"] is None: - known_hosts = env.get_known_hosts([node_name]) - if known_hosts: - new_addr = known_hosts[0].dest.addr - new_addr_source = ( - reports.const.DEFAULT_ADDRESS_SOURCE_KNOWN_HOSTS - ) - else: - new_addr = node_name - new_addr_source = reports.const.DEFAULT_ADDRESS_SOURCE_HOST_NAME - options["remote-addr"] = new_addr - report_processor.report( - ReportItem.info( - reports.messages.UsingDefaultAddressForHost( - node_name, new_addr, new_addr_source - ) + # default remote-addr to an address from known-hosts + elif "remote-addr" not in options or options["remote-addr"] is None: + known_hosts = env.get_known_hosts([node_name]) + if known_hosts: + new_addr = known_hosts[0].dest.addr + new_addr_source = reports.const.DEFAULT_ADDRESS_SOURCE_KNOWN_HOSTS + else: + new_addr = node_name + new_addr_source = reports.const.DEFAULT_ADDRESS_SOURCE_HOST_NAME + options["remote-addr"] = new_addr + report_processor.report( + ReportItem.info( + reports.messages.UsingDefaultAddressForHost( + node_name, new_addr, new_addr_source ) ) + ) # validate inputs report_list = guest_node.validate_set_as_guest( diff --git a/pcs/lib/commands/resource.py b/pcs/lib/commands/resource.py index 2d1b0dfcc..9a8eb070b 100644 --- a/pcs/lib/commands/resource.py +++ b/pcs/lib/commands/resource.py @@ -360,7 +360,7 @@ def _check_special_cases( ) -def create( +def create( # noqa: PLR0913 env: LibraryEnvironment, resource_id: str, resource_agent_name: str, @@ -467,7 +467,7 @@ def create( resource.common.disable(primitive_element, id_provider) -def create_as_clone( +def create_as_clone( # noqa: PLR0913 env: LibraryEnvironment, resource_id: str, resource_agent_name: str, @@ -617,7 +617,7 @@ def create_as_clone( resource.common.disable(clone_element, id_provider) -def create_in_group( +def create_in_group( # noqa: PLR0913 env: LibraryEnvironment, resource_id: str, resource_agent_name: str, @@ -764,7 +764,7 @@ def create_in_group( ) -def create_into_bundle( +def create_into_bundle( # noqa: PLR0913 env: LibraryEnvironment, resource_id: str, resource_agent_name: str, @@ -883,7 +883,7 @@ def create_into_bundle( resource.bundle.add_resource(bundle_el, primitive_element) -def bundle_create( +def bundle_create( # noqa: PLR0913 env, bundle_id, container_type, @@ -963,7 +963,7 @@ def bundle_create( resource.common.disable(bundle_element, id_provider) -def bundle_reset( +def bundle_reset( # noqa: PLR0913 env, bundle_id, *, @@ -1048,7 +1048,7 @@ def bundle_reset( resource.common.disable(bundle_element, id_provider) -def bundle_update( +def bundle_update( # noqa: PLR0913 env, bundle_id, *, @@ -1510,7 +1510,7 @@ def manage( env.push_cib() -def group_add( +def group_add( # noqa: PLR0912 env: LibraryEnvironment, group_id: str, resource_id_list: List[str], @@ -1740,7 +1740,7 @@ def other_resources_affected(self) -> bool: return self._other_resources_affected -def move_autoclean( +def move_autoclean( # noqa: PLR0912, PLR0915 env: LibraryEnvironment, resource_id: str, node: Optional[str] = None, @@ -2014,14 +2014,13 @@ def _ensure_resource_moved_and_not_moved_back( if strict: if clean_operations: raise ResourceMoveAutocleanSimulationFailure(True) - else: - if any( - rsc == resource_id - for rsc in simulate_tools.get_resources_from_operations( - clean_operations - ) - ): - raise ResourceMoveAutocleanSimulationFailure(False) + elif any( + rsc == resource_id + for rsc in simulate_tools.get_resources_from_operations( + clean_operations + ) + ): + raise ResourceMoveAutocleanSimulationFailure(False) def ban(env, resource_id, node=None, master=False, lifetime=None, wait=False): diff --git a/pcs/lib/commands/sbd.py b/pcs/lib/commands/sbd.py index 346c200f5..8479f782b 100644 --- a/pcs/lib/commands/sbd.py +++ b/pcs/lib/commands/sbd.py @@ -142,7 +142,7 @@ def _get_full_target_dict(target_list, node_value_dict, default_value): } -def enable_sbd( +def enable_sbd( # noqa: PLR0913 lib_env, default_watchdog, watchdog_dict, diff --git a/pcs/lib/commands/status.py b/pcs/lib/commands/status.py index c7767ede0..650dc9693 100644 --- a/pcs/lib/commands/status.py +++ b/pcs/lib/commands/status.py @@ -95,7 +95,7 @@ def resources_status(env: LibraryEnvironment) -> ResourcesStatusDto: return dto -def full_cluster_status_plaintext( +def full_cluster_status_plaintext( # noqa: PLR0912, PLR0915 env: LibraryEnvironment, hide_inactive_resources: bool = False, verbose: bool = False, diff --git a/pcs/lib/commands/stonith.py b/pcs/lib/commands/stonith.py index 096779685..e92fdaded 100644 --- a/pcs/lib/commands/stonith.py +++ b/pcs/lib/commands/stonith.py @@ -102,7 +102,7 @@ def _get_agent_facade( raise LibraryError() from e -def create( +def create( # noqa: PLR0913 env: LibraryEnvironment, stonith_id: str, stonith_agent_name: str, @@ -185,7 +185,7 @@ def create( # DEPRECATED: this command is deprecated and will be removed in a future release -def create_in_group( +def create_in_group( # noqa: PLR0913 env: LibraryEnvironment, stonith_id: str, stonith_agent_name: str, diff --git a/pcs/lib/communication/nodes.py b/pcs/lib/communication/nodes.py index 4b608a021..c8841f8ed 100644 --- a/pcs/lib/communication/nodes.py +++ b/pcs/lib/communication/nodes.py @@ -441,12 +441,11 @@ def _process_response(self, response): reports.messages.InvalidResponseFormat(target.label) ) - else: - if not response.was_connected: - self._not_yet_started_target_list.append(target) - report = response_to_report_item( - response, severity=ReportItemSeverity.WARNING - ) + elif not response.was_connected: + self._not_yet_started_target_list.append(target) + report = response_to_report_item( + response, severity=ReportItemSeverity.WARNING + ) self._report(report) def before(self): diff --git a/pcs/lib/corosync/config_facade.py b/pcs/lib/corosync/config_facade.py index f25c44681..3ac7f52b6 100644 --- a/pcs/lib/corosync/config_facade.py +++ b/pcs/lib/corosync/config_facade.py @@ -262,8 +262,8 @@ def create_link_list(self, link_list: Sequence[Mapping[str, str]]) -> None: else: linknumber_missing.append(link) - for link in linknumber_missing: - link = dict(link) + for link_missing_linknumber in linknumber_missing: + link = dict(link_missing_linknumber) try: link["linknumber"] = str(available_link_numbers.pop(0)) except IndexError as e: @@ -955,13 +955,12 @@ def __translate_link_options( result["broadcast"] = "" else: del result["broadcast"] + # When displaying config to users, do the opposite + # transformation: only "yes" is allowed. + elif result["broadcast"] == "yes": + result["broadcast"] = "1" else: - # When displaying config to users, do the opposite - # transformation: only "yes" is allowed. - if result["broadcast"] == "yes": - result["broadcast"] = "1" - else: - del result["broadcast"] + del result["broadcast"] return result diff --git a/pcs/lib/corosync/config_validators.py b/pcs/lib/corosync/config_validators.py index 492700060..f1b43cf02 100644 --- a/pcs/lib/corosync/config_validators.py +++ b/pcs/lib/corosync/config_validators.py @@ -92,7 +92,7 @@ def _validate_value(self, value: validate.ValuePair) -> ReportItemList: return [] -def create( +def create( # noqa: PLR0912, PLR0915 cluster_name: str, # TODO change to DTO, needs new validator node_list: Iterable[Mapping[str, Any]], @@ -402,7 +402,7 @@ def _report_unresolvable_addresses_if_any( ] -def add_nodes( +def add_nodes( # noqa: PLR0912, PLR0915 # TODO change to DTO, needs new validator node_list: Iterable[Mapping[str, Any]], coro_existing_nodes: Iterable[CorosyncNode], @@ -1138,7 +1138,7 @@ def remove_links( return report_items -def update_link( +def update_link( # noqa: PLR0912, PLR0913 linknumber: str, node_addr_map: Mapping[str, str], link_options: Mapping[str, str], diff --git a/pcs/lib/corosync/live.py b/pcs/lib/corosync/live.py index 7137936c9..23f3d1d7f 100644 --- a/pcs/lib/corosync/live.py +++ b/pcs/lib/corosync/live.py @@ -175,7 +175,7 @@ def stopping_local_node_cause_quorum_loss(self) -> bool: ) -def _parse_quorum_status(quorum_status: str) -> QuorumStatus: +def _parse_quorum_status(quorum_status: str) -> QuorumStatus: # noqa: PLR0912 # pylint: disable=too-many-branches node_list: list[QuorumStatusNode] = [] qdevice_list: list[QuorumStatusNode] = [] @@ -184,8 +184,8 @@ def _parse_quorum_status(quorum_status: str) -> QuorumStatus: in_node_list = False try: - for line in quorum_status.splitlines(): - line = line.strip() + for quorum_status_line in quorum_status.splitlines(): + line = quorum_status_line.strip() if not line: continue if in_node_list: diff --git a/pcs/lib/env.py b/pcs/lib/env.py index 85bcf8065..b098c9748 100644 --- a/pcs/lib/env.py +++ b/pcs/lib/env.py @@ -90,7 +90,7 @@ class LibraryEnvironment: # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-public-methods - def __init__( + def __init__( # noqa: PLR0913 self, logger: Logger, report_processor: reports.ReportProcessor, diff --git a/pcs/lib/external.py b/pcs/lib/external.py index e47dafb29..14ccf7b11 100644 --- a/pcs/lib/external.py +++ b/pcs/lib/external.py @@ -106,7 +106,7 @@ def run( ), stdout=subprocess.PIPE, stderr=subprocess.PIPE, - preexec_fn=( + preexec_fn=( # noqa: PLW1509 lambda: signal.signal(signal.SIGPIPE, signal.SIG_DFL) ), close_fds=True, diff --git a/pcs/lib/file/metadata.py b/pcs/lib/file/metadata.py index 2ba188d0a..a01ff4a61 100644 --- a/pcs/lib/file/metadata.py +++ b/pcs/lib/file/metadata.py @@ -112,7 +112,7 @@ def _for_pcs_settings_conf() -> FileMetadata: ) -def for_file_type( +def for_file_type( # noqa: PLR0911 file_type_code: code.FileTypeCode, filename: Optional[str] = None ) -> FileMetadata: # pylint: disable=too-many-return-statements diff --git a/pcs/lib/node_communication.py b/pcs/lib/node_communication.py index 71bd59588..6a1a4b9cc 100644 --- a/pcs/lib/node_communication.py +++ b/pcs/lib/node_communication.py @@ -262,16 +262,15 @@ def response_to_report_item( elif response_code >= 400: report_item = reports.messages.NodeCommunicationError reason = f"HTTP error: {response_code}" + elif response.errno in [ + pycurl.E_OPERATION_TIMEDOUT, + pycurl.E_OPERATION_TIMEOUTED, + ]: + report_item = reports.messages.NodeCommunicationErrorTimedOut + reason = response.error_msg else: - if response.errno in [ - pycurl.E_OPERATION_TIMEDOUT, - pycurl.E_OPERATION_TIMEOUTED, - ]: - report_item = reports.messages.NodeCommunicationErrorTimedOut - reason = response.error_msg - else: - report_item = reports.messages.NodeCommunicationErrorUnableToConnect - reason = response.error_msg + report_item = reports.messages.NodeCommunicationErrorUnableToConnect + reason = response.error_msg if not report_item: return None return ReportItem( diff --git a/pcs/lib/validate.py b/pcs/lib/validate.py index 6264ded49..6835d182e 100644 --- a/pcs/lib/validate.py +++ b/pcs/lib/validate.py @@ -115,9 +115,11 @@ def values_to_pairs( """ option_dict_with_pairs = {} for key, value in option_dict.items(): - if not isinstance(value, ValuePair): - value = ValuePair(original=value, normalized=normalize(key, value)) - option_dict_with_pairs[key] = value + option_dict_with_pairs[key] = ( + ValuePair(original=value, normalized=normalize(key, value)) + if not isinstance(value, ValuePair) + else value + ) return option_dict_with_pairs @@ -133,9 +135,10 @@ def pairs_to_values( """ raw_option_dict = {} for key, value in option_dict.items(): + new_value = value if isinstance(value, ValuePair): - value = value.normalized - raw_option_dict[key] = str(value) + new_value = value.normalized + raw_option_dict[key] = str(new_value) return raw_option_dict @@ -1154,7 +1157,7 @@ def matches_regexp(value: TypeOptionValue, regexp: Union[str, Pattern]) -> bool: class _ValidateAddRemoveBase: # pylint: disable=too-many-instance-attributes - def __init__( + def __init__( # noqa: PLR0913 self, add_item_list: StringCollection, remove_item_list: StringCollection, diff --git a/pcs/resource.py b/pcs/resource.py index d8994c878..b1b7b83c6 100644 --- a/pcs/resource.py +++ b/pcs/resource.py @@ -1,4 +1,4 @@ -# pylint: disable=too-many-lines +# pylint: disable=too-many-lines #noqa: PLR0915 import json import re import sys @@ -530,20 +530,19 @@ def parse_resource_options( elif arg == "meta": meta_args = True op_args = False - else: - if op_args: - if arg == "op": - op_values.append([]) - elif "=" not in arg and op_values[-1]: - op_values.append([]) - op_values[-1].append(arg) - else: - op_values[-1].append(arg) - elif meta_args: - if "=" in arg: - meta_values.append(arg) + elif op_args: + if arg == "op": + op_values.append([]) + elif "=" not in arg and op_values[-1]: + op_values.append([]) + op_values[-1].append(arg) else: - ra_values.append(arg) + op_values[-1].append(arg) + elif meta_args: + if "=" in arg: + meta_values.append(arg) + else: + ra_values.append(arg) return ra_values, op_values, meta_values @@ -654,7 +653,7 @@ def _format_desc(indentation: int, desc: str) -> str: return output.rstrip() -def resource_create(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: +def resource_create(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: # noqa: PLR0912 """ Options: * --agent-validation - use agent self validation of instance attributes @@ -1018,7 +1017,7 @@ def update_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: # Update a resource, removing any args that are empty and adding/updating # args that are not empty -def resource_update(args: Argv, modifiers: InputModifiers) -> None: +def resource_update(args: Argv, modifiers: InputModifiers) -> None: # noqa: PLR0912, PLR0915 """ Commandline options: * -f - CIB file @@ -1282,7 +1281,7 @@ def transform_master_to_clone(master_element): return clone_element -def resource_operation_add( +def resource_operation_add( # noqa: PLR0912, PLR0915 dom, res_id, argv, validate_strict=True, before_op=None ): """ @@ -1438,7 +1437,7 @@ def resource_operation_add( return dom -def resource_operation_remove(res_id: str, argv: Argv) -> None: +def resource_operation_remove(res_id: str, argv: Argv) -> None: # noqa: PLR0912 """ Commandline options: * -f - CIB file @@ -1523,7 +1522,7 @@ def meta_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: resource_meta(argv, modifiers) -def resource_meta(argv: Argv, modifiers: InputModifiers) -> None: +def resource_meta(argv: Argv, modifiers: InputModifiers) -> None: # noqa: PLR0912 """ Commandline options: * --force - allow not suitable command @@ -1744,7 +1743,7 @@ def _get_resource_agent_facade( ).facade_from_parsed_name(resource_agent) -def resource_clone_create( +def resource_clone_create( # noqa: PLR0912 cib_dom, argv, update_existing=False, promotable=False, force_flags=() ): """ @@ -2187,7 +2186,7 @@ def resource_show( raise_command_replaced([f"pcs {keyword} status"], pcs_version="0.11") -def resource_status( +def resource_status( # noqa: PLR0912, PLR0915 lib: Any, argv: Argv, modifiers: InputModifiers, stonith: bool = False ) -> None: """ @@ -2455,7 +2454,7 @@ def resource_restart_cmd( print_to_stderr(f"{resource} successfully restarted") -def resource_force_action( +def resource_force_action( # noqa: PLR0912 lib: Any, argv: Argv, modifiers: InputModifiers, action: str ) -> None: """ @@ -2953,7 +2952,7 @@ def resource_relocate_location_to_str(location): return "" -def resource_relocate_run(cib_dom, resources=None, dry=True): +def resource_relocate_run(cib_dom, resources=None, dry=True): # noqa: PLR0912 """ Commandline options: * -f - CIB file, explicitly forbids -f if dry is False diff --git a/pcs/rule.py b/pcs/rule.py index 8dfc5d633..810e87208 100644 --- a/pcs/rule.py +++ b/pcs/rule.py @@ -43,7 +43,7 @@ def parse_argv(argv, extra_options=None): return options, argv -def dom_rule_add(dom_element, options, rule_argv, cib_schema_version): +def dom_rule_add(dom_element, options, rule_argv, cib_schema_version): # noqa: PLR0912 # pylint: disable=too-many-branches """ Commandline options: no options diff --git a/pcs/status.py b/pcs/status.py index e4d4b8808..444a9fb03 100644 --- a/pcs/status.py +++ b/pcs/status.py @@ -39,7 +39,7 @@ def full_status(lib, argv, modifiers): # Parse crm_mon for status -def nodes_status(lib, argv, modifiers): +def nodes_status(lib, argv, modifiers): # noqa: PLR0912, PLR0915 """ Options: * -f - CIB file - for config subcommand and not for both or corosync @@ -130,11 +130,10 @@ def nodes_status(lib, argv, modifiers): remote_standbynodes_with_resources.append(node_name) else: remote_standbynodes.append(node_name) + elif is_running_resources: + standbynodes_with_resources.append(node_name) else: - if is_running_resources: - standbynodes_with_resources.append(node_name) - else: - standbynodes.append(node_name) + standbynodes.append(node_name) if node.getAttribute("maintenance") == "true": if node_remote: remote_maintenancenodes.append(node_name) @@ -148,11 +147,10 @@ def nodes_status(lib, argv, modifiers): remote_onlinenodes.append(node_name) else: onlinenodes.append(node_name) + elif node_remote: + remote_offlinenodes.append(node_name) else: - if node_remote: - remote_offlinenodes.append(node_name) - else: - offlinenodes.append(node_name) + offlinenodes.append(node_name) print("Pacemaker Nodes:") print(" ".join([" Online:"] + onlinenodes)) diff --git a/pcs/usage.py b/pcs/usage.py index fcc7fbd9a..ac9ea4a5f 100644 --- a/pcs/usage.py +++ b/pcs/usage.py @@ -103,10 +103,10 @@ def full_usage() -> None: print("Examples:\n" + examples.replace(r" \ ", "")) -def strip_extras(text: str) -> str: +def strip_extras(text: str) -> str: # noqa: PLR0912 # pylint: disable=global-statement # pylint: disable=too-many-branches - global examples + global examples # noqa: PLW0603 ret = "" group_name = text.split(" ")[2] in_commands = False @@ -135,9 +135,8 @@ def strip_extras(text: str) -> str: minicmd = " " + " " + line.lstrip() + " " else: minicmd += line.lstrip() + " " - else: - if in_commands: - break + elif in_commands: + break else: if in_examples: examples += minicmd + "\n\n" diff --git a/pcs/utils.py b/pcs/utils.py index 2479cda39..771727579 100644 --- a/pcs/utils.py +++ b/pcs/utils.py @@ -206,8 +206,8 @@ def read_uid_gid_file(uidgid_filename): ) as myfile: data = myfile.read().split("\n") in_uidgid = False - for line in data: - line = re.sub(r"#.*", "", line) + for data_line in data: + line = re.sub(r"#.*", "", data_line) if not in_uidgid: if re.search(r"uidgid.*{", line): in_uidgid = True @@ -542,7 +542,7 @@ def resumeConfigSyncing(node): # 2 = No response, # 3 = Auth Error # 4 = Permission denied -def sendHTTPRequest( +def sendHTTPRequest( # noqa: PLR0912, PLR0915 host, request, data=None, printResult=True, printSuccess=True, timeout=None ): """ @@ -903,7 +903,7 @@ def run( stdin=stdin_pipe, stdout=subprocess.PIPE, stderr=(subprocess.PIPE if ignore_stderr else subprocess.STDOUT), - preexec_fn=subprocess_setup, + preexec_fn=subprocess_setup, # noqa: PLW1509 close_fds=True, env=env_var, # decodes newlines and in python3 also converts bytes to str @@ -1055,7 +1055,7 @@ def auth_hosts(host_dict): err("Unable to communicate with pcsd") -def call_local_pcsd(argv, options, std_in=None): +def call_local_pcsd(argv, options, std_in=None): # noqa: PLR0911 """ Commandline options: * --request-timeout - timeout of call to local pcsd @@ -1377,7 +1377,7 @@ def dom_get_resource_masterslave(dom, resource_id): # returns tuple (is_valid, error_message, correct_resource_id_if_exists) # there is a duplicate code in pcs/lib/cib/constraint/constraint.py # please use function in pcs/lib/cib/constraint/constraint.py -def validate_constraint_resource(dom, resource_id): +def validate_constraint_resource(dom, resource_id): # noqa: PLR0911 """ Commandline options: * --force - allow constraint on any resource @@ -1752,7 +1752,7 @@ def is_valid_cib_scope(scope): # Checks to see if id exists in the xml dom passed # DEPRECATED use lxml version available in pcs.lib.cib.tools -def does_id_exist(dom, check_id): +def does_id_exist(dom, check_id): # noqa: PLR0912 """ Commandline options: no options """ @@ -2234,7 +2234,7 @@ def write_file(path, data, permissions=0o644, binary=False): return True, "" -def tar_add_file_data( +def tar_add_file_data( # noqa: PLR0913 tarball, data, name, diff --git a/pcs_test/api_v2_client.py b/pcs_test/api_v2_client.py index 6028f57e5..7cf7bb821 100644 --- a/pcs_test/api_v2_client.py +++ b/pcs_test/api_v2_client.py @@ -52,7 +52,7 @@ def signal_handler(sig, frame): # pylint: disable=global-statement del frame if sig == signal.SIGINT: - global kill_requested + global kill_requested # noqa: PLW0603 if not task_ident: error("no task to kill") raise SystemExit(1) from None @@ -145,7 +145,7 @@ def fetch_task_result( ) -> TaskResultDto: # pylint: disable=global-statement # Using global report list to recall reports in signal handler - global report_list + global report_list # noqa: PLW0603 task_state = TaskState.CREATED while task_state != TaskState.FINISHED: response = make_api_request_get( @@ -164,14 +164,14 @@ def fetch_task_result( report_list = task_result_dto.reports # Wait for updates and continue until the task is finished sleep(sleep_interval) # 300ms - global task_ident + global task_ident # noqa: PLW0603 task_ident = "" return task_result_dto def perform_command(command_dto: CommandDto, auth_token: str) -> TaskResultDto: # pylint: disable=global-statement - global task_ident + global task_ident # noqa: PLW0603 response = make_api_request_post( "task/create", json.dumps(to_dict(command_dto)), auth_token ) diff --git a/pcs_test/tier0/common/test_resource_status.py b/pcs_test/tier0/common/test_resource_status.py index 27eecc8dc..84a65827c 100644 --- a/pcs_test/tier0/common/test_resource_status.py +++ b/pcs_test/tier0/common/test_resource_status.py @@ -37,7 +37,7 @@ from pcs.common.types import StringSequence -def fixture_primitive_dto( +def fixture_primitive_dto( # noqa: PLR0913 resource_id: str, instance_id: Optional[str], *, @@ -95,7 +95,7 @@ def fixture_group_dto( ) -def fixture_clone_dto( +def fixture_clone_dto( # noqa: PLR0913 resource_id: str, *, multi_state: bool = False, diff --git a/pcs_test/tier0/lib/commands/cluster/test_setup.py b/pcs_test/tier0/lib/commands/cluster/test_setup.py index 01ec7ccc8..be9063db1 100644 --- a/pcs_test/tier0/lib/commands/cluster/test_setup.py +++ b/pcs_test/tier0/lib/commands/cluster/test_setup.py @@ -121,7 +121,7 @@ def options_fixture(options, template=OPTION_TEMPLATE): ) -def corosync_conf_fixture( +def corosync_conf_fixture( # noqa: PLR0913 node_addrs, *, transport_type="knet", @@ -166,9 +166,10 @@ def corosync_conf_fixture( link["linknumber"] = links_numbers[i] link_translated = {} for name, value in link.items(): + knet_name = name if name in knet_options: - name = f"knet_{name}" - link_translated[name] = value + knet_name = f"knet_{name}" + link_translated[knet_name] = value link_list[i] = link_translated interface_list = "".join( diff --git a/pcs_test/tier0/lib/commands/resource/test_resource_create.py b/pcs_test/tier0/lib/commands/resource/test_resource_create.py index eff6fe03e..fffc3e5ed 100644 --- a/pcs_test/tier0/lib/commands/resource/test_resource_create.py +++ b/pcs_test/tier0/lib/commands/resource/test_resource_create.py @@ -21,7 +21,7 @@ TIMEOUT = 10 -def create( +def create( # noqa: PLR0913 env, *, wait=False, @@ -74,7 +74,7 @@ def create_group( ) -def create_clone( +def create_clone( # noqa: PLR0913 env, *, wait=TIMEOUT, diff --git a/pcs_test/tier0/lib/commands/resource/test_resource_enable_disable.py b/pcs_test/tier0/lib/commands/resource/test_resource_enable_disable.py index 51c71d94a..8196fc544 100644 --- a/pcs_test/tier0/lib/commands/resource/test_resource_enable_disable.py +++ b/pcs_test/tier0/lib/commands/resource/test_resource_enable_disable.py @@ -376,7 +376,7 @@ def get_fixture_master_cib( ) -def get_fixture_clone_group_cib( +def get_fixture_clone_group_cib( # noqa: PLR0913 *, clone_disabled=False, clone_meta=False, diff --git a/pcs_test/tier0/lib/commands/resource/test_resource_manage_unmanage.py b/pcs_test/tier0/lib/commands/resource/test_resource_manage_unmanage.py index d540799f2..8758ea335 100644 --- a/pcs_test/tier0/lib/commands/resource/test_resource_manage_unmanage.py +++ b/pcs_test/tier0/lib/commands/resource/test_resource_manage_unmanage.py @@ -381,7 +381,7 @@ def get_fixture_clone_cib( """ -def get_fixture_clone_group_cib( +def get_fixture_clone_group_cib( # noqa: PLR0913 *, clone_unmanaged=False, clone_meta=False, diff --git a/pcs_test/tier0/lib/commands/test_status.py b/pcs_test/tier0/lib/commands/test_status.py index 30dacf725..9bfb3f7bc 100644 --- a/pcs_test/tier0/lib/commands/test_status.py +++ b/pcs_test/tier0/lib/commands/test_status.py @@ -140,7 +140,7 @@ def _fixture_config_live_remote_minimal(self): ) ) - def _fixture_config_local_daemons( + def _fixture_config_local_daemons( # noqa: PLR0913 self, *, corosync_enabled=True, diff --git a/pcs_test/tier0/lib/commands/test_stonith_update_scsi_devices.py b/pcs_test/tier0/lib/commands/test_stonith_update_scsi_devices.py index 31e9ac69e..152b51cae 100644 --- a/pcs_test/tier0/lib/commands/test_stonith_update_scsi_devices.py +++ b/pcs_test/tier0/lib/commands/test_stonith_update_scsi_devices.py @@ -382,7 +382,7 @@ def setUp(self): ] self.config.env.set_known_nodes(self.existing_nodes) - def config_cib( + def config_cib( # noqa: PLR0913 self, *, devices_before=DEVICES_1, @@ -464,7 +464,7 @@ def config_cib( args=args, ) - def assert_command_success( + def assert_command_success( # noqa: PLR0913 self, *, devices_before=DEVICES_1, diff --git a/pcs_test/tier0/lib/resource_agent/test_pcs_transform.py b/pcs_test/tier0/lib/resource_agent/test_pcs_transform.py index e3214a867..5578a3c1b 100644 --- a/pcs_test/tier0/lib/resource_agent/test_pcs_transform.py +++ b/pcs_test/tier0/lib/resource_agent/test_pcs_transform.py @@ -108,7 +108,7 @@ def _fixture_metadata(name): actions=[], ) - def test_resource( + def test_resource( # noqa: PLR0913 self, mock_action_role, mock_parameter_enum, @@ -148,7 +148,7 @@ def test_resource( mock_stonith_action.assert_not_called() mock_stonith_port.assert_not_called() - def test_stonith( + def test_stonith( # noqa: PLR0913 self, mock_action_role, mock_parameter_enum, @@ -188,7 +188,7 @@ def test_stonith( mock_stonith_action.assert_called_once_with("from stonith parameters") mock_stonith_port.assert_called_once_with("from stonith action") - def test_pcmk_fake( + def test_pcmk_fake( # noqa: PLR0913 self, mock_action_role, mock_parameter_enum, diff --git a/pcs_test/tier0/lib/test_cluster_property.py b/pcs_test/tier0/lib/test_cluster_property.py index 29383c0c0..b1c0c28af 100644 --- a/pcs_test/tier0/lib/test_cluster_property.py +++ b/pcs_test/tier0/lib/test_cluster_property.py @@ -274,16 +274,13 @@ def assert_validate_set( ) if sbd_enabled: self.mock_sbd_devices.assert_called_once_with() - if sbd_devices: + if sbd_devices or ( + new_properties["stonith-watchdog-timeout"] + in STONITH_WATCHDOG_TIMEOUT_UNSET_VALUES + ): self.mock_sbd_timeout.assert_not_called() else: - if ( - new_properties["stonith-watchdog-timeout"] - in STONITH_WATCHDOG_TIMEOUT_UNSET_VALUES - ): - self.mock_sbd_timeout.assert_not_called() - else: - self.mock_sbd_timeout.assert_called_once_with() + self.mock_sbd_timeout.assert_called_once_with() self.mock_sbd_devices.reset_mock() else: self.mock_sbd_devices.assert_not_called() diff --git a/pcs_test/tier0/test_host.py b/pcs_test/tier0/test_host.py index ea0c13972..ddc293913 100644 --- a/pcs_test/tier0/test_host.py +++ b/pcs_test/tier0/test_host.py @@ -35,13 +35,8 @@ def setUp(self): def _fixture_args(name_addr_port_tuple_list): arg_list = [] for name, addr, port in name_addr_port_tuple_list: - port = ":{}".format(port) if port is not None else "" - arg_list.extend( - [ - name, - "addr={}{}".format(addr, port), - ] - ) + port_str = ":{}".format(port) if port is not None else "" + arg_list.extend([name, f"addr={addr}{port_str}"]) return arg_list def _assert_invalid_port(self, name_addr_port_tuple_list): diff --git a/pcs_test/tier1/legacy/test_cluster.py b/pcs_test/tier1/legacy/test_cluster.py index 9051fd0f0..7d30b7c70 100644 --- a/pcs_test/tier1/legacy/test_cluster.py +++ b/pcs_test/tier1/legacy/test_cluster.py @@ -45,7 +45,7 @@ def assert_uidgid_file_removed(self, filename): False, ) - def test_uidgid(self): + def test_uidgid(self): # noqa: PLR0915 # pylint: disable=too-many-statements stdout, stderr, retval = self._pcs("cluster uidgid".split()) self.assertEqual(stdout, "") diff --git a/pcs_test/tier1/legacy/test_constraints.py b/pcs_test/tier1/legacy/test_constraints.py index 273a62084..9a351b6b0 100644 --- a/pcs_test/tier1/legacy/test_constraints.py +++ b/pcs_test/tier1/legacy/test_constraints.py @@ -686,7 +686,7 @@ def test_constraint_removal(self): self.assert_pcs_success("constraint location config --full".split()) # see also BundleColocation - def test_colocation_constraints(self): + def test_colocation_constraints(self): # noqa: PLR0915 self.fixture_resources() # pcs no longer allows creating masters but supports existing ones. In # order to test it, we need to put a master in the CIB without pcs. @@ -1034,7 +1034,7 @@ def test_colocation_options_empty_value(self): self.assertEqual(retval, 1) # see also BundleColocation - def test_colocation_sets(self): + def test_colocation_sets(self): # noqa: PLR0915 # pylint: disable=too-many-statements self.fixture_resources() self.assert_pcs_success( @@ -1556,7 +1556,7 @@ def test_order_sets_removal(self): self.assert_pcs_success("constraint order".split()) # see also BundleOrder - def test_order_sets(self): + def test_order_sets(self): # noqa: PLR0915 # pylint: disable=too-many-statements self.fixture_resources() self.assert_pcs_success( @@ -1881,7 +1881,7 @@ def test_order_sets(self): ), ) - def test_location_constraint_rule(self): + def test_location_constraint_rule(self): # noqa: PLR0915 # pylint: disable=too-many-statements self.fixture_resources() stdout, stderr, retval = pcs( @@ -2140,7 +2140,7 @@ def test_location_bad_rules(self): self.assertEqual(stdout, "") self.assertEqual(retval, 1) - def test_master_slave_constraint(self): + def test_master_slave_constraint(self): # noqa: PLR0915 # pylint: disable=too-many-statements os.system( "CIB_file=" @@ -2391,7 +2391,7 @@ def test_master_slave_constraint(self): ), ) - def test_clone_constraint(self): + def test_clone_constraint(self): # noqa: PLR0915 # pylint: disable=too-many-statements os.system( "CIB_file=" @@ -3197,7 +3197,7 @@ def test_duplicate_colocation(self): ), ) - def test_duplicate_set_constraints(self): + def test_duplicate_set_constraints(self): # noqa: PLR0915 # pylint: disable=too-many-statements self.fixture_resources() stdout, stderr, retval = pcs( @@ -3570,7 +3570,7 @@ def test_duplicate_location_rules(self): ), ) - def test_constraints_custom_id(self): + def test_constraints_custom_id(self): # noqa: PLR0915 # pylint: disable=too-many-statements self.fixture_resources() stdout, stderr, retval = pcs( diff --git a/pcs_test/tier1/legacy/test_stonith.py b/pcs_test/tier1/legacy/test_stonith.py index 96724dfd8..a02096106 100644 --- a/pcs_test/tier1/legacy/test_stonith.py +++ b/pcs_test/tier1/legacy/test_stonith.py @@ -1459,8 +1459,8 @@ def fixture_full_configuration(self): write_data_to_tmpfile(cib, self.temp_cib) def fixture_cib_config_cache(self): - # pylint: disable=global-variable-not-assigned - global _fixture_stonith_level_cache, _fixture_stonith_level_cache_lock + # pylint: disable=global-statement + global _fixture_stonith_level_cache # noqa: PLW0603 with _fixture_stonith_level_cache_lock: if _fixture_stonith_level_cache is None: _fixture_stonith_level_cache = self.fixture_cib_config() diff --git a/pcs_test/tier1/legacy/test_utils.py b/pcs_test/tier1/legacy/test_utils.py index f75ceec6a..d6c621b56 100644 --- a/pcs_test/tier1/legacy/test_utils.py +++ b/pcs_test/tier1/legacy/test_utils.py @@ -87,7 +87,7 @@ def get_cib_resources(self): resources.parentNode.replaceChild(new_resources, resources) return cib_dom - def test_dom_get_resources(self): + def test_dom_get_resources(self): # noqa: PLR0915 # pylint: disable=too-many-statements def test_dom_get(method, dom, ok_ids, bad_ids): for element_id in ok_ids: diff --git a/pcs_test/tools/assertions.py b/pcs_test/tools/assertions.py index e48bfabae..517c6b263 100644 --- a/pcs_test/tools/assertions.py +++ b/pcs_test/tools/assertions.py @@ -142,7 +142,7 @@ def assert_pcs_fail_regardless_of_force( stderr_regexp=stderr_regexp, ) - def assert_pcs_result( + def assert_pcs_result( # noqa: PLR0913 self, command, *, diff --git a/pcs_test/tools/bin_mock/pcmk/crm_resource_mock.py b/pcs_test/tools/bin_mock/pcmk/crm_resource_mock.py index 875595961..c3ae5411f 100644 --- a/pcs_test/tools/bin_mock/pcmk/crm_resource_mock.py +++ b/pcs_test/tools/bin_mock/pcmk/crm_resource_mock.py @@ -25,7 +25,8 @@ def get_arg_values(argv, name): return values -def main(): +def main(): # noqa: PLR0912 + # pylint: disable=too-many-branches argv = sys.argv[1:] if not argv: raise AssertionError() diff --git a/pcs_test/tools/command_env/config_http_status.py b/pcs_test/tools/command_env/config_http_status.py index 16472f206..ba0f8d6da 100644 --- a/pcs_test/tools/command_env/config_http_status.py +++ b/pcs_test/tools/command_env/config_http_status.py @@ -9,7 +9,7 @@ class StatusShortcuts: def __init__(self, calls): self.__calls = calls - def get_full_cluster_status_plaintext( + def get_full_cluster_status_plaintext( # noqa: PLR0913 self, *, node_labels=None, diff --git a/pcs_test/tools/command_env/config_raw_file.py b/pcs_test/tools/command_env/config_raw_file.py index ce972297f..2d38a6546 100644 --- a/pcs_test/tools/command_env/config_raw_file.py +++ b/pcs_test/tools/command_env/config_raw_file.py @@ -65,7 +65,7 @@ def read( ) self.__calls.place(name, call, before, instead) - def write( + def write( # noqa: PLR0913 self, file_type_code, path, diff --git a/pcs_test/tools/command_env/config_runner.py b/pcs_test/tools/command_env/config_runner.py index 1a6161588..2c905607f 100644 --- a/pcs_test/tools/command_env/config_runner.py +++ b/pcs_test/tools/command_env/config_runner.py @@ -18,7 +18,7 @@ def __init__(self, call_collection, wrap_helper): self.sbd = wrap_helper(SbdShortcuts(self.__calls)) self.scsi = wrap_helper(ScsiShortcuts(self.__calls)) - def place( + def place( # noqa: PLR0913 self, command, *, diff --git a/pcs_test/tools/command_env/config_runner_pcmk.py b/pcs_test/tools/command_env/config_runner_pcmk.py index 269d310bb..9e003c3a8 100644 --- a/pcs_test/tools/command_env/config_runner_pcmk.py +++ b/pcs_test/tools/command_env/config_runner_pcmk.py @@ -571,7 +571,7 @@ def resource_restart( ), ) - def resource_cleanup( + def resource_cleanup( # noqa: PLR0913 self, *, name="runner.pcmk.cleanup", @@ -618,7 +618,7 @@ def resource_cleanup( instead=instead, ) - def resource_refresh( + def resource_refresh( # noqa: PLR0913 self, *, name="runner.pcmk.refresh", @@ -665,7 +665,7 @@ def resource_refresh( instead=instead, ) - def resource_move( + def resource_move( # noqa: PLR0913 self, *, name="runner.pcmk.resource_move", @@ -704,7 +704,7 @@ def resource_move( all_args["action"] = "--move" self._resource_move_ban_clear(**all_args) - def resource_ban( + def resource_ban( # noqa: PLR0913 self, *, name="runner.pcmk.resource_ban", @@ -741,7 +741,7 @@ def resource_ban( all_args["action"] = "--ban" self._resource_move_ban_clear(**all_args) - def resource_clear( + def resource_clear( # noqa: PLR0913 self, *, name="runner.pcmk.resource_clear", @@ -780,7 +780,7 @@ def resource_clear( all_args["action"] = "--clear" self._resource_move_ban_clear(**all_args) - def _resource_move_ban_clear( + def _resource_move_ban_clear( # noqa: PLR0913 self, name, action, @@ -979,7 +979,7 @@ def remove_node( ), ) - def simulate_cib( + def simulate_cib( # noqa: PLR0913 self, new_cib_filepath, transitions_filepath, @@ -1070,7 +1070,7 @@ def get_rule_in_effect_status( ), ) - def resource_agent_self_validation( + def resource_agent_self_validation( # noqa: PLR0913 self, attributes, standard="ocf", diff --git a/pcs_test/tools/command_env/mock_node_communicator.py b/pcs_test/tools/command_env/mock_node_communicator.py index 47890cf79..523b8b5c2 100644 --- a/pcs_test/tools/command_env/mock_node_communicator.py +++ b/pcs_test/tools/command_env/mock_node_communicator.py @@ -109,7 +109,7 @@ def bad_request_list_content(errors): ) -def _communication_to_response( +def _communication_to_response( # noqa: PLR0913 label, dest_list, action, @@ -147,7 +147,7 @@ def _communication_to_response( ) -def create_communication( +def create_communication( # noqa: PLR0913 communication_list, *, action="", diff --git a/pcs_test/tools/pcs_runner.py b/pcs_test/tools/pcs_runner.py index 3456d5e7d..6b72275dc 100644 --- a/pcs_test/tools/pcs_runner.py +++ b/pcs_test/tools/pcs_runner.py @@ -74,7 +74,7 @@ def _run( stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - preexec_fn=(lambda: signal.signal(signal.SIGPIPE, signal.SIG_DFL)), + preexec_fn=(lambda: signal.signal(signal.SIGPIPE, signal.SIG_DFL)), # noqa: PLW1509 close_fds=True, shell=False, env=env_vars, diff --git a/pyproject.toml b/pyproject.toml index 406fb0e06..cfa690ce9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,16 +107,6 @@ ignore = [ "ARG005", # https://docs.astral.sh/ruff/rules/unused-lambda-argument/ "C408", # https://docs.astral.sh/ruff/rules/unnecessary-collection-call/ "PERF203", # http://docs.astral.sh/ruff/rules/try-except-in-loop/ - "PLR2004", # magic-value-comparison (99) - "PLR0913", # too-many-arguments (54) - "PLR0912", # too-many-branches (49) - "PLR0915", # too-many-statements (31) - "PLW2901", # redefined-loop-name (22) - "PLR5501", # collapsible-else-if (14) - "PLW0603", # global-statement (9) - "PLR0911", # too-many-return-statements (6) - "PLW1509", # subprocess-popen-preexec-fn (3) - "PLW0602", # global-variable-not-assigned (1) "SIM102", # https://docs.astral.sh/ruff/rules/collapsible-if/ ] @@ -140,5 +130,6 @@ section-order = ["future", "standard-library", "third-party", "first-party", "te "pcs_test/**.py" = ["SLF001"] [tool.ruff.lint.pylint] +allow-magic-value-types = ["str", "bytes", "int"] max-args = 8 max-positional-args = 8 From 555355a8773452bb98b0b173d68d66d780b9ba41 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Thu, 28 Nov 2024 13:07:48 +0100 Subject: [PATCH 081/227] gitlab-ci: add ruff, remove black and isort --- .gitlab-ci.yml | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fa1d098bb..b48bad734 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -55,23 +55,33 @@ typos: - make - make typos_check -black: +ruff_isort: extends: .parallel stage: stage1 script: - python3 -m pip install --upgrade -r dev_requirements.txt - ./autogen.sh - - ./configure --enable-local-build --enable-dev-tests --enable-tests-only - - make black_check + - ./configure --enable-local-build --enable-dev-tests --enable-tests-only --enable-individual-bundling + - make ruff_isort_check -isort: +ruff_format: extends: .parallel stage: stage1 script: - python3 -m pip install --upgrade -r dev_requirements.txt - ./autogen.sh - - ./configure --enable-local-build --enable-dev-tests --enable-tests-only - - make isort_check + - ./configure --enable-local-build --enable-dev-tests --enable-tests-only --enable-individual-bundling + - make ruff_format_check + +ruff_lint: + extends: .parallel + stage: stage1 + script: + - python3 -m pip install --upgrade -r dev_requirements.txt + - ./autogen.sh + - ./configure --enable-local-build --enable-dev-tests --enable-tests-only --enable-individual-bundling + - make + - make ruff_lint pylint: extends: .parallel From b27e65b5ab793b618cdb463a93611d7f61143203 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Fri, 13 Dec 2024 13:51:33 +0100 Subject: [PATCH 082/227] remove black code formatter --- CONTRIBUTING.md | 4 ++-- Makefile.am | 12 ------------ configure.ac | 2 +- dev_requirements.txt | 1 - pyproject.toml | 6 +----- 5 files changed, 4 insertions(+), 21 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6e1570a70..18ef6188a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,7 +41,7 @@ ### Pcs test suite * To run all the tests, type `make check`. * You may run specific tests like this: - * `make black_check` + * `make ruff_format_check` * `make isort_check` * `make mypy` * `make pylint` @@ -58,7 +58,7 @@ `make distcheck DISTCHECK_CONFIGURE_FLAGS='...'`. * The point of this test is to make sure all necessary files are present in the tarball. -* To run black code formatter, type `make black`. +* To run ruff code formatter, type `make ruff_format`. * To run isort code formatter, type `make isort`. ### Distribution tarball diff --git a/Makefile.am b/Makefile.am index 5ec454a18..d528d3141 100644 --- a/Makefile.am +++ b/Makefile.am @@ -240,18 +240,6 @@ if DEV_TESTS $(TIME) $(PYTHON) -m isort ${PCS_PYTHON_PACKAGES} endif -black_check: pyproject.toml -if DEV_TESTS - export PYTHONPATH=${abs_top_builddir}/${PCS_BUNDLED_DIR_LOCAL}/packages && \ - $(TIME) $(PYTHON) -m black --config pyproject.toml --check ${PCS_PYTHON_PACKAGES} -endif - -black: pyproject.toml -if DEV_TESTS - export PYTHONPATH=${abs_top_builddir}/${PCS_BUNDLED_DIR_LOCAL}/packages && \ - $(PYTHON) -m black --config pyproject.toml ${PCS_PYTHON_PACKAGES} -endif - mypy: if DEV_TESTS export PYTHONPATH=${abs_top_builddir}/${PCS_BUNDLED_DIR_LOCAL}/packages && \ diff --git a/configure.ac b/configure.ac index 607c15176..37cf46ada 100644 --- a/configure.ac +++ b/configure.ac @@ -129,7 +129,7 @@ fi # configure options section AC_ARG_ENABLE([dev-tests], - [AS_HELP_STRING([--enable-dev-tests], [Enable extra developers tests (black, isort, mypy, pylint, ruff) (default: no)])], + [AS_HELP_STRING([--enable-dev-tests], [Enable extra developers tests (isort, mypy, pylint, ruff) (default: no)])], [dev_tests="yes"]) AM_CONDITIONAL([DEV_TESTS], [test "x$dev_tests" = "xyes"]) diff --git a/dev_requirements.txt b/dev_requirements.txt index 29718086d..ec9da367b 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -2,7 +2,6 @@ lxml-stubs pylint==3.3.2 astroid==3.3.5 mypy==1.13.0 -black==24.10.0 isort ruff==0.8.0 types-cryptography diff --git a/pyproject.toml b/pyproject.toml index cfa690ce9..44a01b325 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,3 @@ -[tool.black] -line-length = 80 -target-version = ['py39'] - [tool.isort] profile = "black" line_length = 80 @@ -22,7 +18,7 @@ disable = [ # TODO is used to mark e.g. deprecations which are to be resolved in next pcs # major version "fixme", - # handled by black + # handled by formatter "line-too-long", # we dont require pointless docstring to be present "missing-docstring", From cc0e7f01ab8e239f1745499d16959545ce0cb799 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Fri, 13 Dec 2024 14:00:03 +0100 Subject: [PATCH 083/227] remove isort --- CONTRIBUTING.md | 4 ++-- Makefile.am | 12 ------------ configure.ac | 2 +- dev_requirements.txt | 1 - pyproject.toml | 13 ------------- 5 files changed, 3 insertions(+), 29 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 18ef6188a..e18da8c9d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,7 +42,7 @@ * To run all the tests, type `make check`. * You may run specific tests like this: * `make ruff_format_check` - * `make isort_check` + * `make ruff_isort_check` * `make mypy` * `make pylint` * `make tests_tier0` @@ -58,8 +58,8 @@ `make distcheck DISTCHECK_CONFIGURE_FLAGS='...'`. * The point of this test is to make sure all necessary files are present in the tarball. +* To run ruff isort code formatter, type `make ruff_isort`. * To run ruff code formatter, type `make ruff_format`. -* To run isort code formatter, type `make isort`. ### Distribution tarball * To create a tarball for distribution, run `make dist`. diff --git a/Makefile.am b/Makefile.am index d528d3141..dd214b8e8 100644 --- a/Makefile.am +++ b/Makefile.am @@ -228,18 +228,6 @@ if DEV_TESTS $(TIME) ruff --config pyproject.toml check ${PCS_PYTHON_PACKAGES} endif -isort_check: pyproject.toml -if DEV_TESTS - export PYTHONPATH=${abs_top_builddir}/${PCS_BUNDLED_DIR_LOCAL}/packages && \ - $(TIME) $(PYTHON) -m isort --check-only ${PCS_PYTHON_PACKAGES} -endif - -isort: pyproject.toml -if DEV_TESTS - export PYTHONPATH=${abs_top_builddir}/${PCS_BUNDLED_DIR_LOCAL}/packages && \ - $(TIME) $(PYTHON) -m isort ${PCS_PYTHON_PACKAGES} -endif - mypy: if DEV_TESTS export PYTHONPATH=${abs_top_builddir}/${PCS_BUNDLED_DIR_LOCAL}/packages && \ diff --git a/configure.ac b/configure.ac index 37cf46ada..d0183c5ea 100644 --- a/configure.ac +++ b/configure.ac @@ -129,7 +129,7 @@ fi # configure options section AC_ARG_ENABLE([dev-tests], - [AS_HELP_STRING([--enable-dev-tests], [Enable extra developers tests (isort, mypy, pylint, ruff) (default: no)])], + [AS_HELP_STRING([--enable-dev-tests], [Enable extra developers tests (mypy, pylint, ruff) (default: no)])], [dev_tests="yes"]) AM_CONDITIONAL([DEV_TESTS], [test "x$dev_tests" = "xyes"]) diff --git a/dev_requirements.txt b/dev_requirements.txt index ec9da367b..f20d319a7 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -2,7 +2,6 @@ lxml-stubs pylint==3.3.2 astroid==3.3.5 mypy==1.13.0 -isort ruff==0.8.0 types-cryptography types-dataclasses diff --git a/pyproject.toml b/pyproject.toml index 44a01b325..8f8b679ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,16 +1,3 @@ -[tool.isort] -profile = "black" -line_length = 80 -multi_line_output = 3 -force_grid_wrap = 2 -atomic = true -py_version = 39 -skip_gitignore = true -sections = ['FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'TESTS', 'LOCALFOLDER'] -known_first_party = ["pcs"] -known_tests = ["pcs_test"] -supported_extensions = ["py", "py.in"] - [tool.pylint.main] disable = [ # not critical, plus we use str.format() for readability From af7946c645639250ac89f7ee5da5a7ce071a5da5 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Fri, 13 Dec 2024 14:09:52 +0100 Subject: [PATCH 084/227] remove pylint --- .gitlab-ci.yml | 10 -------- CONTRIBUTING.md | 2 +- Makefile.am | 13 +--------- configure.ac | 7 +----- dev_requirements.txt | 2 -- pyproject.toml | 56 -------------------------------------------- 6 files changed, 3 insertions(+), 87 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b48bad734..bc944de68 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -83,16 +83,6 @@ ruff_lint: - make - make ruff_lint -pylint: - extends: .parallel - stage: stage1 - script: - - python3 -m pip install --upgrade -r dev_requirements.txt - - ./autogen.sh - - ./configure --enable-local-build --enable-dev-tests --enable-parallel-pylint - - make - - make pylint - mypy: extends: .parallel stage: stage1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e18da8c9d..c927a6a24 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,7 +44,7 @@ * `make ruff_format_check` * `make ruff_isort_check` * `make mypy` - * `make pylint` + * `make ruff_lint` * `make tests_tier0` * `make tests_tier1` * `make pcsd-tests` diff --git a/Makefile.am b/Makefile.am index dd214b8e8..9231708ad 100644 --- a/Makefile.am +++ b/Makefile.am @@ -191,17 +191,6 @@ else python_test_options = -v --vanilla endif -pylint: -if DEV_TESTS -if PARALLEL_PYLINT -pylint_options = --jobs=0 -else -pylint_options = -endif - export PYTHONPATH=${abs_top_builddir}/${PCS_BUNDLED_DIR_LOCAL}/packages && \ - $(TIME) $(PYTHON) -m pylint --rcfile pyproject.toml ${pylint_options} ${PCS_PYTHON_PACKAGES} -endif - ruff_format_check: pyproject.toml if DEV_TESTS $(TIME) ruff --config pyproject.toml format --check ${PCS_PYTHON_PACKAGES} @@ -326,7 +315,7 @@ test-tree-clean: fi find ${abs_top_builddir} -type d -name __pycache__ -exec rm -rf {} \; 2>/dev/null || : -check-local: check-local-deps test-tree-prep typos_check pylint ruff_lint ruff_isort_check ruff_format_check mypy tests_tier0 tests_tier1 pcsd-tests test-tree-clean +check-local: check-local-deps test-tree-prep typos_check ruff_lint ruff_isort_check ruff_format_check mypy tests_tier0 tests_tier1 pcsd-tests test-tree-clean clean-local: test-tree-clean $(PYTHON) setup.py clean diff --git a/configure.ac b/configure.ac index d0183c5ea..d7dea450d 100644 --- a/configure.ac +++ b/configure.ac @@ -129,7 +129,7 @@ fi # configure options section AC_ARG_ENABLE([dev-tests], - [AS_HELP_STRING([--enable-dev-tests], [Enable extra developers tests (mypy, pylint, ruff) (default: no)])], + [AS_HELP_STRING([--enable-dev-tests], [Enable extra developers tests (mypy, ruff) (default: no)])], [dev_tests="yes"]) AM_CONDITIONAL([DEV_TESTS], [test "x$dev_tests" = "xyes"]) @@ -143,11 +143,6 @@ AC_ARG_ENABLE([concise-tests], [concise_tests="yes"]) AM_CONDITIONAL([CONCISE_TESTS], [test "x$concise_tests" = "xyes"]) -AC_ARG_ENABLE([parallel-pylint], - [AS_HELP_STRING([--enable-parallel-pylint], [Enable running pylint in multiple threads (default: no)])], - [parallel_pylint="yes"]) -AM_CONDITIONAL([PARALLEL_PYLINT], [test "x$parallel_pylint" = "xyes"]) - AC_ARG_ENABLE([typos-check], [AS_HELP_STRING([--enable-typos-check], [Enable checking source code for typos (needs https://github.com/crate-ci/typos to be installed) (default: no)])], [typos_check="yes"]) diff --git a/dev_requirements.txt b/dev_requirements.txt index f20d319a7..1c10c4cab 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,6 +1,4 @@ lxml-stubs -pylint==3.3.2 -astroid==3.3.5 mypy==1.13.0 ruff==0.8.0 types-cryptography diff --git a/pyproject.toml b/pyproject.toml index 8f8b679ce..0a6c4345e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,59 +1,3 @@ -[tool.pylint.main] -disable = [ - # not critical, plus we use str.format() for readability - "consider-using-f-string", - # TODO is used to mark e.g. deprecations which are to be resolved in next pcs - # major version - "fixme", - # handled by formatter - "line-too-long", - # we dont require pointless docstring to be present - "missing-docstring", - "similarities", - "unspecified-encoding", - # lot of dict() in code to be replaced, not worth the effort now - "use-dict-literal", - # handled by isort - "wrong-import-order", -] -extension-pkg-allow-list = ["lxml.etree", "pycurl"] -load-plugins = ["pylint.extensions.no_self_use"] -persistent = false -reports = false -score = false - -[tool.pylint.basic] -# Everything in module context is a constant, but our naming convention allows -# constants to have the same name format as variables -const-rgx = "(([A-Z_][A-Z0-9_]*)|(__.*__))|([a-z_][a-z0-9_]*)$" -good-names = [ - "e", - "i", - "op", - "ip", - "el", - "maxDiff", - "cm", - "ok", - "T", - "dr", - "setUp", - "tearDown", -] - -[tool.pylint.design] -max-args = 8 -max-parents = 10 -max-positional-arguments = 8 -min-public-methods = 0 - -[tool.pylint.format] -max-module-lines = 1500 -max-line-length = 80 - -[tool.pylint.variables] -dummy-variables-rgx = "_$|dummy" - [tool.ruff] # ruff settings docs: https://docs.astral.sh/ruff/settings/ line-length = 80 From 3ab34ccb6f29b607a14491e3a23ac08f5d2350c2 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Wed, 18 Dec 2024 11:22:46 +0100 Subject: [PATCH 085/227] remove report processor side effects from conditions --- pcs/lib/commands/cluster.py | 15 +++++++++------ pcs/lib/commands/remote_node.py | 5 +++-- pcs/lib/commands/resource.py | 5 ++--- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/pcs/lib/commands/cluster.py b/pcs/lib/commands/cluster.py index 360c74dd6..cf135d1e9 100644 --- a/pcs/lib/commands/cluster.py +++ b/pcs/lib/commands/cluster.py @@ -124,7 +124,7 @@ def node_clear( raise LibraryError() if node_name in current_nodes: - if env.report_processor.report( + env.report_processor.report( ReportItem( severity=reports.item.get_severity( report_codes.FORCE, @@ -132,7 +132,8 @@ def node_clear( ), message=reports.messages.NodeToClearIsStillInCluster(node_name), ) - ).has_errors: + ) + if env.report_processor.has_errors: raise LibraryError() remove_node(env.cmd_runner(), node_name) @@ -1438,7 +1439,7 @@ def _start_cluster( communicator_factory.get_communicator(request_timeout=timeout), com_cmd ) if wait_timeout is not False: - if report_processor.report_list( + report_processor.report_list( _wait_for_pacemaker_to_start( communicator_factory.get_communicator(), report_processor, @@ -1446,7 +1447,8 @@ def _start_cluster( # wait_timeout is either None or a timeout timeout=wait_timeout, ) - ).has_errors: + ) + if report_processor.has_errors: raise LibraryError() @@ -2207,11 +2209,12 @@ def corosync_authkey_change( ) if not online_cluster_target_list: - if report_processor.report( + report_processor.report( ReportItem.error( reports.messages.UnableToPerformOperationOnAnyNode() ) - ).has_errors: + ) + if report_processor.has_errors: raise LibraryError() com_cmd = DistributeFilesWithoutForces( diff --git a/pcs/lib/commands/remote_node.py b/pcs/lib/commands/remote_node.py index 6e4a22edb..064f403f5 100644 --- a/pcs/lib/commands/remote_node.py +++ b/pcs/lib/commands/remote_node.py @@ -629,7 +629,7 @@ def _find_resources_to_remove( ) if len(resource_element_list) > 1: - if report_processor.report( + report_processor.report( ReportItem( severity=reports.item.get_severity( reports.codes.FORCE, @@ -644,7 +644,8 @@ def _find_resources_to_remove( node_identifier, ), ) - ).has_errors: + ) + if report_processor.has_errors: raise LibraryError() return resource_element_list diff --git a/pcs/lib/commands/resource.py b/pcs/lib/commands/resource.py index 9a8eb070b..4f485d18f 100644 --- a/pcs/lib/commands/resource.py +++ b/pcs/lib/commands/resource.py @@ -1641,9 +1641,8 @@ def group_add( # noqa: PLR0912 isinstance(report.message, reports.messages.CibPushError) for report in e.args ): - if env.report_processor.report_list( - empty_group_report_list - ).has_errors: + env.report_processor.report_list(empty_group_report_list) + if env.report_processor.has_errors: raise LibraryError() from None except AttributeError: # For accessing message inside something that's not a report From a24b36ac19befcbdc983cd6af3edc319fa9cf2e7 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Tue, 14 Jan 2025 14:07:18 +0100 Subject: [PATCH 086/227] ruff: update to 0.9.1 * https://astral.sh/blog/ruff-v0.9.0 * https://github.com/astral-sh/ruff/releases/tag/0.9.0 * https://github.com/astral-sh/ruff/releases/tag/0.9.1 * fix A005 https://docs.astral.sh/ruff/rules/stdlib-module-shadowing/ * fix A006 https://docs.astral.sh/ruff/rules/builtin-lambda-argument-shadowing/ * https://docs.astral.sh/ruff/formatter/#f-string-formatting * https://docs.astral.sh/ruff/formatter/#format-suppression --- dev_requirements.txt | 2 +- pcs/cli/dr.py | 3 +-- pcs/lib/corosync/config_validators.py | 6 +++--- pcs/lib/pacemaker/status.py | 3 +-- pcs/lib/validate.py | 2 +- pcs/resource.py | 2 +- pcs/stonith.py | 3 +-- .../tier0/common/reports/test_messages.py | 17 +++------------- .../lib/cib/resource/test_validations.py | 4 ++-- pcs_test/tier0/lib/cib/test_resource_set.py | 2 +- .../lib/commands/cluster/test_add_link.py | 4 ++-- .../resource/test_resource_move_autoclean.py | 8 ++++---- pcs_test/tier0/lib/test_cluster_property.py | 3 +-- .../tier1/cib_resource/test_clone_unclone.py | 2 +- pcs_test/tier1/cib_resource/test_create.py | 3 +-- pcs_test/tier1/legacy/test_constraints.py | 20 +++++++++---------- pcs_test/tier1/legacy/test_rule.py | 5 +---- pcs_test/tier1/legacy/test_utils.py | 3 +-- .../tools/bin_mock/pcmk/stonith_admin_mock.py | 2 +- pcs_test/tools/fixture.py | 6 +++--- pyproject.toml | 10 ++++++++++ 21 files changed, 50 insertions(+), 60 deletions(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index 1c10c4cab..65ace9e22 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,6 +1,6 @@ lxml-stubs mypy==1.13.0 -ruff==0.8.0 +ruff==0.9.1 types-cryptography types-dataclasses # later versions remove type annotations from a few functions causing diff --git a/pcs/cli/dr.py b/pcs/cli/dr.py index 834300acb..ffbce97f3 100644 --- a/pcs/cli/dr.py +++ b/pcs/cli/dr.py @@ -41,8 +41,7 @@ def config( dto.PayloadConversionError, ) as e: raise error( - "Unable to communicate with pcsd, received response:\n" - f"{config_raw}" + f"Unable to communicate with pcsd, received response:\n{config_raw}" ) from e lines = ["Local site:"] diff --git a/pcs/lib/corosync/config_validators.py b/pcs/lib/corosync/config_validators.py index f1b43cf02..15c94d05b 100644 --- a/pcs/lib/corosync/config_validators.py +++ b/pcs/lib/corosync/config_validators.py @@ -861,9 +861,9 @@ def _get_link_options_validators_knet( ) -def _get_link_options_validators_knet_relations() -> ( - list[validate.ValidatorInterface] -): +def _get_link_options_validators_knet_relations() -> list[ + validate.ValidatorInterface +]: return [ validate.DependsOnOption( ["ping_interval"], diff --git a/pcs/lib/pacemaker/status.py b/pcs/lib/pacemaker/status.py index 4beea62c2..4f609770e 100644 --- a/pcs/lib/pacemaker/status.py +++ b/pcs/lib/pacemaker/status.py @@ -116,8 +116,7 @@ def cluster_status_parsing_error_to_report( ) elif isinstance(e, UnknownPcmkRoleError): reason = ( - f"Resource '{e.resource_id}' contains an unknown " - f"role '{e.role}'" + f"Resource '{e.resource_id}' contains an unknown role '{e.role}'" ) elif isinstance(e, UnexpectedMemberError): reason = ( diff --git a/pcs/lib/validate.py b/pcs/lib/validate.py index 6835d182e..647d6a555 100644 --- a/pcs/lib/validate.py +++ b/pcs/lib/validate.py @@ -897,7 +897,7 @@ def _get_allowed_values(self) -> Any: return "an integer or integer-integer" return ( f"{self._at_least}..{self._at_most} or " - f"{self._at_least}..{self._at_most-1}-{self._at_least+1}..{self._at_most}" + f"{self._at_least}..{self._at_most - 1}-{self._at_least + 1}..{self._at_most}" ) diff --git a/pcs/resource.py b/pcs/resource.py index b1b7b83c6..362dff8bd 100644 --- a/pcs/resource.py +++ b/pcs/resource.py @@ -2710,7 +2710,7 @@ def operation_to_string(op_el): continue parts.append(name + "=" + value) parts.extend( - f'{nvpair.getAttribute("name")}={nvpair.getAttribute("value")}' + f"{nvpair.getAttribute('name')}={nvpair.getAttribute('value')}" for nvpair in op_el.getElementsByTagName("nvpair") ) parts.append("(" + op_el.getAttribute("id") + ")") diff --git a/pcs/stonith.py b/pcs/stonith.py index b6b037eea..9df9bd43a 100644 --- a/pcs/stonith.py +++ b/pcs/stonith.py @@ -87,8 +87,7 @@ def stonith_list_available( if search: utils.err("No stonith agents matching the filter.") utils.err( - "No stonith agents available. " - "Do you have fence agents installed?" + "No stonith agents available. Do you have fence agents installed?" ) for agent_info in agent_list: diff --git a/pcs_test/tier0/common/reports/test_messages.py b/pcs_test/tier0/common/reports/test_messages.py index 4a6ceb6ce..3411efd88 100644 --- a/pcs_test/tier0/common/reports/test_messages.py +++ b/pcs_test/tier0/common/reports/test_messages.py @@ -1357,10 +1357,7 @@ def test_more_addresses(self): class NodeAddressesCannotBeEmpty(NameBuildTest): def test_one_node(self): self.assert_message_from_report( - ( - "Empty address set for node 'node2', " - "an address cannot be empty" - ), + ("Empty address set for node 'node2', an address cannot be empty"), reports.NodeAddressesCannotBeEmpty(["node2"]), ) @@ -3807,11 +3804,7 @@ def test_multiple_element_types_with_multiple_ids(self): class CibRemoveReferences(NameBuildTest): def test_one_element_single_reference(self): self.assert_message_from_report( - ( - "Removing references:\n" - " Resource 'id1' from:\n" - " Tag: 'id2'" - ), + ("Removing references:\n Resource 'id1' from:\n Tag: 'id2'"), reports.CibRemoveReferences( {"id1": "primitive", "id2": "tag"}, {"id1": ["id2"]} ), @@ -3819,11 +3812,7 @@ def test_one_element_single_reference(self): def test_missing_tag_mapping(self): self.assert_message_from_report( - ( - "Removing references:\n" - " Element 'id1' from:\n" - " Element: 'id2'" - ), + ("Removing references:\n Element 'id1' from:\n Element: 'id2'"), reports.CibRemoveReferences({}, {"id1": ["id2"]}), ) diff --git a/pcs_test/tier0/lib/cib/resource/test_validations.py b/pcs_test/tier0/lib/cib/resource/test_validations.py index 75e4388d2..1b7c306ba 100644 --- a/pcs_test/tier0/lib/cib/resource/test_validations.py +++ b/pcs_test/tier0/lib/cib/resource/test_validations.py @@ -328,7 +328,7 @@ def _fixture_clone(promotable=False): - + """ @@ -343,7 +343,7 @@ def _fixture_group_clone(promotable=False): - + """ diff --git a/pcs_test/tier0/lib/cib/test_resource_set.py b/pcs_test/tier0/lib/cib/test_resource_set.py index 4c0fb863b..99351e04f 100644 --- a/pcs_test/tier0/lib/cib/test_resource_set.py +++ b/pcs_test/tier0/lib/cib/test_resource_set.py @@ -25,7 +25,7 @@ def setUp(self): def test_return_corrected_resource_set(self): find_valid_id = mock.Mock() - find_valid_id.side_effect = lambda id: {"A": "AA", "B": "BB"}[id] + find_valid_id.side_effect = lambda id_: {"A": "AA", "B": "BB"}[id_] self.assertEqual( {"ids": ["AA", "BB"], "options": {"sequential": "true"}}, resource_set.prepare_set( diff --git a/pcs_test/tier0/lib/commands/cluster/test_add_link.py b/pcs_test/tier0/lib/commands/cluster/test_add_link.py index 64ef33198..d259caf90 100644 --- a/pcs_test/tier0/lib/commands/cluster/test_add_link.py +++ b/pcs_test/tier0/lib/commands/cluster/test_add_link.py @@ -253,7 +253,7 @@ def test_cib_guest_node(self): @@ -289,7 +289,7 @@ def test_cib_remote_node(self): > diff --git a/pcs_test/tier0/lib/commands/resource/test_resource_move_autoclean.py b/pcs_test/tier0/lib/commands/resource/test_resource_move_autoclean.py index eb749039f..6d482296f 100644 --- a/pcs_test/tier0/lib/commands/resource/test_resource_move_autoclean.py +++ b/pcs_test/tier0/lib/commands/resource/test_resource_move_autoclean.py @@ -111,23 +111,23 @@ def _simulation_synapses_fixture(resource_id, start_id=0): return f""" - + - + - + - + diff --git a/pcs_test/tier0/lib/test_cluster_property.py b/pcs_test/tier0/lib/test_cluster_property.py index b1c0c28af..8ae581b6a 100644 --- a/pcs_test/tier0/lib/test_cluster_property.py +++ b/pcs_test/tier0/lib/test_cluster_property.py @@ -156,8 +156,7 @@ def _fixture_parameter(name, param_type, default, enum_values): option_name="percentage_param", option_value="20", allowed_values=( - "a non-negative integer followed by '%' (e.g. 0%, 50%, " - "200%, ...)" + "a non-negative integer followed by '%' (e.g. 0%, 50%, 200%, ...)" ), cannot_be_empty=False, forbidden_characters=None, diff --git a/pcs_test/tier1/cib_resource/test_clone_unclone.py b/pcs_test/tier1/cib_resource/test_clone_unclone.py index 9c54e8917..13bb1765a 100644 --- a/pcs_test/tier1/cib_resource/test_clone_unclone.py +++ b/pcs_test/tier1/cib_resource/test_clone_unclone.py @@ -191,7 +191,7 @@ def fixture_clone_stonith_msg(forced=False, group=False): ).format( severity="Warning" if forced else "Error", group="Group 'Group' contains stonith resource. " if group else "", - use_force="\n" if forced else ", use " "--force to override\n", + use_force="\n" if forced else ", use --force to override\n", ) diff --git a/pcs_test/tier1/cib_resource/test_create.py b/pcs_test/tier1/cib_resource/test_create.py index 01c8ead8a..241a0a569 100644 --- a/pcs_test/tier1/cib_resource/test_create.py +++ b/pcs_test/tier1/cib_resource/test_create.py @@ -964,8 +964,7 @@ def test_alias_for_clone(self, mock_print_to_stderr): def test_fail_on_promotable(self): self.assert_pcs_fail( ( - "resource create R ocf:pcsmock:stateful promotable " - "promotable=a" + "resource create R ocf:pcsmock:stateful promotable promotable=a" ).split(), ( self.msg_promotable_without_meta diff --git a/pcs_test/tier1/legacy/test_constraints.py b/pcs_test/tier1/legacy/test_constraints.py index 9a351b6b0..8c4bae2e3 100644 --- a/pcs_test/tier1/legacy/test_constraints.py +++ b/pcs_test/tier1/legacy/test_constraints.py @@ -5068,10 +5068,10 @@ def assert_in_effect_primitive(self, flag_all, flag_full): outdent( f"""\ Location Constraints: - resource 'dummy'{' (id: location-dummy)' if flag_full else ''} + resource 'dummy'{" (id: location-dummy)" if flag_full else ""} Rules: - Rule: score=INFINITY{' (id: test-rule)' if flag_full else ''} - Expression: date gt 2019-01-01{' (id: test-rule-expr)' if flag_full else ''} + Rule: score=INFINITY{" (id: test-rule)" if flag_full else ""} + Expression: date gt 2019-01-01{" (id: test-rule-expr)" if flag_full else ""} """ ), ) @@ -5183,11 +5183,11 @@ def assert_indeterminate_primitive(self, flag_full, flag_all): outdent( f"""\ Location Constraints: - resource 'dummy'{' (id: location-dummy)' if flag_full else ''} + resource 'dummy'{" (id: location-dummy)" if flag_full else ""} Rules: - Rule: boolean-op=or score=INFINITY{' (id: test-rule)' if flag_full else ''} - Expression: date eq 2019-01-01{' (id: test-rule-expr)' if flag_full else ''} - Expression: date eq 2019-03-01{' (id: test-rule-expr-1)' if flag_full else ''} + Rule: boolean-op=or score=INFINITY{" (id: test-rule)" if flag_full else ""} + Expression: date eq 2019-01-01{" (id: test-rule-expr)" if flag_full else ""} + Expression: date eq 2019-03-01{" (id: test-rule-expr-1)" if flag_full else ""} """ ), ) @@ -5241,10 +5241,10 @@ def assert_not_yet_in_effect_primitive(self, flag_full, flag_all): outdent( f"""\ Location Constraints: - resource 'dummy'{' (id: location-dummy)' if flag_full else ''} + resource 'dummy'{" (id: location-dummy)" if flag_full else ""} Rules: - Rule (not yet in effect): score=INFINITY{' (id: test-rule)' if flag_full else ''} - Expression: date gt {self._tomorrow}{' (id: test-rule-expr)' if flag_full else ''} + Rule (not yet in effect): score=INFINITY{" (id: test-rule)" if flag_full else ""} + Expression: date gt {self._tomorrow}{" (id: test-rule-expr)" if flag_full else ""} """ ), ) diff --git a/pcs_test/tier1/legacy/test_rule.py b/pcs_test/tier1/legacy/test_rule.py index 4578f2603..002c214a4 100644 --- a/pcs_test/tier1/legacy/test_rule.py +++ b/pcs_test/tier1/legacy/test_rule.py @@ -699,10 +699,7 @@ def testAndOrExpression(self): ), ) self.assertEqual( - "(and " - "(defined (literal pingd)) " - "(lte (literal pingd) (literal 1))" - ")", + "(and (defined (literal pingd)) (lte (literal pingd) (literal 1)))", str( self.parser.parse( ["defined", "pingd", "and", "pingd", "lte", "1"] diff --git a/pcs_test/tier1/legacy/test_utils.py b/pcs_test/tier1/legacy/test_utils.py index d6c621b56..078b6b615 100644 --- a/pcs_test/tier1/legacy/test_utils.py +++ b/pcs_test/tier1/legacy/test_utils.py @@ -961,8 +961,7 @@ def test_resource_running_on(self): self.assertEqual( utils.resource_running_on("myGroup", status), { - "message": "Resource 'myGroup' is running on node " - "rh70-node2.", + "message": "Resource 'myGroup' is running on node rh70-node2.", "is_running": True, }, ) diff --git a/pcs_test/tools/bin_mock/pcmk/stonith_admin_mock.py b/pcs_test/tools/bin_mock/pcmk/stonith_admin_mock.py index 133097b52..484162e90 100644 --- a/pcs_test/tools/bin_mock/pcmk/stonith_admin_mock.py +++ b/pcs_test/tools/bin_mock/pcmk/stonith_admin_mock.py @@ -45,7 +45,7 @@ def main(): {output} - + """ ) diff --git a/pcs_test/tools/fixture.py b/pcs_test/tools/fixture.py index 5fba4b840..9b61cb75e 100644 --- a/pcs_test/tools/fixture.py +++ b/pcs_test/tools/fixture.py @@ -255,9 +255,9 @@ def __delitem__(self, name: str) -> None: def __add__(self, other: "NameValueSequence") -> "NameValueSequence": my_name = type(self).__name__ other_name = type(other).__name__ - assert isinstance( - other, type(self) - ), f"Can only concatenate {my_name} with {my_name}, not {other_name}" + assert isinstance(other, type(self)), ( + f"Can only concatenate {my_name} with {my_name}, not {other_name}" + ) return type(self)( self.names + other.names, diff --git a/pyproject.toml b/pyproject.toml index 0a6c4345e..320dd0422 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,16 @@ ignore = [ "PERF203", # http://docs.astral.sh/ruff/rules/try-except-in-loop/ "SIM102", # https://docs.astral.sh/ruff/rules/collapsible-if/ ] +[tool.ruff.lint.flake8-builtins] +builtins-allowed-modules = [ + "json", + "logging", + "parser", + "resource", + "ssl", + "types", + "xml", +] [tool.ruff.lint.isort] # known deviations from isort: From ace166543c15545ebca7227f8cbb179e9beadbb6 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Tue, 7 Jan 2025 16:00:21 +0100 Subject: [PATCH 087/227] Allow to use custom Gemfile --- configure.ac | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/configure.ac b/configure.ac index d7dea450d..7e9819c1b 100644 --- a/configure.ac +++ b/configure.ac @@ -283,6 +283,10 @@ AC_ARG_WITH([snmp-mibs-dir], [SNMP_MIB_DIR="$prefix/share/snmp/mibs"]) AC_SUBST([SNMP_MIB_DIR]) +AC_ARG_WITH([custom-gemfile], + [AS_HELP_STRING([--with-custom-gemfile=PATH], [Use custom gemfile instead of autogenerated. Effective only with the local-build option. Default: empty])], + [PCS_CUSTOM_GEMFILE="$withval"]) + # python detection section PCS_BUNDLED_DIR_LOCAL="pcs_bundled" AC_SUBST([PCS_BUNDLED_DIR_LOCAL]) @@ -366,12 +370,26 @@ AC_SUBST([PCSD_BUNDLED_DIR_LOCAL]) AC_SUBST([PCSD_BUNDLED_CACHE_DIR]) rm -rf Gemfile Gemfile.lock -echo "source 'https://rubygems.org'" > Gemfile -echo "" >> Gemfile + +if test "x$local_build" = "xyes"; then + if test "x$PCS_CUSTOM_GEMFILE" != "x"; then + if ! test -e "$PCS_CUSTOM_GEMFILE"; then + AC_MSG_ERROR([custom gemfile '$PCS_CUSTOM_GEMFILE' does not exist]) + fi + cp "$PCS_CUSTOM_GEMFILE" Gemfile + else + echo "source 'https://rubygems.org'" > Gemfile + echo "" >> Gemfile + fi +fi # PCS_BUNDLE_GEM([module]) AC_DEFUN([PCS_BUNDLE_GEM], [ - echo "gem '$1'" >> Gemfile + if test "x$PCS_CUSTOM_GEMFILE" = "x"; then + echo "gem '$1'" >> Gemfile + else + grep "$1" Gemfile || AC_MSG_ERROR([custom gemfile missing required gem '$1']) + fi if test "x$cache_only" = "xyes"; then src=`ls $PCSD_BUNDLED_CACHE_DIR/$1-*` if test "x$src" = "x"; then From 25586b49f9ba962c539ea3714add75ae62b3dcc4 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Thu, 16 Jan 2025 11:06:17 +0100 Subject: [PATCH 088/227] fix validating integer values --- pcs/common/reports/messages.py | 9 ++- pcs/common/tools.py | 7 +- pcs/common/validate.py | 5 ++ pcs/lib/cib/resource/common.py | 4 +- pcs/lib/pacemaker/values.py | 3 +- pcs/resource.py | 2 +- pcs/rule.py | 6 +- pcs/utils.py | 8 +- pcs_test/Makefile.am | 1 + .../tier0/common/reports/test_messages.py | 16 +++- pcs_test/tier0/common/test_tools.py | 9 +++ pcs_test/tier0/common/test_validate.py | 75 +++++++++++++++++++ pcs_test/tier0/lib/test_validate.py | 72 ------------------ 13 files changed, 133 insertions(+), 84 deletions(-) create mode 100644 pcs_test/tier0/common/test_validate.py diff --git a/pcs/common/reports/messages.py b/pcs/common/reports/messages.py index 3c0eee4ba..4e6326fbf 100644 --- a/pcs/common/reports/messages.py +++ b/pcs/common/reports/messages.py @@ -111,7 +111,10 @@ def _format_booth_default(value: Optional[str], template: str) -> str: def _key_numeric(item: str) -> Tuple[int, str]: - return (int(item), item) if item.isdigit() else (-1, item) + try: + return int(item), item + except ValueError: + return -1, item _add_remove_container_translation = { @@ -1936,7 +1939,9 @@ class CorosyncLinkNumberDuplication(ReportItemMessage): @property def message(self) -> str: - nums = format_list(sorted(self.link_number_list, key=_key_numeric)) + nums = format_list_dont_sort( + sorted(self.link_number_list, key=_key_numeric) + ) return f"Link numbers must be unique, duplicate link numbers: {nums}" diff --git a/pcs/common/tools.py b/pcs/common/tools.py index 919f8b340..58bf5e8f2 100644 --- a/pcs/common/tools.py +++ b/pcs/common/tools.py @@ -15,6 +15,7 @@ from lxml.etree import _Element from pcs.common.types import StringCollection +from pcs.common.validate import is_integer T = TypeVar("T", bound=type) @@ -83,8 +84,10 @@ def timeout_to_seconds(timeout: Union[int, str]) -> Optional[int]: "hr": 3600, } for suffix, multiplier in suffix_multiplier.items(): - if timeout.endswith(suffix) and timeout[: -len(suffix)].isdigit(): - return int(timeout[: -len(suffix)]) * multiplier + if timeout.endswith(suffix): + candidate2 = timeout[: -len(suffix)] + if is_integer(candidate2, at_least=0): + return int(candidate2) * multiplier return None diff --git a/pcs/common/validate.py b/pcs/common/validate.py index 4c695fd7a..9205d9220 100644 --- a/pcs/common/validate.py +++ b/pcs/common/validate.py @@ -19,6 +19,11 @@ def is_integer( at_least -- minimal allowed value at_most -- maximal allowed value """ + # Using str.isnumeric(), str.isdigit() or str.isdecimal() is not good + # enough, as they return True for unicode characters which cannot be + # processed by int() and turned to an integer. + # Using int() to check a string is not enough, because it allows whitespace + # in the value. try: if value is None or isinstance(value, float): return False diff --git a/pcs/lib/cib/resource/common.py b/pcs/lib/cib/resource/common.py index 5acaf3405..b8905f238 100644 --- a/pcs/lib/cib/resource/common.py +++ b/pcs/lib/cib/resource/common.py @@ -34,8 +34,8 @@ def are_meta_disabled(meta_attributes: Mapping[str, str]) -> bool: def _can_be_evaluated_as_positive_num(value: str) -> bool: string_wo_leading_zeros = str(value).lstrip("0") - return ( - bool(string_wo_leading_zeros) and string_wo_leading_zeros[0].isdigit() + return bool(string_wo_leading_zeros) and ( + string_wo_leading_zeros[0] in list("123456789") ) diff --git a/pcs/lib/pacemaker/values.py b/pcs/lib/pacemaker/values.py index a289d084d..087970d41 100644 --- a/pcs/lib/pacemaker/values.py +++ b/pcs/lib/pacemaker/values.py @@ -12,6 +12,7 @@ ReportItemList, ) from pcs.common.tools import timeout_to_seconds +from pcs.common.validate import is_integer from pcs.lib.errors import LibraryError from pcs.lib.external import CommandRunner @@ -60,7 +61,7 @@ def is_score(value: str) -> bool: if not value: return False unsigned_value = value[1:] if value[0] in ("+", "-") else value - return unsigned_value == SCORE_INFINITY or unsigned_value.isdigit() + return unsigned_value == SCORE_INFINITY or is_integer(value) def is_duration(runner: CommandRunner, value: str) -> bool: diff --git a/pcs/resource.py b/pcs/resource.py index 362dff8bd..e07ad4f85 100644 --- a/pcs/resource.py +++ b/pcs/resource.py @@ -818,7 +818,7 @@ def _parse_resource_move_ban( if lifetime: raise CmdLineInputError() lifetime = arg.split("=")[1] - if lifetime and lifetime[0].isdigit(): + if lifetime and lifetime[0] in list("0123456789"): lifetime = "P" + lifetime elif not node: node = arg diff --git a/pcs/rule.py b/pcs/rule.py index 810e87208..59fa0bde0 100644 --- a/pcs/rule.py +++ b/pcs/rule.py @@ -752,9 +752,13 @@ class DateDurationValue(DateCommonValue): def __init__(self, parts_string): super().__init__(parts_string, self.KEYWORD) + @staticmethod + def __isnum(value): + return all(char in list("0123456789") for char in value) + def validate(self): for name, value in self.parts.items(): - if not value.isdigit(): + if not self.__isnum(value): raise SyntaxError( "invalid %s '%s' in '%s'" % (name, value, DateDurationValue.KEYWORD) diff --git a/pcs/utils.py b/pcs/utils.py index 771727579..ebb1dd9a5 100644 --- a/pcs/utils.py +++ b/pcs/utils.py @@ -1542,6 +1542,10 @@ def get_resource_for_running_check(cluster_state, resource_id, stopped=False): """ Commandline options: no options """ + + def _isnum(value): + return all(char in list("0123456789") for char in value) + # pylint: disable=too-many-nested-blocks for clone in cluster_state.getElementsByTagName("clone"): if clone.getAttribute("id") == resource_id: @@ -1551,10 +1555,10 @@ def get_resource_for_running_check(cluster_state, resource_id, stopped=False): "group", ]: resource_id = child.getAttribute("id") - # in a clone a resource can have an id of ':N' + # in a clone, a resource can have an id of ':N' if ":" in resource_id: parts = resource_id.rsplit(":", 1) - if parts[1].isdigit(): + if _isnum(parts[1]): resource_id = parts[0] break for group in cluster_state.getElementsByTagName("group"): diff --git a/pcs_test/Makefile.am b/pcs_test/Makefile.am index cd9d32ebc..9cc0d3310 100644 --- a/pcs_test/Makefile.am +++ b/pcs_test/Makefile.am @@ -140,6 +140,7 @@ EXTRA_DIST = \ tier0/common/test_str_tools.py \ tier0/common/test_tools.py \ tier0/common/test_tools_xml_fromstring.py \ + tier0/common/test_validate.py \ tier0/daemon/app/fixtures_app.py \ tier0/daemon/app/__init__.py \ tier0/daemon/app/test_api_v0.py \ diff --git a/pcs_test/tier0/common/reports/test_messages.py b/pcs_test/tier0/common/reports/test_messages.py index 3411efd88..8142d9003 100644 --- a/pcs_test/tier0/common/reports/test_messages.py +++ b/pcs_test/tier0/common/reports/test_messages.py @@ -1311,12 +1311,26 @@ def test_with_links(self): class CorosyncLinkNumberDuplication(NameBuildTest): + _template = "Link numbers must be unique, duplicate link numbers: {values}" + def test_message(self): self.assert_message_from_report( - "Link numbers must be unique, duplicate link numbers: '1', '3'", + self._template.format(values="'1', '3'"), reports.CorosyncLinkNumberDuplication(["1", "3"]), ) + def test_sort(self): + self.assert_message_from_report( + self._template.format(values="'1', '3'"), + reports.CorosyncLinkNumberDuplication(["3", "1"]), + ) + + def test_sort_not_int(self): + self.assert_message_from_report( + self._template.format(values="'-5', 'x3', '1', '3'"), + reports.CorosyncLinkNumberDuplication(["3", "1", "x3", "-5"]), + ) + class CorosyncNodeAddressCountMismatch(NameBuildTest): def test_message(self): diff --git a/pcs_test/tier0/common/test_tools.py b/pcs_test/tier0/common/test_tools.py index 816466431..1ba2fe77e 100644 --- a/pcs_test/tier0/common/test_tools.py +++ b/pcs_test/tier0/common/test_tools.py @@ -126,9 +126,18 @@ def test_valid(self): self.assertEqual(36000, tools.timeout_to_seconds("10h")) self.assertEqual(36000, tools.timeout_to_seconds("10hr")) + # not 100% sure if these should be considered valid, but it looks like + # pacemaker accepts them and they have always been considered valid by + # pcs + self.assertEqual(10, tools.timeout_to_seconds("+10")) + self.assertEqual(600, tools.timeout_to_seconds("+10min")) + def test_invalid(self): self.assertEqual(None, tools.timeout_to_seconds(-10)) + self.assertEqual(None, tools.timeout_to_seconds("-10")) + self.assertEqual(None, tools.timeout_to_seconds("-10min")) self.assertEqual(None, tools.timeout_to_seconds("1a1s")) + self.assertEqual(None, tools.timeout_to_seconds("1min10s")) self.assertEqual(None, tools.timeout_to_seconds("10mm")) self.assertEqual(None, tools.timeout_to_seconds("10mim")) self.assertEqual(None, tools.timeout_to_seconds("aaa")) diff --git a/pcs_test/tier0/common/test_validate.py b/pcs_test/tier0/common/test_validate.py new file mode 100644 index 000000000..c68885745 --- /dev/null +++ b/pcs_test/tier0/common/test_validate.py @@ -0,0 +1,75 @@ +from unittest import TestCase + +from pcs.common import validate + + +class IsInteger(TestCase): + def test_no_range(self): + self.assertTrue(validate.is_integer(1)) + self.assertTrue(validate.is_integer("1")) + self.assertTrue(validate.is_integer(-1)) + self.assertTrue(validate.is_integer("-1")) + self.assertTrue(validate.is_integer(+1)) + self.assertTrue(validate.is_integer("+1")) + + self.assertFalse(validate.is_integer(" 1")) + self.assertFalse(validate.is_integer("\n-1")) + self.assertFalse(validate.is_integer("\r+1")) + self.assertFalse(validate.is_integer("1\n")) + self.assertFalse(validate.is_integer("-1 ")) + self.assertFalse(validate.is_integer("+1\r")) + + self.assertFalse(validate.is_integer("")) + self.assertFalse(validate.is_integer("1a")) + self.assertFalse(validate.is_integer("a1")) + self.assertFalse(validate.is_integer("aaa")) + self.assertFalse(validate.is_integer(1.0)) + self.assertFalse(validate.is_integer("1.0")) + + def test_at_least(self): + self.assertTrue(validate.is_integer(5, 5)) + self.assertTrue(validate.is_integer(5, 4)) + self.assertTrue(validate.is_integer("5", 5)) + self.assertTrue(validate.is_integer("5", 4)) + + self.assertFalse(validate.is_integer(5, 6)) + self.assertFalse(validate.is_integer("5", 6)) + + def test_at_most(self): + self.assertTrue(validate.is_integer(5, None, 5)) + self.assertTrue(validate.is_integer(5, None, 6)) + self.assertTrue(validate.is_integer("5", None, 5)) + self.assertTrue(validate.is_integer("5", None, 6)) + + self.assertFalse(validate.is_integer(5, None, 4)) + self.assertFalse(validate.is_integer("5", None, 4)) + + def test_range(self): + self.assertTrue(validate.is_integer(5, 5, 5)) + self.assertTrue(validate.is_integer(5, 4, 6)) + self.assertTrue(validate.is_integer("5", 5, 5)) + self.assertTrue(validate.is_integer("5", 4, 6)) + + self.assertFalse(validate.is_integer(3, 4, 6)) + self.assertFalse(validate.is_integer(7, 4, 6)) + self.assertFalse(validate.is_integer("3", 4, 6)) + self.assertFalse(validate.is_integer("7", 4, 6)) + + +class IsPortNumber(TestCase): + def test_valid_port(self): + self.assertTrue(validate.is_port_number(1)) + self.assertTrue(validate.is_port_number("1")) + self.assertTrue(validate.is_port_number(65535)) + self.assertTrue(validate.is_port_number("65535")) + self.assertTrue(validate.is_port_number(8192)) + + def test_bad_port(self): + self.assertFalse(validate.is_port_number(0)) + self.assertFalse(validate.is_port_number("0")) + self.assertFalse(validate.is_port_number(65536)) + self.assertFalse(validate.is_port_number("65536")) + self.assertFalse(validate.is_port_number(" 8192 ")) + self.assertFalse(validate.is_port_number(-128)) + self.assertFalse(validate.is_port_number("-128")) + self.assertFalse(validate.is_port_number("abcd")) diff --git a/pcs_test/tier0/lib/test_validate.py b/pcs_test/tier0/lib/test_validate.py index 4f636c5b6..48c0b89eb 100644 --- a/pcs_test/tier0/lib/test_validate.py +++ b/pcs_test/tier0/lib/test_validate.py @@ -1876,59 +1876,6 @@ def test_success(self): self.assertFalse(validate.is_float("aaa")) -class IsInteger(TestCase): - def test_no_range(self): - self.assertTrue(validate.is_integer(1)) - self.assertTrue(validate.is_integer("1")) - self.assertTrue(validate.is_integer(-1)) - self.assertTrue(validate.is_integer("-1")) - self.assertTrue(validate.is_integer(+1)) - self.assertTrue(validate.is_integer("+1")) - - self.assertFalse(validate.is_integer(" 1")) - self.assertFalse(validate.is_integer("\n-1")) - self.assertFalse(validate.is_integer("\r+1")) - self.assertFalse(validate.is_integer("1\n")) - self.assertFalse(validate.is_integer("-1 ")) - self.assertFalse(validate.is_integer("+1\r")) - - self.assertFalse(validate.is_integer("")) - self.assertFalse(validate.is_integer("1a")) - self.assertFalse(validate.is_integer("a1")) - self.assertFalse(validate.is_integer("aaa")) - self.assertFalse(validate.is_integer(1.0)) - self.assertFalse(validate.is_integer("1.0")) - - def test_at_least(self): - self.assertTrue(validate.is_integer(5, 5)) - self.assertTrue(validate.is_integer(5, 4)) - self.assertTrue(validate.is_integer("5", 5)) - self.assertTrue(validate.is_integer("5", 4)) - - self.assertFalse(validate.is_integer(5, 6)) - self.assertFalse(validate.is_integer("5", 6)) - - def test_at_most(self): - self.assertTrue(validate.is_integer(5, None, 5)) - self.assertTrue(validate.is_integer(5, None, 6)) - self.assertTrue(validate.is_integer("5", None, 5)) - self.assertTrue(validate.is_integer("5", None, 6)) - - self.assertFalse(validate.is_integer(5, None, 4)) - self.assertFalse(validate.is_integer("5", None, 4)) - - def test_range(self): - self.assertTrue(validate.is_integer(5, 5, 5)) - self.assertTrue(validate.is_integer(5, 4, 6)) - self.assertTrue(validate.is_integer("5", 5, 5)) - self.assertTrue(validate.is_integer("5", 4, 6)) - - self.assertFalse(validate.is_integer(3, 4, 6)) - self.assertFalse(validate.is_integer(7, 4, 6)) - self.assertFalse(validate.is_integer("3", 4, 6)) - self.assertFalse(validate.is_integer("7", 4, 6)) - - class IsIpv4Address(TestCase): def test_valid(self): self.assertTrue(validate.is_ipv4_address("192.168.1.1")) @@ -1979,25 +1926,6 @@ def test_bad_limits(self): self.assertFalse(validate.is_pcmk_datespec_part("15-25", 10, 20)) -class IsPortNumber(TestCase): - def test_valid_port(self): - self.assertTrue(validate.is_port_number(1)) - self.assertTrue(validate.is_port_number("1")) - self.assertTrue(validate.is_port_number(65535)) - self.assertTrue(validate.is_port_number("65535")) - self.assertTrue(validate.is_port_number(8192)) - - def test_bad_port(self): - self.assertFalse(validate.is_port_number(0)) - self.assertFalse(validate.is_port_number("0")) - self.assertFalse(validate.is_port_number(65536)) - self.assertFalse(validate.is_port_number("65536")) - self.assertFalse(validate.is_port_number(" 8192 ")) - self.assertFalse(validate.is_port_number(-128)) - self.assertFalse(validate.is_port_number("-128")) - self.assertFalse(validate.is_port_number("abcd")) - - class MatchesRegexp(TestCase): def test_matches_string(self): self.assertTrue(validate.matches_regexp("abcdcba", "^[a-d]+$")) From 5c34ba7dbd2e70489b0a956ea8ccf03a6440463c Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Thu, 5 Dec 2024 12:20:35 +0100 Subject: [PATCH 089/227] replace deprecated function find_unique_id --- pcs/lib/cib/acl.py | 67 +++--- pcs/lib/commands/acl.py | 56 ++++- pcs_test/tier0/lib/cib/test_acl.py | 263 +++++++++++------------- pcs_test/tier0/lib/commands/test_acl.py | 254 +++++++++++++++++++---- pcs_test/tier1/legacy/test_acl.py | 26 ++- 5 files changed, 449 insertions(+), 217 deletions(-) diff --git a/pcs/lib/cib/acl.py b/pcs/lib/cib/acl.py index 8a982998f..53c4658d7 100644 --- a/pcs/lib/cib/acl.py +++ b/pcs/lib/cib/acl.py @@ -1,13 +1,16 @@ from functools import partial +from typing import Optional from lxml import etree +from lxml.etree import _Element from pcs.common import reports +from pcs.lib import validate from pcs.lib.cib.tools import ( + IdProvider, check_new_id_applicable, does_id_exist, find_element_by_tag_and_id, - find_unique_id, ) from pcs.lib.errors import LibraryError @@ -16,11 +19,14 @@ TAG_TARGET = "acl_target" TAG_PERMISSION = "acl_permission" +PermissionInfoList = list[tuple[str, str, str]] -def validate_permissions(tree, permission_info_list): + +def validate_permissions( + tree: _Element, permission_info_list: PermissionInfoList +) -> reports.ReportItemList: """ - Validate given permission list. - Raise LibraryError if any of permission is not valid. + Validate given permission list tree -- cib tree permission_info_list -- list of tuples like this: @@ -55,8 +61,7 @@ def validate_permissions(tree, permission_info_list): ) ) - if report_items: - raise LibraryError(*report_items) + return report_items def _find(tag, acl_section, element_id, none_if_id_unused=False, id_types=None): @@ -104,15 +109,33 @@ def find_target_or_group(acl_section, target_or_group_id): ) -def create_role(acl_section, role_id, description=None): +def validate_create_role( + id_provider: IdProvider, role_id: str, description: Optional[str] = None +) -> reports.ReportItemList: + """ + Validate creating a new role + + id_provider -- id provider + role_id -- id of desired role + description -- role description """ - Create new role element and add it to cib. - Returns newly created role element. + del description + validators = [ + validate.ValueId("role id", "ACL role", id_provider), + ] + return validate.ValidatorAll(validators).validate({"role id": role_id}) + - role_id id of desired role - description role description +def create_role( + acl_section: _Element, role_id: str, description: Optional[str] = None +) -> _Element: + """ + Create new role element, add it to cib and return it + + acl_section -- parent element for the new role + role_id -- id of desired role + description -- role description """ - check_new_id_applicable(acl_section, "ACL role", role_id) role = etree.SubElement(acl_section, TAG_ROLE, id=role_id) if description: role.set("description", description) @@ -212,15 +235,6 @@ def unassign_role(target_el, role_id, autodelete_target=False): target_el.getparent().remove(target_el) -def provide_role(acl_section, role_id): - """ - Returns role with id role_id. If doesn't exist, it will be created. - role_id id of desired role - """ - role = find_role(acl_section, role_id, none_if_id_unused=True) - return role if role is not None else create_role(acl_section, role_id) - - def create_target(acl_section, target_id): """ Creates new acl_target element with id target_id. @@ -278,13 +292,18 @@ def remove_group(acl_section, group_id): group.getparent().remove(group) -def add_permissions_to_role(role_el, permission_info_list): +def add_permissions_to_role( + role_el: _Element, + permission_info_list: PermissionInfoList, + id_provider: IdProvider, +) -> None: """ Add permissions from permission_info_list to role_el. role_el -- acl_role element to which permissions should be added permission_info_list -- list of tuples, each contains (permission, scope_type, scope) + id_provider -- id provider """ area_type_attribute_map = { "xpath": "xpath", @@ -294,8 +313,8 @@ def add_permissions_to_role(role_el, permission_info_list): perm = etree.SubElement(role_el, "acl_permission") perm.set( "id", - find_unique_id( - role_el, "{0}-{1}".format(role_el.get("id", "role"), permission) + id_provider.allocate_id( + "{0}-{1}".format(role_el.get("id", "role"), permission) ), ) perm.set("kind", permission) diff --git a/pcs/lib/commands/acl.py b/pcs/lib/commands/acl.py index 88c685e13..d992acbe9 100644 --- a/pcs/lib/commands/acl.py +++ b/pcs/lib/commands/acl.py @@ -1,7 +1,16 @@ from contextlib import contextmanager +from typing import TYPE_CHECKING from pcs.lib.cib import acl -from pcs.lib.cib.tools import get_acls +from pcs.lib.cib.tools import ( + IdProvider, + get_acls, +) +from pcs.lib.env import LibraryEnvironment +from pcs.lib.errors import LibraryError + +if TYPE_CHECKING: + from pcs.common import reports @contextmanager @@ -10,7 +19,12 @@ def cib_acl_section(env): env.push_cib() -def create_role(lib_env, role_id, permission_info_list, description): +def create_role( + lib_env: LibraryEnvironment, + role_id: str, + permission_info_list: acl.PermissionInfoList, + description: str, +) -> None: """ Create new acl role. Raises LibraryError on any failure. @@ -22,11 +36,22 @@ def create_role(lib_env, role_id, permission_info_list, description): description -- text description for role """ with cib_acl_section(lib_env) as acl_section: + id_provider = IdProvider(acl_section) + report_list = acl.validate_create_role( + id_provider, role_id, description + ) if permission_info_list: - acl.validate_permissions(acl_section, permission_info_list) + report_list += acl.validate_permissions( + acl_section, permission_info_list + ) + if lib_env.report_processor.report_list(report_list).has_errors: + raise LibraryError() + role_el = acl.create_role(acl_section, role_id, description) if permission_info_list: - acl.add_permissions_to_role(role_el, permission_info_list) + acl.add_permissions_to_role( + role_el, permission_info_list, id_provider + ) def remove_role(lib_env, role_id, autodelete_users_groups=False): @@ -219,7 +244,11 @@ def remove_group(lib_env, group_id): acl.remove_group(acl_section, group_id) -def add_permission(lib_env, role_id, permission_info_list): +def add_permission( + lib_env: LibraryEnvironment, + role_id: str, + permission_info_list: acl.PermissionInfoList, +) -> None: """ Add permissions to a role with id role_id. If role doesn't exist it will be created. @@ -231,10 +260,21 @@ def add_permission(lib_env, role_id, permission_info_list): (, , ) """ with cib_acl_section(lib_env) as acl_section: - acl.validate_permissions(acl_section, permission_info_list) - acl.add_permissions_to_role( - acl.provide_role(acl_section, role_id), permission_info_list + report_list: reports.ReportItemList = [] + id_provider = IdProvider(acl_section) + + role_el = acl.find_role(acl_section, role_id, none_if_id_unused=True) + if role_el is None: + report_list += acl.validate_create_role(id_provider, role_id) + report_list += acl.validate_permissions( + acl_section, permission_info_list ) + if lib_env.report_processor.report_list(report_list).has_errors: + raise LibraryError() + + if role_el is None: + role_el = acl.create_role(acl_section, role_id) + acl.add_permissions_to_role(role_el, permission_info_list, id_provider) def remove_permission(lib_env, permission_id): diff --git a/pcs_test/tier0/lib/cib/test_acl.py b/pcs_test/tier0/lib/cib/test_acl.py index 92847368b..20c8bd748 100644 --- a/pcs_test/tier0/lib/cib/test_acl.py +++ b/pcs_test/tier0/lib/cib/test_acl.py @@ -8,12 +8,16 @@ from pcs.common.reports import ReportItemSeverity as severities from pcs.common.reports import codes as report_codes from pcs.lib.cib import acl as lib -from pcs.lib.cib.tools import get_acls +from pcs.lib.cib.tools import ( + IdProvider, + get_acls, +) from pcs.lib.errors import LibraryError from pcs_test.tools.assertions import ( ExtendedAssertionsMixin, assert_raise_library_error, + assert_report_item_list_equal, assert_xml_equal, ) from pcs_test.tools.misc import get_test_resource as rc @@ -74,32 +78,34 @@ def test_unknown_permission(self): ("write", "xpath", "my xpath"), ("allow", "xpath", "xpath"), ] - assert_raise_library_error( - lambda: lib.validate_permissions(self.tree, permissions), - ( - severities.ERROR, - report_codes.INVALID_OPTION_VALUE, - { - "option_value": "unknown", - "option_name": "permission", - "allowed_values": self.allowed_permissions, - "cannot_be_empty": False, - "forbidden_characters": None, - }, - None, - ), - ( - severities.ERROR, - report_codes.INVALID_OPTION_VALUE, - { - "option_value": "allow", - "option_name": "permission", - "allowed_values": self.allowed_permissions, - "cannot_be_empty": False, - "forbidden_characters": None, - }, - None, - ), + assert_report_item_list_equal( + lib.validate_permissions(self.tree, permissions), + [ + ( + severities.ERROR, + report_codes.INVALID_OPTION_VALUE, + { + "option_value": "unknown", + "option_name": "permission", + "allowed_values": self.allowed_permissions, + "cannot_be_empty": False, + "forbidden_characters": None, + }, + None, + ), + ( + severities.ERROR, + report_codes.INVALID_OPTION_VALUE, + { + "option_value": "allow", + "option_name": "permission", + "allowed_values": self.allowed_permissions, + "cannot_be_empty": False, + "forbidden_characters": None, + }, + None, + ), + ], ) def test_unknown_scope(self): @@ -109,32 +115,34 @@ def test_unknown_scope(self): ("deny", "not_xpath", "some xpath"), ("read", "xpath", "xpath"), ] - assert_raise_library_error( - lambda: lib.validate_permissions(self.tree, permissions), - ( - severities.ERROR, - report_codes.INVALID_OPTION_VALUE, - { - "option_value": "not_id", - "option_name": "scope type", - "allowed_values": self.allowed_scopes, - "cannot_be_empty": False, - "forbidden_characters": None, - }, - None, - ), - ( - severities.ERROR, - report_codes.INVALID_OPTION_VALUE, - { - "option_value": "not_xpath", - "option_name": "scope type", - "allowed_values": self.allowed_scopes, - "cannot_be_empty": False, - "forbidden_characters": None, - }, - None, - ), + assert_report_item_list_equal( + lib.validate_permissions(self.tree, permissions), + [ + ( + severities.ERROR, + report_codes.INVALID_OPTION_VALUE, + { + "option_value": "not_id", + "option_name": "scope type", + "allowed_values": self.allowed_scopes, + "cannot_be_empty": False, + "forbidden_characters": None, + }, + None, + ), + ( + severities.ERROR, + report_codes.INVALID_OPTION_VALUE, + { + "option_value": "not_xpath", + "option_name": "scope type", + "allowed_values": self.allowed_scopes, + "cannot_be_empty": False, + "forbidden_characters": None, + }, + None, + ), + ], ) def test_not_existing_id(self): @@ -144,30 +152,67 @@ def test_not_existing_id(self): ("deny", "id", "last"), ("write", "xpath", "maybe xpath"), ] - assert_raise_library_error( - lambda: lib.validate_permissions(self.tree, permissions), - ( - severities.ERROR, - report_codes.ID_NOT_FOUND, - { - "id": "id", - "expected_types": ["id"], - "context_type": "", - "context_id": "", - }, - None, - ), - ( - severities.ERROR, - report_codes.ID_NOT_FOUND, - { - "id": "last", - "expected_types": ["id"], - "context_type": "", - "context_id": "", - }, - None, - ), + assert_report_item_list_equal( + lib.validate_permissions(self.tree, permissions), + [ + ( + severities.ERROR, + report_codes.ID_NOT_FOUND, + { + "id": "id", + "expected_types": ["id"], + "context_type": "", + "context_id": "", + }, + None, + ), + ( + severities.ERROR, + report_codes.ID_NOT_FOUND, + { + "id": "last", + "expected_types": ["id"], + "context_type": "", + "context_id": "", + }, + None, + ), + ], + ) + + +class ValidateCreateRole(LibraryAclTest): + def test_refuse_invalid_id(self): + assert_report_item_list_equal( + lib.validate_create_role(IdProvider(self.cib.tree), "#invalid"), + [ + ( + severities.ERROR, + report_codes.INVALID_ID_BAD_CHAR, + { + "id": "#invalid", + "id_description": "ACL role", + "invalid_character": "#", + "is_first_char": True, + }, + ), + ], + ) + + def test_refuse_existing_non_role_id(self): + self.cib.append_to_first_tag_name( + "nodes", '' + ) + + assert_report_item_list_equal( + lib.validate_create_role(IdProvider(self.cib.tree), "node-id"), + [ + ( + severities.ERROR, + report_codes.ID_ALREADY_EXISTS, + {"id": "node-id"}, + ), + ], ) @@ -183,35 +228,6 @@ def test_create_for_new_role_id(self): ) ) - def test_refuse_invalid_id(self): - assert_raise_library_error( - lambda: lib.create_role(self.cib.tree, "#invalid"), - ( - severities.ERROR, - report_codes.INVALID_ID_BAD_CHAR, - { - "id": "#invalid", - "id_description": "ACL role", - "invalid_character": "#", - "is_first_char": True, - }, - ), - ) - - def test_refuse_existing_non_role_id(self): - self.cib.append_to_first_tag_name( - "nodes", '' - ) - - assert_raise_library_error( - lambda: lib.create_role(self.cib.tree, "node-id"), - ( - severities.ERROR, - report_codes.ID_ALREADY_EXISTS, - {"id": "node-id"}, - ), - ) - class RemoveRoleTest(LibraryAclTest, ExtendedAssertionsMixin): def setUp(self): @@ -508,6 +524,7 @@ def test_add_for_correct_permissions(self): lib.add_permissions_to_role( self.cib.tree.find(".//acl_role[@id='{0}']".format(role_id)), [("read", "xpath", "/whatever")], + IdProvider(self.cib.tree), ) self.assert_cib_equal( @@ -524,40 +541,6 @@ def test_add_for_correct_permissions(self): ) -class ProvideRoleTest(LibraryAclTest): - def test_add_role_for_nonexisting_id(self): - role_id = "new-id" - lib.provide_role(self.acls, role_id) - - self.assert_cib_equal( - self.create_cib().append_to_first_tag_name( - "configuration", - """ - - - - """.format(role_id), - ) - ) - - def test_add_role_for_nonexisting_role_id(self): - self.fixture_add_role("role1") - - role_id = "role1" - lib.provide_role(self.cib.tree, role_id) - - self.assert_cib_equal( - self.create_cib().append_to_first_tag_name( - "configuration", - """ - - - - """.format(role_id), - ) - ) - - class CreateTargetTest(LibraryAclTest): def setUp(self): LibraryAclTest.setUp(self) diff --git a/pcs_test/tier0/lib/commands/test_acl.py b/pcs_test/tier0/lib/commands/test_acl.py index ecd9ac228..2427ccaee 100644 --- a/pcs_test/tier0/lib/commands/test_acl.py +++ b/pcs_test/tier0/lib/commands/test_acl.py @@ -4,9 +4,12 @@ ) import pcs.lib.commands.acl as cmd_acl +from pcs.common import reports from pcs.lib.env import LibraryEnvironment +from pcs_test.tools import fixture from pcs_test.tools.assertions import ExtendedAssertionsMixin +from pcs_test.tools.command_env import get_env_tools from pcs_test.tools.custom_mock import MockLibraryReportProcessor @@ -51,31 +54,103 @@ def run(): env.push_cib.assert_not_called() -@mock.patch("pcs.lib.commands.acl.get_acls", mock.Mock(side_effect=lambda x: x)) -@mock.patch("pcs.lib.cib.acl.validate_permissions") -@mock.patch("pcs.lib.cib.acl.create_role") -@mock.patch("pcs.lib.cib.acl.add_permissions_to_role") -class CreateRoleTest(AclCommandsTest): - def test_success(self, mock_add_perm, mock_create_role, mock_validate): - perm_list = ["my", "list"] - mock_create_role.return_value = "role el" - cmd_acl.create_role(self.mock_env, "role_id", perm_list, "desc") - self.assert_get_cib_called() - mock_validate.assert_called_once_with(self.cib, perm_list) - mock_create_role.assert_called_once_with(self.cib, "role_id", "desc") - mock_add_perm.assert_called_once_with("role el", perm_list) - self.assert_same_cib_pushed() +class CreateRoleTest(TestCase): + def setUp(self): + self.env_assist, self.config = get_env_tools(self) + self.config.runner.cib.load( + resources=""" + + + + + """ + ) - def test_no_permission( - self, mock_add_perm, mock_create_role, mock_validate - ): - mock_create_role.return_value = "role el" - cmd_acl.create_role(self.mock_env, "role_id", [], "desc") - self.assert_get_cib_called() - self.assertEqual(0, mock_validate.call_count) - mock_create_role.assert_called_once_with(self.cib, "role_id", "desc") - self.assertEqual(0, mock_add_perm.call_count) - self.assert_same_cib_pushed() + def test_success_just_role(self): + acl_xml = """ + + + + """ + self.config.env.push_cib(optional_in_conf=acl_xml) + + cmd_acl.create_role(self.env_assist.get_env(), "role_id", [], "desc") + + def test_success_with_permissions(self): + acl_xml = """ + + + + + + + """ + self.config.env.push_cib(optional_in_conf=acl_xml) + + cmd_acl.create_role( + self.env_assist.get_env(), + "role_id", + [("write", "id", "R1"), ("read", "xpath", "./")], + "", + ) + + def test_id_already_used(self): + self.env_assist.assert_raise_library_error( + lambda: cmd_acl.create_role(self.env_assist.get_env(), "R1", [], "") + ) + self.env_assist.assert_reports( + [ + fixture.error(reports.codes.ID_ALREADY_EXISTS, id="R1"), + ] + ) + + def test_validation(self): + self.env_assist.assert_raise_library_error( + lambda: cmd_acl.create_role( + self.env_assist.get_env(), + "#role_id", + [ + ("change", "id", "R1"), + ("read", "path", "./"), + ("deny", "id", "R3"), + ], + "", + ) + ) + self.env_assist.assert_reports( + [ + fixture.error( + reports.codes.INVALID_ID_BAD_CHAR, + id="#role_id", + id_description="ACL role", + invalid_character="#", + is_first_char=True, + ), + fixture.error( + reports.codes.INVALID_OPTION_VALUE, + option_name="permission", + option_value="change", + allowed_values=["read", "write", "deny"], + cannot_be_empty=False, + forbidden_characters=None, + ), + fixture.error( + reports.codes.INVALID_OPTION_VALUE, + option_name="scope type", + option_value="path", + allowed_values=["xpath", "id"], + cannot_be_empty=False, + forbidden_characters=None, + ), + fixture.error( + reports.codes.ID_NOT_FOUND, + id="R3", + expected_types=["id"], + context_type="", + context_id="", + ), + ] + ) @mock.patch("pcs.lib.commands.acl.get_acls", mock.Mock(side_effect=lambda x: x)) @@ -220,19 +295,124 @@ def test_success(self, mock_remove): self.assert_same_cib_pushed() -@mock.patch("pcs.lib.commands.acl.get_acls", mock.Mock(side_effect=lambda x: x)) -@mock.patch("pcs.lib.cib.acl.validate_permissions") -@mock.patch("pcs.lib.cib.acl.provide_role") -@mock.patch("pcs.lib.cib.acl.add_permissions_to_role") -class AddPermissionTest(AclCommandsTest): - def test_success(self, mock_add_perm, mock_provide_role, mock_validate): - mock_provide_role.return_value = "role_el" - cmd_acl.add_permission(self.mock_env, "role_id", "permission_list") - self.assert_get_cib_called() - mock_validate.assert_called_once_with(self.cib, "permission_list") - mock_provide_role.assert_called_once_with(self.cib, "role_id") - mock_add_perm.assert_called_once_with("role_el", "permission_list") - self.assert_same_cib_pushed() +class AddPermissionTest(TestCase): + def setUp(self): + self.env_assist, self.config = get_env_tools(self) + self.config.runner.cib.load( + resources=""" + + + + + """, + optional_in_conf=self.fixture_acls_xml(), + ) + + @staticmethod + def fixture_acls_xml(roles="", permissions=""): + return f""" + + + + + {permissions} + + {roles} + + """ + + def test_existing_role(self): + self.config.env.push_cib( + optional_in_conf=self.fixture_acls_xml( + permissions=""" + + + """ + ) + ) + cmd_acl.add_permission( + self.env_assist.get_env(), + "role1", + [("deny", "id", "R2"), ("write", "xpath", "/")], + ) + + def test_nonexisting_role(self): + self.config.env.push_cib( + optional_in_conf=self.fixture_acls_xml( + roles=""" + + + + + """ + ) + ) + cmd_acl.add_permission( + self.env_assist.get_env(), + "role2", + [("deny", "id", "R2"), ("write", "xpath", "/")], + ) + + def test_not_role(self): + self.env_assist.assert_raise_library_error( + lambda: cmd_acl.add_permission(self.env_assist.get_env(), "R1", []), + [ + fixture.error( + reports.codes.ID_BELONGS_TO_UNEXPECTED_TYPE, + id="R1", + expected_types=["acl_role"], + current_type="primitive", + ), + ], + expected_in_processor=False, + ) + + def test_validation(self): + self.env_assist.assert_raise_library_error( + lambda: cmd_acl.add_permission( + self.env_assist.get_env(), + "#role_id", + [ + ("change", "id", "R1"), + ("read", "path", "./"), + ("deny", "id", "R3"), + ], + ) + ) + self.env_assist.assert_reports( + [ + fixture.error( + reports.codes.INVALID_ID_BAD_CHAR, + id="#role_id", + id_description="ACL role", + invalid_character="#", + is_first_char=True, + ), + fixture.error( + reports.codes.INVALID_OPTION_VALUE, + option_name="permission", + option_value="change", + allowed_values=["read", "write", "deny"], + cannot_be_empty=False, + forbidden_characters=None, + ), + fixture.error( + reports.codes.INVALID_OPTION_VALUE, + option_name="scope type", + option_value="path", + allowed_values=["xpath", "id"], + cannot_be_empty=False, + forbidden_characters=None, + ), + fixture.error( + reports.codes.ID_NOT_FOUND, + id="R3", + expected_types=["id"], + context_type="", + context_id="", + ), + ] + ) @mock.patch("pcs.lib.commands.acl.get_acls", mock.Mock(side_effect=lambda x: x)) diff --git a/pcs_test/tier1/legacy/test_acl.py b/pcs_test/tier1/legacy/test_acl.py index f0eb5e574..d60038bed 100644 --- a/pcs_test/tier1/legacy/test_acl.py +++ b/pcs_test/tier1/legacy/test_acl.py @@ -11,6 +11,10 @@ empty_cib = rc("cib-empty.xml") +ERRORS_HAVE_OCCURRED = ( + "Error: Errors have occurred, therefore pcs is unable to continue\n" +) + class ACLTest(TestCase, AssertPcsMixin): # pylint: disable=too-many-public-methods @@ -109,11 +113,11 @@ def test_user_group_create_delete_with_roles(self): ) self.assert_pcs_fail( "acl role create group1".split(), - "Error: 'group1' already exists\n", + "Error: 'group1' already exists\n" + ERRORS_HAVE_OCCURRED, ) self.assert_pcs_fail( "acl role create role1".split(), - "Error: 'role1' already exists\n", + "Error: 'role1' already exists\n" + ERRORS_HAVE_OCCURRED, ) self.assert_pcs_fail( "acl user create user1".split(), @@ -545,7 +549,8 @@ def test_role_create_delete(self): ) self.assert_pcs_success("acl role create role0".split()) self.assert_pcs_fail( - "acl role create role0".split(), "Error: 'role0' already exists\n" + "acl role create role0".split(), + "Error: 'role0' already exists\n" + ERRORS_HAVE_OCCURRED, ) self.assert_pcs_success( ["acl", "role", "create", "role0d", "description=empty role"] @@ -825,7 +830,8 @@ def test_can_not_add_permission_for_nonexisting_id(self): self.assert_pcs_success("acl role create role1".split()) self.assert_pcs_fail( "acl permission add role1 read id non-existent-id".split(), - "Error: id 'non-existent-id' does not exist\n", + "Error: id 'non-existent-id' does not exist\n" + + ERRORS_HAVE_OCCURRED, ) def test_can_not_add_permission_for_nonexisting_id_in_later_part(self): @@ -833,7 +839,8 @@ def test_can_not_add_permission_for_nonexisting_id_in_later_part(self): self.assert_pcs_success("acl role create role2".split()) self.assert_pcs_fail( "acl permission add role1 read id role2 read id non-existent-id".split(), - "Error: id 'non-existent-id' does not exist\n", + "Error: id 'non-existent-id' does not exist\n" + + ERRORS_HAVE_OCCURRED, ) def test_can_not_add_permission_for_nonexisting_role_with_bad_id(self): @@ -841,7 +848,8 @@ def test_can_not_add_permission_for_nonexisting_role_with_bad_id(self): self.assert_pcs_fail( "acl permission add #bad-name read id role1".split(), "Error: invalid ACL role '#bad-name'" - + ", '#' is not a valid first character for a ACL role\n", + ", '#' is not a valid first character for a ACL role\n" + + ERRORS_HAVE_OCCURRED, ) def test_can_create_role_with_permission_for_existing_id(self): @@ -851,14 +859,16 @@ def test_can_create_role_with_permission_for_existing_id(self): def test_can_not_crate_role_with_permission_for_nonexisting_id(self): self.assert_pcs_fail( "acl role create role1 read id non-existent-id".split(), - "Error: id 'non-existent-id' does not exist\n", + "Error: id 'non-existent-id' does not exist\n" + + ERRORS_HAVE_OCCURRED, ) def test_can_not_create_role_with_bad_name(self): self.assert_pcs_fail( "acl role create #bad-name".split(), "Error: invalid ACL role '#bad-name'" - + ", '#' is not a valid first character for a ACL role\n", + ", '#' is not a valid first character for a ACL role\n" + + ERRORS_HAVE_OCCURRED, ) def test_fail_on_unknown_role_method(self): From e38be0d5050a01f14bf7de526c5ec33dec652abb Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Thu, 5 Dec 2024 12:40:21 +0100 Subject: [PATCH 090/227] acls: move dom code out of pcs.lib --- pcs/lib/cib/acl.py | 7 ------- pcs/resource.py | 7 +++++-- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/pcs/lib/cib/acl.py b/pcs/lib/cib/acl.py index 53c4658d7..239f49239 100644 --- a/pcs/lib/cib/acl.py +++ b/pcs/lib/cib/acl.py @@ -447,10 +447,3 @@ def remove_permissions_referencing(tree, reference): ".//acl_permission[@reference=$reference]", reference=reference ): permission.getparent().remove(permission) - - -def dom_remove_permissions_referencing(dom, reference): - # TODO: remove once we go fully lxml - for permission in dom.getElementsByTagName("acl_permission"): - if permission.getAttribute("reference") == reference: - permission.parentNode.removeChild(permission) diff --git a/pcs/resource.py b/pcs/resource.py index e07ad4f85..b9aa53081 100644 --- a/pcs/resource.py +++ b/pcs/resource.py @@ -13,7 +13,6 @@ ) from xml.dom.minidom import parseString -import pcs.lib.cib.acl as lib_acl import pcs.lib.pacemaker.live as lib_pacemaker import pcs.lib.resource_agent as lib_ra from pcs import ( @@ -2058,7 +2057,11 @@ def remove_resource_references( resource_id, output, constraints_element, dom ) stonith_level_rm_device(dom, resource_id) - lib_acl.dom_remove_permissions_referencing(dom, resource_id) + + for permission in dom.getElementsByTagName("acl_permission"): + if permission.getAttribute("reference") == resource_id: + permission.parentNode.removeChild(permission) + return dom From 806c85807ee12a58717ad5ec54db5b14a991c406 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Fri, 3 Jan 2025 13:32:34 +0100 Subject: [PATCH 091/227] remove unused build and test dependencies --- .gitlab-ci.yml | 2 -- pcs_test/tools/case_analysis.py | 2 -- pcs_test/tools/command_env/assistant.py | 5 ----- 3 files changed, 9 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bc944de68..3a80b4c53 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -108,7 +108,6 @@ python_tier0_tests: script: # make sure that tier0 tests run without cluster packages installed - dnf remove -y corosync* pacemaker* fence-agents* resource-agents* booth* sbd - - python3 -m pip install concurrencytest - ./autogen.sh - ./configure --enable-local-build - make @@ -121,7 +120,6 @@ python_tier1_tests: - rpm_build script: - "dnf install -y rpms/pcs-*${BASE_IMAGE_NAME}*$(rpm -E %{dist}).*.rpm" - - python3 -m pip install concurrencytest - ./autogen.sh - ./configure --enable-local-build --enable-destructive-tests --enable-tests-only - rm -rf pcs pcsd pcs_bundled # make sure we are testing installed package diff --git a/pcs_test/tools/case_analysis.py b/pcs_test/tools/case_analysis.py index 662a92fa3..3d647d5ce 100644 --- a/pcs_test/tools/case_analysis.py +++ b/pcs_test/tools/case_analysis.py @@ -17,8 +17,6 @@ def test_failed(test): # Python 3.11+ result = test._outcome.result if not hasattr(result, "errors") or not hasattr(result, "failures"): - # test probably run by python concurrencytest module and therefore - # we cannot get test results in a teardown method in Python 3.11+ return None return _list2reason(test, result.errors) or _list2reason( diff --git a/pcs_test/tools/command_env/assistant.py b/pcs_test/tools/command_env/assistant.py index b92736c2c..196ea1187 100644 --- a/pcs_test/tools/command_env/assistant.py +++ b/pcs_test/tools/command_env/assistant.py @@ -201,11 +201,6 @@ def cleanup(self, current_test): self.__unpatch() if test_failed(current_test): - # NOTE: This detection of failed test does not work when tests are - # running in parallel by python concurrencytest module with Python - # version 3.11. This cause that the reports for failed tests will - # be a little more confusing - # # We have already got the message that main test failed. There is # a high probability that something remains in reports or in the # queue etc. But it is only consequence of the main test fail. And From 091687fa0ce6ef5a9ede0277209b2de2fce2b2a6 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Tue, 14 Jan 2025 13:00:40 +0100 Subject: [PATCH 092/227] fix tests for rubygem-json 2.8+ --- pcsd/test/test_cfgsync.rb | 16 +++++++++++++-- pcsd/test/test_config.rb | 42 +++++++++++++++++++++++---------------- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/pcsd/test/test_cfgsync.rb b/pcsd/test/test_cfgsync.rb index 143e170c1..2a00b0654 100644 --- a/pcsd/test/test_cfgsync.rb +++ b/pcsd/test/test_cfgsync.rb @@ -153,9 +153,22 @@ def test_basics() assert_equal(3, cfg.version) assert_equal('b35f951a228ac0734d4c1e45fe73c03b18bca380', cfg.hash) + # rubygem-json shipped with ruby 3.4 changed the way JSON.pretty_generate + # works a bit. This results in different strings produced by the gem in + # ruby older that 3.4 compared to ruby 3.4+. By examining the output of + # JSON.pretty_generate, we can figure out which version of the gem we run + # against and use the correct hash for the produced string. + # https://bugzilla.redhat.com/show_bug.cgi?id=2331005 + old_rubygem = JSON.pretty_generate([]) == "[\n\n]" + if old_rubygem + expected_hash = '26579b79a27f9f56e1acd398eb761d2eb1872c6d' + else + expected_hash = 'db2b44331c63c25874ec30398fa82decd740fef4' + end + cfg.version = 4 assert_equal(4, cfg.version) - assert_equal('26579b79a27f9f56e1acd398eb761d2eb1872c6d', cfg.hash) + assert_equal(expected_hash, cfg.hash) cfg.text = '{ "format_version": 2, @@ -1174,4 +1187,3 @@ def test_ok_and_errors() ) end end - diff --git a/pcsd/test/test_config.rb b/pcsd/test/test_config.rb index a580b24fa..2f36ffd22 100644 --- a/pcsd/test/test_config.rb +++ b/pcsd/test/test_config.rb @@ -5,6 +5,14 @@ require 'config.rb' require 'permissions.rb' +def assert_equal_json(json1, json2) + # https://bugzilla.redhat.com/show_bug.cgi?id=2331005 + assert_equal( + JSON.pretty_generate(JSON.parse(json1)), + JSON.pretty_generate(JSON.parse(json2)) + ) +end + class TestConfig < Test::Unit::TestCase def setup $logger = MockLogger.new @@ -56,7 +64,7 @@ def test_parse_nil() cfg = PCSConfig.new(text) assert_equal(0, cfg.clusters.length) assert_equal([], $logger.log) - assert_equal(fixture_nil_config, cfg.text) + assert_equal_json(fixture_nil_config, cfg.text) end def test_parse_empty() @@ -64,7 +72,7 @@ def test_parse_empty() cfg = PCSConfig.new(text) assert_equal(0, cfg.clusters.length) assert_equal([], $logger.log) - assert_equal(fixture_empty_config, cfg.text) + assert_equal_json(fixture_empty_config, cfg.text) end def test_parse_whitespace() @@ -72,7 +80,7 @@ def test_parse_whitespace() cfg = PCSConfig.new(text) assert_equal(0, cfg.clusters.length) assert_equal([], $logger.log) - assert_equal(fixture_empty_config, cfg.text) + assert_equal_json(fixture_empty_config, cfg.text) end def test_parse_hash_empty() @@ -82,7 +90,7 @@ def test_parse_hash_empty() [['error', 'Unable to parse pcs_settings file: invalid file format']], $logger.log ) - assert_equal(fixture_empty_config, cfg.text) + assert_equal_json(fixture_empty_config, cfg.text) end def test_parse_hash_no_version() @@ -104,7 +112,7 @@ def test_parse_hash_no_version() [['error', 'Unable to parse pcs_settings file: invalid file format']], $logger.log ) - assert_equal(fixture_empty_config, cfg.text) + assert_equal_json(fixture_empty_config, cfg.text) end def test_parse_malformed() @@ -129,14 +137,14 @@ def test_parse_malformed() /Unable to parse pcs_settings file: (\d+: )?unexpected token/, $logger.log[0][1] ) - assert_equal(fixture_empty_config, cfg.text) + assert_equal_json(fixture_empty_config, cfg.text) end def test_parse_format1_empty() text = '[]' cfg = PCSConfig.new(text) assert_equal(0, cfg.clusters.length) - assert_equal( + assert_equal_json( '{ "format_version": 2, "data_version": 0, @@ -177,7 +185,7 @@ def test_parse_format1_one_cluster() assert_equal(1, cfg.clusters.length) assert_equal("cluster71", cfg.clusters[0].name) assert_equal(["rh71-node1", "rh71-node2"], cfg.clusters[0].nodes) - assert_equal( + assert_equal_json( '{ "format_version": 2, "data_version": 0, @@ -218,7 +226,7 @@ def test_parse_format2_empty() assert_equal(2, cfg.format_version) assert_equal(0, cfg.data_version) assert_equal(0, cfg.clusters.length) - assert_equal(fixture_empty_config, cfg.text) + assert_equal_json(fixture_empty_config, cfg.text) end def test_parse_format2_one_cluster() @@ -247,7 +255,7 @@ def test_parse_format2_one_cluster() assert_equal(1, cfg.clusters.length) assert_equal("cluster71", cfg.clusters[0].name) assert_equal(["rh71-node1", "rh71-node2"], cfg.clusters[0].nodes) - assert_equal(text, cfg.text) + assert_equal_json(text, cfg.text) end def test_parse_format2_two_clusters() @@ -318,7 +326,7 @@ def test_parse_format2_two_clusters() ] } }' - assert_equal(out_text, cfg.text) + assert_equal_json(out_text, cfg.text) end def test_parse_format2_bad_cluster() @@ -347,7 +355,7 @@ def test_parse_format2_bad_cluster() assert_equal(1, cfg.clusters.length) assert_equal("cluster71", cfg.clusters[0].name) assert_equal(["rh71-node1", "rh71-node2"], cfg.clusters[0].nodes) - assert_equal( + assert_equal_json( '{ "format_version": 2, "data_version": 9, @@ -475,7 +483,7 @@ def test_parse_format2_permissions() } }' cfg = PCSConfig.new(text) - assert_equal(out_text, cfg.text) + assert_equal_json(out_text, cfg.text) perms = cfg.permissions_local assert_equal(false, perms.allows?('user1', [], Permissions::FULL)) @@ -684,7 +692,7 @@ def assert_empty_data(cfg) assert_equal(1, cfg.format_version) assert_equal(0, cfg.data_version) assert_equal(0, cfg.known_hosts.length) - assert_equal(fixture_empty_config(), cfg.text) + assert_equal_json(fixture_empty_config(), cfg.text) end def assert_known_host(host, name, token, dest_list) @@ -765,7 +773,7 @@ def test_parse_format1_simple() ], cfg.known_hosts['node1'].dest_list ) - assert_equal(text, cfg.text) + assert_equal_json(text, cfg.text) end def test_parse_format1_complex() @@ -825,7 +833,7 @@ def test_parse_format1_complex() {'addr' => '10.0.2.2', 'port' => 2235} ] ) - assert_equal(text, cfg.text) + assert_equal_json(text, cfg.text) end def test_parse_format1_error() @@ -888,7 +896,7 @@ def test_update() {'addr' => '10.0.1.3', 'port' => 2224} ] ) - assert_equal( + assert_equal_json( cfg.text, '{ "format_version": 1, From 7767af58805fcfc3b4c0a4791479236b77860705 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Tue, 14 Jan 2025 13:33:11 +0100 Subject: [PATCH 093/227] fix for ruby 3.4+ fixes the following issue: pcs/pcsd/corosyncconf.rb:114: warning: literal string will be frozen in the future --- pcsd/corosyncconf.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pcsd/corosyncconf.rb b/pcsd/corosyncconf.rb index bcb6df295..4f7a7154c 100644 --- a/pcsd/corosyncconf.rb +++ b/pcsd/corosyncconf.rb @@ -111,7 +111,7 @@ def parent=(parent) def CorosyncConf::parse_string(conf_text) - conf_text = conf_text.force_encoding("utf-8") + conf_text = String.new(conf_text, encoding: Encoding::UTF_8) root = Section.new('') self.parse_section(conf_text.split("\n"), root) return root From cf752c8554c3f7e057ea9b3d330c39946a20f858 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Tue, 21 Jan 2025 11:23:36 +0100 Subject: [PATCH 094/227] be more verbose when a user pushes an invalid cib --- CHANGELOG.md | 9 +++++++++ pcs/cluster.py | 47 +++++++++++++++++++++++++++++++++++++++++++++-- pcs/utils.py | 5 +++-- 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15b0dd587..cde1936f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Change Log +## [Unreleased] + +### Added +- Commands `pcs cluster cib-push` and `pcs cluster edit` now print more info + when new CIB does not conform to the CIB schema [RHEL-76059] + +[RHEL-76059]: https://issues.redhat.com/browse/RHEL-76059 + + ## [0.11.9] - 2025-01-10 ### Added diff --git a/pcs/cluster.py b/pcs/cluster.py index 2e8553a31..d27d1c6d7 100644 --- a/pcs/cluster.py +++ b/pcs/cluster.py @@ -60,6 +60,7 @@ from pcs.common.str_tools import ( format_list, indent, + join_multilines, ) from pcs.common.tools import format_os_error from pcs.common.types import ( @@ -802,6 +803,25 @@ def cluster_push(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: # no # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-statements + + def get_details_from_crm_verify(): + # get a new runner to run crm_verify command and pass the CIB filename + # into it so that the verify is run on the file instead on the live + # cluster CIB + verify_runner = utils.cmd_runner(cib_file_override=filename) + # Request verbose output, otherwise we may only get an unhelpful + # message: + # Configuration invalid (with errors) (-V may provide more detail) + # verify_returncode is always expected to be non-zero to indicate + # invalid CIB - ve run the verify because the CIB is invalid + ( + verify_stdout, + verify_stderr, + verify_returncode, + verify_can_be_more_verbose, + ) = lib_pacemaker.verify(verify_runner, verbose=True) + return join_multilines([verify_stdout, verify_stderr]) + del lib modifiers.ensure_only_supported("--wait", "--config", "-f") if len(argv) > 2: @@ -846,6 +866,8 @@ def cluster_push(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: # no except (EnvironmentError, xml.parsers.expat.ExpatError) as e: utils.err("unable to parse new cib: %s" % e) + EXITCODE_INVALID_CIB = 78 + if diff_against: runner = utils.cmd_runner() command = [ @@ -876,7 +898,18 @@ def cluster_push(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: # no ] output, stderr, retval = runner.run(command, patch) if retval != 0: - utils.err("unable to push cib\n" + stderr + output) + push_output = stderr + output + verify_output = ( + get_details_from_crm_verify() + if retval == EXITCODE_INVALID_CIB + else "" + ) + error_text = ( + f"{push_output}\n\n{verify_output}" + if verify_output.strip() + else push_output + ) + utils.err("unable to push cib\n" + error_text) else: command = ["cibadmin", "--replace", "--xml-file", filename] @@ -894,7 +927,17 @@ def cluster_push(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: # no " that." ) elif retval != 0: - utils.err("unable to push cib\n" + output) + verify_output = ( + get_details_from_crm_verify() + if retval == EXITCODE_INVALID_CIB + else "" + ) + error_text = ( + f"{output}\n\n{verify_output}" + if verify_output.strip() + else output + ) + utils.err("unable to push cib\n" + error_text) print_to_stderr("CIB updated") diff --git a/pcs/utils.py b/pcs/utils.py index ebb1dd9a5..f23d9bd8e 100644 --- a/pcs/utils.py +++ b/pcs/utils.py @@ -928,8 +928,7 @@ def run( return output, retval -@lru_cache() -def cmd_runner(): +def cmd_runner(cib_file_override=None): """ Commandline options: * -f - CIB file @@ -937,6 +936,8 @@ def cmd_runner(): env_vars = {} if usefile: env_vars["CIB_file"] = filename + if cib_file_override: + env_vars["CIB_file"] = cib_file_override env_vars.update(os.environ) env_vars["LC_ALL"] = "C" return CommandRunner( From 8dae6c69f79895205ace8e9b35847a53071f6b30 Mon Sep 17 00:00:00 2001 From: Ivan Devat Date: Mon, 1 Jul 2024 11:18:03 +0200 Subject: [PATCH 095/227] fix local public dir path --- pcs/daemon/env.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pcs/daemon/env.py b/pcs/daemon/env.py index 3139a4e06..969767938 100644 --- a/pcs/daemon/env.py +++ b/pcs/daemon/env.py @@ -12,7 +12,7 @@ # Relative location instead of system location is used for development purposes. LOCAL_PUBLIC_DIR = os.path.realpath( - os.path.dirname(os.path.abspath(__file__)) + "/../../pcsd/static" + os.path.dirname(os.path.abspath(__file__)) + "/../../pcsd/public" ) LOCAL_WEBUI_DIR = os.path.join(LOCAL_PUBLIC_DIR, "ui") WEBUI_FALLBACK_FILE = "ui_instructions.html" From 9447dfae3005cc959b1319b071d8051b45974a89 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Wed, 29 Jan 2025 10:12:17 +0100 Subject: [PATCH 096/227] include watchdog identity in watchdog listing --- CHANGELOG.md | 5 ++- pcs/stonith.py | 13 ++++++- pcs_test/tier0/cli/test_stonith.py | 59 ++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cde1936f5..6044ebcc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,12 @@ ### Added - Commands `pcs cluster cib-push` and `pcs cluster edit` now print more info - when new CIB does not conform to the CIB schema [RHEL-76059] + when new CIB does not conform to the CIB schema ([RHEL-76059]) +- Command `pcs stonith sbd watchdog list` now prints watchdogs' identity and + driver ([RHEL-76177]) [RHEL-76059]: https://issues.redhat.com/browse/RHEL-76059 +[RHEL-76177]: https://issues.redhat.com/browse/RHEL-76177 ## [0.11.9] - 2025-01-10 diff --git a/pcs/stonith.py b/pcs/stonith.py index 9df9bd43a..13b46ad43 100644 --- a/pcs/stonith.py +++ b/pcs/stonith.py @@ -558,9 +558,18 @@ def sbd_watchdog_list(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: available_watchdogs = lib.sbd.get_local_available_watchdogs() if available_watchdogs: - print("Available watchdog(s):") + key_label = { + "identity": "Identity", + "driver": "Driver", + } + lines = [] for watchdog in sorted(available_watchdogs.keys()): - print(f" {watchdog}") + lines += [watchdog] + for key, label in key_label.items(): + value = available_watchdogs[watchdog].get(key) + if value: + lines += indent([f"{label}: {value}"]) + print("Available watchdog(s):\n" + "\n".join(indent(lines))) else: print_to_stderr("No available watchdog") diff --git a/pcs_test/tier0/cli/test_stonith.py b/pcs_test/tier0/cli/test_stonith.py index ffcac5d6d..50da98ca3 100644 --- a/pcs_test/tier0/cli/test_stonith.py +++ b/pcs_test/tier0/cli/test_stonith.py @@ -1,3 +1,4 @@ +from textwrap import dedent from unittest import ( TestCase, mock, @@ -28,6 +29,64 @@ def _convert_val(val): ) +@mock.patch("pcs.stonith.print_to_stderr") +@mock.patch("pcs.stonith.print") +class SbdWatchdogList(TestCase): + def setUp(self): + self.lib = mock.Mock(spec_set=["sbd"]) + self.sbd = mock.Mock(spec_set=["get_local_available_watchdogs"]) + self.lib.sbd = self.sbd + + def call_cmd(self, argv, modifiers=None): + stonith.sbd_watchdog_list( + self.lib, argv, _dict_to_modifiers(modifiers or {}) + ) + + def test_args(self, mock_print, mock_stderr): + with self.assertRaises(CmdLineInputError) as cm: + self.call_cmd(["foo"]) + self.assertEqual(cm.exception.message, None) + self.sbd.get_local_available_watchdogs.assert_not_called() + mock_print.assert_not_called() + mock_stderr.assert_not_called() + + def test_no_watchdogs(self, mock_print, mock_stderr): + self.sbd.get_local_available_watchdogs.return_value = {} + self.call_cmd([]) + mock_print.assert_not_called() + mock_stderr.assert_called_once_with("No available watchdog") + + def test_watchdogs(self, mock_print, mock_stderr): + self.sbd.get_local_available_watchdogs.return_value = { + "/dev/watchdog1": { + "identity": "iTCO_wdt", + "driver": "iTCO_wdt", + "caution": "unused", + }, + "/dev/watchdog0": { + "identity": "i6300ESB timer", + }, + "/dev/watchdog2": { + "driver": "iTCO_wdt", + }, + "/dev/watchdog": {}, + } + self.call_cmd([]) + mock_print.assert_called_once_with( + dedent("""\ + Available watchdog(s): + /dev/watchdog + /dev/watchdog0 + Identity: i6300ESB timer + /dev/watchdog1 + Identity: iTCO_wdt + Driver: iTCO_wdt + /dev/watchdog2 + Driver: iTCO_wdt""") + ) + mock_stderr.assert_not_called() + + class SbdEnable(TestCase): def setUp(self): self.lib = mock.Mock(spec_set=["sbd"]) From 1418454445b616d8e717971ecf41808f6579c703 Mon Sep 17 00:00:00 2001 From: Ivan Devat Date: Mon, 3 Feb 2025 12:17:05 +0100 Subject: [PATCH 097/227] add script usable as githook with various checks --- scripts/pre-commit/README.md | 10 ++++ scripts/pre-commit/check-all.sh | 53 +++++++++++++++++++ scripts/pre-commit/check-format.sh | 7 +++ scripts/pre-commit/check-lint.sh | 7 +++ .../pre-commit/check-makefile-file-listing.sh | 46 ++++++++++++++++ scripts/pre-commit/extract-extra-dist.sh | 30 +++++++++++ scripts/pre-commit/pre-commit.sh | 7 +++ 7 files changed, 160 insertions(+) create mode 100644 scripts/pre-commit/README.md create mode 100755 scripts/pre-commit/check-all.sh create mode 100755 scripts/pre-commit/check-format.sh create mode 100755 scripts/pre-commit/check-lint.sh create mode 100755 scripts/pre-commit/check-makefile-file-listing.sh create mode 100755 scripts/pre-commit/extract-extra-dist.sh create mode 100755 scripts/pre-commit/pre-commit.sh diff --git a/scripts/pre-commit/README.md b/scripts/pre-commit/README.md new file mode 100644 index 000000000..8bd3c349e --- /dev/null +++ b/scripts/pre-commit/README.md @@ -0,0 +1,10 @@ +# Pre-commit hook + +It makes checks and warns the committer when there is something suspicious. + +## Install + +Just copy the main file to githooks (assume you are in project root dir): +``` +cp ./scripts/pre-commit/pre-commit.sh .git/hooks/pre-commit +``` diff --git a/scripts/pre-commit/check-all.sh b/scripts/pre-commit/check-all.sh new file mode 100755 index 000000000..243a56eb7 --- /dev/null +++ b/scripts/pre-commit/check-all.sh @@ -0,0 +1,53 @@ +#!/bin/sh + +# Check definition. Multiline string, every line is for one check. +# Format of line is: +# `Check title ./path/relative/to/this/script/check-script.sh`. +# The last space separates title from script path. +check_list=" +RUFF LINT CHECK ./check-lint.sh +RUFF FORMAT CHECK ./check-format.sh +MAKEFILE FILE LISTING CHECK ./check-makefile-file-listing.sh +" + +extract_title() { + echo "$1" | awk '{$NF=""; print $0}' +} + +extract_command() { + realpath "$(dirname "$0")"/"$(echo "$1" | awk '{print $NF}')" +} + +err_report="" +while IFS= read -r line; do + [ -n "$line" ] || continue + + check=$(extract_command "$line") + + if ! output=$("$check" 2>&1); then + err_report="${err_report}$(extract_title "$line") ($check)\n$output\n\n" + fi +done << EOF +$check_list +EOF + +if [ -z "$err_report" ]; then + exit 0 +fi + +echo "Warning: some check failed" +printf "%b" "$err_report" + +printf "Checks failed. Continue with commit? (c)Continue (a)Abort: " > /dev/tty +IFS= read -r resolution < /dev/tty + +case "$resolution" in + c | C) exit 0 ;; + a | A) + echo "Commit aborted." + exit 1 + ;; +esac + +echo "Unknown resolution '$resolution', aborting commit..." +exit 1 diff --git a/scripts/pre-commit/check-format.sh b/scripts/pre-commit/check-format.sh new file mode 100755 index 000000000..dd6e4ef31 --- /dev/null +++ b/scripts/pre-commit/check-format.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +if make --dry-run ruff_format_check > /dev/null 2>&1; then + make ruff_format_check +else + echo "No 'make ruff_format_check', skipping..." +fi diff --git a/scripts/pre-commit/check-lint.sh b/scripts/pre-commit/check-lint.sh new file mode 100755 index 000000000..89ab675bc --- /dev/null +++ b/scripts/pre-commit/check-lint.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +if make --dry-run ruff_lint > /dev/null 2>&1; then + make ruff_lint +else + echo "No 'make ruff_lint', skipping..." +fi diff --git a/scripts/pre-commit/check-makefile-file-listing.sh b/scripts/pre-commit/check-makefile-file-listing.sh new file mode 100755 index 000000000..fd81d1109 --- /dev/null +++ b/scripts/pre-commit/check-makefile-file-listing.sh @@ -0,0 +1,46 @@ +#!/bin/sh + +makefile_list="\ +Makefile.am +pcs/Makefile.am +pcs_test/Makefile.am +pcsd/Makefile.am +data/Makefile.am +" + +get_mentioned_files() { + for makefile in $1; do + [ -n "$makefile" ] || continue + "$(dirname "$0")"/extract-extra-dist.sh "$makefile" + done +} + +get_unlisted_files() { + makefiles=$1 + added_files=$2 + + mentioned_files=$(get_mentioned_files "$makefiles") + + for file in $added_files; do + if ! echo "$mentioned_files" | + grep --quiet --fixed-strings --line-regexp "$file" 2> /dev/null; then + echo "$file" + fi + done +} + +git_added="$(git diff --cached --name-only --diff-filter=A)" + +if [ -z "$git_added" ]; then + exit 0 +fi + +unlisted_files="$(get_unlisted_files "$makefile_list" "$git_added")" + +if [ -z "$unlisted_files" ]; then + exit 0 +fi + +echo "Warning: The following files are not listed in any Makefile.am:" +echo "$unlisted_files" +exit 1 diff --git a/scripts/pre-commit/extract-extra-dist.sh b/scripts/pre-commit/extract-extra-dist.sh new file mode 100755 index 000000000..16608e721 --- /dev/null +++ b/scripts/pre-commit/extract-extra-dist.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +makefile="$1" +inside_extra_dist=0 +files="" + +while IFS= read -r line; do + case "$line" in + *EXTRA_DIST[[:space:]]*=*) + inside_extra_dist=1 + files="${line#*=}" + files="${files%\\}" + ;; + *\\) + [ $inside_extra_dist -eq 1 ] && files="$files ${line%\\}" + ;; + *) # the last line in EXTRA_DIST + [ $inside_extra_dist -eq 1 ] && files="$files $line" && break + ;; + esac +done < "$makefile" + +# no globing +set -f +# split to arguments +# shellcheck disable=2086 +set -- $files +for file in "$@"; do + printf "%s\n" "${makefile%Makefile.am}$file" +done diff --git a/scripts/pre-commit/pre-commit.sh b/scripts/pre-commit/pre-commit.sh new file mode 100755 index 000000000..6361e878a --- /dev/null +++ b/scripts/pre-commit/pre-commit.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +checks=./scripts/pre-commit/check-all.sh + +if [ -x "$checks" ]; then + "$checks" +fi From edabcd683526e526e5ccf10a8719cf91de8d45ae Mon Sep 17 00:00:00 2001 From: Ivan Devat Date: Mon, 10 Feb 2025 12:17:17 +0100 Subject: [PATCH 098/227] fix headings in README.md --- README.md | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 181c3c8ac..56750bfbd 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,10 @@ -## PCS - Pacemaker/Corosync Configuration System +# PCS - Pacemaker/Corosync Configuration System Pcs is a Corosync and Pacemaker configuration tool. It permits users to easily view, modify and create Pacemaker based clusters. Pcs contains pcsd, a pcs daemon, which operates as a remote server for pcs. ---- - -### Pcs Branches +## Pcs Branches * main * This is where pcs-0.12 lives. @@ -26,9 +24,7 @@ daemon, which operates as a remote server for pcs. CMAN are supported. * This branch is no longer maintained. ---- - -### Dependencies +## Dependencies These are the runtime dependencies of pcs and pcsd: * python 3.9+ @@ -46,9 +42,7 @@ These are the runtime dependencies of pcs and pcsd: * corosync 3.x * pacemaker 2.1 ---- - -### Installation from Source +## Installation from Source Apart from the dependencies listed above, these are also required for installation: @@ -90,9 +84,7 @@ systemctl start pcsd systemctl enable pcsd ``` ---- - -### Packages +## Packages Currently this is built into Fedora, RHEL, CentOS and Debian and its derivates. It is likely that other Linux distributions also contain pcs @@ -101,9 +93,7 @@ packages. * [Current Fedora .spec](https://src.fedoraproject.org/rpms/pcs/blob/rawhide/f/pcs.spec) * [Debian-HA project home page](https://wiki.debian.org/Debian-HA) ---- - -### Quick Start +## Quick Start * **Authenticate cluster nodes** @@ -149,9 +139,7 @@ packages. pcs resource create --help ``` ---- - -### Further Documentation +## Further Documentation [ClusterLabs website](https://clusterlabs.org) is an excellent place to learn more about Pacemaker clusters. @@ -159,9 +147,7 @@ more about Pacemaker clusters. * [Clusters from Scratch](https://clusterlabs.org/pacemaker/doc/2.1/Clusters_from_Scratch/html/) * [ClusterLabs documentation page](https://clusterlabs.org/pacemaker/doc/) ---- - -### Inquiries +## Inquiries If you have any bug reports or feature requests please feel free to open a github issue on the pcs project. From 89fc8edf8a1ea11e9e6cce05b05d62f269e17d4e Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Wed, 4 Dec 2024 17:11:16 +0100 Subject: [PATCH 099/227] add command for renaming cluster --- pcs/cli/cluster/command.py | 18 ++ pcs/cli/common/lib_wrapper.py | 1 + pcs/cli/routing/cluster.py | 1 + pcs/common/reports/codes.py | 2 + pcs/common/reports/messages.py | 34 +++ .../async_tasks/worker/command_mapping.py | 4 + pcs/lib/cib/resource/primitive.py | 4 +- pcs/lib/commands/cluster.py | 76 ++++++ pcs/lib/corosync/config_facade.py | 13 + pcs/lib/corosync/config_validators.py | 43 ++- pcs/lib/pacemaker/live.py | 31 ++- pcs/pcs.8.in | 3 + pcs/usage.py | 4 + pcs_test/Makefile.am | 3 + pcs_test/tier0/cli/cluster/test_command.py | 48 ++++ .../tier0/common/reports/test_messages.py | 33 ++- .../tier0/lib/cib/test_resource_primitive.py | 4 +- pcs_test/tier0/lib/commands/cluster/common.py | 9 +- .../tier0/lib/commands/cluster/test_rename.py | 244 ++++++++++++++++++ .../lib/corosync/test_config_facade_misc.py | 24 ++ .../test_config_validators_rename_cluster.py | 74 ++++++ pcs_test/tier0/lib/pacemaker/test_live.py | 40 ++- pcs_test/tier1/cluster/test_cluster_rename.py | 45 ++++ pcsd/capabilities.xml.in | 8 + 24 files changed, 729 insertions(+), 37 deletions(-) create mode 100644 pcs_test/tier0/lib/commands/cluster/test_rename.py create mode 100644 pcs_test/tier0/lib/corosync/test_config_validators_rename_cluster.py create mode 100644 pcs_test/tier1/cluster/test_cluster_rename.py diff --git a/pcs/cli/cluster/command.py b/pcs/cli/cluster/command.py index 4efde680b..1b7538cdd 100644 --- a/pcs/cli/cluster/command.py +++ b/pcs/cli/cluster/command.py @@ -201,3 +201,21 @@ def node_clear(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: lib.cluster.node_clear( arg_list[0], allow_clear_cluster_node=modifiers.get("--force") ) + + +def cluster_rename(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: + """ + Options: + * --force - allow using cluster name that does not work with gfs2 + * --skip-offline - skip offline nodes + """ + modifiers.ensure_only_supported("--force", "--skip-offline") + if len(argv) != 1: + raise CmdLineInputError() + force_flags = [] + if modifiers.get("--force"): + force_flags.append(report_codes.FORCE) + if modifiers.get("--skip-offline"): + force_flags.append(report_codes.SKIP_OFFLINE_NODES) + + lib.cluster.rename(argv[0], force_flags) diff --git a/pcs/cli/common/lib_wrapper.py b/pcs/cli/common/lib_wrapper.py index b0a88a6df..ef2f389a2 100644 --- a/pcs/cli/common/lib_wrapper.py +++ b/pcs/cli/common/lib_wrapper.py @@ -205,6 +205,7 @@ def load_module(env, middleware_factory, name): # noqa: PLR0911, PLR0912 "remove_links": cluster.remove_links, "remove_nodes": cluster.remove_nodes, "remove_nodes_from_cib": cluster.remove_nodes_from_cib, + "rename": cluster.rename, "setup": cluster.setup, "setup_local": cluster.setup_local, "update_link": cluster.update_link, diff --git a/pcs/cli/routing/cluster.py b/pcs/cli/routing/cluster.py index ca5697290..ad49edb28 100644 --- a/pcs/cli/routing/cluster.py +++ b/pcs/cli/routing/cluster.py @@ -116,6 +116,7 @@ def pcsd_status(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: "destroy": cluster.cluster_destroy, "verify": cluster.cluster_verify, "report": cluster.cluster_report, + "rename": cluster_command.cluster_rename, "remove_nodes_from_cib": cluster.remove_nodes_from_cib, }, ["cluster"], diff --git a/pcs/common/reports/codes.py b/pcs/common/reports/codes.py index 6a7049f23..6e3a19855 100644 --- a/pcs/common/reports/codes.py +++ b/pcs/common/reports/codes.py @@ -276,6 +276,7 @@ DEFAULTS_CAN_BE_OVERRIDDEN = M("DEFAULTS_CAN_BE_OVERRIDDEN") DEPRECATED_OPTION = M("DEPRECATED_OPTION") DEPRECATED_OPTION_VALUE = M("DEPRECATED_OPTION_VALUE") +DLM_CLUSTER_RENAME_NEEDED = M("DLM_CLUSTER_RENAME_NEEDED") DR_CONFIG_ALREADY_EXIST = M("DR_CONFIG_ALREADY_EXIST") DR_CONFIG_DOES_NOT_EXIST = M("DR_CONFIG_DOES_NOT_EXIST") DUPLICATE_CONSTRAINTS_EXIST = M("DUPLICATE_CONSTRAINTS_EXIST") @@ -292,6 +293,7 @@ FILE_IO_ERROR = M("FILE_IO_ERROR") FILE_REMOVE_FROM_NODE_ERROR = M("FILE_REMOVE_FROM_NODE_ERROR") FILE_REMOVE_FROM_NODE_SUCCESS = M("FILE_REMOVE_FROM_NODE_SUCCESS") +GFS2_LOCK_TABLE_RENAME_NEEDED = M("GFS2_LOCK_TABLE_RENAME_NEEDED") HOST_NOT_FOUND = M("HOST_NOT_FOUND") HOST_ALREADY_AUTHORIZED = M("HOST_ALREADY_AUTHORIZED") HOST_ALREADY_IN_CLUSTER_CONFIG = M("HOST_ALREADY_IN_CLUSTER_CONFIG") diff --git a/pcs/common/reports/messages.py b/pcs/common/reports/messages.py index 4e6326fbf..29cfe75c6 100644 --- a/pcs/common/reports/messages.py +++ b/pcs/common/reports/messages.py @@ -8160,3 +8160,37 @@ def message(self) -> str: f"'{self.nvset_id}' already exists. Find elements with the ID and " "remove them from cluster configuration." ) + + +@dataclass(frozen=True) +class DlmClusterRenameNeeded(ReportItemMessage): + """ + Dlm cluster name in volume group metadata must be updated + """ + + _code = codes.DLM_CLUSTER_RENAME_NEEDED + + @property + def message(self) -> str: + return ( + "The DLM cluster name in the shared volume groups metadata must be " + "updated to reflect the name of the cluster so that the volume " + "groups can start" + ) + + +@dataclass(frozen=True) +class Gfs2LockTableRenameNeeded(ReportItemMessage): + """ + Lock table name on each GFS2 filesystem must be updated + """ + + _code = codes.GFS2_LOCK_TABLE_RENAME_NEEDED + + @property + def message(self) -> str: + return ( + "The lock table name on each GFS2 filesystem must be updated to " + "reflect the name of the cluster so that the filesystems can be " + "mounted" + ) diff --git a/pcs/daemon/async_tasks/worker/command_mapping.py b/pcs/daemon/async_tasks/worker/command_mapping.py index 4dcbbd347..3a2d24daa 100644 --- a/pcs/daemon/async_tasks/worker/command_mapping.py +++ b/pcs/daemon/async_tasks/worker/command_mapping.py @@ -141,6 +141,10 @@ class _Cmd: cmd=cluster.remove_nodes, required_permission=p.FULL, ), + "cluster.rename": _Cmd( + cmd=cluster.rename, + required_permission=p.WRITE, + ), "cluster.setup": _Cmd( cmd=cluster.setup, required_permission=p.SUPERUSER, diff --git a/pcs/lib/cib/resource/primitive.py b/pcs/lib/cib/resource/primitive.py index 3a2822276..a82bd92fa 100644 --- a/pcs/lib/cib/resource/primitive.py +++ b/pcs/lib/cib/resource/primitive.py @@ -96,7 +96,7 @@ def primitive_element_to_dto( ) -def _find_primitives_by_agent( +def find_primitives_by_agent( resources_section: _Element, agent_name: ResourceAgentName ) -> List[_Element]: """ @@ -308,7 +308,7 @@ def _validate_unique_instance_attributes( return [] report_list = [] - same_agent_resources = _find_primitives_by_agent( + same_agent_resources = find_primitives_by_agent( resources_section, resource_agent.name ) diff --git a/pcs/lib/commands/cluster.py b/pcs/lib/commands/cluster.py index cf135d1e9..7abee5d88 100644 --- a/pcs/lib/commands/cluster.py +++ b/pcs/lib/commands/cluster.py @@ -12,6 +12,8 @@ cast, ) +from lxml.etree import _Element + from pcs import settings from pcs.common import ( file_type_codes, @@ -43,8 +45,14 @@ ) from pcs.lib.booth import sync as booth_sync from pcs.lib.cib import fencing_topology +from pcs.lib.cib.nvpair_multi import ( + NVSET_INSTANCE, + find_nvsets, +) from pcs.lib.cib.resource.guest_node import find_node_list as get_guest_nodes +from pcs.lib.cib.resource.primitive import find_primitives_by_agent from pcs.lib.cib.resource.remote_node import find_node_list as get_remote_nodes +from pcs.lib.cib.tools import get_resources from pcs.lib.communication import cluster from pcs.lib.communication.corosync import ( CheckCorosyncOffline, @@ -88,13 +96,16 @@ from pcs.lib.node import get_existing_nodes_names from pcs.lib.pacemaker.live import ( get_cib, + get_cib_direct_xml, get_cib_xml, get_cib_xml_cmd_results, + has_cib_xml, remove_node, ) from pcs.lib.pacemaker.live import verify as verify_cmd from pcs.lib.pacemaker.state import ClusterState from pcs.lib.pacemaker.values import get_valid_timeout_seconds +from pcs.lib.resource_agent.types import ResourceAgentName from pcs.lib.tools import ( environment_file_to_dict, generate_binary_key, @@ -2317,3 +2328,68 @@ def wait_for_pcmk_idle(env: LibraryEnvironment, wait_value: WaitType) -> None: """ timeout = env.ensure_wait_satisfiable(wait_value) env.wait_for_idle(timeout) + + +def _warn_dlm_resources(resources: _Element) -> reports.ReportItemList: + if find_primitives_by_agent( + resources, ResourceAgentName("ocf", "pacemaker", "controld") + ): + return [ + reports.ReportItem.warning( + reports.messages.DlmClusterRenameNeeded() + ) + ] + return [] + + +def _warn_gfs2_resources(resources: _Element) -> reports.ReportItemList: + for resource in find_primitives_by_agent( + resources, ResourceAgentName("ocf", "heartbeat", "Filesystem") + ): + for nvset in find_nvsets(resource, NVSET_INSTANCE): + if any( + ( + nvpair.get("name") == "fstype" + and nvpair.get("value") == "gfs2" + ) + for nvpair in nvset + ): + return [ + reports.ReportItem.warning( + reports.messages.Gfs2LockTableRenameNeeded() + ) + ] + return [] + + +def rename( + env: LibraryEnvironment, + new_name: str, + force_flags: Collection[reports.types.ForceCode] = (), +) -> None: + """ + Change the name of the local cluster + + new_name -- new name for the cluster + """ + _ensure_live_env(env) + + if env.report_processor.report_list( + config_validators.rename_cluster( + new_name, force_cluster_name=reports.codes.FORCE in force_flags + ) + ).has_errors: + raise LibraryError() + + if has_cib_xml(): + cib = get_cib(get_cib_direct_xml(env.cmd_runner())) + resources = get_resources(cib) + env.report_processor.report_list(_warn_dlm_resources(resources)) + env.report_processor.report_list(_warn_gfs2_resources(resources)) + + corosync_conf = env.get_corosync_conf() + corosync_conf.set_cluster_name(new_name) + env.push_corosync_conf( + corosync_conf, + skip_offline_nodes=reports.codes.SKIP_OFFLINE_NODES in force_flags, + ) diff --git a/pcs/lib/corosync/config_facade.py b/pcs/lib/corosync/config_facade.py index 3ac7f52b6..5d70d660d 100644 --- a/pcs/lib/corosync/config_facade.py +++ b/pcs/lib/corosync/config_facade.py @@ -105,6 +105,19 @@ def need_qdevice_reload(self) -> bool: def get_cluster_name(self) -> str: return self._get_option_value("totem", "cluster_name", "") + def set_cluster_name(self, new_name: str) -> None: + """ + Updates or adds cluster name + + new_name - new name for the cluster + """ + self._need_stopped_cluster = True + totem_section_list = self.__ensure_section(self.config, "totem") + self.__set_section_options( + totem_section_list, {"cluster_name": new_name} + ) + self.__remove_empty_sections(self.config) + def get_cluster_uuid(self) -> Optional[str]: return self._get_option_value("totem", "cluster_uuid") diff --git a/pcs/lib/corosync/config_validators.py b/pcs/lib/corosync/config_validators.py index 15c94d05b..8636f3756 100644 --- a/pcs/lib/corosync/config_validators.py +++ b/pcs/lib/corosync/config_validators.py @@ -117,20 +117,7 @@ def create( # noqa: PLR0912, PLR0915 warnings instead of errors """ # cluster name and transport validation - validators = [ - validate.ValueNotEmpty( - "name", None, option_name_for_report="cluster name" - ), - _ClusterNameGfs2Validator( - "name", - option_name_for_report="cluster name", - severity=reports.item.get_severity( - reports.codes.FORCE, force_cluster_name - ), - ), - validate.ValueCorosyncValue( - "name", option_name_for_report="cluster name" - ), + validators = _get_cluster_name_validators(force_cluster_name) + [ validate.ValueIn("transport", constants.TRANSPORTS_ALL), validate.ValueCorosyncValue("transport"), ] @@ -286,6 +273,26 @@ def create( # noqa: PLR0912, PLR0915 return report_items +def _get_cluster_name_validators( + force_cluster_name: bool, +) -> list[validate.ValidatorInterface]: + return [ + validate.ValueNotEmpty( + "name", None, option_name_for_report="cluster name" + ), + _ClusterNameGfs2Validator( + "name", + option_name_for_report="cluster name", + severity=reports.item.get_severity( + reports.codes.FORCE, force_cluster_name + ), + ), + validate.ValueCorosyncValue( + "name", option_name_for_report="cluster name" + ), + ] + + def _get_node_name_validators( node_index: int, ) -> list[validate.ValidatorInterface]: @@ -2161,3 +2168,11 @@ def _mixes_ipv4_ipv6(addr_types: Collection[CorosyncNodeAddressType]) -> bool: return {CorosyncNodeAddressType.IPV4, CorosyncNodeAddressType.IPV6} <= set( addr_types ) + + +def rename_cluster( + cluster_name: str, force_cluster_name: bool = False +) -> ReportItemList: + return validate.ValidatorAll( + _get_cluster_name_validators(force_cluster_name) + ).validate({"name": cluster_name}) diff --git a/pcs/lib/pacemaker/live.py b/pcs/lib/pacemaker/live.py index bd271eb5a..c3a502714 100644 --- a/pcs/lib/pacemaker/live.py +++ b/pcs/lib/pacemaker/live.py @@ -150,17 +150,42 @@ def has_cib_xml() -> bool: def get_cib_xml_cmd_results( - runner: CommandRunner, scope: Optional[str] = None + runner: CommandRunner, + scope: Optional[str] = None, + env_extend: Optional[Mapping[str, str]] = None, ) -> tuple[str, str, int]: command = [settings.cibadmin_exec, "--local", "--query"] if scope: command.append(f"--scope={scope}") - stdout, stderr, returncode = runner.run(command) + stdout, stderr, returncode = runner.run(command, env_extend=env_extend) return stdout, stderr, returncode def get_cib_xml(runner: CommandRunner, scope: Optional[str] = None) -> str: - stdout, stderr, retval = get_cib_xml_cmd_results(runner, scope) + return _get_cib_xml_common(*get_cib_xml_cmd_results(runner, scope), scope) + + +def get_cib_direct_xml( + runner: CommandRunner, scope: Optional[str] = None +) -> str: + """ + Return cib directly from filesystem. Useful when working with cib on + stopped cluster + """ + + return _get_cib_xml_common( + *get_cib_xml_cmd_results( + runner, + scope, + {"CIB_file": os.path.join(settings.cib_dir, "cib.xml")}, + ), + scope, + ) + + +def _get_cib_xml_common( + stdout: str, stderr: str, retval: int, scope: Optional[str] = None +) -> str: if retval != 0: if retval == __EXITCODE_CIB_SCOPE_VALID_BUT_NOT_PRESENT and scope: raise LibraryError( diff --git a/pcs/pcs.8.in b/pcs/pcs.8.in index fb9e1ef85..814ac2bc3 100644 --- a/pcs/pcs.8.in +++ b/pcs/pcs.8.in @@ -579,6 +579,9 @@ Authenticate pcs/pcsd to pcsd on nodes configured in the local cluster. status View current cluster status (an alias of 'pcs status cluster'). .TP +rename +Rename configured cluster. The cluster has to be stopped to complete this operation. +.TP sync Sync cluster configuration (files which are supported by all subcommands of this command) to all cluster nodes. .TP diff --git a/pcs/usage.py b/pcs/usage.py index ac9ea4a5f..3bf56d302 100644 --- a/pcs/usage.py +++ b/pcs/usage.py @@ -1670,6 +1670,10 @@ def cluster(args: Argv) -> str: status View current cluster status (an alias of 'pcs status cluster'). + rename + Rename configured cluster. The cluster has to be stopped to complete + this operation. + sync Sync cluster configuration (files which are supported by all subcommands of this command) to all cluster nodes. diff --git a/pcs_test/Makefile.am b/pcs_test/Makefile.am index 9cc0d3310..3ec209d2f 100644 --- a/pcs_test/Makefile.am +++ b/pcs_test/Makefile.am @@ -234,6 +234,7 @@ EXTRA_DIST = \ tier0/lib/commands/cluster/test_remove_links.py \ tier0/lib/commands/cluster/test_remove_nodes_from_cib.py \ tier0/lib/commands/cluster/test_remove_nodes.py \ + tier0/lib/commands/cluster/test_rename.py \ tier0/lib/commands/cluster/test_setup.py \ tier0/lib/commands/cluster/test_update_link.py \ tier0/lib/commands/cluster/test_verify.py \ @@ -322,6 +323,7 @@ EXTRA_DIST = \ tier0/lib/corosync/test_config_validators_links.py \ tier0/lib/corosync/test_config_validators_nodes.py \ tier0/lib/corosync/test_config_validators_quorum.py \ + tier0/lib/corosync/test_config_validators_rename_cluster.py \ tier0/lib/corosync/test_config_validators_update.py \ tier0/lib/corosync/test_live.py \ tier0/lib/corosync/test_node.py \ @@ -384,6 +386,7 @@ EXTRA_DIST = \ tier1/cluster/test_config_show.py \ tier1/cluster/test_config_update.py \ tier1/cluster/test_setup_local.py \ + tier1/cluster/test_cluster_rename.py \ tier1/constraint/__init__.py \ tier1/constraint/test_config.py \ tier1/__init__.py \ diff --git a/pcs_test/tier0/cli/cluster/test_command.py b/pcs_test/tier0/cli/cluster/test_command.py index 13246f690..1f1266707 100644 --- a/pcs_test/tier0/cli/cluster/test_command.py +++ b/pcs_test/tier0/cli/cluster/test_command.py @@ -71,3 +71,51 @@ def test_all_flags(self): self.remote_node.node_remove_remote.assert_called_once_with( "A", [report_codes.FORCE, report_codes.SKIP_OFFLINE_NODES] ) + + +class ClusterRename(TestCase): + def setUp(self): + self.lib = mock.Mock(spec_set=["cluster"]) + self.lib.cluster = mock.Mock(spec_set=["rename"]) + + def test_no_args(self): + with self.assertRaises(CmdLineInputError) as cm: + command.cluster_rename(self.lib, [], dict_to_modifiers({})) + self.assertIsNone(cm.exception.message) + self.lib.cluster.rename.assert_not_called() + + def test_too_many_args(self): + with self.assertRaises(CmdLineInputError) as cm: + command.cluster_rename(self.lib, ["A", "B"], dict_to_modifiers({})) + self.assertIsNone(cm.exception.message) + self.lib.cluster.rename.assert_not_called() + + def test_success(self): + command.cluster_rename(self.lib, ["A"], dict_to_modifiers({})) + self.lib.cluster.rename.assert_called_once_with("A", []) + + def test_force(self): + command.cluster_rename( + self.lib, ["A"], dict_to_modifiers({"force": True}) + ) + self.lib.cluster.rename.assert_called_once_with( + "A", [report_codes.FORCE] + ) + + def test_skip_offline(self): + command.cluster_rename( + self.lib, ["A"], dict_to_modifiers({"skip-offline": True}) + ) + self.lib.cluster.rename.assert_called_once_with( + "A", [report_codes.SKIP_OFFLINE_NODES] + ) + + def test_all_flags(self): + command.cluster_rename( + self.lib, + ["A"], + dict_to_modifiers({"force": True, "skip-offline": True}), + ) + self.lib.cluster.rename.assert_called_once_with( + "A", [report_codes.FORCE, report_codes.SKIP_OFFLINE_NODES] + ) diff --git a/pcs_test/tier0/common/reports/test_messages.py b/pcs_test/tier0/common/reports/test_messages.py index 8142d9003..0956c524a 100644 --- a/pcs_test/tier0/common/reports/test_messages.py +++ b/pcs_test/tier0/common/reports/test_messages.py @@ -6012,13 +6012,13 @@ def test_message(self) -> str: class StoppingResourcesBeforeDeleting(NameBuildTest): - def test_one_resource(self) -> str: + def test_one_resource(self): self.assert_message_from_report( "Stopping resource 'resourceId' before deleting", reports.StoppingResourcesBeforeDeleting(["resourceId"]), ) - def test_multiple_resources(self) -> str: + def test_multiple_resources(self): self.assert_message_from_report( "Stopping resources 'resourceId1', 'resourceId2' before deleting", reports.StoppingResourcesBeforeDeleting( @@ -6028,7 +6028,7 @@ def test_multiple_resources(self) -> str: class StoppingResourcesBeforeDeletingSkipped(NameBuildTest): - def test_success(self) -> str: + def test_success(self): self.assert_message_from_report( ( "Resources are not going to be stopped before deletion, this " @@ -6039,13 +6039,13 @@ def test_success(self) -> str: class CannotStopResourcesBeforeDeleting(NameBuildTest): - def test_one_resource(self) -> str: + def test_one_resource(self): self.assert_message_from_report( "Cannot stop resource 'resourceId' before deleting", reports.CannotStopResourcesBeforeDeleting(["resourceId"]), ) - def test_multiple_resources(self) -> str: + def test_multiple_resources(self): self.assert_message_from_report( "Cannot stop resources 'resourceId1', 'resourceId2' before deleting", reports.CannotStopResourcesBeforeDeleting( @@ -6078,3 +6078,26 @@ def test_success(self) -> str: ), reports.GuestNodeRemovalIncomplete("guest-node"), ) + +class DlmClusterRenameNeeded(NameBuildTest): + def test_success(self): + self.assert_message_from_report( + ( + "The DLM cluster name in the shared volume groups metadata " + "must be updated to reflect the name of the cluster so that " + "the volume groups can start" + ), + reports.DlmClusterRenameNeeded(), + ) + + +class Gfs2LockTableRenameNeeded(NameBuildTest): + def test_success(self): + self.assert_message_from_report( + ( + "The lock table name on each GFS2 filesystem must be updated " + "to reflect the name of the cluster so that the filesystems " + "can be mounted" + ), + reports.Gfs2LockTableRenameNeeded(), + ) diff --git a/pcs_test/tier0/lib/cib/test_resource_primitive.py b/pcs_test/tier0/lib/cib/test_resource_primitive.py index afb797d69..f90562cf6 100644 --- a/pcs_test/tier0/lib/cib/test_resource_primitive.py +++ b/pcs_test/tier0/lib/cib/test_resource_primitive.py @@ -59,7 +59,7 @@ def setUp(self): def test_stonith(self): # pylint: disable=protected-access - results = primitive._find_primitives_by_agent( + results = primitive.find_primitives_by_agent( self.resources_section, ResourceAgentName( "stonith", @@ -77,7 +77,7 @@ def test_stonith(self): def test_with_provider(self): # pylint: disable=protected-access - results = primitive._find_primitives_by_agent( + results = primitive.find_primitives_by_agent( self.resources_section, ResourceAgentName( "standard", diff --git a/pcs_test/tier0/lib/commands/cluster/common.py b/pcs_test/tier0/lib/commands/cluster/common.py index 7f3d570f3..e5c1dee63 100644 --- a/pcs_test/tier0/lib/commands/cluster/common.py +++ b/pcs_test/tier0/lib/commands/cluster/common.py @@ -20,7 +20,12 @@ def _corosync_options_fixture(option_list, indent_level=2): ) -def corosync_conf_fixture(node_list=(), quorum_options=(), qdevice_net=False): +def corosync_conf_fixture( + node_list=(), + quorum_options=(), + qdevice_net=False, + cluster_name=CLUSTER_NAME, +): nodes = [ dedent( """\ @@ -65,7 +70,7 @@ def corosync_conf_fixture(node_list=(), quorum_options=(), qdevice_net=False): }} """ ).format( - cluster_name=CLUSTER_NAME, + cluster_name=cluster_name, nodes="\n".join(nodes), quorum=_corosync_options_fixture(quorum_options, indent_level=1), device=device, diff --git a/pcs_test/tier0/lib/commands/cluster/test_rename.py b/pcs_test/tier0/lib/commands/cluster/test_rename.py new file mode 100644 index 000000000..5801d72e7 --- /dev/null +++ b/pcs_test/tier0/lib/commands/cluster/test_rename.py @@ -0,0 +1,244 @@ +import os +from typing import Optional +from unittest import TestCase + +from pcs import settings +from pcs.common.reports import codes as report_codes +from pcs.lib.commands import cluster + +from pcs_test.tools import fixture +from pcs_test.tools.command_env import get_env_tools + +from .common import ( + corosync_conf_fixture, + node_fixture, +) + +_FIXTURE_NEW_NAME = "new" +_FIXTURE_GFS2_INVALID_NAME = "a.b" + +_FIXTURE_GENERIC_FS_RESOURCE = """ + + + + + + +""" + +_FIXTURE_GFS2_FS_RESOURCE = """ + + + + + +""" + +_FIXTURE_DLM_RESOURCE = """ + +""" + + +class CorosyncFixtureMixin: + @staticmethod + def fixture_corosync_conf(cluster_name: Optional[str] = None) -> str: + return corosync_conf_fixture( + node_list=[node_fixture("node", 1)], cluster_name=cluster_name + ) + + +class RenameCluster(CorosyncFixtureMixin, TestCase): + def setUp(self): + self.env_assist, self.config = get_env_tools(self) + self.cib_path = os.path.join(settings.cib_dir, "cib.xml") + self.runner_env = {"CIB_file": self.cib_path} + + def test_success(self): + self.config.runner.cib.load(env=self.runner_env) + self.config.corosync_conf.load_content(self.fixture_corosync_conf()) + self.config.env.push_corosync_conf( + corosync_conf_text=self.fixture_corosync_conf(_FIXTURE_NEW_NAME), + need_stopped_cluster=True, + ) + cluster.rename(self.env_assist.get_env(), _FIXTURE_NEW_NAME) + + def test_no_cib(self): + self.config.fs.exists(self.cib_path, False) + self.config.corosync_conf.load_content(self.fixture_corosync_conf()) + self.config.env.push_corosync_conf( + corosync_conf_text=self.fixture_corosync_conf(_FIXTURE_NEW_NAME), + need_stopped_cluster=True, + ) + cluster.rename(self.env_assist.get_env(), _FIXTURE_NEW_NAME) + + def test_invalid_name(self): + self.env_assist.assert_raise_library_error( + lambda: cluster.rename(self.env_assist.get_env(), "") + ) + self.env_assist.assert_reports( + [ + fixture.error( + report_codes.INVALID_OPTION_VALUE, + option_name="cluster name", + option_value="", + allowed_values=None, + cannot_be_empty=True, + forbidden_characters=None, + ) + ] + ) + + def test_gfs2_invalid_name(self): + self.env_assist.assert_raise_library_error( + lambda: cluster.rename( + self.env_assist.get_env(), _FIXTURE_GFS2_INVALID_NAME + ) + ) + self.env_assist.assert_reports( + [ + fixture.error( + report_codes.COROSYNC_CLUSTER_NAME_INVALID_FOR_GFS2, + force_code=report_codes.FORCE, + cluster_name=_FIXTURE_GFS2_INVALID_NAME, + max_length=32, + allowed_characters="a-z A-Z 0-9 _-", + ) + ] + ) + + def test_gfs2_invalid_name_forced(self): + self.config.runner.cib.load(env=self.runner_env) + self.config.corosync_conf.load_content(self.fixture_corosync_conf()) + self.config.env.push_corosync_conf( + corosync_conf_text=self.fixture_corosync_conf( + _FIXTURE_GFS2_INVALID_NAME + ), + need_stopped_cluster=True, + ) + cluster.rename( + self.env_assist.get_env(), + _FIXTURE_GFS2_INVALID_NAME, + force_flags=[report_codes.FORCE], + ) + self.env_assist.assert_reports( + [ + fixture.warn( + report_codes.COROSYNC_CLUSTER_NAME_INVALID_FOR_GFS2, + cluster_name=_FIXTURE_GFS2_INVALID_NAME, + max_length=32, + allowed_characters="a-z A-Z 0-9 _-", + ) + ] + ) + + def test_skip_offline(self): + self.config.runner.cib.load(env=self.runner_env) + self.config.corosync_conf.load_content(self.fixture_corosync_conf()) + self.config.env.push_corosync_conf( + corosync_conf_text=self.fixture_corosync_conf(_FIXTURE_NEW_NAME), + need_stopped_cluster=True, + skip_offline_targets=True, + ) + cluster.rename( + self.env_assist.get_env(), + _FIXTURE_NEW_NAME, + force_flags=[report_codes.SKIP_OFFLINE_NODES], + ) + + +class ClusterRenameCheckGfs2Resources(CorosyncFixtureMixin, TestCase): + def setUp(self): + self.env_assist, self.config = get_env_tools(self) + self.cib_path = os.path.join(settings.cib_dir, "cib.xml") + self.runner_env = {"CIB_file": self.cib_path} + + def fixture_env(self, resources): + self.config.runner.cib.load( + filename="cib-empty.xml", + env=self.runner_env, + resources=resources, + ) + self.config.corosync_conf.load_content(self.fixture_corosync_conf()) + self.config.env.push_corosync_conf( + corosync_conf_text=self.fixture_corosync_conf(_FIXTURE_NEW_NAME), + need_stopped_cluster=True, + ) + + def test_no_gfs2_in_cib(self): + self.fixture_env( + f"{_FIXTURE_GENERIC_FS_RESOURCE}" + ) + cluster.rename( + self.env_assist.get_env(), _FIXTURE_NEW_NAME, force_flags=[] + ) + + def test_gfs2_in_cib(self): + self.fixture_env(f"{_FIXTURE_GFS2_FS_RESOURCE}") + cluster.rename( + self.env_assist.get_env(), _FIXTURE_NEW_NAME, force_flags=[] + ) + self.env_assist.assert_reports( + [fixture.warn(report_codes.GFS2_LOCK_TABLE_RENAME_NEEDED)] + ) + + def test_gfs2_cloned_in_cib(self): + self.fixture_env( + f""" + + + {_FIXTURE_GFS2_FS_RESOURCE} + + + """ + ) + cluster.rename( + self.env_assist.get_env(), _FIXTURE_NEW_NAME, force_flags=[] + ) + self.env_assist.assert_reports( + [fixture.warn(report_codes.GFS2_LOCK_TABLE_RENAME_NEEDED)] + ) + + def test_dlm_in_cib(self): + self.fixture_env(f"{_FIXTURE_DLM_RESOURCE}") + cluster.rename( + self.env_assist.get_env(), _FIXTURE_NEW_NAME, force_flags=[] + ) + self.env_assist.assert_reports( + [fixture.warn(report_codes.DLM_CLUSTER_RENAME_NEEDED)] + ) + + def test_dlm_cloned_in_cib(self): + self.fixture_env( + f""" + + + {_FIXTURE_DLM_RESOURCE} + + + """ + ) + cluster.rename( + self.env_assist.get_env(), _FIXTURE_NEW_NAME, force_flags=[] + ) + self.env_assist.assert_reports( + [fixture.warn(report_codes.DLM_CLUSTER_RENAME_NEEDED)] + ) + + def test_dlm_and_gfs2(self): + self.fixture_env( + f""" + + {_FIXTURE_GFS2_FS_RESOURCE} + {_FIXTURE_DLM_RESOURCE} + + """ + ) + cluster.rename( + self.env_assist.get_env(), _FIXTURE_NEW_NAME, force_flags=[] + ) + self.env_assist.assert_reports( + [ + fixture.warn(report_codes.DLM_CLUSTER_RENAME_NEEDED), + fixture.warn(report_codes.GFS2_LOCK_TABLE_RENAME_NEEDED), + ] + ) diff --git a/pcs_test/tier0/lib/corosync/test_config_facade_misc.py b/pcs_test/tier0/lib/corosync/test_config_facade_misc.py index 1d8d63d37..ba3359dc4 100644 --- a/pcs_test/tier0/lib/corosync/test_config_facade_misc.py +++ b/pcs_test/tier0/lib/corosync/test_config_facade_misc.py @@ -39,6 +39,30 @@ def test_more_sections(self): ) +class SetClusterName(TestCase): + @staticmethod + def _fixture_facade(config: str) -> lib.ConfigFacade: + return lib.ConfigFacade(Parser.parse(config.encode("utf-8"))) + + def test_replace_old_name(self): + facade = self._fixture_facade("totem {\n cluster_name: NAME\n}\n") + facade.set_cluster_name("a") + self.assertTrue(facade.need_stopped_cluster) + self.assertEqual(facade.get_cluster_name(), "a") + + def test_missing_totem_section(self): + facade = self._fixture_facade("") + facade.set_cluster_name("a") + self.assertTrue(facade.need_stopped_cluster) + self.assertEqual(facade.get_cluster_name(), "a") + + def test_missing_cluster_name_option(self): + facade = self._fixture_facade("totem {\n}\n") + facade.set_cluster_name("a") + self.assertTrue(facade.need_stopped_cluster) + self.assertEqual(facade.get_cluster_name(), "a") + + class GetTransport(GetSimpleValueMixin, TestCase): @staticmethod def getter(facade): diff --git a/pcs_test/tier0/lib/corosync/test_config_validators_rename_cluster.py b/pcs_test/tier0/lib/corosync/test_config_validators_rename_cluster.py new file mode 100644 index 000000000..790b23844 --- /dev/null +++ b/pcs_test/tier0/lib/corosync/test_config_validators_rename_cluster.py @@ -0,0 +1,74 @@ +from unittest import TestCase + +from pcs.common.reports import codes as report_codes +from pcs.lib.corosync import config_validators + +from pcs_test.tools import fixture +from pcs_test.tools.assertions import assert_report_item_list_equal + + +class RenameCluster(TestCase): + # pylint: disable=no-self-use + def test_valid_cluster_name(self): + assert_report_item_list_equal( + config_validators.rename_cluster("my-cluster", False), [] + ) + + def test_empty_name(self): + assert_report_item_list_equal( + config_validators.rename_cluster(""), + [ + fixture.error( + report_codes.INVALID_OPTION_VALUE, + option_name="cluster name", + option_value="", + allowed_values=None, + cannot_be_empty=True, + forbidden_characters=None, + ) + ], + ) + + def test_gfs2_too_long(self): + assert_report_item_list_equal( + config_validators.rename_cluster(33 * "a"), + [ + fixture.error( + report_codes.COROSYNC_CLUSTER_NAME_INVALID_FOR_GFS2, + force_code=report_codes.FORCE, + cluster_name=(33 * "a"), + max_length=32, + allowed_characters="a-z A-Z 0-9 _-", + ), + ], + ) + + def test_gfs2_bad_characters(self): + assert_report_item_list_equal( + config_validators.rename_cluster("cluster.name"), + [ + fixture.error( + report_codes.COROSYNC_CLUSTER_NAME_INVALID_FOR_GFS2, + force_code=report_codes.FORCE, + cluster_name="cluster.name", + max_length=32, + allowed_characters="a-z A-Z 0-9 _-", + ), + ], + ) + + def test_gfs2_forced(self): + cluster_name = (16 * "a") + ".: @" + (16 * "b") + assert_report_item_list_equal( + config_validators.rename_cluster( + cluster_name, force_cluster_name=True + ), + [ + fixture.warn( + report_codes.COROSYNC_CLUSTER_NAME_INVALID_FOR_GFS2, + cluster_name=cluster_name, + max_length=32, + allowed_characters="a-z A-Z 0-9 _-", + ), + ], + ) diff --git a/pcs_test/tier0/lib/pacemaker/test_live.py b/pcs_test/tier0/lib/pacemaker/test_live.py index fab9f8291..1864e6968 100644 --- a/pcs_test/tier0/lib/pacemaker/test_live.py +++ b/pcs_test/tier0/lib/pacemaker/test_live.py @@ -1,4 +1,5 @@ # pylint: disable=too-many-lines +import os from unittest import ( TestCase, mock, @@ -352,7 +353,10 @@ def test_warnings_verbose(self): ) -class GetCibXmlTest(TestCase): +class GetCibXmlTestMixin: + def _call_command(self, runner, scope=None): + raise NotImplementedError() + def test_success(self): expected_stdout = "" expected_stderr = "" @@ -361,10 +365,11 @@ def test_success(self): expected_stdout, expected_stderr, expected_retval ) - real_xml = lib.get_cib_xml(mock_runner) + real_xml = self._call_command(mock_runner) mock_runner.run.assert_called_once_with( - [settings.cibadmin_exec, "--local", "--query"] + [settings.cibadmin_exec, "--local", "--query"], + env_extend=self.env_extend, ) self.assertEqual(expected_stdout, real_xml) @@ -378,7 +383,7 @@ def test_error(self): ) assert_raise_library_error( - lambda: lib.get_cib_xml(mock_runner), + lambda: self._call_command(mock_runner), ( Severity.ERROR, report_codes.CIB_LOAD_ERROR, @@ -389,7 +394,8 @@ def test_error(self): ) mock_runner.run.assert_called_once_with( - [settings.cibadmin_exec, "--local", "--query"] + [settings.cibadmin_exec, "--local", "--query"], + env_extend=self.env_extend, ) def test_success_scope(self): @@ -401,7 +407,7 @@ def test_success_scope(self): expected_stdout, expected_stderr, expected_retval ) - real_xml = lib.get_cib_xml(mock_runner, scope) + real_xml = self._call_command(mock_runner, scope) mock_runner.run.assert_called_once_with( [ @@ -409,7 +415,8 @@ def test_success_scope(self): "--local", "--query", "--scope={0}".format(scope), - ] + ], + env_extend=self.env_extend, ) self.assertEqual(expected_stdout, real_xml) @@ -428,7 +435,7 @@ def test_scope_error(self): ) assert_raise_library_error( - lambda: lib.get_cib_xml(mock_runner, scope=scope), + lambda: self._call_command(mock_runner, scope=scope), ( Severity.ERROR, report_codes.CIB_LOAD_ERROR_SCOPE_MISSING, @@ -445,10 +452,25 @@ def test_scope_error(self): "--local", "--query", "--scope={0}".format(scope), - ] + ], + env_extend=self.env_extend, ) +class GetCibXmlTest(GetCibXmlTestMixin, TestCase): + env_extend = None + + def _call_command(self, runner, scope=None): + return lib.get_cib_xml(runner, scope) + + +class GetCibDirectXmlTest(GetCibXmlTestMixin, TestCase): + env_extend = {"CIB_file": os.path.join(settings.cib_dir, "cib.xml")} + + def _call_command(self, runner, scope=None): + return lib.get_cib_direct_xml(runner, scope) + + class GetCibTest(TestCase): # pylint: disable=no-self-use def test_success(self): diff --git a/pcs_test/tier1/cluster/test_cluster_rename.py b/pcs_test/tier1/cluster/test_cluster_rename.py new file mode 100644 index 000000000..17021bf4a --- /dev/null +++ b/pcs_test/tier1/cluster/test_cluster_rename.py @@ -0,0 +1,45 @@ +from unittest import TestCase + +from pcs_test.tools.assertions import AssertPcsMixin +from pcs_test.tools.misc import get_test_resource +from pcs_test.tools.pcs_runner import PcsRunner + + +class ClusterRename(AssertPcsMixin, TestCase): + def setUp(self): + self.pcs_runner = PcsRunner(None) + + def test_no_args(self): + self.assert_pcs_fail( + ["cluster", "rename"], + stderr_start="\nUsage: pcs cluster rename...\n", + ) + + def test_too_many_args(self): + self.assert_pcs_fail( + ["cluster", "rename", "A", "B"], + stderr_start="\nUsage: pcs cluster rename...\n", + ) + + def test_not_live_pcmk(self): + self.pcs_runner = PcsRunner(cib_file=get_test_resource("cib-empty.xml")) + self.assert_pcs_fail( + ["cluster", "rename", "A"], + stderr_full=( + "Error: Specified option '-f' is not supported in this " + "command\n" + ), + ) + + def test_not_live_corosync(self): + self.pcs_runner = PcsRunner( + cib_file=None, + corosync_conf_opt=get_test_resource("corosync_conf"), + ) + self.assert_pcs_fail( + ["cluster", "rename", "A"], + stderr_full=( + "Error: Specified option '--corosync_conf' is not supported in " + "this command\n" + ), + ) diff --git a/pcsd/capabilities.xml.in b/pcsd/capabilities.xml.in index 43805dbae..29926233d 100644 --- a/pcsd/capabilities.xml.in +++ b/pcsd/capabilities.xml.in @@ -249,6 +249,14 @@ Check cluster configuration for errors. + + + Rename configured cluster. The cluster has to be stopped + + pcs commands: cluster rename + API v2: cluster.rename + + From 60451d745bed2a084e9e2aac2db332dfb45b3fcf Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Thu, 23 Jan 2025 08:35:25 +0100 Subject: [PATCH 100/227] remove cluster-name property from CIB on nodes --- CHANGELOG.md | 3 + pcs/cli/routing/cluster.py | 2 +- pcs/common/reports/codes.py | 7 +- pcs/common/reports/messages.py | 71 ++++ pcs/daemon/app/api_v1.py | 1 + .../async_tasks/worker/command_mapping.py | 4 + pcs/lib/commands/cluster.py | 54 ++- pcs/lib/commands/cluster_property.py | 36 ++ pcs/lib/communication/cluster.py | 64 ++++ pcs/lib/pacemaker/live.py | 35 +- pcs/pcs.8.in | 10 + pcs/usage.py | 11 +- .../tier0/common/reports/test_messages.py | 47 ++- .../tier0/lib/commands/cluster/test_rename.py | 316 +++++++++++++++++- .../lib/commands/test_cluster_property.py | 73 +++- pcs_test/tier0/lib/pacemaker/test_live.py | 40 +-- pcsd/capabilities.xml.in | 23 +- 17 files changed, 702 insertions(+), 95 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6044ebcc3..942716ea7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,10 @@ when new CIB does not conform to the CIB schema ([RHEL-76059]) - Command `pcs stonith sbd watchdog list` now prints watchdogs' identity and driver ([RHEL-76177]) +- Command `pcs cluster rename` for changing cluster name [RHEL-76055] + +[RHEL-76055]: https://issues.redhat.com/browse/RHEL-76055 [RHEL-76059]: https://issues.redhat.com/browse/RHEL-76059 [RHEL-76177]: https://issues.redhat.com/browse/RHEL-76177 diff --git a/pcs/cli/routing/cluster.py b/pcs/cli/routing/cluster.py index ad49edb28..2dc98adcc 100644 --- a/pcs/cli/routing/cluster.py +++ b/pcs/cli/routing/cluster.py @@ -116,8 +116,8 @@ def pcsd_status(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: "destroy": cluster.cluster_destroy, "verify": cluster.cluster_verify, "report": cluster.cluster_report, - "rename": cluster_command.cluster_rename, "remove_nodes_from_cib": cluster.remove_nodes_from_cib, + "rename": cluster_command.cluster_rename, }, ["cluster"], ) diff --git a/pcs/common/reports/codes.py b/pcs/common/reports/codes.py index 6e3a19855..226f82438 100644 --- a/pcs/common/reports/codes.py +++ b/pcs/common/reports/codes.py @@ -138,6 +138,9 @@ CIB_ALERT_RECIPIENT_ALREADY_EXISTS = M("CIB_ALERT_RECIPIENT_ALREADY_EXISTS") CIB_ALERT_RECIPIENT_VALUE_INVALID = M("CIB_ALERT_RECIPIENT_VALUE_INVALID") CIB_CANNOT_FIND_MANDATORY_SECTION = M("CIB_CANNOT_FIND_MANDATORY_SECTION") +CIB_CLUSTER_NAME_REMOVAL_FAILED = M("CIB_CLUSTER_NAME_REMOVAL_FAILED") +CIB_CLUSTER_NAME_REMOVAL_STARTED = M("CIB_CLUSTER_NAME_REMOVAL_STARTED") +CIB_CLUSTER_NAME_REMOVED = M("CIB_CLUSTER_NAME_REMOVED") CIB_DIFF_ERROR = M("CIB_DIFF_ERROR") CIB_FENCING_LEVEL_ALREADY_EXISTS = M("CIB_FENCING_LEVEL_ALREADY_EXISTS") CIB_FENCING_LEVEL_DOES_NOT_EXIST = M("CIB_FENCING_LEVEL_DOES_NOT_EXIST") @@ -159,6 +162,7 @@ "CIB_UPGRADE_FAILED_TO_MINIMAL_REQUIRED_VERSION" ) CIB_UPGRADE_SUCCESSFUL = M("CIB_UPGRADE_SUCCESSFUL") +CIB_XML_MISSING = M("CIB_XML_MISSING") CLUSTER_DESTROY_STARTED = M("CLUSTER_DESTROY_STARTED") CLUSTER_DESTROY_SUCCESS = M("CLUSTER_DESTROY_SUCCESS") CLUSTER_ENABLE_STARTED = M("CLUSTER_ENABLE_STARTED") @@ -356,8 +360,9 @@ NOT_AUTHORIZED = M("NOT_AUTHORIZED") OBJECT_WITH_ID_IN_UNEXPECTED_CONTEXT = M("OBJECT_WITH_ID_IN_UNEXPECTED_CONTEXT") OMITTING_NODE = M("OMITTING_NODE") -PACEMAKER_SIMULATION_RESULT = M("PACEMAKER_SIMULATION_RESULT") PACEMAKER_LOCAL_NODE_NAME_NOT_FOUND = M("PACEMAKER_LOCAL_NODE_NAME_NOT_FOUND") +PACEMAKER_RUNNING = M("PACEMAKER_RUNNING") +PACEMAKER_SIMULATION_RESULT = M("PACEMAKER_SIMULATION_RESULT") PARSE_ERROR_COROSYNC_CONF = M("PARSE_ERROR_COROSYNC_CONF") PARSE_ERROR_COROSYNC_CONF_EXTRA_CHARACTERS_AFTER_OPENING_BRACE = M( "PARSE_ERROR_COROSYNC_CONF_EXTRA_CHARACTERS_AFTER_OPENING_BRACE" diff --git a/pcs/common/reports/messages.py b/pcs/common/reports/messages.py index 29cfe75c6..9751ec863 100644 --- a/pcs/common/reports/messages.py +++ b/pcs/common/reports/messages.py @@ -8194,3 +8194,74 @@ def message(self) -> str: "reflect the name of the cluster so that the filesystems can be " "mounted" ) + + +@dataclass(frozen=True) +class CibClusterNameRemovalStarted(ReportItemMessage): + """ + Cluster name property is about to be deleted on nodes + """ + + _code = codes.CIB_CLUSTER_NAME_REMOVAL_STARTED + + @property + def message(self) -> str: + return "Removing CIB cluster name property on nodes..." + + +@dataclass(frozen=True) +class CibClusterNameRemoved(ReportItemMessage): + """ + Cluster name property has been deleted from CIB on node. + + node -- node address / name + """ + + node: str + _code = codes.CIB_CLUSTER_NAME_REMOVED + + @property + def message(self) -> str: + return f"{self.node}: Succeeded" + + +@dataclass(frozen=True) +class CibClusterNameRemovalFailed(ReportItemMessage): + """ + Cluster name property removal has failed + + reason -- description of the error + """ + + reason: str + _code = codes.CIB_CLUSTER_NAME_REMOVAL_FAILED + + @property + def message(self) -> str: + return f"CIB cluster name property removal failed: {self.reason}" + + +@dataclass(frozen=True) +class PacemakerRunning(ReportItemMessage): + """ + Pacemaker is running on a node. + """ + + _code = codes.PACEMAKER_RUNNING + + @property + def message(self) -> str: + return "Pacemaker is running" + + +@dataclass(frozen=True) +class CibXmlMissing(ReportItemMessage): + """ + CIB XML file cannot be found + """ + + _code = codes.CIB_XML_MISSING + + @property + def message(self) -> str: + return "CIB XML file cannot be found" diff --git a/pcs/daemon/app/api_v1.py b/pcs/daemon/app/api_v1.py index 11d94e375..5a5b265d1 100644 --- a/pcs/daemon/app/api_v1.py +++ b/pcs/daemon/app/api_v1.py @@ -59,6 +59,7 @@ "cluster-remove-nodes/v1": "cluster.remove_nodes", "cluster-setup/v1": "cluster.setup", "cluster-generate-cluster-uuid/v1": "cluster.generate_cluster_uuid", + "cluster-property-remove-name/v1": "cluster_property.remove_cluster_name", "constraint-colocation-create-with-set/v1": "constraint.colocation.create_with_set", "constraint-order-create-with-set/v1": "constraint.order.create_with_set", "constraint-ticket-create-with-set/v1": "constraint.ticket.create_with_set", diff --git a/pcs/daemon/async_tasks/worker/command_mapping.py b/pcs/daemon/async_tasks/worker/command_mapping.py index 3a2d24daa..005a50e0c 100644 --- a/pcs/daemon/async_tasks/worker/command_mapping.py +++ b/pcs/daemon/async_tasks/worker/command_mapping.py @@ -161,6 +161,10 @@ class _Cmd: cmd=cluster_property.set_properties, required_permission=p.WRITE, ), + "cluster_property.remove_cluster_name": _Cmd( + cmd=cluster_property.remove_cluster_name, + required_permission=p.WRITE, # ?? maybe superuser + ), "cluster.wait_for_pcmk_idle": _Cmd( cmd=cluster.wait_for_pcmk_idle, required_permission=p.READ, diff --git a/pcs/lib/commands/cluster.py b/pcs/lib/commands/cluster.py index 7abee5d88..ebea42da0 100644 --- a/pcs/lib/commands/cluster.py +++ b/pcs/lib/commands/cluster.py @@ -77,7 +77,12 @@ EnableSbdService, SetSbdConfig, ) -from pcs.lib.communication.tools import AllSameDataMixin, run_and_raise +from pcs.lib.communication.tools import ( + AllSameDataMixin, + CommunicationCommandInterface, + run, + run_and_raise, +) from pcs.lib.communication.tools import run as run_com from pcs.lib.corosync import ( config_facade, @@ -96,7 +101,7 @@ from pcs.lib.node import get_existing_nodes_names from pcs.lib.pacemaker.live import ( get_cib, - get_cib_direct_xml, + get_cib_file_runner_env, get_cib_xml, get_cib_xml_cmd_results, has_cib_xml, @@ -1388,6 +1393,7 @@ def add_nodes( # noqa: PLR0912, PLR0915 if atb_has_to_be_enabled: corosync_conf.set_quorum_options(dict(auto_tie_breaker="1")) + # TODO why this does not use push_corosync_conf? _verify_corosync_conf(corosync_conf) # raises if corosync not valid com_cmd = DistributeCorosyncConf( env.report_processor, @@ -1971,7 +1977,7 @@ def remove_nodes_from_cib(env: LibraryEnvironment, node_list): "--force", f"--xpath=/cib/configuration/nodes/node[@uname='{node}']", ], - env_extend={"CIB_file": os.path.join(settings.cib_dir, "cib.xml")}, + env_extend=get_cib_file_runner_env(), ) if retval != 0: raise LibraryError( @@ -2381,15 +2387,47 @@ def rename( ).has_errors: raise LibraryError() + cib = None if has_cib_xml(): - cib = get_cib(get_cib_direct_xml(env.cmd_runner())) + cib = get_cib(get_cib_xml(env.cmd_runner(get_cib_file_runner_env()))) resources = get_resources(cib) env.report_processor.report_list(_warn_dlm_resources(resources)) env.report_processor.report_list(_warn_gfs2_resources(resources)) corosync_conf = env.get_corosync_conf() - corosync_conf.set_cluster_name(new_name) - env.push_corosync_conf( - corosync_conf, - skip_offline_nodes=reports.codes.SKIP_OFFLINE_NODES in force_flags, + skip_offline = report_codes.SKIP_OFFLINE_NODES in force_flags + + corosync_nodes, report_list = get_existing_nodes_names(corosync_conf, None) + if env.report_processor.report_list(report_list).has_errors: + raise LibraryError() + target_list = env.get_node_target_factory().get_target_list( + corosync_nodes, + allow_skip=skip_offline, ) + + node_communicator = env.get_node_communicator() + com_cmd: CommunicationCommandInterface + + com_cmd = CheckCorosyncOffline(env.report_processor, skip_offline) + com_cmd.set_targets(target_list) + running_targets = run(node_communicator, com_cmd) + if running_targets: + env.report_processor.report( + reports.ReportItem.error( + reports.messages.CorosyncNotRunningCheckFinishedRunning( + [target.label for target in running_targets] + ) + ) + ) + if env.report_processor.has_errors: + raise LibraryError() + + # The 'cluster-name' property has to be removed from CIB on all nodes, so + # that it is initialized with the new cluster name from corosync after the + # cluster is started + com_cmd = cluster.RemoveCibClusterName(env.report_processor, skip_offline) + com_cmd.set_targets(target_list) + run_and_raise(node_communicator, com_cmd) + + corosync_conf.set_cluster_name(new_name) + env.push_corosync_conf(corosync_conf, skip_offline) diff --git a/pcs/lib/commands/cluster_property.py b/pcs/lib/commands/cluster_property.py index 000dacfb1..71b0523ea 100644 --- a/pcs/lib/commands/cluster_property.py +++ b/pcs/lib/commands/cluster_property.py @@ -4,9 +4,11 @@ Union, ) +from pcs import settings from pcs.common import reports from pcs.common.pacemaker.cluster_property import ClusterPropertyMetadataDto from pcs.common.pacemaker.nvset import ListCibNvsetDto +from pcs.common.str_tools import join_multilines from pcs.common.types import StringSequence from pcs.lib import cluster_property from pcs.lib.cib import ( @@ -20,6 +22,7 @@ ) from pcs.lib.env import LibraryEnvironment from pcs.lib.errors import LibraryError +from pcs.lib.pacemaker.live import get_cib_file_runner_env, has_cib_xml from pcs.lib.resource_agent import ( ResourceAgentError, ResourceAgentFacade, @@ -228,3 +231,36 @@ def get_properties_metadata( ], readonly_properties=cluster_property.READONLY_CLUSTER_PROPERTY_LIST, ) + + +def remove_cluster_name(env: LibraryEnvironment) -> None: + """ + Remove cluster-name property from CIB on local node. The cluster has to be + stopped and the property is removed directly from the CIB file. + """ + if env.service_manager.is_running("pacemaker"): + env.report_processor.report( + reports.ReportItem.error(reports.messages.PacemakerRunning()) + ) + raise LibraryError() + + if not has_cib_xml(): + env.report_processor.report( + reports.ReportItem.error(reports.messages.CibXmlMissing()) + ) + raise LibraryError() + + xpath = "/cib/configuration/crm_config/cluster_property_set/nvpair[@name='cluster-name']" + stdout, stderr, retval = env.cmd_runner().run( + [settings.cibadmin_exec, "--delete-all", "--force", f"--xpath={xpath}"], + env_extend=get_cib_file_runner_env(), + ) + if retval != 0: + env.report_processor.report( + reports.ReportItem.error( + reports.messages.CibClusterNameRemovalFailed( + reason=join_multilines([stderr, stdout]) + ) + ) + ) + raise LibraryError() diff --git a/pcs/lib/communication/cluster.py b/pcs/lib/communication/cluster.py index 4018dfae7..7c94bc0bf 100644 --- a/pcs/lib/communication/cluster.py +++ b/pcs/lib/communication/cluster.py @@ -1,6 +1,11 @@ +import json from typing import Optional +from dacite import DaciteError + from pcs.common import reports +from pcs.common.communication.dto import InternalCommunicationResultDto +from pcs.common.interface.dto import from_dict from pcs.common.node_communicator import RequestData from pcs.common.reports.item import ReportItem from pcs.lib.communication.tools import ( @@ -129,3 +134,62 @@ def on_complete( self, ) -> tuple[Optional[bool], Optional[QuorumStatusFacade]]: return self._has_failure, self._quorum_status_facade + + +class RemoveCibClusterName( + SkipOfflineMixin, + AllSameDataMixin, + AllAtOnceStrategyMixin, + RunRemotelyBase, +): + def __init__(self, report_processor, skip_offline_targets=False): + super().__init__(report_processor) + self._set_skip_offline(skip_offline_targets) + + def before(self): + self._report( + reports.ReportItem.info( + reports.messages.CibClusterNameRemovalStarted() + ) + ) + + def _get_request_data(self): + return RequestData( + "api/v1/cluster-property-remove-name/v1", data=json.dumps({}) + ) + + def _process_response(self, response): + report_item = self._get_response_report(response) + if report_item: + self._report(report_item) + return + node_label = response.request.target.label + + report_list = [] + try: + result = from_dict( + InternalCommunicationResultDto, json.loads(response.data) + ) + context = reports.ReportItemContext(node_label) + + report_list.extend( + reports.report_dto_to_item(report, context) + for report in result.report_list + ) + except (json.JSONDecodeError, DaciteError): + report_list.append( + reports.ReportItem.error( + reports.messages.InvalidResponseFormat(node_label) + ) + ) + + self._report_list(report_list) + if not any( + report.severity.level == reports.ReportItemSeverity.ERROR + for report in report_list + ): + self._report( + reports.ReportItem.info( + reports.messages.CibClusterNameRemoved(node_label) + ) + ) diff --git a/pcs/lib/pacemaker/live.py b/pcs/lib/pacemaker/live.py index c3a502714..b96467661 100644 --- a/pcs/lib/pacemaker/live.py +++ b/pcs/lib/pacemaker/live.py @@ -150,42 +150,17 @@ def has_cib_xml() -> bool: def get_cib_xml_cmd_results( - runner: CommandRunner, - scope: Optional[str] = None, - env_extend: Optional[Mapping[str, str]] = None, + runner: CommandRunner, scope: Optional[str] = None ) -> tuple[str, str, int]: command = [settings.cibadmin_exec, "--local", "--query"] if scope: command.append(f"--scope={scope}") - stdout, stderr, returncode = runner.run(command, env_extend=env_extend) + stdout, stderr, returncode = runner.run(command) return stdout, stderr, returncode def get_cib_xml(runner: CommandRunner, scope: Optional[str] = None) -> str: - return _get_cib_xml_common(*get_cib_xml_cmd_results(runner, scope), scope) - - -def get_cib_direct_xml( - runner: CommandRunner, scope: Optional[str] = None -) -> str: - """ - Return cib directly from filesystem. Useful when working with cib on - stopped cluster - """ - - return _get_cib_xml_common( - *get_cib_xml_cmd_results( - runner, - scope, - {"CIB_file": os.path.join(settings.cib_dir, "cib.xml")}, - ), - scope, - ) - - -def _get_cib_xml_common( - stdout: str, stderr: str, retval: int, scope: Optional[str] = None -) -> str: + stdout, stderr, retval = get_cib_xml_cmd_results(runner, scope) if retval != 0: if retval == __EXITCODE_CIB_SCOPE_VALID_BUT_NOT_PRESENT and scope: raise LibraryError( @@ -203,6 +178,10 @@ def _get_cib_xml_common( return stdout +def get_cib_file_runner_env() -> dict[str, str]: + return {"CIB_file": os.path.join(settings.cib_dir, "cib.xml")} + + def parse_cib_xml(xml: str) -> _Element: return xml_fromstring(xml) diff --git a/pcs/pcs.8.in b/pcs/pcs.8.in index 814ac2bc3..b4a70b6ac 100644 --- a/pcs/pcs.8.in +++ b/pcs/pcs.8.in @@ -581,6 +581,16 @@ View current cluster status (an alias of 'pcs status cluster'). .TP rename Rename configured cluster. The cluster has to be stopped to complete this operation. + +Manual steps are needed in case the cluster uses GFS2 filesystem or DLM: +.br +for GFS2: +.br + The lock table name on each GFS2 filesystem must be updated to reflect the new name of the cluster so that the filesystems can be mounted. +.br +for DLM: +.br + The DLM cluster name in the shared volume groups metadata must be updated so that the volume groups can start. .TP sync Sync cluster configuration (files which are supported by all subcommands of this command) to all cluster nodes. diff --git a/pcs/usage.py b/pcs/usage.py index 3bf56d302..2db89ed56 100644 --- a/pcs/usage.py +++ b/pcs/usage.py @@ -1672,7 +1672,16 @@ def cluster(args: Argv) -> str: rename Rename configured cluster. The cluster has to be stopped to complete - this operation. + this operation. + + Manual steps are needed in case the cluster uses GFS2 filesystem or DLM: + for GFS2: + The lock table name on each GFS2 filesystem must be updated + to reflect the new name of the cluster so that the filesystems + can be mounted. + for DLM: + The DLM cluster name in the shared volume groups metadata must + be updated so that the volume groups can start. sync Sync cluster configuration (files which are supported by all diff --git a/pcs_test/tier0/common/reports/test_messages.py b/pcs_test/tier0/common/reports/test_messages.py index 0956c524a..b73927ad5 100644 --- a/pcs_test/tier0/common/reports/test_messages.py +++ b/pcs_test/tier0/common/reports/test_messages.py @@ -6012,13 +6012,13 @@ def test_message(self) -> str: class StoppingResourcesBeforeDeleting(NameBuildTest): - def test_one_resource(self): + def test_one_resource(self) -> str: self.assert_message_from_report( "Stopping resource 'resourceId' before deleting", reports.StoppingResourcesBeforeDeleting(["resourceId"]), ) - def test_multiple_resources(self): + def test_multiple_resources(self) -> str: self.assert_message_from_report( "Stopping resources 'resourceId1', 'resourceId2' before deleting", reports.StoppingResourcesBeforeDeleting( @@ -6028,7 +6028,7 @@ def test_multiple_resources(self): class StoppingResourcesBeforeDeletingSkipped(NameBuildTest): - def test_success(self): + def test_success(self) -> str: self.assert_message_from_report( ( "Resources are not going to be stopped before deletion, this " @@ -6039,13 +6039,13 @@ def test_success(self): class CannotStopResourcesBeforeDeleting(NameBuildTest): - def test_one_resource(self): + def test_one_resource(self) -> str: self.assert_message_from_report( "Cannot stop resource 'resourceId' before deleting", reports.CannotStopResourcesBeforeDeleting(["resourceId"]), ) - def test_multiple_resources(self): + def test_multiple_resources(self) -> str: self.assert_message_from_report( "Cannot stop resources 'resourceId1', 'resourceId2' before deleting", reports.CannotStopResourcesBeforeDeleting( @@ -6101,3 +6101,40 @@ def test_success(self): ), reports.Gfs2LockTableRenameNeeded(), ) + + +class CibClusterNameRemovalStarted(NameBuildTest): + def test_success(self): + self.assert_message_from_report( + "Removing CIB cluster name property on nodes...", + reports.CibClusterNameRemovalStarted(), + ) + + +class CibClusterNameRemoved(NameBuildTest): + def test_success(self): + self.assert_message_from_report( + "node: Succeeded", reports.CibClusterNameRemoved("node") + ) + + +class CibClusterNameRemovalFailed(NameBuildTest): + def test_success(self): + self.assert_message_from_report( + "CIB cluster name property removal failed: reason", + reports.CibClusterNameRemovalFailed("reason"), + ) + + +class PacemakerRunning(NameBuildTest): + def test_success(self): + self.assert_message_from_report( + "Pacemaker is running", reports.PacemakerRunning() + ) + + +class CibXmlMissing(NameBuildTest): + def test_success(self): + self.assert_message_from_report( + "CIB XML file cannot be found", reports.CibXmlMissing() + ) diff --git a/pcs_test/tier0/lib/commands/cluster/test_rename.py b/pcs_test/tier0/lib/commands/cluster/test_rename.py index 5801d72e7..90d971486 100644 --- a/pcs_test/tier0/lib/commands/cluster/test_rename.py +++ b/pcs_test/tier0/lib/commands/cluster/test_rename.py @@ -1,19 +1,21 @@ +import json import os from typing import Optional from unittest import TestCase from pcs import settings +from pcs.common import reports from pcs.common.reports import codes as report_codes from pcs.lib.commands import cluster from pcs_test.tools import fixture from pcs_test.tools.command_env import get_env_tools - -from .common import ( - corosync_conf_fixture, - node_fixture, +from pcs_test.tools.command_env.config_http_corosync import ( + corosync_running_check_response, ) +from .common import corosync_conf_fixture, node_fixture + _FIXTURE_NEW_NAME = "new" _FIXTURE_GFS2_INVALID_NAME = "a.b" @@ -39,37 +41,111 @@ """ -class CorosyncFixtureMixin: +class FixtureMixin: + node_labels = ["node-1", "node-2"] + @staticmethod def fixture_corosync_conf(cluster_name: Optional[str] = None) -> str: return corosync_conf_fixture( - node_list=[node_fixture("node", 1)], cluster_name=cluster_name + node_list=[node_fixture("node-1", 1), node_fixture("node-2", 2)], + cluster_name=cluster_name, + ) + + def fixture_remove_name_prop_call( + self, communication_list=None, output=None + ): + self.config.http.place_multinode_call( + "cluster.remove-name", + communication_list=( + communication_list + if communication_list + else [{"label": node} for node in self.node_labels] + ), + action="api/v1/cluster-property-remove-name/v1", + raw_data=json.dumps({}), + output=( + output + if output + else json.dumps( + { + "status": "success", + "status_msg": None, + "report_list": [], + "data": "", + } + ) + ), + ) + + def fixture_corosync_offline_check_reports(self): + reports = [ + fixture.info(report_codes.COROSYNC_NOT_RUNNING_CHECK_STARTED) + ] + reports.extend( + fixture.info( + report_codes.COROSYNC_NOT_RUNNING_CHECK_NODE_STOPPED, node=n + ) + for n in self.node_labels + ) + return reports + + def fixture_remove_name_prop_reports(self): + reports = [fixture.info(report_codes.CIB_CLUSTER_NAME_REMOVAL_STARTED)] + reports.extend( + fixture.info(report_codes.CIB_CLUSTER_NAME_REMOVED, node=n) + for n in self.node_labels ) + return reports + + def _offline_node_communication_list(self): + return [ + { + "label": self.node_labels[0], + "response_code": 400, + "output": "Fail", + } + ] + [{"label": node} for node in self.node_labels[1:]] -class RenameCluster(CorosyncFixtureMixin, TestCase): +class RenameCluster(FixtureMixin, TestCase): def setUp(self): self.env_assist, self.config = get_env_tools(self) self.cib_path = os.path.join(settings.cib_dir, "cib.xml") self.runner_env = {"CIB_file": self.cib_path} + self.config.env.set_known_nodes(self.node_labels) def test_success(self): + self.config.fs.exists(self.cib_path, return_value=True) self.config.runner.cib.load(env=self.runner_env) self.config.corosync_conf.load_content(self.fixture_corosync_conf()) + self.config.http.corosync.check_corosync_offline(self.node_labels) + self.fixture_remove_name_prop_call() self.config.env.push_corosync_conf( corosync_conf_text=self.fixture_corosync_conf(_FIXTURE_NEW_NAME), need_stopped_cluster=True, ) + cluster.rename(self.env_assist.get_env(), _FIXTURE_NEW_NAME) + self.env_assist.assert_reports( + self.fixture_corosync_offline_check_reports() + + self.fixture_remove_name_prop_reports() + ) def test_no_cib(self): self.config.fs.exists(self.cib_path, False) self.config.corosync_conf.load_content(self.fixture_corosync_conf()) + self.config.http.corosync.check_corosync_offline(self.node_labels) + self.fixture_remove_name_prop_call() self.config.env.push_corosync_conf( corosync_conf_text=self.fixture_corosync_conf(_FIXTURE_NEW_NAME), need_stopped_cluster=True, ) + cluster.rename(self.env_assist.get_env(), _FIXTURE_NEW_NAME) + self.env_assist.assert_reports( + self.fixture_corosync_offline_check_reports() + + self.fixture_remove_name_prop_reports() + ) def test_invalid_name(self): self.env_assist.assert_raise_library_error( @@ -107,14 +183,18 @@ def test_gfs2_invalid_name(self): ) def test_gfs2_invalid_name_forced(self): + self.config.fs.exists(self.cib_path, return_value=True) self.config.runner.cib.load(env=self.runner_env) self.config.corosync_conf.load_content(self.fixture_corosync_conf()) + self.config.http.corosync.check_corosync_offline(self.node_labels) + self.fixture_remove_name_prop_call() self.config.env.push_corosync_conf( corosync_conf_text=self.fixture_corosync_conf( _FIXTURE_GFS2_INVALID_NAME ), need_stopped_cluster=True, ) + cluster.rename( self.env_assist.get_env(), _FIXTURE_GFS2_INVALID_NAME, @@ -129,36 +209,242 @@ def test_gfs2_invalid_name_forced(self): allowed_characters="a-z A-Z 0-9 _-", ) ] + + self.fixture_corosync_offline_check_reports() + + self.fixture_remove_name_prop_reports() + ) + + def test_node_offline_corosync_check(self): + self.config.fs.exists(self.cib_path, return_value=True) + self.config.runner.cib.load(env=self.runner_env) + self.config.corosync_conf.load_content(self.fixture_corosync_conf()) + self.config.http.corosync.check_corosync_offline( + communication_list=self._offline_node_communication_list() + ) + + self.env_assist.assert_raise_library_error( + lambda: cluster.rename(self.env_assist.get_env(), _FIXTURE_NEW_NAME) + ) + self.env_assist.assert_reports( + [ + fixture.info(report_codes.COROSYNC_NOT_RUNNING_CHECK_STARTED), + fixture.error( + report_codes.NODE_COMMUNICATION_COMMAND_UNSUCCESSFUL, + node=self.node_labels[0], + command="remote/status", + reason="Fail", + force_code=report_codes.SKIP_OFFLINE_NODES, + ), + fixture.error( + report_codes.COROSYNC_NOT_RUNNING_CHECK_NODE_ERROR, + node=self.node_labels[0], + force_code=report_codes.SKIP_OFFLINE_NODES, + ), + fixture.info( + report_codes.COROSYNC_NOT_RUNNING_CHECK_NODE_STOPPED, + node=self.node_labels[1], + ), + ] + ) + + def test_node_offline_property_removal(self): + self.config.fs.exists(self.cib_path, return_value=True) + self.config.runner.cib.load(env=self.runner_env) + self.config.corosync_conf.load_content(self.fixture_corosync_conf()) + self.config.http.corosync.check_corosync_offline(self.node_labels) + self.fixture_remove_name_prop_call( + communication_list=self._offline_node_communication_list() + ) + + self.env_assist.assert_raise_library_error( + lambda: cluster.rename(self.env_assist.get_env(), _FIXTURE_NEW_NAME) + ) + self.env_assist.assert_reports( + self.fixture_corosync_offline_check_reports() + + [ + fixture.info(report_codes.CIB_CLUSTER_NAME_REMOVAL_STARTED), + fixture.error( + report_codes.NODE_COMMUNICATION_COMMAND_UNSUCCESSFUL, + node=self.node_labels[0], + command="api/v1/cluster-property-remove-name/v1", + reason="Fail", + force_code=report_codes.SKIP_OFFLINE_NODES, + ), + fixture.info( + report_codes.CIB_CLUSTER_NAME_REMOVED, + node=self.node_labels[1], + ), + ] ) def test_skip_offline(self): + self.config.fs.exists(self.cib_path, return_value=True) self.config.runner.cib.load(env=self.runner_env) self.config.corosync_conf.load_content(self.fixture_corosync_conf()) + self.config.http.corosync.check_corosync_offline( + communication_list=self._offline_node_communication_list() + ) + self.fixture_remove_name_prop_call( + communication_list=self._offline_node_communication_list() + ) self.config.env.push_corosync_conf( corosync_conf_text=self.fixture_corosync_conf(_FIXTURE_NEW_NAME), need_stopped_cluster=True, skip_offline_targets=True, ) + cluster.rename( self.env_assist.get_env(), _FIXTURE_NEW_NAME, force_flags=[report_codes.SKIP_OFFLINE_NODES], ) + self.env_assist.assert_reports( + [ + fixture.info(report_codes.COROSYNC_NOT_RUNNING_CHECK_STARTED), + fixture.warn( + report_codes.NODE_COMMUNICATION_COMMAND_UNSUCCESSFUL, + node=self.node_labels[0], + command="remote/status", + reason="Fail", + ), + fixture.warn( + report_codes.COROSYNC_NOT_RUNNING_CHECK_NODE_ERROR, + node=self.node_labels[0], + ), + fixture.info( + report_codes.COROSYNC_NOT_RUNNING_CHECK_NODE_STOPPED, + node=self.node_labels[1], + ), + fixture.info(report_codes.CIB_CLUSTER_NAME_REMOVAL_STARTED), + fixture.warn( + report_codes.NODE_COMMUNICATION_COMMAND_UNSUCCESSFUL, + node=self.node_labels[0], + command="api/v1/cluster-property-remove-name/v1", + reason="Fail", + ), + fixture.info( + report_codes.CIB_CLUSTER_NAME_REMOVED, + node=self.node_labels[1], + ), + ] + ) + + def test_cluster_name_property_remove_invalid_response(self): + self.config.fs.exists(self.cib_path, return_value=True) + self.config.runner.cib.load(env=self.runner_env) + self.config.corosync_conf.load_content(self.fixture_corosync_conf()) + self.config.http.corosync.check_corosync_offline(self.node_labels) + self.fixture_remove_name_prop_call(output="Invalid json") + + self.env_assist.assert_raise_library_error( + lambda: cluster.rename(self.env_assist.get_env(), _FIXTURE_NEW_NAME) + ) + self.env_assist.assert_reports( + self.fixture_corosync_offline_check_reports() + + [ + fixture.info(report_codes.CIB_CLUSTER_NAME_REMOVAL_STARTED), + ] + + [ + fixture.error(report_codes.INVALID_RESPONSE_FORMAT, node=n) + for n in self.node_labels + ] + ) + def test_cluster_name_property_remove_error_report(self): + self.config.fs.exists(self.cib_path, return_value=True) + self.config.runner.cib.load(env=self.runner_env) + self.config.corosync_conf.load_content(self.fixture_corosync_conf()) + self.config.http.corosync.check_corosync_offline(self.node_labels) + self.fixture_remove_name_prop_call( + output=json.dumps( + { + "status": "error", + "status_msg": None, + "report_list": [ + { + "severity": {"level": "ERROR", "force_code": None}, + "message": { + "code": "CIB_XML_MISSING", + "message": "CIB XML file cannot be found", + "payload": {}, + }, + "context": None, + } + ], + "data": "", + } + ) + ) -class ClusterRenameCheckGfs2Resources(CorosyncFixtureMixin, TestCase): + self.env_assist.assert_raise_library_error( + lambda: cluster.rename(self.env_assist.get_env(), _FIXTURE_NEW_NAME) + ) + self.env_assist.assert_reports( + self.fixture_corosync_offline_check_reports() + + [ + fixture.info(report_codes.CIB_CLUSTER_NAME_REMOVAL_STARTED), + ] + + [ + fixture.error( + report_codes.CIB_XML_MISSING, + context=reports.dto.ReportItemContextDto(node=n), + ) + for n in self.node_labels + ] + ) + + def test_corosync_running_on_node(self): + self.config.fs.exists(self.cib_path, return_value=True) + self.config.runner.cib.load(env=self.runner_env) + self.config.corosync_conf.load_content(self.fixture_corosync_conf()) + self.config.http.corosync.check_corosync_offline( + communication_list=[ + { + "label": self.node_labels[0], + "output": corosync_running_check_response(True), + } + ] + + [{"label": node} for node in self.node_labels[1:]] + ) + + self.env_assist.assert_raise_library_error( + lambda: cluster.rename(self.env_assist.get_env(), _FIXTURE_NEW_NAME) + ) + self.env_assist.assert_reports( + [ + fixture.info(report_codes.COROSYNC_NOT_RUNNING_CHECK_STARTED), + fixture.error( + report_codes.COROSYNC_NOT_RUNNING_CHECK_NODE_RUNNING, + node=self.node_labels[0], + ), + fixture.info( + report_codes.COROSYNC_NOT_RUNNING_CHECK_NODE_STOPPED, + node=self.node_labels[1], + ), + fixture.error( + report_codes.COROSYNC_NOT_RUNNING_CHECK_FINISHED_RUNNING, + node_list=[self.node_labels[0]], + ), + ] + ) + + +class ClusterRenameCheckGfs2Resources(FixtureMixin, TestCase): def setUp(self): self.env_assist, self.config = get_env_tools(self) self.cib_path = os.path.join(settings.cib_dir, "cib.xml") self.runner_env = {"CIB_file": self.cib_path} def fixture_env(self, resources): + self.config.fs.exists(self.cib_path, return_value=True) self.config.runner.cib.load( filename="cib-empty.xml", env=self.runner_env, resources=resources, ) self.config.corosync_conf.load_content(self.fixture_corosync_conf()) + self.config.env.set_known_nodes(self.node_labels) + self.config.http.corosync.check_corosync_offline(self.node_labels) + self.fixture_remove_name_prop_call() self.config.env.push_corosync_conf( corosync_conf_text=self.fixture_corosync_conf(_FIXTURE_NEW_NAME), need_stopped_cluster=True, @@ -171,6 +457,10 @@ def test_no_gfs2_in_cib(self): cluster.rename( self.env_assist.get_env(), _FIXTURE_NEW_NAME, force_flags=[] ) + self.env_assist.assert_reports( + self.fixture_corosync_offline_check_reports() + + self.fixture_remove_name_prop_reports() + ) def test_gfs2_in_cib(self): self.fixture_env(f"{_FIXTURE_GFS2_FS_RESOURCE}") @@ -179,6 +469,8 @@ def test_gfs2_in_cib(self): ) self.env_assist.assert_reports( [fixture.warn(report_codes.GFS2_LOCK_TABLE_RENAME_NEEDED)] + + self.fixture_corosync_offline_check_reports() + + self.fixture_remove_name_prop_reports() ) def test_gfs2_cloned_in_cib(self): @@ -196,6 +488,8 @@ def test_gfs2_cloned_in_cib(self): ) self.env_assist.assert_reports( [fixture.warn(report_codes.GFS2_LOCK_TABLE_RENAME_NEEDED)] + + self.fixture_corosync_offline_check_reports() + + self.fixture_remove_name_prop_reports() ) def test_dlm_in_cib(self): @@ -205,6 +499,8 @@ def test_dlm_in_cib(self): ) self.env_assist.assert_reports( [fixture.warn(report_codes.DLM_CLUSTER_RENAME_NEEDED)] + + self.fixture_corosync_offline_check_reports() + + self.fixture_remove_name_prop_reports() ) def test_dlm_cloned_in_cib(self): @@ -222,6 +518,8 @@ def test_dlm_cloned_in_cib(self): ) self.env_assist.assert_reports( [fixture.warn(report_codes.DLM_CLUSTER_RENAME_NEEDED)] + + self.fixture_corosync_offline_check_reports() + + self.fixture_remove_name_prop_reports() ) def test_dlm_and_gfs2(self): @@ -241,4 +539,6 @@ def test_dlm_and_gfs2(self): fixture.warn(report_codes.DLM_CLUSTER_RENAME_NEEDED), fixture.warn(report_codes.GFS2_LOCK_TABLE_RENAME_NEEDED), ] + + self.fixture_corosync_offline_check_reports() + + self.fixture_remove_name_prop_reports() ) diff --git a/pcs_test/tier0/lib/commands/test_cluster_property.py b/pcs_test/tier0/lib/commands/test_cluster_property.py index 4e4df1062..4b1f527de 100644 --- a/pcs_test/tier0/lib/commands/test_cluster_property.py +++ b/pcs_test/tier0/lib/commands/test_cluster_property.py @@ -1,8 +1,7 @@ -from unittest import ( - TestCase, - mock, -) +import os +from unittest import TestCase, mock +from pcs import settings from pcs.common import reports from pcs.common.pacemaker.cluster_property import ClusterPropertyMetadataDto from pcs.common.pacemaker.nvset import ( @@ -1091,3 +1090,69 @@ def test_get_properties_metadata(self): ), ) self.env_assist.assert_reports([]) + + +class RemoveClusterName(TestCase): + def setUp(self): + self.env_assist, self.config = get_env_tools(self) + self.runner_args = [ + settings.cibadmin_exec, + "--delete-all", + "--force", + "--xpath=/cib/configuration/crm_config/cluster_property_set/nvpair[@name='cluster-name']", + ] + self.cib_path = os.path.join(settings.cib_dir, "cib.xml") + self.runner_env = {"CIB_file": self.cib_path} + + def test_success(self): + self.config.services.is_running("pacemaker", return_value=False) + self.config.fs.exists(self.cib_path) + self.config.runner.place(self.runner_args, env=self.runner_env) + cluster_property.remove_cluster_name(self.env_assist.get_env()) + + def test_pacemaker_running(self): + self.config.services.is_running("pacemaker") + self.env_assist.assert_raise_library_error( + lambda: cluster_property.remove_cluster_name( + self.env_assist.get_env() + ) + ) + self.env_assist.assert_reports( + [fixture.error(reports.codes.PACEMAKER_RUNNING)] + ) + + def test_no_cib_xml(self): + self.config.services.is_running("pacemaker", return_value=False) + self.config.fs.exists(self.cib_path, return_value=False) + self.env_assist.assert_raise_library_error( + lambda: cluster_property.remove_cluster_name( + self.env_assist.get_env() + ) + ) + self.env_assist.assert_reports( + [fixture.error(reports.codes.CIB_XML_MISSING)] + ) + + def test_runner_failed(self): + self.config.services.is_running("pacemaker", return_value=False) + self.config.fs.exists(self.cib_path) + self.config.runner.place( + self.runner_args, + env=self.runner_env, + returncode=1, + stdout="foo", + stderr="bar", + ) + self.env_assist.assert_raise_library_error( + lambda: cluster_property.remove_cluster_name( + self.env_assist.get_env() + ) + ) + self.env_assist.assert_reports( + [ + fixture.error( + reports.codes.CIB_CLUSTER_NAME_REMOVAL_FAILED, + reason="bar\nfoo", + ) + ] + ) diff --git a/pcs_test/tier0/lib/pacemaker/test_live.py b/pcs_test/tier0/lib/pacemaker/test_live.py index 1864e6968..fab9f8291 100644 --- a/pcs_test/tier0/lib/pacemaker/test_live.py +++ b/pcs_test/tier0/lib/pacemaker/test_live.py @@ -1,5 +1,4 @@ # pylint: disable=too-many-lines -import os from unittest import ( TestCase, mock, @@ -353,10 +352,7 @@ def test_warnings_verbose(self): ) -class GetCibXmlTestMixin: - def _call_command(self, runner, scope=None): - raise NotImplementedError() - +class GetCibXmlTest(TestCase): def test_success(self): expected_stdout = "" expected_stderr = "" @@ -365,11 +361,10 @@ def test_success(self): expected_stdout, expected_stderr, expected_retval ) - real_xml = self._call_command(mock_runner) + real_xml = lib.get_cib_xml(mock_runner) mock_runner.run.assert_called_once_with( - [settings.cibadmin_exec, "--local", "--query"], - env_extend=self.env_extend, + [settings.cibadmin_exec, "--local", "--query"] ) self.assertEqual(expected_stdout, real_xml) @@ -383,7 +378,7 @@ def test_error(self): ) assert_raise_library_error( - lambda: self._call_command(mock_runner), + lambda: lib.get_cib_xml(mock_runner), ( Severity.ERROR, report_codes.CIB_LOAD_ERROR, @@ -394,8 +389,7 @@ def test_error(self): ) mock_runner.run.assert_called_once_with( - [settings.cibadmin_exec, "--local", "--query"], - env_extend=self.env_extend, + [settings.cibadmin_exec, "--local", "--query"] ) def test_success_scope(self): @@ -407,7 +401,7 @@ def test_success_scope(self): expected_stdout, expected_stderr, expected_retval ) - real_xml = self._call_command(mock_runner, scope) + real_xml = lib.get_cib_xml(mock_runner, scope) mock_runner.run.assert_called_once_with( [ @@ -415,8 +409,7 @@ def test_success_scope(self): "--local", "--query", "--scope={0}".format(scope), - ], - env_extend=self.env_extend, + ] ) self.assertEqual(expected_stdout, real_xml) @@ -435,7 +428,7 @@ def test_scope_error(self): ) assert_raise_library_error( - lambda: self._call_command(mock_runner, scope=scope), + lambda: lib.get_cib_xml(mock_runner, scope=scope), ( Severity.ERROR, report_codes.CIB_LOAD_ERROR_SCOPE_MISSING, @@ -452,25 +445,10 @@ def test_scope_error(self): "--local", "--query", "--scope={0}".format(scope), - ], - env_extend=self.env_extend, + ] ) -class GetCibXmlTest(GetCibXmlTestMixin, TestCase): - env_extend = None - - def _call_command(self, runner, scope=None): - return lib.get_cib_xml(runner, scope) - - -class GetCibDirectXmlTest(GetCibXmlTestMixin, TestCase): - env_extend = {"CIB_file": os.path.join(settings.cib_dir, "cib.xml")} - - def _call_command(self, runner, scope=None): - return lib.get_cib_direct_xml(runner, scope) - - class GetCibTest(TestCase): # pylint: disable=no-self-use def test_success(self): diff --git a/pcsd/capabilities.xml.in b/pcsd/capabilities.xml.in index 29926233d..8fd4dc173 100644 --- a/pcsd/capabilities.xml.in +++ b/pcsd/capabilities.xml.in @@ -236,6 +236,14 @@ daemon urls: cluster_destroy (parameter all=1) + + + Rename configured cluster. The cluster has to be stopped + + pcs commands: cluster rename + API v2: cluster.rename + + Create a tarball containing everything needed when reporting cluster @@ -249,14 +257,6 @@ Check cluster configuration for errors. - - - Rename configured cluster. The cluster has to be stopped - - pcs commands: cluster rename - API v2: cluster.rename - - @@ -1225,6 +1225,13 @@ pcs commands: property defaults + + + Remove cluster-name property + + daemon urls: /api/v1/cluster-property-remove-name/v1 + + Show and set resource operations defaults, can set multiple defaults at From 34514db25115f5d9a30aa94df84f56fb3aa71907 Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Tue, 28 Jan 2025 14:52:13 +0100 Subject: [PATCH 101/227] print report context in report asserts --- pcs_test/tools/assertions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pcs_test/tools/assertions.py b/pcs_test/tools/assertions.py index 517c6b263..4703c5f90 100644 --- a/pcs_test/tools/assertions.py +++ b/pcs_test/tools/assertions.py @@ -384,6 +384,7 @@ def _format_report_item(report_item): report_item.message.code, report_item.message.to_dto().payload, report_item.severity.force_code, + report_item.context, ) ) From 5acd6087436799f5a7a0b4604f2d9adc85c3e56b Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Wed, 29 Jan 2025 12:56:06 +0100 Subject: [PATCH 102/227] fixup issues --- CHANGELOG.md | 1 - pcs/cli/cluster/command.py | 2 +- .../async_tasks/worker/command_mapping.py | 4 +- pcs/lib/commands/cluster.py | 67 +++++++++---------- pcs/lib/corosync/config_facade.py | 2 +- pcs/usage.py | 6 +- .../tier0/common/reports/test_messages.py | 1 + 7 files changed, 41 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 942716ea7..81989f874 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,6 @@ driver ([RHEL-76177]) - Command `pcs cluster rename` for changing cluster name [RHEL-76055] - [RHEL-76055]: https://issues.redhat.com/browse/RHEL-76055 [RHEL-76059]: https://issues.redhat.com/browse/RHEL-76059 [RHEL-76177]: https://issues.redhat.com/browse/RHEL-76177 diff --git a/pcs/cli/cluster/command.py b/pcs/cli/cluster/command.py index 1b7538cdd..79a4c5bdb 100644 --- a/pcs/cli/cluster/command.py +++ b/pcs/cli/cluster/command.py @@ -206,7 +206,7 @@ def node_clear(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: def cluster_rename(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: - * --force - allow using cluster name that does not work with gfs2 + * --force * --skip-offline - skip offline nodes """ modifiers.ensure_only_supported("--force", "--skip-offline") diff --git a/pcs/daemon/async_tasks/worker/command_mapping.py b/pcs/daemon/async_tasks/worker/command_mapping.py index 005a50e0c..394b341bc 100644 --- a/pcs/daemon/async_tasks/worker/command_mapping.py +++ b/pcs/daemon/async_tasks/worker/command_mapping.py @@ -143,7 +143,7 @@ class _Cmd: ), "cluster.rename": _Cmd( cmd=cluster.rename, - required_permission=p.WRITE, + required_permission=p.FULL, ), "cluster.setup": _Cmd( cmd=cluster.setup, @@ -163,7 +163,7 @@ class _Cmd: ), "cluster_property.remove_cluster_name": _Cmd( cmd=cluster_property.remove_cluster_name, - required_permission=p.WRITE, # ?? maybe superuser + required_permission=p.WRITE, ), "cluster.wait_for_pcmk_idle": _Cmd( cmd=cluster.wait_for_pcmk_idle, diff --git a/pcs/lib/commands/cluster.py b/pcs/lib/commands/cluster.py index ebea42da0..6919b9af5 100644 --- a/pcs/lib/commands/cluster.py +++ b/pcs/lib/commands/cluster.py @@ -2336,38 +2336,6 @@ def wait_for_pcmk_idle(env: LibraryEnvironment, wait_value: WaitType) -> None: env.wait_for_idle(timeout) -def _warn_dlm_resources(resources: _Element) -> reports.ReportItemList: - if find_primitives_by_agent( - resources, ResourceAgentName("ocf", "pacemaker", "controld") - ): - return [ - reports.ReportItem.warning( - reports.messages.DlmClusterRenameNeeded() - ) - ] - return [] - - -def _warn_gfs2_resources(resources: _Element) -> reports.ReportItemList: - for resource in find_primitives_by_agent( - resources, ResourceAgentName("ocf", "heartbeat", "Filesystem") - ): - for nvset in find_nvsets(resource, NVSET_INSTANCE): - if any( - ( - nvpair.get("name") == "fstype" - and nvpair.get("value") == "gfs2" - ) - for nvpair in nvset - ): - return [ - reports.ReportItem.warning( - reports.messages.Gfs2LockTableRenameNeeded() - ) - ] - return [] - - def rename( env: LibraryEnvironment, new_name: str, @@ -2378,6 +2346,37 @@ def rename( new_name -- new name for the cluster """ + + def warn_dlm_resources(resources: _Element) -> reports.ReportItemList: + if find_primitives_by_agent( + resources, ResourceAgentName("ocf", "pacemaker", "controld") + ): + return [ + reports.ReportItem.warning( + reports.messages.DlmClusterRenameNeeded() + ) + ] + return [] + + def warn_gfs2_resources(resources: _Element) -> reports.ReportItemList: + for resource in find_primitives_by_agent( + resources, ResourceAgentName("ocf", "heartbeat", "Filesystem") + ): + for nvset in find_nvsets(resource, NVSET_INSTANCE): + if any( + ( + nvpair.get("name") == "fstype" + and nvpair.get("value") == "gfs2" + ) + for nvpair in nvset + ): + return [ + reports.ReportItem.warning( + reports.messages.Gfs2LockTableRenameNeeded() + ) + ] + return [] + _ensure_live_env(env) if env.report_processor.report_list( @@ -2391,8 +2390,8 @@ def rename( if has_cib_xml(): cib = get_cib(get_cib_xml(env.cmd_runner(get_cib_file_runner_env()))) resources = get_resources(cib) - env.report_processor.report_list(_warn_dlm_resources(resources)) - env.report_processor.report_list(_warn_gfs2_resources(resources)) + env.report_processor.report_list(warn_dlm_resources(resources)) + env.report_processor.report_list(warn_gfs2_resources(resources)) corosync_conf = env.get_corosync_conf() skip_offline = report_codes.SKIP_OFFLINE_NODES in force_flags diff --git a/pcs/lib/corosync/config_facade.py b/pcs/lib/corosync/config_facade.py index 5d70d660d..7782f484d 100644 --- a/pcs/lib/corosync/config_facade.py +++ b/pcs/lib/corosync/config_facade.py @@ -109,7 +109,7 @@ def set_cluster_name(self, new_name: str) -> None: """ Updates or adds cluster name - new_name - new name for the cluster + new_name -- new name for the cluster """ self._need_stopped_cluster = True totem_section_list = self.__ensure_section(self.config, "totem") diff --git a/pcs/usage.py b/pcs/usage.py index 2db89ed56..80a537151 100644 --- a/pcs/usage.py +++ b/pcs/usage.py @@ -1672,14 +1672,14 @@ def cluster(args: Argv) -> str: rename Rename configured cluster. The cluster has to be stopped to complete - this operation. + this operation. Manual steps are needed in case the cluster uses GFS2 filesystem or DLM: - for GFS2: + for GFS2: The lock table name on each GFS2 filesystem must be updated to reflect the new name of the cluster so that the filesystems can be mounted. - for DLM: + for DLM: The DLM cluster name in the shared volume groups metadata must be updated so that the volume groups can start. diff --git a/pcs_test/tier0/common/reports/test_messages.py b/pcs_test/tier0/common/reports/test_messages.py index b73927ad5..631f4d4af 100644 --- a/pcs_test/tier0/common/reports/test_messages.py +++ b/pcs_test/tier0/common/reports/test_messages.py @@ -6079,6 +6079,7 @@ def test_success(self) -> str: reports.GuestNodeRemovalIncomplete("guest-node"), ) + class DlmClusterRenameNeeded(NameBuildTest): def test_success(self): self.assert_message_from_report( From c790627f0e6a237624a568b3b8625e4a92d6194b Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Mon, 10 Feb 2025 12:07:01 +0100 Subject: [PATCH 103/227] handle errors from api v1 better --- pcs/lib/communication/cluster.py | 37 ++++-- .../tier0/lib/commands/cluster/test_rename.py | 122 ++++++++++++++++++ 2 files changed, 148 insertions(+), 11 deletions(-) diff --git a/pcs/lib/communication/cluster.py b/pcs/lib/communication/cluster.py index 7c94bc0bf..1e08f052d 100644 --- a/pcs/lib/communication/cluster.py +++ b/pcs/lib/communication/cluster.py @@ -4,10 +4,12 @@ from dacite import DaciteError from pcs.common import reports +from pcs.common.communication import const from pcs.common.communication.dto import InternalCommunicationResultDto from pcs.common.interface.dto import from_dict from pcs.common.node_communicator import RequestData from pcs.common.reports.item import ReportItem +from pcs.common.reports.processor import has_errors from pcs.lib.communication.tools import ( AllAtOnceStrategyMixin, AllSameDataMixin, @@ -165,31 +167,44 @@ def _process_response(self, response): return node_label = response.request.target.label - report_list = [] try: result = from_dict( InternalCommunicationResultDto, json.loads(response.data) ) - context = reports.ReportItemContext(node_label) - - report_list.extend( - reports.report_dto_to_item(report, context) - for report in result.report_list - ) except (json.JSONDecodeError, DaciteError): - report_list.append( + self._report( reports.ReportItem.error( reports.messages.InvalidResponseFormat(node_label) ) ) + return + context = reports.ReportItemContext(node_label) + report_list = [ + reports.report_dto_to_item(report, context) + for report in result.report_list + ] self._report_list(report_list) - if not any( - report.severity.level == reports.ReportItemSeverity.ERROR - for report in report_list + + if ( + not has_errors(report_list) + and result.status == const.COM_STATUS_SUCCESS ): self._report( reports.ReportItem.info( reports.messages.CibClusterNameRemoved(node_label) ) ) + return + + # Make sure we report an error when the command was not successful + if result.status_msg or not has_errors(report_list): + self._report( + reports.ReportItem.error( + reports.messages.NodeCommunicationCommandUnsuccessful( + node_label, + response.request.action, + result.status_msg or "Unknown error", + ) + ) + ) diff --git a/pcs_test/tier0/lib/commands/cluster/test_rename.py b/pcs_test/tier0/lib/commands/cluster/test_rename.py index 90d971486..41def8b08 100644 --- a/pcs_test/tier0/lib/commands/cluster/test_rename.py +++ b/pcs_test/tier0/lib/commands/cluster/test_rename.py @@ -392,6 +392,128 @@ def test_cluster_name_property_remove_error_report(self): ] ) + def test_cluster_name_property_remove_error_status_msg(self): + self.config.fs.exists(self.cib_path, return_value=True) + self.config.runner.cib.load(env=self.runner_env) + self.config.corosync_conf.load_content(self.fixture_corosync_conf()) + self.config.http.corosync.check_corosync_offline(self.node_labels) + self.fixture_remove_name_prop_call( + output=json.dumps( + { + "status": "error", + "status_msg": "very bad error", + "report_list": [], + "data": "", + } + ) + ) + + self.env_assist.assert_raise_library_error( + lambda: cluster.rename(self.env_assist.get_env(), _FIXTURE_NEW_NAME) + ) + self.env_assist.assert_reports( + self.fixture_corosync_offline_check_reports() + + [ + fixture.info(report_codes.CIB_CLUSTER_NAME_REMOVAL_STARTED), + ] + + [ + fixture.error( + report_codes.NODE_COMMUNICATION_COMMAND_UNSUCCESSFUL, + node=n, + command="api/v1/cluster-property-remove-name/v1", + reason="very bad error", + ) + for n in self.node_labels + ] + ) + + def test_cluster_name_property_remove_error_report_status_msg(self): + self.config.fs.exists(self.cib_path, return_value=True) + self.config.runner.cib.load(env=self.runner_env) + self.config.corosync_conf.load_content(self.fixture_corosync_conf()) + self.config.http.corosync.check_corosync_offline(self.node_labels) + self.fixture_remove_name_prop_call( + output=json.dumps( + { + "status": "error", + "status_msg": "very bad error", + "report_list": [ + { + "severity": {"level": "ERROR", "force_code": None}, + "message": { + "code": "CIB_XML_MISSING", + "message": "CIB XML file cannot be found", + "payload": {}, + }, + "context": None, + } + ], + "data": "", + } + ) + ) + + self.env_assist.assert_raise_library_error( + lambda: cluster.rename(self.env_assist.get_env(), _FIXTURE_NEW_NAME) + ) + self.env_assist.assert_reports( + self.fixture_corosync_offline_check_reports() + + [ + fixture.info(report_codes.CIB_CLUSTER_NAME_REMOVAL_STARTED), + ] + + [ + fixture.error( + report_codes.CIB_XML_MISSING, + context=reports.dto.ReportItemContextDto(node=n), + ) + for n in self.node_labels + ] + + [ + fixture.error( + report_codes.NODE_COMMUNICATION_COMMAND_UNSUCCESSFUL, + node=n, + command="api/v1/cluster-property-remove-name/v1", + reason="very bad error", + ) + for n in self.node_labels + ] + ) + + def test_cluster_name_property_remove_error_no_report_no_message(self): + self.config.fs.exists(self.cib_path, return_value=True) + self.config.runner.cib.load(env=self.runner_env) + self.config.corosync_conf.load_content(self.fixture_corosync_conf()) + self.config.http.corosync.check_corosync_offline(self.node_labels) + self.fixture_remove_name_prop_call( + output=json.dumps( + { + "status": "error", + "status_msg": None, + "report_list": [], + "data": "", + } + ) + ) + + self.env_assist.assert_raise_library_error( + lambda: cluster.rename(self.env_assist.get_env(), _FIXTURE_NEW_NAME) + ) + self.env_assist.assert_reports( + self.fixture_corosync_offline_check_reports() + + [ + fixture.info(report_codes.CIB_CLUSTER_NAME_REMOVAL_STARTED), + ] + + [ + fixture.error( + report_codes.NODE_COMMUNICATION_COMMAND_UNSUCCESSFUL, + node=n, + command="api/v1/cluster-property-remove-name/v1", + reason="Unknown error", + ) + for n in self.node_labels + ] + ) + def test_corosync_running_on_node(self): self.config.fs.exists(self.cib_path, return_value=True) self.config.runner.cib.load(env=self.runner_env) From d92b50b3debf07c052a3b3a8e2531fd7ce07fab0 Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Tue, 11 Feb 2025 13:02:49 +0100 Subject: [PATCH 104/227] fix changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81989f874..a9675bb6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ when new CIB does not conform to the CIB schema ([RHEL-76059]) - Command `pcs stonith sbd watchdog list` now prints watchdogs' identity and driver ([RHEL-76177]) -- Command `pcs cluster rename` for changing cluster name [RHEL-76055] +- Command `pcs cluster rename` for changing cluster name ([RHEL-76055]) [RHEL-76055]: https://issues.redhat.com/browse/RHEL-76055 [RHEL-76059]: https://issues.redhat.com/browse/RHEL-76059 From 437a441c94103395f31fca841c1cf94a5c3c5cf0 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Wed, 12 Feb 2025 14:00:26 +0100 Subject: [PATCH 105/227] fix restarting bundle instances * fixes a regression introduced in ccffba735128ea4be62aa33e7319114d8b26a8b0 --- CHANGELOG.md | 5 ++ pcs/lib/commands/resource.py | 85 ++++++++++--------- .../lib/commands/resource/test_restart.py | 16 +--- 3 files changed, 51 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9675bb6b..a1c268c06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,14 @@ driver ([RHEL-76177]) - Command `pcs cluster rename` for changing cluster name ([RHEL-76055]) +### Fixed +- Command `pcs resource restart` allows restarting bundle instances (broken + since pcs-0.11.9) ([RHEL-79055]) + [RHEL-76055]: https://issues.redhat.com/browse/RHEL-76055 [RHEL-76059]: https://issues.redhat.com/browse/RHEL-76059 [RHEL-76177]: https://issues.redhat.com/browse/RHEL-76177 +[RHEL-79055]: https://issues.redhat.com/browse/RHEL-79055 ## [0.11.9] - 2025-01-10 diff --git a/pcs/lib/commands/resource.py b/pcs/lib/commands/resource.py index 4f485d18f..641b51a47 100644 --- a/pcs/lib/commands/resource.py +++ b/pcs/lib/commands/resource.py @@ -2564,56 +2564,59 @@ def restart( timeout -- abort if the command doesn't finish in this time (integer + unit) """ cib = env.get_cib() + + # To be able to restart bundle instances, which are not to be found in CIB, + # do not fail if specified ID is not found in CIB. Pacemaker provides + # reasonable messages when the ID to be restarted is not a resource or + # doesn't exist. We only search for the resource in order to provide hints + # when the user attempts to restart bundle's or clone's inner resources. + resource_found = False try: resource_el = get_element_by_id(cib, resource_id) - except ElementNotFound as e: - env.report_processor.report( - ReportItem.error( - reports.messages.IdNotFound( - resource_id, expected_types=["resource"] - ) - ) - ) - raise LibraryError() from e - if not resource.common.is_resource(resource_el): - env.report_processor.report( - ReportItem.error( - reports.messages.IdBelongsToUnexpectedType( - resource_id, - expected_types=["resource"], - current_type=resource_el.tag, + resource_found = True + except ElementNotFound: + pass + + if resource_found: + if not resource.common.is_resource(resource_el): + env.report_processor.report( + ReportItem.error( + reports.messages.IdBelongsToUnexpectedType( + resource_id, + expected_types=["resource"], + current_type=resource_el.tag, + ) ) ) - ) - raise LibraryError() + raise LibraryError() - parent_resource_el = resource.clone.get_parent_any_clone(resource_el) - if parent_resource_el is None: - parent_resource_el = resource.bundle.get_parent_bundle(resource_el) - if parent_resource_el is not None: - env.report_processor.report( - reports.ReportItem.warning( - reports.messages.ResourceRestartUsingParentRersource( - str(resource_el.attrib["id"]), - str(parent_resource_el.attrib["id"]), + parent_resource_el = resource.clone.get_parent_any_clone(resource_el) + if parent_resource_el is None: + parent_resource_el = resource.bundle.get_parent_bundle(resource_el) + if parent_resource_el is not None: + env.report_processor.report( + reports.ReportItem.warning( + reports.messages.ResourceRestartUsingParentRersource( + str(resource_el.attrib["id"]), + str(parent_resource_el.attrib["id"]), + ) ) ) - ) - resource_el = parent_resource_el + resource_el = parent_resource_el - if node and not ( - resource.clone.is_any_clone(resource_el) - or resource.bundle.is_bundle(resource_el) - ): - env.report_processor.report( - reports.ReportItem.error( - reports.messages.ResourceRestartNodeIsForMultiinstanceOnly( - str(resource_el.attrib["id"]), - resource_el.tag, - node, + if node and not ( + resource.clone.is_any_clone(resource_el) + or resource.bundle.is_bundle(resource_el) + ): + env.report_processor.report( + reports.ReportItem.error( + reports.messages.ResourceRestartNodeIsForMultiinstanceOnly( + str(resource_el.attrib["id"]), + resource_el.tag, + node, + ) ) ) - ) if timeout is not None: env.report_processor.report_list( @@ -2625,7 +2628,7 @@ def restart( resource_restart( env.cmd_runner(), - str(resource_el.attrib["id"]), + str(resource_el.attrib["id"]) if resource_found else resource_id, node=node, timeout=timeout, ) diff --git a/pcs_test/tier0/lib/commands/resource/test_restart.py b/pcs_test/tier0/lib/commands/resource/test_restart.py index 9d61f5704..af5004b43 100644 --- a/pcs_test/tier0/lib/commands/resource/test_restart.py +++ b/pcs_test/tier0/lib/commands/resource/test_restart.py @@ -104,20 +104,8 @@ def test_bad_timeout(self): ) def test_resource_not_found(self): - self.env_assist.assert_raise_library_error( - lambda: resource.restart(self.env_assist.get_env(), "RX") - ) - self.env_assist.assert_reports( - [ - fixture.error( - reports.codes.ID_NOT_FOUND, - id="RX", - expected_types=["resource"], - context_type="", - context_id="", - ) - ] - ) + self.config.runner.pcmk.resource_restart("RX") + resource.restart(self.env_assist.get_env(), "RX") def test_not_a_resource(self): self.env_assist.assert_raise_library_error( From 9034688a6d8ad55f70a3d4b8d390e263dd682f75 Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Thu, 13 Feb 2025 11:03:20 +0100 Subject: [PATCH 106/227] fix deletion of misconfigured bundles --- CHANGELOG.md | 3 + pcs/common/reports/codes.py | 3 + pcs/common/reports/messages.py | 30 +++++++++ pcs/lib/cib/remove_elements.py | 59 +++++++++++------ .../tier0/common/reports/test_messages.py | 23 +++++++ pcs_test/tier0/lib/commands/test_cib.py | 66 +++++++++++++++++++ 6 files changed, 165 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1c268c06..e8feb5c2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,11 +12,14 @@ ### Fixed - Command `pcs resource restart` allows restarting bundle instances (broken since pcs-0.11.9) ([RHEL-79055]) +- Do not end with traceback when using `pcs resource delete` to remove bundle + resources when the bundle has no IP address specified ([RHEL-79160]) [RHEL-76055]: https://issues.redhat.com/browse/RHEL-76055 [RHEL-76059]: https://issues.redhat.com/browse/RHEL-76059 [RHEL-76177]: https://issues.redhat.com/browse/RHEL-76177 [RHEL-79055]: https://issues.redhat.com/browse/RHEL-79055 +[RHEL-79160]: https://issues.redhat.com/browse/RHEL-79160 ## [0.11.9] - 2025-01-10 diff --git a/pcs/common/reports/codes.py b/pcs/common/reports/codes.py index 226f82438..48e83afd8 100644 --- a/pcs/common/reports/codes.py +++ b/pcs/common/reports/codes.py @@ -180,6 +180,9 @@ CLUSTER_WILL_BE_DESTROYED = M("CLUSTER_WILL_BE_DESTROYED") COMMAND_INVALID_PAYLOAD = M("COMMAND_INVALID_PAYLOAD") COMMAND_UNKNOWN = M("COMMAND_UNKNOWN") +CONFIGURED_RESOURCE_MISSING_IN_STATUS = M( + "CONFIGURED_RESOURCE_MISSING_IN_STATUS" +) LIVE_ENVIRONMENT_NOT_CONSISTENT = M("LIVE_ENVIRONMENT_NOT_CONSISTENT") LIVE_ENVIRONMENT_REQUIRED = M("LIVE_ENVIRONMENT_REQUIRED") LIVE_ENVIRONMENT_REQUIRED_FOR_LOCAL_NODE = M( diff --git a/pcs/common/reports/messages.py b/pcs/common/reports/messages.py index 9751ec863..e4e8b332e 100644 --- a/pcs/common/reports/messages.py +++ b/pcs/common/reports/messages.py @@ -31,6 +31,7 @@ ResourceAgentNameDto, get_resource_agent_full_name, ) +from pcs.common.resource_status import ResourceState from pcs.common.str_tools import ( format_list, format_list_custom_last_separator, @@ -6445,6 +6446,35 @@ def message(self) -> str: ) +@dataclass(frozen=True) +class ConfiguredResourceMissingInStatus(ReportItemMessage): + """ + Cannot check status of resource, because the resource is missing in cluster + status despite being configured in CIB. This happens for misconfigured + resources, e.g. bundle with primitive resource inside and no IP address + for the bundle specified. + + resource_id -- id of the resource + checked_state -- expected state of the resource + """ + + resource_id: str + checked_state: Optional[ResourceState] = None + _code = codes.CONFIGURED_RESOURCE_MISSING_IN_STATUS + + @property + def message(self) -> str: + return ( + "Cannot check if the resource '{resource_id}' is in expected " + "state{state}, since the resource is missing in cluster status" + ).format( + resource_id=self.resource_id, + state=format_optional( + self.checked_state and self.checked_state.name.lower(), " ({})" + ), + ) + + @dataclass(frozen=True) class ResourceBanPcmkError(ReportItemMessage): """ diff --git a/pcs/lib/cib/remove_elements.py b/pcs/lib/cib/remove_elements.py index f4339dd3d..47834229c 100644 --- a/pcs/lib/cib/remove_elements.py +++ b/pcs/lib/cib/remove_elements.py @@ -259,17 +259,27 @@ def warn_resource_unmanaged( report_list.extend(parser.get_warnings()) status = ResourcesStatusFacade.from_resources_status_dto(status_dto) - report_list.extend( - reports.ReportItem.warning( - reports.messages.ResourceIsUnmanaged(resource_id) - ) - for resource_id in resource_ids - if status.is_state( - resource_id, - None, - ResourceState.UNMANAGED, - ) - ) + for r_id in resource_ids: + if not status.exists(r_id, None): + # Pacemaker does not put misconfigured resources into cluster + # status and we are unable to check state of such resources. + # This happens for e.g. undle with primitive resource inside and + # no IP address for the bundle specified. We expect the resource + # to be stopped since it is misconfigured. Stopping it again + # even when it is unmanaged should not break anything. + report_list.append( + reports.ReportItem.debug( + reports.messages.ConfiguredResourceMissingInStatus( + r_id, ResourceState.UNMANAGED + ) + ) + ) + elif status.is_state(r_id, None, ResourceState.UNMANAGED): + report_list.append( + reports.ReportItem.warning( + reports.messages.ResourceIsUnmanaged(r_id) + ) + ) except NotImplementedError: # TODO remove when issue with bundles in status is fixed report_list.extend( @@ -318,20 +328,31 @@ def ensure_resources_stopped( report_list.extend(parser.get_warnings()) status = ResourcesStatusFacade.from_resources_status_dto(status_dto) - not_stopped_ids = [ - resource_id - for resource_id in resource_ids - if not status.is_state( - resource_id, + for r_id in resource_ids: + if not status.exists(r_id, None): + # Pacemaker does not put misconfigured resources into cluster + # status and we are unable to check state of such resources. + # This happens for e.g. undle with primitive resource inside and + # no IP address for the bundle specified. We expect the resource + # to be stopped since it is misconfigured. + report_list.append( + reports.ReportItem.debug( + reports.messages.ConfiguredResourceMissingInStatus( + r_id, ResourceState.STOPPED + ) + ) + ) + elif not status.is_state( + r_id, None, ResourceState.STOPPED, instances_quantifier=( MoreChildrenQuantifierType.ALL - if status.can_have_multiple_instances(resource_id) + if status.can_have_multiple_instances(r_id) else None ), - ) - ] + ): + not_stopped_ids.append(r_id) except NotImplementedError: # TODO remove when issue with bundles in status is fixed not_stopped_ids = [ diff --git a/pcs_test/tier0/common/reports/test_messages.py b/pcs_test/tier0/common/reports/test_messages.py index 631f4d4af..82e42e655 100644 --- a/pcs_test/tier0/common/reports/test_messages.py +++ b/pcs_test/tier0/common/reports/test_messages.py @@ -11,6 +11,7 @@ from pcs.common.reports import const from pcs.common.reports import messages as reports from pcs.common.resource_agent.dto import ResourceAgentNameDto +from pcs.common.resource_status import ResourceState from pcs.common.types import CibRuleExpressionType # pylint: disable=too-many-lines @@ -6139,3 +6140,25 @@ def test_success(self): self.assert_message_from_report( "CIB XML file cannot be found", reports.CibXmlMissing() ) + + +class ConfiguredResourceMissingInStatus(NameBuildTest): + def test_only_resource_id(self): + self.assert_message_from_report( + ( + "Cannot check if the resource 'id' is in expected state, " + "since the resource is missing in cluster status" + ), + reports.ConfiguredResourceMissingInStatus("id"), + ) + + def test_with_expected_state(self): + self.assert_message_from_report( + ( + "Cannot check if the resource 'id' is in expected state " + "(stopped), since the resource is missing in cluster status" + ), + reports.ConfiguredResourceMissingInStatus( + "id", ResourceState.STOPPED + ), + ) diff --git a/pcs_test/tier0/lib/commands/test_cib.py b/pcs_test/tier0/lib/commands/test_cib.py index 7c72fd047..a6d68ae36 100644 --- a/pcs_test/tier0/lib/commands/test_cib.py +++ b/pcs_test/tier0/lib/commands/test_cib.py @@ -5,6 +5,7 @@ ) from pcs.common import reports +from pcs.common.resource_status import ResourceState from pcs.lib.commands import cib as lib from pcs_test.tools import fixture @@ -991,3 +992,68 @@ def test_disable_only_needed_resources(self): ), ] ) + + def test_skip_state_check_on_missing_from_status(self): + self.config.runner.cib.load( + resources=""" + + + + + + + """ + ) + self.fixture_stop_resources_wait_calls( + self.config.calls.get("runner.cib.load").stdout, + initial_state_modifiers={"resources": ""}, + after_disable_cib_modifiers={ + "resources": """ + + + + + + + + + + + """ + }, + after_disable_state_modifiers={"resources": ""}, + ) + self.fixture_push_cib_after_stopping( + resources=""" + + + + + + """ + ) + lib.remove_elements(self.env_assist.get_env(), ["apa"]) + self.env_assist.assert_reports( + [ + fixture.info( + reports.codes.STOPPING_RESOURCES_BEFORE_DELETING, + resource_id_list=["apa"], + ), + fixture.debug( + reports.codes.CONFIGURED_RESOURCE_MISSING_IN_STATUS, + resource_id="apa", + checked_state=ResourceState.UNMANAGED, + ), + fixture.info(reports.codes.WAIT_FOR_IDLE_STARTED, timeout=0), + fixture.debug( + reports.codes.CONFIGURED_RESOURCE_MISSING_IN_STATUS, + resource_id="apa", + checked_state=ResourceState.STOPPED, + ), + fixture.info( + reports.codes.CIB_REMOVE_REFERENCES, + id_tag_map={"apa": "primitive", "test-bundle": "bundle"}, + removing_references_from={"apa": {"test-bundle"}}, + ), + ] + ) From f57e54c6c9eac12146867bcc866190d2992c3c8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Posp=C3=AD=C5=A1il?= Date: Fri, 7 Mar 2025 19:43:28 +0100 Subject: [PATCH 107/227] spec: fix license string --- rpm/pcs.spec.in | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/rpm/pcs.spec.in b/rpm/pcs.spec.in index 35d8aa7a5..b221248b5 100644 --- a/rpm/pcs.spec.in +++ b/rpm/pcs.spec.in @@ -11,9 +11,10 @@ Release: 99+git%{?numcomm:.%{numcomm}}%{?alphatag:.%{alphatag}}%{?dirty:.%{dirty # GPL-2.0-only: pcs # Apache-2.0: dataclasses, tornado # Apache-2.0 or BSD-3-Clause: dateutil -# MIT: backports, childprocess, dacite, ethon, mustermann, nio4r, rack, +# MIT: backports, childprocess, dacite, ethon, mustermann, rack, # rack-protection, rack-test, sinatra, tilt -# BSD-2-Clause or Ruby: ruby2_keywords +# MIT AND (BSD-2-Clause OR GPL-2.0-or-later): nio4r +# BSD-2-Clause or Ruby: base64, ruby2_keywords, strscan # BSD 3-Clause: puma # BSD-3-Clause and MIT: ffi # Some gems we bundle are just dependencies of gems that we use, @@ -24,9 +25,16 @@ Release: 99+git%{?numcomm:.%{numcomm}}%{?alphatag:.%{alphatag}}%{?dirty:.%{dirty # nio4r # sinatra: # mustermann: -# ruby2_keywords +# ruby2_keywords (default gem - included with ruby 3.1+, part of ruby before) +# rack +# rack-protection +# rack-session # tilt -License: GPL-2.0-only AND Apache-2.0 AND MIT AND BSD-2-Clause AND BSD-3-Clause AND (Apache-2.0 OR BSD-3-CLause) AND (BSD-2-Clause OR Ruby) +# rack-protection: +# base64 (default gem before ruby3.4) +# rexml: +# strscan (default gem - included with ruby) +License: GPL-2.0-only AND Apache-2.0 AND MIT AND BSD-2-Clause AND BSD-3-Clause AND (BSD-2-Clause OR GPL-2.0-or-later) AND (Apache-2.0 OR BSD-3-Clause) AND (BSD-2-Clause OR Ruby) URL: https://github.com/ClusterLabs/pcs Summary: Pacemaker/Corosync Configuration System From 3eb8e5e151bb0e4e6548a907993eb89030544829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Posp=C3=AD=C5=A1il?= Date: Fri, 7 Mar 2025 19:44:46 +0100 Subject: [PATCH 108/227] spec: disallow install with Pacemaker 3 --- rpm/pcs.spec.in | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/rpm/pcs.spec.in b/rpm/pcs.spec.in index b221248b5..f21d5039e 100644 --- a/rpm/pcs.spec.in +++ b/rpm/pcs.spec.in @@ -44,8 +44,8 @@ Summary: Pacemaker/Corosync Configuration System %global pcs_bundled_dir @pcs_bundled_dir@ -%global required_pacemaker_version 2.1.0 - +%global min_compatible_pacemaker_version 2.1.0 +%global first_incompatible_pacemaker_version 3.0.0 # mangling shebang in /usr/lib/pcsd/vendor/bundle/ruby/gems/rack-2.0.5/test/cgi/test from /usr/bin/env ruby to #!/usr/bin/ruby #*** ERROR: ./usr/lib/pcsd/vendor/bundle/ruby/gems/rack-2.0.5/test/cgi/test.ru has shebang which doesn't start with '/' (../../bin/rackup) @@ -101,20 +101,18 @@ BuildRequires: ruby-devel BuildRequires: rubygems # cluster stack packages for pkg-config BuildRequires: booth -# not distributed in rhel 9 -%if 0%{?rhel} < 9 +# Buildroot only since RHEL 9 BuildRequires: corosync-qdevice-devel -%endif BuildRequires: corosynclib-devel >= 3.0 BuildRequires: fence-agents-common %if 0%{?suse_version} %if 0%{?suse_version} > 1500 -BuildRequires: libpacemaker3-devel >= %{required_pacemaker_version} +BuildRequires: libpacemaker3-devel >= %{min_compatible_pacemaker_version}, libpacemaker3-devel < %{first_incompatible_pacemaker_version} %else -BuildRequires: libpacemaker-devel >= %{required_pacemaker_version} +BuildRequires: libpacemaker-devel >= %{min_compatible_pacemaker_version}, libpacemaker-devel < %{first_incompatible_pacemaker_version} %endif %else -BuildRequires: pacemaker-libs-devel >= %{required_pacemaker_version} +BuildRequires: pacemaker-libs-devel >= %{min_compatible_pacemaker_version}, pacemaker-libs-devel < %{first_incompatible_pacemaker_version} %endif BuildRequires: resource-agents BuildRequires: sbd @@ -137,7 +135,7 @@ Requires: rubygems # for killall Requires: psmisc # cluster stack and related packages -Requires: pacemaker >= %{required_pacemaker_version} +Requires: pacemaker >= %{min_compatible_pacemaker_version}, pacemaker < %{first_incompatible_pacemaker_version} Requires: corosync >= 3.0 # pcs enables corosync encryption by default so we require libknet1-plugins-all Requires: libknet1-plugins-all From 5bb7fa6fffc050d513a11ea10b7e56dc47eb4cab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Posp=C3=AD=C5=A1il?= Date: Fri, 7 Mar 2025 19:45:16 +0100 Subject: [PATCH 109/227] spec: sync dependencies with downstream --- rpm/pcs.spec.in | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/rpm/pcs.spec.in b/rpm/pcs.spec.in index f21d5039e..2cb90e823 100644 --- a/rpm/pcs.spec.in +++ b/rpm/pcs.spec.in @@ -70,18 +70,11 @@ Source41: pyagentx-%{pyagentx_version}.tar.gz @gemsrc@ # python for pcs -%if 0%{?fedora} >= 30 BuildRequires: python3 >= 3.9 BuildRequires: python3-setuptools -%endif -%if 0%{?rhel} >= 8 -BuildRequires: platform-python -BuildRequires: platform-python-setuptools # for bundled python dateutil BuildRequires: python3-setuptools_scm -%endif - BuildRequires: python3-devel # for tier0 tests BuildRequires: python3-cryptography @@ -91,6 +84,12 @@ BuildRequires: python3-wheel BuildRequires: python3-lxml BuildRequires: python3-pycurl +# for building pcs tarballs +BuildRequires: make +# printf from coreutils is used in makefile, head is used in spec +BuildRequires: coreutils +# find is used in Makefile and also somewhere else +BuildRequires: findutils # gcc for compiling custom rubygems BuildRequires: gcc BuildRequires: gcc-c++ @@ -124,11 +123,14 @@ BuildRequires: mozilla-nss-tools %else BuildRequires: nss-tools %endif +BuildRequires: pkgconf Requires: python3-cryptography Requires: python3-lxml Requires: python3-pycurl Requires: python3-pyparsing +# entrypoints import parts of setuptools +Requires: python3-setuptools # ruby and gems for pcsd Requires: ruby >= 2.5 Requires: rubygems From e736347623608c60aaad07918447cf8ea3c45aab Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Wed, 19 Mar 2025 13:22:05 +0100 Subject: [PATCH 110/227] fix a traceback when resource remove fails in web ui --- CHANGELOG.md | 1 + pcsd/remote.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8feb5c2a..0de297e18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ since pcs-0.11.9) ([RHEL-79055]) - Do not end with traceback when using `pcs resource delete` to remove bundle resources when the bundle has no IP address specified ([RHEL-79160]) +- Fixed a traceback when removing a resource fails in web UI [RHEL-76055]: https://issues.redhat.com/browse/RHEL-76055 [RHEL-76059]: https://issues.redhat.com/browse/RHEL-76059 diff --git a/pcsd/remote.rb b/pcsd/remote.rb index 5e36d11d5..a1ade55b9 100644 --- a/pcsd/remote.rb +++ b/pcsd/remote.rb @@ -1031,7 +1031,7 @@ def remove_resource(params, request, auth_user) if retval == 0 return 200 else - $logger.info("Remove resource errors:\n"+err) + $logger.info("Remove resource errors:\n"+err.join('\n')) return [400, err] end end From 1d8917320ce184697ecf94447403734821840fb5 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Wed, 19 Mar 2025 13:41:01 +0100 Subject: [PATCH 111/227] pcsd: pass force to lower layer in update_cluster_settings --- CHANGELOG.md | 2 ++ pcsd/remote.rb | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0de297e18..50a3ddc85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ - Do not end with traceback when using `pcs resource delete` to remove bundle resources when the bundle has no IP address specified ([RHEL-79160]) - Fixed a traceback when removing a resource fails in web UI +- It is now possible to override errors when editing cluster properties in web + UI [RHEL-76055]: https://issues.redhat.com/browse/RHEL-76055 [RHEL-76059]: https://issues.redhat.com/browse/RHEL-76059 diff --git a/pcsd/remote.rb b/pcsd/remote.rb index a1ade55b9..27d71652c 100644 --- a/pcsd/remote.rb +++ b/pcsd/remote.rb @@ -1355,6 +1355,9 @@ def update_cluster_settings(params, request, auth_user) end end + options = [] + options << "--force" if params["force"] + if to_update.empty? $logger.info('No properties to update') else @@ -1363,10 +1366,10 @@ def update_cluster_settings(params, request, auth_user) cmd_args << "#{prop.downcase}=#{properties[prop]}" } stdout, stderr, retval = run_cmd( - auth_user, PCS, '--', 'property', 'set', *cmd_args + auth_user, PCS, *options, '--', 'property', 'set', *cmd_args ) if retval != 0 - return [400, stderr.join('').gsub(', (use --force to override)', '')] + return [400, stderr.join('')] end end return [200, "Update Successful"] From ed947cc168f8654b16baf0e147bb7499a91e89b0 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Tue, 11 Mar 2025 14:49:31 +0100 Subject: [PATCH 112/227] fix tests for rubygem-json 2.10+ * the rubygem-json parser has been reimplemented and error messages have changed https://github.com/ruby/json/blob/master/CHANGES.md#2025-02-10-2100 --- pcsd/test/test_config.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pcsd/test/test_config.rb b/pcsd/test/test_config.rb index 2f36ffd22..f29785f66 100644 --- a/pcsd/test/test_config.rb +++ b/pcsd/test/test_config.rb @@ -134,7 +134,7 @@ def test_parse_malformed() assert_equal('error', $logger.log[0][0]) assert_match( # the number is based on JSON gem version - /Unable to parse pcs_settings file: (\d+: )?unexpected token/, + /Unable to parse pcs_settings file: ((\d+: )?unexpected token|expected )/, $logger.log[0][1] ) assert_equal_json(fixture_empty_config, cfg.text) @@ -731,7 +731,7 @@ def test_parse_malformed() assert_equal('error', $logger.log[0][0]) assert_match( # the number is based on JSON gem version - /Unable to parse known-hosts file: (\d+: )?unexpected token/, + /Unable to parse known-hosts file: ((\d+: )?unexpected token|expected )/, $logger.log[0][1] ) assert_empty_data(cfg) From 49b512cbd59b16a3d7f2afbfa9df7f160f5ac1a2 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Tue, 1 Apr 2025 12:38:38 +0200 Subject: [PATCH 113/227] fix displaying node-attribute in colocation --- CHANGELOG.md | 2 ++ pcs/cli/constraint/output/colocation.py | 18 +++++++++++++++--- pcs_test/tier1/constraint/test_config.py | 2 +- pcs_test/tier1/legacy/test_constraints.py | 2 +- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50a3ddc85..d5aebd1d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,12 +17,14 @@ - Fixed a traceback when removing a resource fails in web UI - It is now possible to override errors when editing cluster properties in web UI +- Display node-attribute in colocation constraints configuration ([RHEL-82894]) [RHEL-76055]: https://issues.redhat.com/browse/RHEL-76055 [RHEL-76059]: https://issues.redhat.com/browse/RHEL-76059 [RHEL-76177]: https://issues.redhat.com/browse/RHEL-76177 [RHEL-79055]: https://issues.redhat.com/browse/RHEL-79055 [RHEL-79160]: https://issues.redhat.com/browse/RHEL-79160 +[RHEL-82894]: https://issues.redhat.com/browse/RHEL-82894 ## [0.11.9] - 2025-01-10 diff --git a/pcs/cli/constraint/output/colocation.py b/pcs/cli/constraint/output/colocation.py index 7a13ed27b..7d4365b4b 100644 --- a/pcs/cli/constraint/output/colocation.py +++ b/pcs/cli/constraint/output/colocation.py @@ -39,9 +39,14 @@ def _attributes_to_pairs( def _attributes_to_text( attributes_dto: CibConstraintColocationAttributesDto, with_id: bool, + extra_attributes: Iterable[tuple[str, str]] = (), ) -> list[str]: result = [ - " ".join(format_name_value_list(_attributes_to_pairs(attributes_dto))) + " ".join( + format_name_value_list( + _attributes_to_pairs(attributes_dto) + list(extra_attributes) + ) + ) ] if attributes_dto.lifetime: result.append("Lifetime:") @@ -72,9 +77,16 @@ def plain_constraint_to_text( ] if with_id: result[0] += f" (id: {constraint_dto.attributes.constraint_id})" + extra_attributes = [] + if constraint_dto.node_attribute: + extra_attributes += [ + ("node-attribute", constraint_dto.node_attribute), + ] result.extend( indent( - _attributes_to_text(constraint_dto.attributes, with_id), + _attributes_to_text( + constraint_dto.attributes, with_id, extra_attributes + ), indent_step=INDENT_STEP, ) ) @@ -167,7 +179,7 @@ def plain_constraint_to_cmd( return [] if constraint_dto.node_attribute is not None: warn( - "Option 'node_attribute' detected in constraint " + "Option 'node-attribute' detected in constraint " f"'{constraint_dto.attributes.constraint_id}' but not supported by " "this command." " Command for creating the constraint is omitted." diff --git a/pcs_test/tier1/constraint/test_config.py b/pcs_test/tier1/constraint/test_config.py index 47e5a1b26..09da60fa3 100644 --- a/pcs_test/tier1/constraint/test_config.py +++ b/pcs_test/tier1/constraint/test_config.py @@ -223,7 +223,7 @@ def test_dont_export_unsupported_constraints(self): f"Warning: Lifetime configuration detected in constraint 'location-lifetime' but {sufix}" f"Warning: Option 'influence' detected in constraint 'colocation-influence' but {sufix}" f"Warning: Lifetime configuration detected in constraint 'colocation-lifetime' but {sufix}" - f"Warning: Option 'node_attribute' detected in constraint 'colocation-node-attribute' but {sufix}" + f"Warning: Option 'node-attribute' detected in constraint 'colocation-node-attribute' but {sufix}" f"Warning: Option 'ordering' detected in resource set 'colocation-set-ordering-set' but {sufix}" f"Warning: Option 'require-all' detected in constraint 'order-set-require-all' but {sufix}" f"Warning: Option 'ordering' detected in resource set 'order-set-ordering-set' but {sufix}" diff --git a/pcs_test/tier1/legacy/test_constraints.py b/pcs_test/tier1/legacy/test_constraints.py index 8c4bae2e3..ef18cc73d 100644 --- a/pcs_test/tier1/legacy/test_constraints.py +++ b/pcs_test/tier1/legacy/test_constraints.py @@ -908,7 +908,7 @@ def test_colocation_with_score_and_options(self): """\ Colocation Constraints: resource 'D1' with resource 'D2' - score=-100 + score=-100 node-attribute=y """ ), ) From c20c0b8556f52a91d5e32d6cc73fa3d801a300ec Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Wed, 26 Feb 2025 14:23:57 +0100 Subject: [PATCH 114/227] add type hints --- mypy.ini | 10 ++ pcs/lib/cib/resource/stonith.py | 40 +++---- pcs/lib/commands/sbd.py | 204 ++++++++++++++++++-------------- pcs/lib/sbd.py | 95 ++++++++------- 4 files changed, 192 insertions(+), 157 deletions(-) diff --git a/mypy.ini b/mypy.ini index e20e204b9..e995fc6c0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -113,6 +113,9 @@ disallow_untyped_calls = True disallow_untyped_defs = True disallow_untyped_calls = True +[mypy-pcs.lib.cib.resource.stonith] +disallow_untyped_defs = True + [mypy-pcs.lib.cib.resource.relations] disallow_untyped_defs = True disallow_untyped_calls = True @@ -152,6 +155,9 @@ disallow_untyped_defs = True disallow_untyped_defs = True disallow_untyped_calls = True +[mypy-pcs.lib.commands.sbd] +disallow_untyped_defs = True + [mypy-pcs.lib.commands.status] disallow_untyped_defs = True @@ -190,6 +196,10 @@ disallow_untyped_calls = True disallow_untyped_defs = True disallow_untyped_calls = True +[mypy-pcs.lib.sbd] +disallow_untyped_defs = True +disallow_untyped_calls = True + [mypy-pcs.lib.services] disallow_untyped_defs = True diff --git a/pcs/lib/cib/resource/stonith.py b/pcs/lib/cib/resource/stonith.py index c9eef2e02..e844b306d 100644 --- a/pcs/lib/cib/resource/stonith.py +++ b/pcs/lib/cib/resource/stonith.py @@ -1,9 +1,7 @@ import re from typing import ( - Dict, - List, + Mapping, Optional, - Tuple, cast, ) @@ -36,7 +34,7 @@ ) -def is_stonith(resource_el: _Element): +def is_stonith(resource_el: _Element) -> bool: return ( resource_el.tag == TAG_RESOURCE_PRIMITIVE and resource_el.get("class") == "stonith" @@ -59,12 +57,12 @@ def is_stonith_enabled(crm_config_el: _Element) -> bool: def get_misconfigured_resources( resources_el: _Element, -) -> Tuple[List[_Element], List[_Element], List[_Element]]: +) -> tuple[list[_Element], list[_Element], list[_Element]]: """ Return stonith: all, 'action' option set, 'method' option set to 'cycle' """ stonith_all = cast( - List[_Element], resources_el.xpath("//primitive[@class='stonith']") + list[_Element], resources_el.xpath("//primitive[@class='stonith']") ) stonith_with_action = [] stonith_with_method_cycle = [] @@ -87,7 +85,7 @@ def get_misconfigured_resources( def validate_stonith_restartless_update( cib: _Element, stonith_id: str, -) -> Tuple[Optional[_Element], ReportItemList]: +) -> tuple[Optional[_Element], ReportItemList]: """ Validate that stonith device exists and its type is supported for restartless update of scsi devices and has defined option 'devices'. @@ -132,7 +130,7 @@ def validate_stonith_restartless_update( def get_node_key_map_for_mpath( stonith_el: _Element, node_labels: StringIterable -) -> Dict[str, str]: +) -> dict[str, str]: def library_error( host_map: Optional[str], missing_nodes: StringIterable ) -> LibraryError: @@ -179,8 +177,8 @@ def library_error( def _get_digest( attr: str, - attr_to_type_map: Dict[str, str], - calculated_digests: Dict[str, Optional[str]], + attr_to_type_map: Mapping[str, str], + calculated_digests: Mapping[str, Optional[str]], ) -> str: """ Return digest of right type for the specified attribute. If missing, raise @@ -207,7 +205,7 @@ def _get_digest( return digest -def _get_transient_instance_attributes(cib: _Element) -> List[_Element]: +def _get_transient_instance_attributes(cib: _Element) -> list[_Element]: """ Return list of instance_attributes elements which could contain digest attributes. @@ -215,7 +213,7 @@ def _get_transient_instance_attributes(cib: _Element) -> List[_Element]: cib -- CIB root element """ return cast( - List[_Element], + list[_Element], cib.xpath( "./status/node_state/transient_attributes/instance_attributes" ), @@ -228,7 +226,7 @@ def _get_lrm_rsc_op_elements( node_name: str, op_name: str, interval: Optional[str] = None, -) -> List[_Element]: +) -> list[_Element]: """ Get a lrm_rsc_op element from cib status. @@ -238,7 +236,7 @@ def _get_lrm_rsc_op_elements( interval -- operation interval using for monitor operation selection """ return cast( - List[_Element], + list[_Element], cib.xpath( """ ./status/node_state[@uname=$node_name] @@ -255,9 +253,9 @@ def _get_lrm_rsc_op_elements( def _get_monitor_attrs( resource_el: _Element, -) -> List[Dict[str, Optional[str]]]: +) -> list[dict[str, Optional[str]]]: """ - Get list of interval/timeout attributes of all monitor oparations of + Get list of interval/timeout attributes of all monitor operations of the resource which is being updated. Only interval and timeout attributes are needed for digests @@ -271,7 +269,7 @@ def _get_monitor_attrs( from the resource definition and lrm_rsc_op elements from the cluster status, it will be found later. """ - monitor_attrs_list: List[Dict[str, Optional[str]]] = [] + monitor_attrs_list: list[dict[str, Optional[str]]] = [] for operation_el in operations.get_resource_operations( resource_el, names=["monitor"] ): @@ -296,8 +294,8 @@ def _get_monitor_attrs( def _update_digest_attrs_in_lrm_rsc_op( - lrm_rsc_op: _Element, calculated_digests: Dict[str, Optional[str]] -): + lrm_rsc_op: _Element, calculated_digests: Mapping[str, Optional[str]] +) -> None: """ Update digest attributes in lrm_rsc_op elements. If there are missing digests values from pacemaker or missing digests attributes in lrm_rsc_op @@ -367,7 +365,7 @@ def _update_digest_attrs_in_transient_instance_attributes( nvset_el: _Element, stonith_id: str, stonith_type: str, - calculated_digests: Dict[str, Optional[str]], + calculated_digests: Mapping[str, Optional[str]], ) -> None: """ Update digests attributes in transient instance attributes element. @@ -380,7 +378,7 @@ def _update_digest_attrs_in_transient_instance_attributes( """ for attr in TRANSIENT_DIGEST_ATTRS: nvpair_list = cast( - List[_Element], + list[_Element], nvset_el.xpath("./nvpair[@name=$name]", name=attr), ) if not nvpair_list: diff --git a/pcs/lib/commands/sbd.py b/pcs/lib/commands/sbd.py index 8479f782b..3180acaed 100644 --- a/pcs/lib/commands/sbd.py +++ b/pcs/lib/commands/sbd.py @@ -1,8 +1,9 @@ -from typing import Any +from typing import Any, Iterable, Mapping, Optional, TypeVar from pcs import settings from pcs.common import reports -from pcs.common.reports.item import ReportItem +from pcs.common.node_communicator import RequestTarget +from pcs.common.types import StringSequence from pcs.common.validate import is_integer from pcs.lib import ( sbd, @@ -21,10 +22,13 @@ ) from pcs.lib.communication.tools import run as run_com from pcs.lib.communication.tools import run_and_raise +from pcs.lib.env import LibraryEnvironment from pcs.lib.errors import LibraryError from pcs.lib.node import get_existing_nodes_names from pcs.lib.tools import environment_file_to_dict +T = TypeVar("T") + _UNSUPPORTED_SBD_OPTION_LIST = [ "SBD_WATCHDOG_DEV", "SBD_OPTS", @@ -44,7 +48,7 @@ _STARTMODE_ALLOWED_VALUES = ["always", "clean"] -def __tuple(set1, set2): +def __tuple(set1: set[str], set2: set[str]) -> set[str]: return {f"{v1},{v2}" for v1 in set1 for v2 in set2} @@ -70,11 +74,13 @@ def _get_allowed_values(self) -> Any: def _validate_sbd_options( - sbd_config, allow_unknown_opts=False, allow_invalid_option_values=False -): + sbd_config: Mapping[str, str], + allow_unknown_opts: bool = False, + allow_invalid_option_values: bool = False, +) -> reports.ReportItemList: """ Validate user SBD configuration. Options 'SBD_WATCHDOG_DEV' and 'SBD_OPTS' - are restricted. Returns list of ReportItem + are restricted sbd_config -- dictionary in format: : allow_unknown_opts -- if True, accept also unknown options. @@ -112,28 +118,33 @@ def _validate_sbd_options( return validate.ValidatorAll(validators).validate(sbd_config) -def _validate_watchdog_dict(watchdog_dict): +def _validate_watchdog_dict( + watchdog_dict: Mapping[str, str], +) -> reports.ReportItemList: """ Validates if all watchdogs are not empty strings. - Returns list of ReportItem. watchdog_dict -- dictionary with node names as keys and value as watchdog """ return [ - ReportItem.error(reports.messages.WatchdogInvalid(watchdog)) + reports.ReportItem.error(reports.messages.WatchdogInvalid(watchdog)) for watchdog in watchdog_dict.values() if not watchdog ] -def _get_full_target_dict(target_list, node_value_dict, default_value): +def _get_full_target_dict( + target_list: Iterable[RequestTarget], + node_value_dict: Mapping[str, T], + default_value: T, +) -> dict[str, T]: """ Returns dictionary where keys are labels of all nodes in cluster and value is obtained from node_value_dict for node name, or default value if node is not specified in node_value_dict. - list node_list -- list of cluster nodes (RequestTarget object) - node_value_dict -- dictionary, keys: node names, values: some velue + node_list -- cluster nodes + node_value_dict -- dictionary, keys: node names, values: some value default_value -- some default value """ return { @@ -143,24 +154,23 @@ def _get_full_target_dict(target_list, node_value_dict, default_value): def enable_sbd( # noqa: PLR0913 - lib_env, - default_watchdog, - watchdog_dict, - sbd_options, - default_device_list=None, - node_device_dict=None, + lib_env: LibraryEnvironment, + default_watchdog: Optional[str], + watchdog_dict: Mapping[str, str], + sbd_options: Mapping[str, str], + default_device_list: Optional[StringSequence] = None, + node_device_dict: Optional[Mapping[str, StringSequence]] = None, *, - allow_unknown_opts=False, - ignore_offline_nodes=False, - no_watchdog_validation=False, - allow_invalid_option_values=False, -): + allow_unknown_opts: bool = False, + ignore_offline_nodes: bool = False, + no_watchdog_validation: bool = False, + allow_invalid_option_values: bool = False, +) -> None: # pylint: disable=too-many-arguments # pylint: disable=too-many-locals """ Enable SBD on all nodes in cluster. - lib_env -- LibraryEnvironment default_watchdog -- watchdog for nodes which are not specified in watchdog_dict. Uses default value from settings if None. watchdog_dict -- dictionary with node names as keys and watchdog path @@ -192,7 +202,9 @@ def enable_sbd( # noqa: PLR0913 node_list, get_nodes_report_list = get_existing_nodes_names(corosync_conf) if not node_list: get_nodes_report_list.append( - ReportItem.error(reports.messages.CorosyncConfigNoNodesDefined()) + reports.ReportItem.error( + reports.messages.CorosyncConfigNoNodesDefined() + ) ) target_list = lib_env.get_node_target_factory().get_target_list( node_list, @@ -209,7 +221,7 @@ def enable_sbd( # noqa: PLR0913 if lib_env.report_processor.report_list( get_nodes_report_list + [ - ReportItem.error(reports.messages.NodeNotFound(node)) + reports.ReportItem.error(reports.messages.NodeNotFound(node)) for node in ( set(list(watchdog_dict.keys()) + list(node_device_dict.keys())) - set(node_list) @@ -227,21 +239,23 @@ def enable_sbd( # noqa: PLR0913 ).has_errors: raise LibraryError() - com_cmd = GetOnlineTargets( + com_cmd_1 = GetOnlineTargets( lib_env.report_processor, ignore_offline_targets=ignore_offline_nodes, ) - com_cmd.set_targets(target_list) - online_targets = run_and_raise(lib_env.get_node_communicator(), com_cmd) + com_cmd_1.set_targets(target_list) + online_targets = run_and_raise(lib_env.get_node_communicator(), com_cmd_1) # check if SBD can be enabled if no_watchdog_validation: lib_env.report_processor.report( - ReportItem.warning(reports.messages.SbdWatchdogValidationInactive()) + reports.ReportItem.warning( + reports.messages.SbdWatchdogValidationInactive() + ) ) - com_cmd = CheckSbd(lib_env.report_processor) + com_cmd_2 = CheckSbd(lib_env.report_processor) for target in online_targets: - com_cmd.add_request( + com_cmd_2.add_request( target, ( # Do not send watchdog if validation is turned off. Listing of @@ -253,13 +267,13 @@ def enable_sbd( # noqa: PLR0913 ), full_device_dict[target.label] if using_devices else [], ) - run_and_raise(lib_env.get_node_communicator(), com_cmd) + run_and_raise(lib_env.get_node_communicator(), com_cmd_2) # enable ATB if needed if not using_devices: if sbd.atb_has_to_be_enabled_pre_enable_check(corosync_conf): lib_env.report_processor.report( - ReportItem.warning( + reports.ReportItem.warning( reports.messages.CorosyncQuorumAtbWillBeEnabledDueToSbd() ) ) @@ -269,9 +283,9 @@ def enable_sbd( # noqa: PLR0913 # distribute SBD configuration config = sbd.get_default_sbd_config() config.update(sbd_options) - com_cmd = SetSbdConfig(lib_env.report_processor) + com_cmd_3 = SetSbdConfig(lib_env.report_processor) for target in online_targets: - com_cmd.add_request( + com_cmd_3.add_request( target, sbd.create_sbd_config( config, @@ -280,30 +294,31 @@ def enable_sbd( # noqa: PLR0913 full_device_dict[target.label], ), ) - run_and_raise(lib_env.get_node_communicator(), com_cmd) + run_and_raise(lib_env.get_node_communicator(), com_cmd_3) # remove cluster prop 'stonith_watchdog_timeout' - com_cmd = RemoveStonithWatchdogTimeout(lib_env.report_processor) - com_cmd.set_targets(online_targets) - run_and_raise(lib_env.get_node_communicator(), com_cmd) + com_cmd_4 = RemoveStonithWatchdogTimeout(lib_env.report_processor) + com_cmd_4.set_targets(online_targets) + run_and_raise(lib_env.get_node_communicator(), com_cmd_4) # enable SBD service an all nodes - com_cmd = EnableSbdService(lib_env.report_processor) - com_cmd.set_targets(online_targets) - run_and_raise(lib_env.get_node_communicator(), com_cmd) + com_cmd_5 = EnableSbdService(lib_env.report_processor) + com_cmd_5.set_targets(online_targets) + run_and_raise(lib_env.get_node_communicator(), com_cmd_5) lib_env.report_processor.report( - ReportItem.warning( + reports.ReportItem.warning( reports.messages.ClusterRestartRequiredToApplyChanges() ) ) -def disable_sbd(lib_env, ignore_offline_nodes=False): +def disable_sbd( + lib_env: LibraryEnvironment, ignore_offline_nodes: bool = False +) -> None: """ Disable SBD on all nodes in cluster. - lib_env -- LibraryEnvironment ignore_offline_nodes -- if True, omit offline nodes """ node_list, get_nodes_report_list = get_existing_nodes_names( @@ -311,39 +326,43 @@ def disable_sbd(lib_env, ignore_offline_nodes=False): ) if not node_list: get_nodes_report_list.append( - ReportItem.error(reports.messages.CorosyncConfigNoNodesDefined()) + reports.ReportItem.error( + reports.messages.CorosyncConfigNoNodesDefined() + ) ) if lib_env.report_processor.report_list(get_nodes_report_list).has_errors: raise LibraryError() - com_cmd = GetOnlineTargets( + com_cmd_1 = GetOnlineTargets( lib_env.report_processor, ignore_offline_targets=ignore_offline_nodes, ) - com_cmd.set_targets( + com_cmd_1.set_targets( lib_env.get_node_target_factory().get_target_list( node_list, skip_non_existing=ignore_offline_nodes, ) ) - online_nodes = run_and_raise(lib_env.get_node_communicator(), com_cmd) + online_nodes = run_and_raise(lib_env.get_node_communicator(), com_cmd_1) - com_cmd = SetStonithWatchdogTimeoutToZero(lib_env.report_processor) - com_cmd.set_targets(online_nodes) - run_and_raise(lib_env.get_node_communicator(), com_cmd) + com_cmd_2 = SetStonithWatchdogTimeoutToZero(lib_env.report_processor) + com_cmd_2.set_targets(online_nodes) + run_and_raise(lib_env.get_node_communicator(), com_cmd_2) - com_cmd = DisableSbdService(lib_env.report_processor) - com_cmd.set_targets(online_nodes) - run_and_raise(lib_env.get_node_communicator(), com_cmd) + com_cmd_3 = DisableSbdService(lib_env.report_processor) + com_cmd_3.set_targets(online_nodes) + run_and_raise(lib_env.get_node_communicator(), com_cmd_3) lib_env.report_processor.report( - ReportItem.warning( + reports.ReportItem.warning( reports.messages.ClusterRestartRequiredToApplyChanges() ) ) -def get_cluster_sbd_status(lib_env): +def get_cluster_sbd_status( + lib_env: LibraryEnvironment, +) -> dict[str, dict[str, bool]]: """ Returns status of SBD service in cluster in dictionary with format: { @@ -354,15 +373,15 @@ def get_cluster_sbd_status(lib_env): }, ... } - - lib_env -- LibraryEnvironment """ node_list, get_nodes_report_list = get_existing_nodes_names( lib_env.get_corosync_conf() ) if not node_list: get_nodes_report_list.append( - ReportItem.error(reports.messages.CorosyncConfigNoNodesDefined()) + reports.ReportItem.error( + reports.messages.CorosyncConfigNoNodesDefined() + ) ) if lib_env.report_processor.report_list(get_nodes_report_list).has_errors: raise LibraryError() @@ -376,7 +395,9 @@ def get_cluster_sbd_status(lib_env): return run_com(lib_env.get_node_communicator(), com_cmd) -def get_cluster_sbd_config(lib_env): +def get_cluster_sbd_config( + lib_env: LibraryEnvironment, +) -> list[dict[str, Optional[str]]]: """ Returns list of SBD config from all cluster nodes in cluster. Structure of data: @@ -389,15 +410,15 @@ def get_cluster_sbd_config(lib_env): ] If error occurs while obtaining config from some node, it's config will be None. If obtaining config fail on all node returns empty dictionary. - - lib_env -- LibraryEnvironment """ node_list, get_nodes_report_list = get_existing_nodes_names( lib_env.get_corosync_conf() ) if not node_list: get_nodes_report_list.append( - ReportItem.error(reports.messages.CorosyncConfigNoNodesDefined()) + reports.ReportItem.error( + reports.messages.CorosyncConfigNoNodesDefined() + ) ) if lib_env.report_processor.report_list(get_nodes_report_list).has_errors: raise LibraryError() @@ -411,28 +432,26 @@ def get_cluster_sbd_config(lib_env): return run_com(lib_env.get_node_communicator(), com_cmd) -def get_local_sbd_config(lib_env): +def get_local_sbd_config(lib_env: LibraryEnvironment) -> dict[str, str]: """ Returns local SBD config as dictionary. - - lib_env -- LibraryEnvironment """ del lib_env return environment_file_to_dict(sbd.get_local_sbd_config()) -def initialize_block_devices(lib_env, device_list, option_dict): +def initialize_block_devices( + lib_env: LibraryEnvironment, + device_list: StringSequence, + option_dict: Mapping[str, str], +) -> None: """ Initialize SBD devices in device_list with options_dict. - - lib_env -- LibraryEnvironment - device_list -- list of strings - option_dict -- dictionary """ report_item_list = [] if not device_list: report_item_list.append( - ReportItem.error( + reports.ReportItem.error( reports.messages.RequiredOptionsAreMissing(["device"]) ) ) @@ -454,7 +473,9 @@ def initialize_block_devices(lib_env, device_list, option_dict): ) -def get_local_devices_info(lib_env, dump=False): +def get_local_devices_info( + lib_env: LibraryEnvironment, dump: bool = False +) -> list[dict[str, Optional[str]]]: """ Returns list of local devices info in format: { @@ -464,13 +485,12 @@ def get_local_devices_info(lib_env, dump=False): } If sbd is not enabled, empty list will be returned. - lib_env -- LibraryEnvironment dump -- if True returns also output of command 'sbd dump' """ if not sbd.is_sbd_enabled(lib_env.service_manager): return [] device_list = sbd.get_local_sbd_device_list() - report_item_list = [] + report_item_list: reports.ReportItemList = [] output = [] for device in device_list: obj = { @@ -498,14 +518,15 @@ def get_local_devices_info(lib_env, dump=False): return output -def set_message(lib_env, device, node_name, message): +def set_message( + lib_env: LibraryEnvironment, device: str, node_name: str, message: str +) -> None: """ Set message on device for node_name. - lib_env -- LibraryEnvironment - device -- string, absolute path to device - node_name -- string - message -- string, message type, should be one of settings.sbd_message_types + device -- absolute path to device + node_name -- + message -- message type, should be one of settings.sbd_message_types """ report_item_list = [] missing_options = [] @@ -515,14 +536,14 @@ def set_message(lib_env, device, node_name, message): missing_options.append("node") if missing_options: report_item_list.append( - ReportItem.error( + reports.ReportItem.error( reports.messages.RequiredOptionsAreMissing(missing_options) ) ) supported_messages = settings.sbd_message_types if message not in supported_messages: report_item_list.append( - ReportItem.error( + reports.ReportItem.error( reports.messages.InvalidOptionValue( "message", message, supported_messages ) @@ -533,25 +554,26 @@ def set_message(lib_env, device, node_name, message): sbd.set_message(lib_env.cmd_runner(), device, node_name, message) -def get_local_available_watchdogs(lib_env): +def get_local_available_watchdogs( + lib_env: LibraryEnvironment, +) -> dict[str, dict[str, str]]: """ Returns available local watchdog devices. - - lib_env LibraryEnvironment """ return sbd.get_available_watchdogs(lib_env.cmd_runner()) -def test_local_watchdog(lib_env, watchdog=None): +def test_local_watchdog( + lib_env: LibraryEnvironment, watchdog: Optional[str] = None +) -> None: """ Test local watchdog device by triggering it. System reset is expected. If watchdog is not specified, available watchdog will be used if there is only one. - lib_env LibraryEnvironment - watchdog string -- watchdog to trigger + watchdog -- watchdog to trigger """ lib_env.report_processor.report( - ReportItem.info(reports.messages.SystemWillReset()) + reports.ReportItem.info(reports.messages.SystemWillReset()) ) sbd.test_watchdog(lib_env.cmd_runner(), watchdog) diff --git a/pcs/lib/sbd.py b/pcs/lib/sbd.py index c77344e32..7069c4c77 100644 --- a/pcs/lib/sbd.py +++ b/pcs/lib/sbd.py @@ -1,6 +1,7 @@ import re from os import path from typing import ( + Mapping, Optional, Union, ) @@ -8,10 +9,12 @@ from pcs import settings from pcs.common import reports from pcs.common.services.interfaces import ServiceManagerInterface +from pcs.common.types import StringSequence from pcs.common.validate import is_integer from pcs.lib import validate from pcs.lib.corosync.config_facade import ConfigFacade as CorosyncConfFacade from pcs.lib.errors import LibraryError +from pcs.lib.external import CommandRunner from pcs.lib.services import is_systemd from pcs.lib.tools import ( dict_to_environment_file, @@ -64,8 +67,8 @@ def _get_allowed_values(self) -> None: def _even_number_of_nodes_and_no_qdevice( - corosync_conf_facade, node_number_modifier=0 -): + corosync_conf_facade: CorosyncConfFacade, node_number_modifier: int = 0 +) -> bool: """ Returns True whenever cluster has no quorum device configured and number of nodes + node_number_modifier is even number, False otherwise. @@ -107,7 +110,9 @@ def is_auto_tie_breaker_needed( ) -def atb_has_to_be_enabled_pre_enable_check(corosync_conf_facade): +def atb_has_to_be_enabled_pre_enable_check( + corosync_conf_facade: CorosyncConfFacade, +) -> bool: """ Returns True whenever quorum option auto_tie_breaker is needed to be enabled for proper working of SBD fencing. False if it is not needed. This function @@ -146,11 +151,13 @@ def atb_has_to_be_enabled( ) -def validate_new_nodes_devices(nodes_devices): +def validate_new_nodes_devices( + nodes_devices: Mapping[str, StringSequence], +) -> reports.ReportItemList: """ Validate if SBD devices are set for new nodes when they should be - dict nodes_devices -- name: node name, key: list of SBD devices + nodes_devices -- name: node name, key: list of SBD devices """ if _is_device_set_local(): return validate_nodes_devices( @@ -166,16 +173,16 @@ def validate_new_nodes_devices(nodes_devices): def validate_nodes_devices( - node_device_dict, adding_nodes_to_sbd_enabled_cluster=False -): + node_device_dict: Mapping[str, StringSequence], + adding_nodes_to_sbd_enabled_cluster: bool = False, +) -> reports.ReportItemList: """ Validates device list for all nodes. If node is present, it checks if there is at least one device and at max settings.sbd_max_device_num. Also devices have to be specified with absolute path. - Returns list of ReportItem - dict node_device_dict -- name: node name, key: list of SBD devices - bool adding_nodes_to_sbd_enabled_cluster -- provides context to reports + node_device_dict -- name: node name, key: list of SBD devices + adding_nodes_to_sbd_enabled_cluster -- provides context to reports """ report_item_list = [] for node_label, device_list in node_device_dict.items(): @@ -194,7 +201,9 @@ def validate_nodes_devices( report_item_list.append( reports.ReportItem.error( reports.messages.SbdTooManyDevicesForNode( - node_label, device_list, settings.sbd_max_device_num + node_label, + list(device_list), + settings.sbd_max_device_num, ) ) ) @@ -208,7 +217,12 @@ def validate_nodes_devices( return report_item_list -def create_sbd_config(base_config, node_label, watchdog, device_list=None): +def create_sbd_config( + base_config: Mapping[str, str], + node_label: str, + watchdog: str, + device_list: Optional[StringSequence] = None, +) -> str: # TODO: figure out which name/ring has to be in SBD_OPTS config = dict(base_config) config["SBD_OPTS"] = f'"-n {node_label}"' @@ -220,7 +234,7 @@ def create_sbd_config(base_config, node_label, watchdog, device_list=None): return dict_to_environment_file(config) -def get_default_sbd_config(): +def get_default_sbd_config() -> dict[str, str]: """ Returns default SBD configuration as dictionary. """ @@ -235,9 +249,7 @@ def get_default_sbd_config(): def get_local_sbd_config() -> str: """ - Get local SBD configuration. - - Raises LibraryError on any failure. + Get local SBD configuration. Raise LibraryError on any failure. """ try: with open(settings.sbd_config, "r") as sbd_cfg: @@ -272,22 +284,18 @@ def is_sbd_installed(service_manager: ServiceManagerInterface) -> bool: def initialize_block_devices( report_processor: reports.ReportProcessor, - cmd_runner, - device_list, - option_dict, -): + cmd_runner: CommandRunner, + device_list: StringSequence, + option_dict: Mapping[str, str], +) -> None: """ Initialize devices with specified options in option_dict. Raise LibraryError on failure. - report_processor -- report processor - cmd_runner -- CommandRunner - device_list -- list of strings - option_dict -- dictionary of options and their values """ report_processor.report( reports.ReportItem.info( - reports.messages.SbdDeviceInitializationStarted(device_list) + reports.messages.SbdDeviceInitializationStarted(list(device_list)) ) ) @@ -304,18 +312,18 @@ def initialize_block_devices( raise LibraryError( reports.ReportItem.error( reports.messages.SbdDeviceInitializationError( - device_list, std_err + list(device_list), std_err ) ) ) report_processor.report( reports.ReportItem.info( - reports.messages.SbdDeviceInitializationSuccess(device_list) + reports.messages.SbdDeviceInitializationSuccess(list(device_list)) ) ) -def get_local_sbd_device_list(): +def get_local_sbd_device_list() -> list[str]: """ Returns list of devices specified in local SBD config """ @@ -339,12 +347,9 @@ def _is_device_set_local() -> bool: return len(get_local_sbd_device_list()) > 0 -def get_device_messages_info(cmd_runner, device): +def get_device_messages_info(cmd_runner: CommandRunner, device: str) -> str: """ Returns info about messages (string) stored on specified SBD device. - - cmd_runner -- CommandRunner - device -- string """ std_out, dummy_std_err, ret_val = cmd_runner.run( [settings.sbd_exec, "-d", device, "list"] @@ -359,12 +364,9 @@ def get_device_messages_info(cmd_runner, device): return std_out -def get_device_sbd_header_dump(cmd_runner, device): +def get_device_sbd_header_dump(cmd_runner: CommandRunner, device: str) -> str: """ Returns header dump (string) of specified SBD device. - - cmd_runner -- CommandRunner - device -- string """ std_out, dummy_std_err, ret_val = cmd_runner.run( [settings.sbd_exec, "-d", device, "dump"] @@ -401,8 +403,6 @@ def validate_stonith_watchdog_timeout( ) -> reports.ReportItemList: """ Check sbd status and config when user is setting stonith-watchdog-timeout - Returns error message if the value is unacceptable, otherwise return nothing - to set the property stonith_watchdog_timeout -- value to be validated """ @@ -442,14 +442,15 @@ def validate_stonith_watchdog_timeout( ).validate({"stonith-watchdog-timeout": stonith_watchdog_timeout}) -def set_message(cmd_runner, device, node_name, message): +def set_message( + cmd_runner: CommandRunner, device: str, node_name: str, message: str +) -> None: """ Set message of specified type 'message' on SBD device for node. - cmd_runner -- CommandRunner - device -- string, device path - node_name -- string, name of node for which message should be set - message -- string, message type + device -- device path + node_name -- name of node for which message should be set + message -- message type """ dummy_std_out, std_err, ret_val = cmd_runner.run( [settings.sbd_exec, "-d", device, "message", node_name, message] @@ -464,7 +465,9 @@ def set_message(cmd_runner, device, node_name, message): ) -def get_available_watchdogs(cmd_runner): +def get_available_watchdogs( + cmd_runner: CommandRunner, +) -> dict[str, dict[str, str]]: regex = ( r"\[\d+\] (?P.+)$\n" r"Identity: (?P.+)$\n" @@ -488,7 +491,9 @@ def get_available_watchdogs(cmd_runner): } -def test_watchdog(cmd_runner, watchdog=None): +def test_watchdog( + cmd_runner: CommandRunner, watchdog: Optional[str] = None +) -> None: cmd = [settings.sbd_exec, "test-watchdog"] if watchdog: cmd.extend(["-w", watchdog]) From eff39d318be3311e0984a5ecbb320a8d4d64fa9b Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Wed, 5 Mar 2025 15:02:32 +0100 Subject: [PATCH 115/227] error when removing last stonith --- CHANGELOG.md | 3 + pcs/common/reports/codes.py | 1 + pcs/common/reports/messages.py | 16 + pcs/common/reports/types.py | 3 +- pcs/lib/cib/resource/stonith.py | 30 +- pcs/lib/commands/cib.py | 88 ++++- pcs/lib/commands/sbd.py | 33 +- pcs/stonith.py | 13 +- .../tier0/common/reports/test_messages.py | 12 + .../tier0/lib/cib/resource/test_stonith.py | 46 +++ .../lib/commands/sbd/test_disable_sbd.py | 70 ++++ pcs_test/tier0/lib/commands/test_cib.py | 342 ++++++++++++++++++ pcs_test/tier1/legacy/test_stonith.py | 17 +- pcs_test/tier1/stonith/test_remove.py | 63 +++- 14 files changed, 703 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5aebd1d2..67d3204a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ - Command `pcs stonith sbd watchdog list` now prints watchdogs' identity and driver ([RHEL-76177]) - Command `pcs cluster rename` for changing cluster name ([RHEL-76055]) +- Removing stonith devices or disabling SBD fails if the cluster would be left + with disabled SBD and no stonith devices ([RHEL-76170]) ### Fixed - Command `pcs resource restart` allows restarting bundle instances (broken @@ -21,6 +23,7 @@ [RHEL-76055]: https://issues.redhat.com/browse/RHEL-76055 [RHEL-76059]: https://issues.redhat.com/browse/RHEL-76059 +[RHEL-76170]: https://issues.redhat.com/browse/RHEL-76170 [RHEL-76177]: https://issues.redhat.com/browse/RHEL-76177 [RHEL-79055]: https://issues.redhat.com/browse/RHEL-79055 [RHEL-79160]: https://issues.redhat.com/browse/RHEL-79160 diff --git a/pcs/common/reports/codes.py b/pcs/common/reports/codes.py index 48e83afd8..393fceb76 100644 --- a/pcs/common/reports/codes.py +++ b/pcs/common/reports/codes.py @@ -323,6 +323,7 @@ MULTIPLE_RESULTS_FOUND = M("MULTIPLE_RESULTS_FOUND") MUTUALLY_EXCLUSIVE_OPTIONS = M("MUTUALLY_EXCLUSIVE_OPTIONS") NO_ACTION_NECESSARY = M("NO_ACTION_NECESSARY") +NO_STONITH_MEANS_WOULD_BE_LEFT = M("NO_STONITH_MEANS_WOULD_BE_LEFT") NODE_ADDRESSES_ALREADY_EXIST = M("NODE_ADDRESSES_ALREADY_EXIST") NODE_ADDRESSES_CANNOT_BE_EMPTY = M("NODE_ADDRESSES_CANNOT_BE_EMPTY") NODE_ADDRESSES_DUPLICATION = M("NODE_ADDRESSES_DUPLICATION") diff --git a/pcs/common/reports/messages.py b/pcs/common/reports/messages.py index e4e8b332e..d9eefadb5 100644 --- a/pcs/common/reports/messages.py +++ b/pcs/common/reports/messages.py @@ -8295,3 +8295,19 @@ class CibXmlMissing(ReportItemMessage): @property def message(self) -> str: return "CIB XML file cannot be found" + + +@dataclass(frozen=True) +class NoStonithMeansWouldBeLeft(ReportItemMessage): + """ + The requested change would left the cluster with no stonith configured + """ + + _code = codes.NO_STONITH_MEANS_WOULD_BE_LEFT + + @property + def message(self) -> str: + return ( + "Requested action removes all stonith means, resulting in the " + "cluster not being able to recover from certain failure conditions" + ) diff --git a/pcs/common/reports/types.py b/pcs/common/reports/types.py index 7fcc1e284..6513f191c 100644 --- a/pcs/common/reports/types.py +++ b/pcs/common/reports/types.py @@ -1,4 +1,4 @@ -from typing import NewType +from typing import Collection, NewType AddRemoveContainerType = NewType("AddRemoveContainerType", str) AddRemoveItemType = NewType("AddRemoveItemType", str) @@ -6,6 +6,7 @@ DefaultAddressSource = NewType("DefaultAddressSource", str) FenceHistoryCommandType = NewType("FenceHistoryCommandType", str) ForceCode = NewType("ForceCode", str) +ForceFlags = Collection[ForceCode] MessageCode = NewType("MessageCode", str) DeprecatedMessageCode = NewType("DeprecatedMessageCode", str) PcsCommand = NewType("PcsCommand", str) diff --git a/pcs/lib/cib/resource/stonith.py b/pcs/lib/cib/resource/stonith.py index e844b306d..c2f4a5ab8 100644 --- a/pcs/lib/cib/resource/stonith.py +++ b/pcs/lib/cib/resource/stonith.py @@ -55,15 +55,39 @@ def is_stonith_enabled(crm_config_el: _Element) -> bool: return stonith_enabled +def get_all_resources(resources_el: _Element) -> list[_Element]: + """ + Return all stonith resources + """ + return cast( + list[_Element], resources_el.xpath("//primitive[@class='stonith']") + ) + + +def get_all_node_isolating_resources(resources_el: _Element) -> list[_Element]: + """ + Return all stonith resources which actually do fencing on their own + """ + return [ + res_el + for res_el in get_all_resources(resources_el) + if res_el.get("type") + not in { + "fence_heuristics_ping", + "fence_kdump", + "fence_sbd", + "fence_watchdog", + } + ] + + def get_misconfigured_resources( resources_el: _Element, ) -> tuple[list[_Element], list[_Element], list[_Element]]: """ Return stonith: all, 'action' option set, 'method' option set to 'cycle' """ - stonith_all = cast( - list[_Element], resources_el.xpath("//primitive[@class='stonith']") - ) + stonith_all = get_all_resources(resources_el) stonith_with_action = [] stonith_with_method_cycle = [] for stonith in stonith_all: diff --git a/pcs/lib/commands/cib.py b/pcs/lib/commands/cib.py index ed72b9710..6b7967e98 100644 --- a/pcs/lib/commands/cib.py +++ b/pcs/lib/commands/cib.py @@ -1,5 +1,4 @@ from typing import ( - Collection, Iterable, Sequence, ) @@ -25,15 +24,23 @@ from pcs.lib.cib.resource.remote_node import ( get_node_name_from_resource as get_node_name_from_remote_resource, ) +from pcs.lib.cib.resource.stonith import ( + get_all_node_isolating_resources, + is_stonith, +) +from pcs.lib.cib.tools import get_resources +from pcs.lib.communication.sbd import GetSbdStatus +from pcs.lib.communication.tools import run as run_communication from pcs.lib.env import LibraryEnvironment from pcs.lib.errors import LibraryError +from pcs.lib.node import get_existing_nodes_names from pcs.lib.pacemaker.live import remove_node def remove_elements( env: LibraryEnvironment, ids: StringCollection, - force_flags: Collection[reports.types.ForceCode] = (), + force_flags: reports.types.ForceFlags = (), ) -> None: """ Remove elements with specified ids from CIB. This function is aware of @@ -70,6 +77,9 @@ def remove_elements( if report_processor.report_list( _validate_elements_to_remove(elements_to_remove) + _warn_remote_guest(remote_node_names, guest_node_names) + + _ensure_some_stonith_remains( + env, get_resources(cib), elements_to_remove, force_flags + ) ).has_errors: raise LibraryError() @@ -96,7 +106,7 @@ def _stop_resources_wait( env: LibraryEnvironment, cib: _Element, resource_elements: Sequence[_Element], - force_flags: Collection[reports.types.ForceCode] = (), + force_flags: reports.types.ForceFlags = (), ) -> _Element: """ Stop all resources that are going to be removed. Push cib, wait for the @@ -194,3 +204,75 @@ def _get_guest_node_names(resource_elements: Iterable[_Element]) -> list[str]: for el in resource_elements if is_guest_node(el) ] + + +def _ensure_some_stonith_remains( + env: LibraryEnvironment, + resources_el: _Element, + elements_to_remove: ElementsToRemove, + force_flags: reports.types.ForceFlags, +) -> reports.ReportItemList: + if not any(is_stonith(el) for el in elements_to_remove.resources_to_remove): + # if no stonith are beieng removed then we don't need to check if any + # stonith will be left + return [] + + stonith_left = [ + stonith_el + for stonith_el in get_all_node_isolating_resources(resources_el) + if stonith_el.attrib["id"] not in elements_to_remove.ids_to_remove + ] + if stonith_left: + return [] + if _is_sbd_enabled(env): + return [] + return [ + reports.ReportItem( + reports.get_severity( + reports.codes.FORCE, reports.codes.FORCE in force_flags + ), + reports.messages.NoStonithMeansWouldBeLeft(), + ) + ] + + +def _is_sbd_enabled(env: LibraryEnvironment) -> bool: + if not env.is_cib_live: + # We cannot tell whether sbd is enabled or not, as we do not have + # access to a cluster. Expect sbd is not enabled. + return False + + # Do not return errors. The check should not prevent deleting a resource + # just because a node in a cluster is temporarily unavailable. We do our + # best to figure out sbd status. + node_list, get_nodes_report_list = get_existing_nodes_names( + env.get_corosync_conf() + ) + if not node_list: + get_nodes_report_list.append( + reports.ReportItem.warning( + reports.messages.CorosyncConfigNoNodesDefined() + ) + ) + return False + + com_cmd = GetSbdStatus(env.report_processor) + com_cmd.set_targets( + env.get_node_target_factory().get_target_list( + node_list, skip_non_existing=True + ) + ) + response_per_node = run_communication(env.get_node_communicator(), com_cmd) + for response in response_per_node: + # It is required to compare with False even if it looks wrong. The + # value can be either True (== sbd is enabled), or False (== sbd is + # disabled), or None (== unknown, not connected). + # We do not want to block removing resources just because we were + # temporarily unable to connect to a node, so we do not return False + # when the result is None. + if ( + response["status"]["enabled"] is False + or response["status"]["running"] is False + ): + return False + return True diff --git a/pcs/lib/commands/sbd.py b/pcs/lib/commands/sbd.py index 3180acaed..3cc7f5383 100644 --- a/pcs/lib/commands/sbd.py +++ b/pcs/lib/commands/sbd.py @@ -9,6 +9,8 @@ sbd, validate, ) +from pcs.lib.cib.resource.stonith import get_all_node_isolating_resources +from pcs.lib.cib.tools import get_resources from pcs.lib.communication.nodes import GetOnlineTargets from pcs.lib.communication.sbd import ( CheckSbd, @@ -314,33 +316,52 @@ def enable_sbd( # noqa: PLR0913 def disable_sbd( - lib_env: LibraryEnvironment, ignore_offline_nodes: bool = False + lib_env: LibraryEnvironment, + ignore_offline_nodes: bool = False, + force_flags: reports.types.ForceFlags = (), ) -> None: """ Disable SBD on all nodes in cluster. ignore_offline_nodes -- if True, omit offline nodes + force_flags -- list of flags codes """ - node_list, get_nodes_report_list = get_existing_nodes_names( + + skip_offline_nodes = ( + ignore_offline_nodes or reports.codes.SKIP_OFFLINE_NODES in force_flags + ) + + node_list, report_list = get_existing_nodes_names( lib_env.get_corosync_conf() ) if not node_list: - get_nodes_report_list.append( + report_list.append( reports.ReportItem.error( reports.messages.CorosyncConfigNoNodesDefined() ) ) - if lib_env.report_processor.report_list(get_nodes_report_list).has_errors: + + if not get_all_node_isolating_resources(get_resources(lib_env.get_cib())): + report_list.append( + reports.ReportItem( + reports.get_severity( + reports.codes.FORCE, reports.codes.FORCE in force_flags + ), + reports.messages.NoStonithMeansWouldBeLeft(), + ) + ) + + if lib_env.report_processor.report_list(report_list).has_errors: raise LibraryError() com_cmd_1 = GetOnlineTargets( lib_env.report_processor, - ignore_offline_targets=ignore_offline_nodes, + ignore_offline_targets=skip_offline_nodes, ) com_cmd_1.set_targets( lib_env.get_node_target_factory().get_target_list( node_list, - skip_non_existing=ignore_offline_nodes, + skip_non_existing=skip_offline_nodes, ) ) online_nodes = run_and_raise(lib_env.get_node_communicator(), com_cmd_1) diff --git a/pcs/stonith.py b/pcs/stonith.py index 13b46ad43..6d6231eb6 100644 --- a/pcs/stonith.py +++ b/pcs/stonith.py @@ -697,12 +697,21 @@ def sbd_disable(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: Options: * --request-timeout - HTTP request timeout * --skip-offline - skip offline cluster nodes + * --force - override validation errors """ - modifiers.ensure_only_supported("--request-timeout", "--skip-offline") + modifiers.ensure_only_supported( + "--request-timeout", "--skip-offline", "--force" + ) if argv: raise CmdLineInputError() - lib.sbd.disable_sbd(modifiers.get("--skip-offline")) + force_flags = set() + if modifiers.is_specified("--force"): + force_flags.add(reports.codes.FORCE) + if modifiers.is_specified("--skip-offline"): + force_flags.add(reports.codes.SKIP_OFFLINE_NODES) + + lib.sbd.disable_sbd(modifiers.get("--skip-offline"), force_flags) def sbd_status(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: diff --git a/pcs_test/tier0/common/reports/test_messages.py b/pcs_test/tier0/common/reports/test_messages.py index 82e42e655..f51d274bd 100644 --- a/pcs_test/tier0/common/reports/test_messages.py +++ b/pcs_test/tier0/common/reports/test_messages.py @@ -6162,3 +6162,15 @@ def test_with_expected_state(self): "id", ResourceState.STOPPED ), ) + + +class NoStonithMeansWouldBeLeft(NameBuildTest): + def test_success(self): + self.assert_message_from_report( + ( + "Requested action removes all stonith means, resulting in the " + "cluster not being able to recover from certain failure " + "conditions" + ), + reports.NoStonithMeansWouldBeLeft(), + ) diff --git a/pcs_test/tier0/lib/cib/resource/test_stonith.py b/pcs_test/tier0/lib/cib/resource/test_stonith.py index 1722c854a..250625e42 100644 --- a/pcs_test/tier0/lib/cib/resource/test_stonith.py +++ b/pcs_test/tier0/lib/cib/resource/test_stonith.py @@ -74,6 +74,52 @@ def test_multiple_sections(self): self.assertFalse(stonith.is_stonith_enabled(crm_config)) +class GetAllResourcesBase(TestCase): + resources = etree.fromstring( + """ + + + + + + + + + + + + + + + + """ + ) + + +class GetAllResources(GetAllResourcesBase): + def test_success(self): + self.assertEqual( + [ + el.attrib["id"] + for el in stonith.get_all_resources(self.resources) + ], + ["S1", "S2", "S3", "S4", "S5", "S6"], + ) + + +class GetAllNodeIsolatingResources(GetAllResourcesBase): + def test_success(self): + self.assertEqual( + [ + el.attrib["id"] + for el in stonith.get_all_node_isolating_resources( + self.resources + ) + ], + ["S1", "S3", "S5"], + ) + + class GetMisconfiguredResources(TestCase): def test_no_stonith(self): resources = etree.fromstring( diff --git a/pcs_test/tier0/lib/commands/sbd/test_disable_sbd.py b/pcs_test/tier0/lib/commands/sbd/test_disable_sbd.py index f8f165bf6..93e8e451a 100644 --- a/pcs_test/tier0/lib/commands/sbd/test_disable_sbd.py +++ b/pcs_test/tier0/lib/commands/sbd/test_disable_sbd.py @@ -14,9 +14,15 @@ def setUp(self): self.corosync_conf_name = "corosync-3nodes.conf" self.node_list = ["rh7-1", "rh7-2", "rh7-3"] self.config.env.set_known_nodes(self.node_list) + self.cib_resources = """ + + + + """ def test_success(self): self.config.corosync_conf.load(filename=self.corosync_conf_name) + self.config.runner.cib.load(resources=self.cib_resources) self.config.http.host.check_auth(node_labels=self.node_list) self.config.http.pcmk.set_stonith_watchdog_timeout_to_zero( communication_list=[[dict(label=node)] for node in self.node_list], @@ -49,11 +55,69 @@ def test_success(self): ] ) + def test_no_stonith_left(self): + self.config.corosync_conf.load(filename=self.corosync_conf_name) + self.config.runner.cib.load() + + self.env_assist.assert_raise_library_error( + lambda: disable_sbd(self.env_assist.get_env()) + ) + self.env_assist.assert_reports( + [ + fixture.error( + reports.codes.NO_STONITH_MEANS_WOULD_BE_LEFT, + force_code=reports.codes.FORCE, + ) + ] + ) + + def test_no_stonith_left_forced(self): + self.config.corosync_conf.load(filename=self.corosync_conf_name) + self.config.runner.cib.load() + self.config.http.host.check_auth(node_labels=self.node_list) + self.config.http.pcmk.set_stonith_watchdog_timeout_to_zero( + communication_list=[[dict(label=node)] for node in self.node_list], + ) + self.config.http.sbd.disable_sbd(node_labels=self.node_list) + + disable_sbd( + self.env_assist.get_env(), force_flags={reports.codes.FORCE} + ) + self.env_assist.assert_reports( + [ + fixture.warn( + reports.codes.NO_STONITH_MEANS_WOULD_BE_LEFT, + ), + fixture.info( + reports.codes.SERVICE_ACTION_STARTED, + action=reports.const.SERVICE_ACTION_DISABLE, + service="sbd", + instance="", + ), + ] + + [ + fixture.info( + reports.codes.SERVICE_ACTION_SUCCEEDED, + action=reports.const.SERVICE_ACTION_DISABLE, + service="sbd", + node=node, + instance="", + ) + for node in self.node_list + ] + + [ + fixture.warn( + report_codes.CLUSTER_RESTART_REQUIRED_TO_APPLY_CHANGES + ) + ] + ) + def test_some_node_names_missing(self): self.corosync_conf_name = "corosync-some-node-names.conf" self.node_list = ["rh7-2"] self.config.corosync_conf.load(filename=self.corosync_conf_name) + self.config.runner.cib.load(resources=self.cib_resources) self.config.http.host.check_auth(node_labels=self.node_list) self.config.http.pcmk.set_stonith_watchdog_timeout_to_zero( communication_list=[[dict(label=node)] for node in self.node_list], @@ -94,6 +158,7 @@ def test_some_node_names_missing(self): def test_all_node_names_missing(self): self.config.corosync_conf.load(filename="corosync-no-node-names.conf") + self.config.runner.cib.load(resources=self.cib_resources) self.env_assist.assert_raise_library_error( lambda: disable_sbd(self.env_assist.get_env()) ) @@ -112,6 +177,7 @@ def test_all_node_names_missing(self): def test_node_offline(self): err_msg = "Failed connect to rh7-3:2224; No route to host" self.config.corosync_conf.load(filename=self.corosync_conf_name) + self.config.runner.cib.load(resources=self.cib_resources) self.config.http.host.check_auth( communication_list=[ {"label": "rh7-1"}, @@ -145,6 +211,7 @@ def test_success_node_offline_skip_offline(self): err_msg = "Failed connect to rh7-3:2224; No route to host" online_nodes_list = ["rh7-2", "rh7-3"] self.config.corosync_conf.load(filename=self.corosync_conf_name) + self.config.runner.cib.load(resources=self.cib_resources) self.config.http.host.check_auth( communication_list=[ { @@ -194,6 +261,7 @@ def test_success_node_offline_skip_offline(self): def test_set_stonith_watchdog_timeout_fails_on_some_nodes(self): err_msg = "Error" self.config.corosync_conf.load(filename=self.corosync_conf_name) + self.config.runner.cib.load(resources=self.cib_resources) self.config.http.host.check_auth(node_labels=self.node_list) self.config.http.pcmk.set_stonith_watchdog_timeout_to_zero( communication_list=[ @@ -260,6 +328,7 @@ def test_set_stonith_watchdog_timeout_fails_on_some_nodes(self): def test_set_stonith_watchdog_timeout_fails_on_all_nodes(self): err_msg = "Error" self.config.corosync_conf.load(filename=self.corosync_conf_name) + self.config.runner.cib.load(resources=self.cib_resources) self.config.http.host.check_auth(node_labels=self.node_list) self.config.http.pcmk.set_stonith_watchdog_timeout_to_zero( communication_list=[ @@ -291,6 +360,7 @@ def test_set_stonith_watchdog_timeout_fails_on_all_nodes(self): def test_disable_failed(self): err_msg = "Error" self.config.corosync_conf.load(filename=self.corosync_conf_name) + self.config.runner.cib.load(resources=self.cib_resources) self.config.http.host.check_auth(node_labels=self.node_list) self.config.http.pcmk.set_stonith_watchdog_timeout_to_zero( communication_list=[[dict(label=node)] for node in self.node_list], diff --git a/pcs_test/tier0/lib/commands/test_cib.py b/pcs_test/tier0/lib/commands/test_cib.py index a6d68ae36..a92f285a5 100644 --- a/pcs_test/tier0/lib/commands/test_cib.py +++ b/pcs_test/tier0/lib/commands/test_cib.py @@ -1,3 +1,4 @@ +import json from typing import Optional from unittest import ( TestCase, @@ -1057,3 +1058,344 @@ def test_skip_state_check_on_missing_from_status(self): ), ] ) + + +class StonithAndSbdCheck(StopResourcesWaitMixin, TestCase): + """ + Test that an error is produced when removing the last stonith resource and + sbd is disabled + """ + + resources = """ + + + + + """ + + def fixture_config_sbd_calls(self, sbd_enabled): + node_name_list = ["node-1", "node-2"] + self.config.env.set_known_nodes(node_name_list) + self.config.corosync_conf.load(node_name_list=node_name_list) + self.config.http.sbd.check_sbd( + communication_list=[ + dict( + label=node, + param_list=[("watchdog", ""), ("device_list", "[]")], + output=json.dumps( + dict( + sbd=dict( + installed=True, + enabled=sbd_enabled, + running=sbd_enabled, + ) + ) + ), + ) + for node in node_name_list + ] + ) + + def fixture_remove_s1_s2(self): + self.fixture_stop_resources_wait_calls( + self.config.calls.get("runner.cib.load").stdout, + initial_state_modifiers={ + "resources": """ + + + + + """ + }, + after_disable_cib_modifiers={ + "resources": """ + + + + + + + + + + + + + """ + }, + after_disable_state_modifiers={ + "resources": """ + + + + + """ + }, + ) + self.fixture_push_cib_after_stopping(resources="") + + def setUp(self): + self.env_assist, self.config = get_env_tools(self) + + def test_some_stonith_left(self): + self.fixture_init_tmp_file_mocker() + + # sbd calls do not happen if some stonith is left + self.config.runner.cib.load(resources=self.resources) + self.fixture_stop_resources_wait_calls( + self.config.calls.get("runner.cib.load").stdout, + initial_state_modifiers={ + "resources": """ + + + + + """ + }, + after_disable_cib_modifiers={ + "resources": """ + + + + + + + + + """ + }, + after_disable_state_modifiers={ + "resources": """ + + + + + """ + }, + ) + self.fixture_push_cib_after_stopping( + resources=""" + + + + """ + ) + + lib.remove_elements(self.env_assist.get_env(), ["S1"]) + self.env_assist.assert_reports( + [ + fixture.info( + reports.codes.STOPPING_RESOURCES_BEFORE_DELETING, + resource_id_list=["S1"], + ), + fixture.info(reports.codes.WAIT_FOR_IDLE_STARTED, timeout=0), + ] + ) + + def test_no_stonith_left_sbd_enabled(self): + self.fixture_init_tmp_file_mocker() + + self.config.runner.cib.load(resources=self.resources) + self.fixture_config_sbd_calls(sbd_enabled=True) + self.fixture_remove_s1_s2() + + lib.remove_elements(self.env_assist.get_env(), ["S1", "S2"]) + self.env_assist.assert_reports( + [ + fixture.info( + reports.codes.STOPPING_RESOURCES_BEFORE_DELETING, + resource_id_list=["S1", "S2"], + ), + fixture.info(reports.codes.WAIT_FOR_IDLE_STARTED, timeout=0), + ] + ) + + def test_fake_stonith_left_sbd_disabled(self): + resources = """ + + + + + """ + self.config.runner.cib.load(resources=resources) + self.fixture_config_sbd_calls(sbd_enabled=False) + + self.env_assist.assert_raise_library_error( + lambda: lib.remove_elements(self.env_assist.get_env(), ["S1"]) + ) + self.env_assist.assert_reports( + [ + fixture.error( + reports.codes.NO_STONITH_MEANS_WOULD_BE_LEFT, + force_code=reports.codes.FORCE, + ) + ] + ) + + def test_no_stonith_left_sbd_disabled(self): + self.config.runner.cib.load(resources=self.resources) + self.fixture_config_sbd_calls(sbd_enabled=False) + + self.env_assist.assert_raise_library_error( + lambda: lib.remove_elements(self.env_assist.get_env(), ["S1", "S2"]) + ) + self.env_assist.assert_reports( + [ + fixture.error( + reports.codes.NO_STONITH_MEANS_WOULD_BE_LEFT, + force_code=reports.codes.FORCE, + ) + ] + ) + + def test_no_stonith_left_sbd_disabled_forced(self): + self.fixture_init_tmp_file_mocker() + + # TODO reports.codes.force does two things: + # 1) overrides NO_STONITH_MEANS_WOULD_BE_LEFT error + # 2) disables stoppiong resources before deleting + # This is wrong, we need those two effects to be independent. + # Possible solutions: + # 1) Use specific forces codes - This is planned, but not yet + # implemented in CLI. Also, the library would have to emit specific + # force codes in error reports. + # 2) Add specific flag to CLI and LIB command to disable stopping + # resources before deleting them. + + self.config.runner.cib.load(resources=self.resources) + self.fixture_config_sbd_calls(sbd_enabled=False) + self.tmp_file_mock_obj.extend_calls( + [ + TmpFileCall( + "cib.delete.before", + orig_content=self.config.calls.get( + "runner.cib.load" + ).stdout, + ), + TmpFileCall( + "cib.delete.after", + orig_content=read_test_resource("cib-empty.xml"), + ), + ] + ) + self.config.runner.cib.diff( + "cib.delete.before", + "cib.delete.after", + name="cib.diff.delete", + stdout="cib.diff.delete", + ) + self.config.runner.cib.push_diff( + name="cib.push.delete", cib_diff="cib.diff.delete" + ) + + lib.remove_elements( + self.env_assist.get_env(), ["S1", "S2"], {reports.codes.FORCE} + ) + + self.env_assist.assert_reports( + [ + fixture.warn( + reports.codes.NO_STONITH_MEANS_WOULD_BE_LEFT, + ), + fixture.warn( + reports.codes.STOPPING_RESOURCES_BEFORE_DELETING_SKIPPED, + ), + ] + ) + + def test_no_stonith_left_sbd_partially_disabled(self): + self.config.runner.cib.load(resources=self.resources) + + node_name_list = ["node-1", "node-2"] + self.config.env.set_known_nodes(node_name_list) + self.config.corosync_conf.load(node_name_list=node_name_list) + self.config.http.sbd.check_sbd( + communication_list=[ + dict( + label="node-1", + param_list=[("watchdog", ""), ("device_list", "[]")], + output=json.dumps( + dict( + sbd=dict(installed=True, enabled=True, running=True) + ) + ), + ), + dict( + label="node-2", + param_list=[("watchdog", ""), ("device_list", "[]")], + output=json.dumps( + dict( + sbd=dict( + installed=True, enabled=False, running=False + ) + ) + ), + ), + ] + ) + + self.env_assist.assert_raise_library_error( + lambda: lib.remove_elements(self.env_assist.get_env(), ["S1", "S2"]) + ) + self.env_assist.assert_reports( + [ + fixture.error( + reports.codes.NO_STONITH_MEANS_WOULD_BE_LEFT, + force_code=reports.codes.FORCE, + ) + ] + ) + + def test_communication_error(self): + self.fixture_init_tmp_file_mocker() + self.config.runner.cib.load(resources=self.resources) + node_name_list = ["node-1", "node-2"] + self.config.env.set_known_nodes(node_name_list) + self.config.corosync_conf.load(node_name_list=node_name_list) + self.config.http.sbd.check_sbd( + communication_list=[ + dict( + label="node-1", + param_list=[("watchdog", ""), ("device_list", "[]")], + output=json.dumps( + dict( + sbd=dict(installed=True, enabled=True, running=True) + ) + ), + ), + dict( + label="node-2", + param_list=[("watchdog", ""), ("device_list", "[]")], + was_connected=False, + ), + ] + ) + self.fixture_remove_s1_s2() + + lib.remove_elements(self.env_assist.get_env(), ["S1", "S2"]) + self.env_assist.assert_reports( + [ + fixture.warn( + reports.codes.NODE_COMMUNICATION_ERROR_UNABLE_TO_CONNECT, + node="node-2", + command="remote/check_sbd", + reason=None, + ), + fixture.warn( + reports.codes.UNABLE_TO_GET_SBD_STATUS, + node="node-2", + reason="", + ), + fixture.info( + reports.codes.STOPPING_RESOURCES_BEFORE_DELETING, + resource_id_list=["S1", "S2"], + ), + fixture.info(reports.codes.WAIT_FOR_IDLE_STARTED, timeout=0), + ] + ) diff --git a/pcs_test/tier1/legacy/test_stonith.py b/pcs_test/tier1/legacy/test_stonith.py index a02096106..86e9e5870 100644 --- a/pcs_test/tier1/legacy/test_stonith.py +++ b/pcs_test/tier1/legacy/test_stonith.py @@ -1380,7 +1380,22 @@ def test_no_stonith_warning(self): ) self.pcs_runner.corosync_conf_opt = None - self.assert_pcs_success("stonith delete test_stonith".split()) + self.assert_pcs_fail( + "stonith delete test_stonith".split(), + ( + "Error: Requested action removes all stonith means, resulting " + "in the cluster not being able to recover from certain failure " + "conditions, use --force to override\n" + ERRORS_HAVE_OCCURRED + ), + ) + self.assert_pcs_success( + "stonith delete test_stonith --force".split(), + stderr_full=( + "Warning: Requested action removes all stonith means, " + "resulting in the cluster not being able to recover from " + "certain failure conditions\n" + ), + ) self.pcs_runner.corosync_conf_opt = self.temp_corosync_conf.name self.assert_pcs_success( diff --git a/pcs_test/tier1/stonith/test_remove.py b/pcs_test/tier1/stonith/test_remove.py index 75a2ec6db..78021c3c6 100644 --- a/pcs_test/tier1/stonith/test_remove.py +++ b/pcs_test/tier1/stonith/test_remove.py @@ -38,6 +38,19 @@ def fixture_stonith_primitive_xml(resource_id: str) -> str: """ +ERRORS_HAVE_OCCURRED = ( + "Error: Errors have occurred, therefore pcs is unable to continue\n" +) +NO_STONITH_LEFT_ERROR = ( + "Error: Requested action removes all stonith means, resulting in the " + "cluster not being able to recover from certain failure conditions, " + "use --force to override\n" +) +NO_STONITH_LEFT_WARNING = ( + "Warning: Requested action removes all stonith means, resulting in the " + "cluster not being able to recover from certain failure conditions\n" +) + class StonithRemoveDeleteBase( get_assert_pcs_effect_mixin( @@ -192,17 +205,24 @@ def test_remove_references(self): ) def test_remove_all_resources(self): - self.assert_effect_single( + self.assert_pcs_fail( ["stonith", self.command, "S1", "S2", "S3"], + NO_STONITH_LEFT_ERROR + ERRORS_HAVE_OCCURRED, + ) + self.assert_effect_single( + ["stonith", self.command, "S1", "S2", "S3", "--force"], "", - stderr_full=dedent( - """\ - Removing dependant elements: - Colocation constraint: 'colocation-constraint' - Fencing level: 'fencing-level' - Resource set: 'set1' - Tag: 'TAG' - """ + stderr_full=( + NO_STONITH_LEFT_WARNING + + dedent( + """\ + Removing dependant elements: + Colocation constraint: 'colocation-constraint' + Fencing level: 'fencing-level' + Resource set: 'set1' + Tag: 'TAG' + """ + ) ), ) self.assert_constraints("") @@ -246,15 +266,22 @@ def tearDown(self): self.temp_cib.close() def test_remove_primitive(self): - self.assert_pcs_success( + self.assert_pcs_fail( ["stonith", "delete", "S1"], - stderr_full=dedent( - """\ - Removing dependant element: - Acl permission: 'PERMISSION' - Removing references: - Acl permission 'PERMISSION' from: - Acl role: 'ROLE' - """ + NO_STONITH_LEFT_ERROR + ERRORS_HAVE_OCCURRED, + ) + self.assert_pcs_success( + ["stonith", "delete", "S1", "--force"], + stderr_full=( + NO_STONITH_LEFT_WARNING + + dedent( + """\ + Removing dependant element: + Acl permission: 'PERMISSION' + Removing references: + Acl permission 'PERMISSION' from: + Acl role: 'ROLE' + """ + ) ), ) From b0508cc962c4092b38d6a68ca51066f932ab422f Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Tue, 11 Mar 2025 15:43:18 +0100 Subject: [PATCH 116/227] fix reporting messages --- pcs/lib/commands/cib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pcs/lib/commands/cib.py b/pcs/lib/commands/cib.py index 6b7967e98..839a32f8f 100644 --- a/pcs/lib/commands/cib.py +++ b/pcs/lib/commands/cib.py @@ -248,8 +248,9 @@ def _is_sbd_enabled(env: LibraryEnvironment) -> bool: node_list, get_nodes_report_list = get_existing_nodes_names( env.get_corosync_conf() ) + env.report_processor.report_list(get_nodes_report_list) if not node_list: - get_nodes_report_list.append( + env.report_processor.report( reports.ReportItem.warning( reports.messages.CorosyncConfigNoNodesDefined() ) From c427d3dba692b1c77a27cedccfe3424b56c09c54 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Wed, 12 Mar 2025 11:29:50 +0100 Subject: [PATCH 117/227] check if stonith resources are enabled when removing last stonith --- pcs/common/reports/messages.py | 5 +- pcs/lib/cib/resource/common.py | 31 +++---- pcs/lib/commands/cib.py | 14 ++++ pcs/lib/commands/resource.py | 37 ++++++--- pcs/lib/commands/sbd.py | 26 +++++- pcs/lib/commands/stonith.py | 7 +- .../tier0/common/reports/test_messages.py | 6 +- .../tier0/lib/cib/test_resource_common.py | 82 ++++++++++--------- .../commands/resource/test_resource_create.py | 40 +++++++++ .../lib/commands/sbd/test_disable_sbd.py | 25 ++++++ pcs_test/tier0/lib/commands/test_cib.py | 26 ++++++ pcs_test/tier1/legacy/test_stonith.py | 13 +-- pcs_test/tier1/stonith/test_remove.py | 11 +-- 13 files changed, 232 insertions(+), 91 deletions(-) diff --git a/pcs/common/reports/messages.py b/pcs/common/reports/messages.py index d9eefadb5..e67f48df0 100644 --- a/pcs/common/reports/messages.py +++ b/pcs/common/reports/messages.py @@ -8308,6 +8308,7 @@ class NoStonithMeansWouldBeLeft(ReportItemMessage): @property def message(self) -> str: return ( - "Requested action removes all stonith means, resulting in the " - "cluster not being able to recover from certain failure conditions" + "Requested action lefts the cluster with no enabled means to fence " + "nodes, resulting in the cluster not being able to recover from " + "certain failure conditions" ) diff --git a/pcs/lib/cib/resource/common.py b/pcs/lib/cib/resource/common.py index b8905f238..46cebff86 100644 --- a/pcs/lib/cib/resource/common.py +++ b/pcs/lib/cib/resource/common.py @@ -1,6 +1,5 @@ from typing import ( List, - Mapping, Optional, Set, Tuple, @@ -28,24 +27,6 @@ from .primitive import is_primitive -def are_meta_disabled(meta_attributes: Mapping[str, str]) -> bool: - return meta_attributes.get("target-role", "Started").lower() == "stopped" - - -def _can_be_evaluated_as_positive_num(value: str) -> bool: - string_wo_leading_zeros = str(value).lstrip("0") - return bool(string_wo_leading_zeros) and ( - string_wo_leading_zeros[0] in list("123456789") - ) - - -def is_clone_deactivated_by_meta(meta_attributes: Mapping[str, str]) -> bool: - return are_meta_disabled(meta_attributes) or any( - not _can_be_evaluated_as_positive_num(meta_attributes.get(key, "1")) - for key in ["clone-max", "clone-node-max"] - ) - - def find_one_resource( context_element: _Element, resource_id: str, @@ -233,6 +214,18 @@ def disable(resource_el: _Element, id_provider: IdProvider) -> None: ) +def is_disabled(resource_el: _Element) -> bool: + """ + Is the resource disabled by its own meta? (Doesn't check parent resources.) + """ + return ( + nvpair.get_value( + nvpair.META_ATTRIBUTES_TAG, resource_el, "target-role", default="" + ).lower() + == "stopped" + ) + + def find_resources_to_manage(resource_el: _Element) -> List[_Element]: """ Get resources to set to managed for the specified resource to become managed diff --git a/pcs/lib/commands/cib.py b/pcs/lib/commands/cib.py index 839a32f8f..4b470d8bf 100644 --- a/pcs/lib/commands/cib.py +++ b/pcs/lib/commands/cib.py @@ -17,6 +17,7 @@ stop_resources, warn_resource_unmanaged, ) +from pcs.lib.cib.resource.common import is_disabled as is_resource_disabled from pcs.lib.cib.resource.guest_node import ( get_node_name_from_resource as get_node_name_from_guest_resource, ) @@ -221,6 +222,19 @@ def _ensure_some_stonith_remains( stonith_el for stonith_el in get_all_node_isolating_resources(resources_el) if stonith_el.attrib["id"] not in elements_to_remove.ids_to_remove + # If any nvset disables the resource, even with a rule to limit it to + # specific time, than the resource wouldn't be able to fence all the + # time. + # However, pcs currently supports only one nvset for meta attributes, + # so we only check that to be consistent. Checking all nvsets could + # lead to a situation not resolvable by pcs, as pcs doesn't allow to + # change other nvsets than the first one. + # Technically, stonith resources can be disabled by their parent clones + # or groups. However, pcs doesn't allow putting stonith to groups and + # clones, so we don't check that. + # The check is not perfect, but it is a reasonable effort, considering + # that multiple nvsets are not supported for meta attributes by pcs now. + and not is_resource_disabled(stonith_el) ] if stonith_left: return [] diff --git a/pcs/lib/commands/resource.py b/pcs/lib/commands/resource.py index 641b51a47..5fee3e77c 100644 --- a/pcs/lib/commands/resource.py +++ b/pcs/lib/commands/resource.py @@ -360,6 +360,24 @@ def _check_special_cases( ) +def _are_meta_disabled(meta_attributes: Mapping[str, str]) -> bool: + return meta_attributes.get("target-role", "Started").lower() == "stopped" + + +def _can_be_evaluated_as_positive_num(value: str) -> bool: + string_wo_leading_zeros = str(value).lstrip("0") + return bool(string_wo_leading_zeros) and ( + string_wo_leading_zeros[0] in list("123456789") + ) + + +def _is_clone_deactivated_by_meta(meta_attributes: Mapping[str, str]) -> bool: + return _are_meta_disabled(meta_attributes) or any( + not _can_be_evaluated_as_positive_num(meta_attributes.get(key, "1")) + for key in ["clone-max", "clone-node-max"] + ) + + def create( # noqa: PLR0913 env: LibraryEnvironment, resource_id: str, @@ -427,8 +445,7 @@ def create( # noqa: PLR0913 wait, [resource_id], _ensure_disabled_after_wait( - ensure_disabled - or resource.common.are_meta_disabled(meta_attributes) + ensure_disabled or _are_meta_disabled(meta_attributes) ), required_cib_version=get_required_cib_version_for_primitive( operation_list @@ -566,8 +583,8 @@ def create_as_clone( # noqa: PLR0913 [resource_id], _ensure_disabled_after_wait( ensure_disabled - or resource.common.are_meta_disabled(meta_attributes) - or resource.common.is_clone_deactivated_by_meta(clone_meta_options) + or _are_meta_disabled(meta_attributes) + or _is_clone_deactivated_by_meta(clone_meta_options) ), required_cib_version=get_required_cib_version_for_primitive( operation_list @@ -682,8 +699,7 @@ def create_in_group( # noqa: PLR0913 wait, [resource_id], _ensure_disabled_after_wait( - ensure_disabled - or resource.common.are_meta_disabled(meta_attributes) + ensure_disabled or _are_meta_disabled(meta_attributes) ), required_cib_version=get_required_cib_version_for_primitive( operation_list @@ -830,8 +846,7 @@ def create_into_bundle( # noqa: PLR0913 wait, [resource_id], _ensure_disabled_after_wait( - ensure_disabled - or resource.common.are_meta_disabled(meta_attributes) + ensure_disabled or _are_meta_disabled(meta_attributes) ), required_cib_version=required_cib_version, ) as resources_section: @@ -924,8 +939,7 @@ def bundle_create( # noqa: PLR0913 wait, [bundle_id], _ensure_disabled_after_wait( - ensure_disabled - or resource.common.are_meta_disabled(meta_attributes) + ensure_disabled or _are_meta_disabled(meta_attributes) ), required_cib_version=( Version(3, 2, 0) if container_type == "podman" else None @@ -1002,8 +1016,7 @@ def bundle_reset( # noqa: PLR0913 wait, [bundle_id], _ensure_disabled_after_wait( - ensure_disabled - or resource.common.are_meta_disabled(meta_attributes) + ensure_disabled or _are_meta_disabled(meta_attributes) ), # The only requirement for CIB schema version currently is: # if container_type == "podman" then required_version = '3.2.0' diff --git a/pcs/lib/commands/sbd.py b/pcs/lib/commands/sbd.py index 3cc7f5383..b482aa188 100644 --- a/pcs/lib/commands/sbd.py +++ b/pcs/lib/commands/sbd.py @@ -9,7 +9,10 @@ sbd, validate, ) -from pcs.lib.cib.resource.stonith import get_all_node_isolating_resources +from pcs.lib.cib.resource.common import is_disabled as is_resource_disabled +from pcs.lib.cib.resource.stonith import ( + get_all_node_isolating_resources, +) from pcs.lib.cib.tools import get_resources from pcs.lib.communication.nodes import GetOnlineTargets from pcs.lib.communication.sbd import ( @@ -341,7 +344,26 @@ def disable_sbd( ) ) - if not get_all_node_isolating_resources(get_resources(lib_env.get_cib())): + stonith_left = [ + stonith_el + for stonith_el in get_all_node_isolating_resources( + get_resources(lib_env.get_cib()) + ) + # If any nvset disables the resource, even with a rule to limit it to + # specific time, than the resource wouldn't be able to fence all the + # time. + # However, pcs currently supports only one nvset for meta attributes, + # so we only check that to be consistent. Checking all nvsets could + # lead to a situation not resolvable by pcs, as pcs doesn't allow to + # change other nvsets than the first one. + # Technically, stonith resources can be disabled by their parent clones + # or groups. However, pcs doesn't allow putting stonith to groups and + # clones, so we don't check that. + # The check is not perfect, but it is a reasonable effort, considering + # that multiple nvsets are not supported for meta attributes by pcs now. + if not is_resource_disabled(stonith_el) + ] + if not stonith_left: report_list.append( reports.ReportItem( reports.get_severity( diff --git a/pcs/lib/commands/stonith.py b/pcs/lib/commands/stonith.py index e92fdaded..021b2fc2b 100644 --- a/pcs/lib/commands/stonith.py +++ b/pcs/lib/commands/stonith.py @@ -24,6 +24,7 @@ get_element_by_id, ) from pcs.lib.commands.resource import ( + _are_meta_disabled, _ensure_disabled_after_wait, resource_environment, ) @@ -159,8 +160,7 @@ def create( # noqa: PLR0913 wait, [stonith_id], _ensure_disabled_after_wait( - ensure_disabled - or resource.common.are_meta_disabled(meta_attributes), + ensure_disabled or _are_meta_disabled(meta_attributes) ), ) as resources_section: id_provider = IdProvider(resources_section) @@ -250,8 +250,7 @@ def create_in_group( # noqa: PLR0913 wait, [stonith_id], _ensure_disabled_after_wait( - ensure_disabled - or resource.common.are_meta_disabled(meta_attributes), + ensure_disabled or _are_meta_disabled(meta_attributes) ), ) as resources_section: id_provider = IdProvider(resources_section) diff --git a/pcs_test/tier0/common/reports/test_messages.py b/pcs_test/tier0/common/reports/test_messages.py index f51d274bd..14adcc085 100644 --- a/pcs_test/tier0/common/reports/test_messages.py +++ b/pcs_test/tier0/common/reports/test_messages.py @@ -6168,9 +6168,9 @@ class NoStonithMeansWouldBeLeft(NameBuildTest): def test_success(self): self.assert_message_from_report( ( - "Requested action removes all stonith means, resulting in the " - "cluster not being able to recover from certain failure " - "conditions" + "Requested action lefts the cluster with no enabled means " + "to fence nodes, resulting in the cluster not being able to " + "recover from certain failure conditions" ), reports.NoStonithMeansWouldBeLeft(), ) diff --git a/pcs_test/tier0/lib/cib/test_resource_common.py b/pcs_test/tier0/lib/cib/test_resource_common.py index 77b5f5583..db3078d53 100644 --- a/pcs_test/tier0/lib/cib/test_resource_common.py +++ b/pcs_test/tier0/lib/cib/test_resource_common.py @@ -64,44 +64,6 @@ ) -class AreMetaDisabled(TestCase): - def test_detect_is_disabled(self): - self.assertTrue(common.are_meta_disabled({"target-role": "Stopped"})) - self.assertTrue(common.are_meta_disabled({"target-role": "stopped"})) - - def test_detect_is_not_disabled(self): - self.assertFalse(common.are_meta_disabled({})) - self.assertFalse(common.are_meta_disabled({"target-role": "any"})) - - -class IsCloneDeactivatedByMeta(TestCase): - def assert_is_disabled(self, meta_attributes): - self.assertTrue(common.is_clone_deactivated_by_meta(meta_attributes)) - - def assert_is_not_disabled(self, meta_attributes): - self.assertFalse(common.is_clone_deactivated_by_meta(meta_attributes)) - - def test_detect_is_disabled(self): - self.assert_is_disabled({"target-role": "Stopped"}) - self.assert_is_disabled({"target-role": "stopped"}) - self.assert_is_disabled({"clone-max": "0"}) - self.assert_is_disabled({"clone-max": "00"}) - self.assert_is_disabled({"clone-max": 0}) - self.assert_is_disabled({"clone-node-max": "0"}) - self.assert_is_disabled({"clone-node-max": "abc1"}) - - def test_detect_is_not_disabled(self): - self.assert_is_not_disabled({}) - self.assert_is_not_disabled({"target-role": "any"}) - self.assert_is_not_disabled({"clone-max": "1"}) - self.assert_is_not_disabled({"clone-max": "01"}) - self.assert_is_not_disabled({"clone-max": 1}) - self.assert_is_not_disabled({"clone-node-max": "1"}) - self.assert_is_not_disabled({"clone-node-max": 1}) - self.assert_is_not_disabled({"clone-node-max": "1abc"}) - self.assert_is_not_disabled({"clone-node-max": "1.1"}) - - class FindOneOrMoreResources(TestCase): def setUp(self): self.cib = etree.fromstring( @@ -616,6 +578,50 @@ def test_only_first_meta(self): ) +class IsDisabled(TestCase): + def test_success(self): + # Using non-existing tag "res" as the function doesn't care whether it's + # a primitive, group or clone + resources = etree.fromstring( + """ + + + + + + + + + + + + + + + + + + + """ + ) + res_disabled = ( + ("R1", False), + ("R2", True), + ("R3", True), + ("R4", False), + ) + for res_name, expected_disabled in res_disabled: + with self.subTest( + resource=res_name, expected_disabled=expected_disabled + ): + self.assertEqual( + common.is_disabled( + resources.find(f".//*[@id='{res_name}']") + ), + expected_disabled, + ) + + class FindResourcesToManage(TestCase): def assert_find_resources(self, input_resource_id, output_resource_ids): self.assertEqual( diff --git a/pcs_test/tier0/lib/commands/resource/test_resource_create.py b/pcs_test/tier0/lib/commands/resource/test_resource_create.py index fffc3e5ed..3ad102f2d 100644 --- a/pcs_test/tier0/lib/commands/resource/test_resource_create.py +++ b/pcs_test/tier0/lib/commands/resource/test_resource_create.py @@ -21,6 +21,46 @@ TIMEOUT = 10 +class AreMetaDisabled(TestCase): + def test_detect_is_disabled(self): + self.assertTrue(resource._are_meta_disabled({"target-role": "Stopped"})) + self.assertTrue(resource._are_meta_disabled({"target-role": "stopped"})) + + def test_detect_is_not_disabled(self): + self.assertFalse(resource._are_meta_disabled({})) + self.assertFalse(resource._are_meta_disabled({"target-role": "any"})) + + +class IsCloneDeactivatedByMeta(TestCase): + def assert_is_disabled(self, meta_attributes): + self.assertTrue(resource._is_clone_deactivated_by_meta(meta_attributes)) + + def assert_is_not_disabled(self, meta_attributes): + self.assertFalse( + resource._is_clone_deactivated_by_meta(meta_attributes) + ) + + def test_detect_is_disabled(self): + self.assert_is_disabled({"target-role": "Stopped"}) + self.assert_is_disabled({"target-role": "stopped"}) + self.assert_is_disabled({"clone-max": "0"}) + self.assert_is_disabled({"clone-max": "00"}) + self.assert_is_disabled({"clone-max": 0}) + self.assert_is_disabled({"clone-node-max": "0"}) + self.assert_is_disabled({"clone-node-max": "abc1"}) + + def test_detect_is_not_disabled(self): + self.assert_is_not_disabled({}) + self.assert_is_not_disabled({"target-role": "any"}) + self.assert_is_not_disabled({"clone-max": "1"}) + self.assert_is_not_disabled({"clone-max": "01"}) + self.assert_is_not_disabled({"clone-max": 1}) + self.assert_is_not_disabled({"clone-node-max": "1"}) + self.assert_is_not_disabled({"clone-node-max": 1}) + self.assert_is_not_disabled({"clone-node-max": "1abc"}) + self.assert_is_not_disabled({"clone-node-max": "1.1"}) + + def create( # noqa: PLR0913 env, *, diff --git a/pcs_test/tier0/lib/commands/sbd/test_disable_sbd.py b/pcs_test/tier0/lib/commands/sbd/test_disable_sbd.py index 93e8e451a..3c353b2d9 100644 --- a/pcs_test/tier0/lib/commands/sbd/test_disable_sbd.py +++ b/pcs_test/tier0/lib/commands/sbd/test_disable_sbd.py @@ -55,6 +55,31 @@ def test_success(self): ] ) + def test_only_disabled_stonith_left(self): + cib_resources = """ + + + + + + + + """ + self.config.corosync_conf.load(filename=self.corosync_conf_name) + self.config.runner.cib.load(resources=cib_resources) + + self.env_assist.assert_raise_library_error( + lambda: disable_sbd(self.env_assist.get_env()) + ) + self.env_assist.assert_reports( + [ + fixture.error( + reports.codes.NO_STONITH_MEANS_WOULD_BE_LEFT, + force_code=reports.codes.FORCE, + ) + ] + ) + def test_no_stonith_left(self): self.config.corosync_conf.load(filename=self.corosync_conf_name) self.config.runner.cib.load() diff --git a/pcs_test/tier0/lib/commands/test_cib.py b/pcs_test/tier0/lib/commands/test_cib.py index a92f285a5..b12b9c830 100644 --- a/pcs_test/tier0/lib/commands/test_cib.py +++ b/pcs_test/tier0/lib/commands/test_cib.py @@ -1238,6 +1238,32 @@ def test_fake_stonith_left_sbd_disabled(self): ] ) + def test_disabled_stonith_left_sbd_disabled(self): + resources = """ + + + + + + + + + """ + self.config.runner.cib.load(resources=resources) + self.fixture_config_sbd_calls(sbd_enabled=False) + + self.env_assist.assert_raise_library_error( + lambda: lib.remove_elements(self.env_assist.get_env(), ["S2"]) + ) + self.env_assist.assert_reports( + [ + fixture.error( + reports.codes.NO_STONITH_MEANS_WOULD_BE_LEFT, + force_code=reports.codes.FORCE, + ) + ] + ) + def test_no_stonith_left_sbd_disabled(self): self.config.runner.cib.load(resources=self.resources) self.fixture_config_sbd_calls(sbd_enabled=False) diff --git a/pcs_test/tier1/legacy/test_stonith.py b/pcs_test/tier1/legacy/test_stonith.py index 86e9e5870..572f48122 100644 --- a/pcs_test/tier1/legacy/test_stonith.py +++ b/pcs_test/tier1/legacy/test_stonith.py @@ -1383,17 +1383,18 @@ def test_no_stonith_warning(self): self.assert_pcs_fail( "stonith delete test_stonith".split(), ( - "Error: Requested action removes all stonith means, resulting " - "in the cluster not being able to recover from certain failure " - "conditions, use --force to override\n" + ERRORS_HAVE_OCCURRED + "Error: Requested action lefts the cluster with no enabled " + "means to fence nodes, resulting in the cluster not being able " + "to recover from certain failure conditions, use --force to " + "override\n" + ERRORS_HAVE_OCCURRED ), ) self.assert_pcs_success( "stonith delete test_stonith --force".split(), stderr_full=( - "Warning: Requested action removes all stonith means, " - "resulting in the cluster not being able to recover from " - "certain failure conditions\n" + "Warning: Requested action lefts the cluster with no enabled " + "means to fence nodes, resulting in the cluster not being able " + "to recover from certain failure conditions\n" ), ) diff --git a/pcs_test/tier1/stonith/test_remove.py b/pcs_test/tier1/stonith/test_remove.py index 78021c3c6..176f55f82 100644 --- a/pcs_test/tier1/stonith/test_remove.py +++ b/pcs_test/tier1/stonith/test_remove.py @@ -42,13 +42,14 @@ def fixture_stonith_primitive_xml(resource_id: str) -> str: "Error: Errors have occurred, therefore pcs is unable to continue\n" ) NO_STONITH_LEFT_ERROR = ( - "Error: Requested action removes all stonith means, resulting in the " - "cluster not being able to recover from certain failure conditions, " - "use --force to override\n" + "Error: Requested action lefts the cluster with no enabled means to fence " + "nodes, resulting in the cluster not being able to recover from certain " + "failure conditions, use --force to override\n" ) NO_STONITH_LEFT_WARNING = ( - "Warning: Requested action removes all stonith means, resulting in the " - "cluster not being able to recover from certain failure conditions\n" + "Warning: Requested action lefts the cluster with no enabled means to fence " + "nodes, resulting in the cluster not being able to recover from certain " + "failure conditions\n" ) From e6ca826793032d922c17ab3a88a2f0e211f49897 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Fri, 14 Mar 2025 13:34:07 +0100 Subject: [PATCH 118/227] error out when disabling last stonith This commit refactors common code for checking whether any stonith or sbd is left. Also, it fixes sbd check to work correctly in clusters where not all nodes use sbd. --- CHANGELOG.md | 4 +- pcs/Makefile.am | 1 + pcs/lib/commands/cib.py | 108 +------- pcs/lib/commands/resource.py | 113 ++++++--- pcs/lib/commands/sbd.py | 41 +-- pcs/lib/sbd_stonith.py | 130 ++++++++++ pcs/resource.py | 14 +- pcs/stonith.py | 1 + pcs_test/tier0/cli/test_resource.py | 24 +- .../resource/test_resource_enable_disable.py | 233 ++++++++++++++++-- .../lib/commands/sbd/test_disable_sbd.py | 65 +++++ pcs_test/tier0/lib/commands/test_cib.py | 57 +++-- .../test_stonith_enable_disable.py | 34 ++- 13 files changed, 626 insertions(+), 199 deletions(-) create mode 100644 pcs/lib/sbd_stonith.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 67d3204a4..a36035ca7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,8 @@ - Command `pcs stonith sbd watchdog list` now prints watchdogs' identity and driver ([RHEL-76177]) - Command `pcs cluster rename` for changing cluster name ([RHEL-76055]) -- Removing stonith devices or disabling SBD fails if the cluster would be left - with disabled SBD and no stonith devices ([RHEL-76170]) +- Removing or disabling stonith devices or disabling SBD fails if the cluster + would be left with disabled SBD and no stonith devices ([RHEL-76170]) ### Fixed - Command `pcs resource restart` allows restarting bundle instances (broken diff --git a/pcs/Makefile.am b/pcs/Makefile.am index 861624a49..bf7dfb5b4 100644 --- a/pcs/Makefile.am +++ b/pcs/Makefile.am @@ -396,6 +396,7 @@ EXTRA_DIST = \ lib/resource_agent/types.py \ lib/resource_agent/xml.py \ lib/sbd.py \ + lib/sbd_stonith.py \ lib/services.py \ lib/tools.py \ lib/validate.py \ diff --git a/pcs/lib/commands/cib.py b/pcs/lib/commands/cib.py index 4b470d8bf..f86341061 100644 --- a/pcs/lib/commands/cib.py +++ b/pcs/lib/commands/cib.py @@ -17,7 +17,6 @@ stop_resources, warn_resource_unmanaged, ) -from pcs.lib.cib.resource.common import is_disabled as is_resource_disabled from pcs.lib.cib.resource.guest_node import ( get_node_name_from_resource as get_node_name_from_guest_resource, ) @@ -25,17 +24,12 @@ from pcs.lib.cib.resource.remote_node import ( get_node_name_from_resource as get_node_name_from_remote_resource, ) -from pcs.lib.cib.resource.stonith import ( - get_all_node_isolating_resources, - is_stonith, -) +from pcs.lib.cib.resource.stonith import is_stonith from pcs.lib.cib.tools import get_resources -from pcs.lib.communication.sbd import GetSbdStatus -from pcs.lib.communication.tools import run as run_communication from pcs.lib.env import LibraryEnvironment from pcs.lib.errors import LibraryError -from pcs.lib.node import get_existing_nodes_names from pcs.lib.pacemaker.live import remove_node +from pcs.lib.sbd_stonith import ensure_some_stonith_remains def remove_elements( @@ -78,8 +72,16 @@ def remove_elements( if report_processor.report_list( _validate_elements_to_remove(elements_to_remove) + _warn_remote_guest(remote_node_names, guest_node_names) - + _ensure_some_stonith_remains( - env, get_resources(cib), elements_to_remove, force_flags + + ensure_some_stonith_remains( + env, + get_resources(cib), + stonith_resources_to_ignore=[ + str(res_el.attrib["id"]) + for res_el in elements_to_remove.resources_to_remove + if is_stonith(res_el) + ], + sbd_being_disabled=False, + force_flags=force_flags, ) ).has_errors: raise LibraryError() @@ -205,89 +207,3 @@ def _get_guest_node_names(resource_elements: Iterable[_Element]) -> list[str]: for el in resource_elements if is_guest_node(el) ] - - -def _ensure_some_stonith_remains( - env: LibraryEnvironment, - resources_el: _Element, - elements_to_remove: ElementsToRemove, - force_flags: reports.types.ForceFlags, -) -> reports.ReportItemList: - if not any(is_stonith(el) for el in elements_to_remove.resources_to_remove): - # if no stonith are beieng removed then we don't need to check if any - # stonith will be left - return [] - - stonith_left = [ - stonith_el - for stonith_el in get_all_node_isolating_resources(resources_el) - if stonith_el.attrib["id"] not in elements_to_remove.ids_to_remove - # If any nvset disables the resource, even with a rule to limit it to - # specific time, than the resource wouldn't be able to fence all the - # time. - # However, pcs currently supports only one nvset for meta attributes, - # so we only check that to be consistent. Checking all nvsets could - # lead to a situation not resolvable by pcs, as pcs doesn't allow to - # change other nvsets than the first one. - # Technically, stonith resources can be disabled by their parent clones - # or groups. However, pcs doesn't allow putting stonith to groups and - # clones, so we don't check that. - # The check is not perfect, but it is a reasonable effort, considering - # that multiple nvsets are not supported for meta attributes by pcs now. - and not is_resource_disabled(stonith_el) - ] - if stonith_left: - return [] - if _is_sbd_enabled(env): - return [] - return [ - reports.ReportItem( - reports.get_severity( - reports.codes.FORCE, reports.codes.FORCE in force_flags - ), - reports.messages.NoStonithMeansWouldBeLeft(), - ) - ] - - -def _is_sbd_enabled(env: LibraryEnvironment) -> bool: - if not env.is_cib_live: - # We cannot tell whether sbd is enabled or not, as we do not have - # access to a cluster. Expect sbd is not enabled. - return False - - # Do not return errors. The check should not prevent deleting a resource - # just because a node in a cluster is temporarily unavailable. We do our - # best to figure out sbd status. - node_list, get_nodes_report_list = get_existing_nodes_names( - env.get_corosync_conf() - ) - env.report_processor.report_list(get_nodes_report_list) - if not node_list: - env.report_processor.report( - reports.ReportItem.warning( - reports.messages.CorosyncConfigNoNodesDefined() - ) - ) - return False - - com_cmd = GetSbdStatus(env.report_processor) - com_cmd.set_targets( - env.get_node_target_factory().get_target_list( - node_list, skip_non_existing=True - ) - ) - response_per_node = run_communication(env.get_node_communicator(), com_cmd) - for response in response_per_node: - # It is required to compare with False even if it looks wrong. The - # value can be either True (== sbd is enabled), or False (== sbd is - # disabled), or None (== unknown, not connected). - # We do not want to block removing resources just because we were - # temporarily unable to connect to a node, so we do not return False - # when the result is None. - if ( - response["status"]["enabled"] is False - or response["status"]["running"] is False - ): - return False - return True diff --git a/pcs/lib/commands/resource.py b/pcs/lib/commands/resource.py index 5fee3e77c..a70acb320 100644 --- a/pcs/lib/commands/resource.py +++ b/pcs/lib/commands/resource.py @@ -92,6 +92,7 @@ resource_agent_error_to_report_item, split_resource_agent_name, ) +from pcs.lib.sbd_stonith import ensure_some_stonith_remains from pcs.lib.tools import get_tmp_cib from pcs.lib.validate import ValueTimeInterval from pcs.lib.xml_tools import ( @@ -1136,27 +1137,6 @@ def bundle_update( # noqa: PLR0913 ) -def _disable_validate_and_edit_cib( - env: LibraryEnvironment, - cib: _Element, - resource_or_tag_ids: StringCollection, -) -> List[_Element]: - resource_el_list, report_list = _find_resources_expand_tags( - cib, resource_or_tag_ids - ) - env.report_processor.report_list(report_list) - if env.report_processor.report_list( - _resource_list_enable_disable( - resource_el_list, - resource.common.disable, - IdProvider(cib), - env.get_cluster_state(), - ) - ).has_errors: - raise LibraryError() - return resource_el_list - - def _disable_get_element_ids( disabled_resource_el_list: Iterable[_Element], ) -> Tuple[Set[str], Set[str]]: @@ -1225,6 +1205,7 @@ def disable( env: LibraryEnvironment, resource_or_tag_ids: StringCollection, wait: WaitType = False, + force_flags: reports.types.ForceFlags = (), ): """ Disallow specified resources to be started by the cluster @@ -1235,7 +1216,42 @@ def disable( wait -- False: no wait, None: wait default timeout, int: wait timeout """ wait_timeout = env.ensure_wait_satisfiable(wait) - _disable_validate_and_edit_cib(env, env.get_cib(), resource_or_tag_ids) + cib = env.get_cib() + resource_el_list, report_list = _find_resources_expand_tags( + cib, resource_or_tag_ids + ) + env.report_processor.report_list(report_list) + + if any( + resource.stonith.is_stonith(resource_el) + for resource_el in resource_el_list + ): + env.report_processor.report_list( + ensure_some_stonith_remains( + env, + get_resources(cib), + [str(res.attrib["id"]) for res in resource_el_list], + sbd_being_disabled=False, + force_flags=force_flags, + ) + ) + + # Validation done, do the disabling. Do not mind errors that happened so + # far. The disabling may report errors on its own and we want the user to + # see those. In case of errors, we exit before pushing CIB, not making any + # change to cluster configuration. + env.report_processor.report_list( + _resource_list_enable_disable( + resource_el_list, + resource.common.disable, + IdProvider(cib), + env.get_cluster_state(), + ) + ) + + if env.report_processor.has_errors: + raise LibraryError() + _push_cib_wait( env, wait_timeout, @@ -1269,9 +1285,11 @@ def disable_safe( wait_timeout = env.ensure_wait_satisfiable(wait) cib = env.get_cib() - resource_el_list = _disable_validate_and_edit_cib( - env, cib, resource_or_tag_ids + resource_el_list, report_list = _find_resources_expand_tags( + cib, resource_or_tag_ids ) + env.report_processor.report_list(report_list) + if any( resource.stonith.is_stonith(resource_el) for resource_el in resource_el_list @@ -1283,6 +1301,22 @@ def disable_safe( ) ) ) + + # Validation done, do the disabling. Do not mind errors that happened so + # far. The disabling may report errors on its own and we want the user to + # see those. In case of errors, we exit before pushing CIB, not making any + # change to cluster configuration. + env.report_processor.report_list( + _resource_list_enable_disable( + resource_el_list, + resource.common.disable, + IdProvider(cib), + env.get_cluster_state(), + ) + ) + if env.report_processor.has_errors: + raise LibraryError() + disabled_resource_id_set, inner_resource_id_set = _disable_get_element_ids( resource_el_list ) @@ -1340,9 +1374,27 @@ def disable_simulate( ) cib = env.get_cib() - resource_el_list = _disable_validate_and_edit_cib( - env, cib, resource_or_tag_ids + resource_el_list, report_list = _find_resources_expand_tags( + cib, resource_or_tag_ids + ) + env.report_processor.report_list(report_list) + + # Validation done, do the disabling. Do not mind errors that happened so + # far. The disabling may report errors on its own and we want the user to + # see those. In case of errors, we exit before pushing CIB, not making any + # change to cluster configuration. + env.report_processor.report_list( + _resource_list_enable_disable( + resource_el_list, + resource.common.disable, + IdProvider(cib), + env.get_cluster_state(), + ) ) + + if env.report_processor.has_errors: + raise LibraryError() + disabled_resource_id_set, inner_resource_id_set = _disable_get_element_ids( resource_el_list ) @@ -1401,11 +1453,14 @@ def enable( def _resource_list_enable_disable( - resource_el_list, func, id_provider, cluster_state -): + resource_el_list: Iterable[_Element], + func: Callable[[_Element, IdProvider], None], + id_provider: IdProvider, + cluster_state, +) -> ReportItemList: report_list = [] for resource_el in resource_el_list: - res_id = resource_el.attrib["id"] + res_id = str(resource_el.attrib["id"]) try: if not is_resource_managed(cluster_state, res_id): report_list.append( diff --git a/pcs/lib/commands/sbd.py b/pcs/lib/commands/sbd.py index b482aa188..b70d1ddd7 100644 --- a/pcs/lib/commands/sbd.py +++ b/pcs/lib/commands/sbd.py @@ -9,10 +9,6 @@ sbd, validate, ) -from pcs.lib.cib.resource.common import is_disabled as is_resource_disabled -from pcs.lib.cib.resource.stonith import ( - get_all_node_isolating_resources, -) from pcs.lib.cib.tools import get_resources from pcs.lib.communication.nodes import GetOnlineTargets from pcs.lib.communication.sbd import ( @@ -30,6 +26,7 @@ from pcs.lib.env import LibraryEnvironment from pcs.lib.errors import LibraryError from pcs.lib.node import get_existing_nodes_names +from pcs.lib.sbd_stonith import ensure_some_stonith_remains from pcs.lib.tools import environment_file_to_dict T = TypeVar("T") @@ -343,35 +340,15 @@ def disable_sbd( reports.messages.CorosyncConfigNoNodesDefined() ) ) - - stonith_left = [ - stonith_el - for stonith_el in get_all_node_isolating_resources( - get_resources(lib_env.get_cib()) - ) - # If any nvset disables the resource, even with a rule to limit it to - # specific time, than the resource wouldn't be able to fence all the - # time. - # However, pcs currently supports only one nvset for meta attributes, - # so we only check that to be consistent. Checking all nvsets could - # lead to a situation not resolvable by pcs, as pcs doesn't allow to - # change other nvsets than the first one. - # Technically, stonith resources can be disabled by their parent clones - # or groups. However, pcs doesn't allow putting stonith to groups and - # clones, so we don't check that. - # The check is not perfect, but it is a reasonable effort, considering - # that multiple nvsets are not supported for meta attributes by pcs now. - if not is_resource_disabled(stonith_el) - ] - if not stonith_left: - report_list.append( - reports.ReportItem( - reports.get_severity( - reports.codes.FORCE, reports.codes.FORCE in force_flags - ), - reports.messages.NoStonithMeansWouldBeLeft(), - ) + report_list.extend( + ensure_some_stonith_remains( + lib_env, + get_resources(lib_env.get_cib()), + stonith_resources_to_ignore=[], + sbd_being_disabled=True, + force_flags=force_flags, ) + ) if lib_env.report_processor.report_list(report_list).has_errors: raise LibraryError() diff --git a/pcs/lib/sbd_stonith.py b/pcs/lib/sbd_stonith.py new file mode 100644 index 000000000..08ad20a48 --- /dev/null +++ b/pcs/lib/sbd_stonith.py @@ -0,0 +1,130 @@ +from lxml.etree import _Element + +from pcs.common import reports +from pcs.common.types import StringCollection +from pcs.lib.cib.resource.common import is_disabled as is_resource_disabled +from pcs.lib.cib.resource.stonith import get_all_node_isolating_resources +from pcs.lib.communication.sbd import GetSbdStatus +from pcs.lib.communication.tools import run as run_communication +from pcs.lib.env import LibraryEnvironment +from pcs.lib.node import get_existing_nodes_names + + +def ensure_some_stonith_remains( + env: LibraryEnvironment, + resources_el: _Element, + stonith_resources_to_ignore: StringCollection, + sbd_being_disabled: bool, + force_flags: reports.types.ForceFlags, +) -> reports.ReportItemList: + """ + Error out when no sbd or enabled stonith would be left after a config change + + resource_el -- cib element holding resources + stonith_resources_to_ignore -- ids of stonith being removed, disabled, etc. + sbd_being_disabled -- ignore working sbd as it is being disabled + force_flags -- use to emit a warning instead of an error + """ + # Checking whether sbd is enabled requires communicating with other cluster + # nodes, which may bring additional issues, like nodes not being + # accessible. To reduce clutter in reports, check for the sbd being enabled + # only when necessary. + + if not stonith_resources_to_ignore and not sbd_being_disabled: + # No stonith resources are being removed or disabled and SBD is not + # being disabled either. There is no change in cluster fencing + # capabilities and therefore nothing to report. + return [] + + current_stonith = [ + stonith_el + for stonith_el in get_all_node_isolating_resources(resources_el) + # If any nvset disables the resource, even with a rule to limit it to + # specific time, then the resource wouldn't be able to fence all the + # time and should be considered disabled. + # However, pcs currently supports only one nvset for meta attributes, + # so we only check that to be consistent. Checking all nvsets could + # lead to a situation not resolvable by pcs, as pcs doesn't allow to + # change other nvsets than the first one. + # Technically, stonith resources can be disabled by their parent clones + # or groups. However, pcs doesn't allow putting stonith to groups and + # clones, so we don't check that. + # The check is not perfect, but it is a reasonable effort, considering + # that multiple nvsets are not supported for meta attributes by pcs now. + # It can be improved when a need for it raises. + if not is_resource_disabled(stonith_el) + ] + stonith_left = [ + stonith_el + for stonith_el in current_stonith + if stonith_el.attrib["id"] not in stonith_resources_to_ignore + ] + + if stonith_left: + # Working stonith devices will be present. No need to check for SBD. + return [] + + # No stonith in the cluster, need to check SBD. + current_sbd_enabled = _is_sbd_enabled_on_any_node(env) + sbd_left_enabled = current_sbd_enabled and not sbd_being_disabled + + if sbd_left_enabled: + # SBD will be left enabled. + return [] + + if not current_stonith and not current_sbd_enabled: + # Now we know that no enabled stonith will be left in the cluster and + # sbd will also be disabled. However, if that already was the case, we + # don't produce an error -> the cluster already cannot fence, saying + # that it is a result of the current change would not be true. + return [] + + return [ + reports.ReportItem( + reports.get_severity( + reports.codes.FORCE, reports.codes.FORCE in force_flags + ), + reports.messages.NoStonithMeansWouldBeLeft(), + ) + ] + + +def _is_sbd_enabled_on_any_node(env: LibraryEnvironment) -> bool: + # SBD can be enabled only partially in the cluster. Even when that is the + # case, we warn the user when disabling it. For example, SBD can be enabled + # for full stack nodes and disabled for remote / guest nodes. + if not env.is_cib_live: + # We cannot tell whether sbd is enabled or not, as we do not have + # access to a cluster. Expect sbd is not enabled. + return False + + # Do not return errors. The check should not prevent deleting a resource + # just because a node in a cluster is temporarily unavailable. We do our + # best to figure out sbd status. + node_list, get_nodes_report_list = get_existing_nodes_names( + env.get_corosync_conf() + ) + env.report_processor.report_list(get_nodes_report_list) + if not node_list: + env.report_processor.report( + reports.ReportItem.warning( + reports.messages.CorosyncConfigNoNodesDefined() + ) + ) + return False + + com_cmd = GetSbdStatus(env.report_processor) + com_cmd.set_targets( + env.get_node_target_factory().get_target_list( + node_list, skip_non_existing=True + ) + ) + response_per_node = run_communication(env.get_node_communicator(), com_cmd) + for response in response_per_node: + # Values can be either True (== sbd is enabled), or False (== sbd is + # disabled), or None (== unknown, not connected). + # We do not want to block removing resources just because we were + # temporarily unable to connect to a node. + if response["status"]["enabled"] or response["status"]["running"]: + return True + return False diff --git a/pcs/resource.py b/pcs/resource.py index b9aa53081..357789b3b 100644 --- a/pcs/resource.py +++ b/pcs/resource.py @@ -2331,6 +2331,7 @@ def resource_disable_common( """ Commandline options: * -f - CIB file + * --force - allow to disable the last stonith resource in the cluster * --brief - show brief output of --simulate * --safe - only disable if no other resource gets stopped or demoted * --simulate - do not push the CIB, print its effects @@ -2338,7 +2339,13 @@ def resource_disable_common( * --wait """ modifiers.ensure_only_supported( - "-f", "--brief", "--safe", "--simulate", "--no-strict", "--wait" + "-f", + "--force", + "--brief", + "--safe", + "--simulate", + "--no-strict", + "--wait", ) modifiers.ensure_not_mutually_exclusive("-f", "--simulate", "--wait") modifiers.ensure_not_incompatible("--simulate", {"-f", "--safe", "--wait"}) @@ -2378,7 +2385,10 @@ def resource_disable_common( raise CmdLineInputError( "'--brief' cannot be used without '--simulate' or '--safe'" ) - lib.resource.disable(argv, modifiers.get("--wait")) + force_flags = set() + if modifiers.get("--force"): + force_flags.add(reports.codes.FORCE) + lib.resource.disable(argv, modifiers.get("--wait"), force_flags) def resource_safe_disable_cmd( diff --git a/pcs/stonith.py b/pcs/stonith.py index 6d6231eb6..2e15d6bad 100644 --- a/pcs/stonith.py +++ b/pcs/stonith.py @@ -1016,6 +1016,7 @@ def disable_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: * --safe - only disable if no other resource gets stopped or demoted * --simulate - do not push the CIB, print its effects * --no-strict - allow disable if other resource is affected + * --force * --wait """ if not argv: diff --git a/pcs_test/tier0/cli/test_resource.py b/pcs_test/tier0/cli/test_resource.py index afb731001..6be4f740e 100644 --- a/pcs_test/tier0/cli/test_resource.py +++ b/pcs_test/tier0/cli/test_resource.py @@ -8,6 +8,7 @@ from pcs import resource from pcs.cli.common.errors import CmdLineInputError from pcs.cli.reports.processor import ReportItemSeverity +from pcs.common.reports.codes import FORCE from pcs_test.tools.assertions import ( AssertPcsMixin, @@ -675,7 +676,7 @@ def test_one_resource(self): resource.resource_disable_common( self.lib, ["R1"], dict_to_modifiers({}) ) - self.resource.disable.assert_called_once_with(["R1"], False) + self.resource.disable.assert_called_once_with(["R1"], False, set()) self.report_processor.suppress_reports_of_severity.assert_not_called() self.resource.disable_safe.assert_not_called() self.resource.disable_simulate.assert_not_called() @@ -684,7 +685,18 @@ def test_more_resources(self): resource.resource_disable_common( self.lib, ["R1", "R2"], dict_to_modifiers({}) ) - self.resource.disable.assert_called_once_with(["R1", "R2"], False) + self.resource.disable.assert_called_once_with( + ["R1", "R2"], False, set() + ) + self.report_processor.suppress_reports_of_severity.assert_not_called() + self.resource.disable_safe.assert_not_called() + self.resource.disable_simulate.assert_not_called() + + def test_force(self): + resource.resource_disable_common( + self.lib, ["R1"], dict_to_modifiers(dict(force=True)) + ) + self.resource.disable.assert_called_once_with(["R1"], False, {FORCE}) self.report_processor.suppress_reports_of_severity.assert_not_called() self.resource.disable_safe.assert_not_called() self.resource.disable_simulate.assert_not_called() @@ -870,7 +882,7 @@ def test_wait(self): self.lib, ["R1", "R2"], dict_to_modifiers(dict(wait="10")) ) self.report_processor.suppress_reports_of_severity.assert_not_called() - self.resource.disable.assert_called_once_with(["R1", "R2"], "10") + self.resource.disable.assert_called_once_with(["R1", "R2"], "10", set()) self.resource.disable_safe.assert_not_called() self.resource.disable_simulate.assert_not_called() @@ -969,7 +981,9 @@ def test_force(self, mock_warn): resource.resource_safe_disable_cmd( self.lib, ["R1", "R2"], dict_to_modifiers({"force": True}) ) - self.resource.disable.assert_called_once_with(["R1", "R2"], False) + self.resource.disable.assert_called_once_with( + ["R1", "R2"], False, set() + ) self.resource.disable_safe.assert_not_called() self.resource.disable_simulate.assert_not_called() mock_warn.assert_called_once_with(self.force_warning) @@ -981,7 +995,7 @@ def test_force_wait(self, mock_warn): ["R1", "R2"], dict_to_modifiers({"force": True, "wait": "10"}), ) - self.resource.disable.assert_called_once_with(["R1", "R2"], "10") + self.resource.disable.assert_called_once_with(["R1", "R2"], "10", set()) self.resource.disable_safe.assert_not_called() self.resource.disable_simulate.assert_not_called() mock_warn.assert_called_once_with(self.force_warning) diff --git a/pcs_test/tier0/lib/commands/resource/test_resource_enable_disable.py b/pcs_test/tier0/lib/commands/resource/test_resource_enable_disable.py index 8196fc544..840ba6abb 100644 --- a/pcs_test/tier0/lib/commands/resource/test_resource_enable_disable.py +++ b/pcs_test/tier0/lib/commands/resource/test_resource_enable_disable.py @@ -1,4 +1,5 @@ # pylint: disable=too-many-lines +import json from unittest import ( TestCase, mock, @@ -7,7 +8,6 @@ from pcs import settings from pcs.common import reports from pcs.common.reports import ReportItemSeverity as severities -from pcs.common.reports import codes as report_codes from pcs.lib.commands import resource from pcs.lib.errors import LibraryError @@ -20,6 +20,7 @@ ) from pcs_test.tools.misc import get_test_resource as rc from pcs_test.tools.misc import outdent +from pcs_test.tools.xml import XmlManipulation TIMEOUT = 10 @@ -595,7 +596,7 @@ def get_fixture_bundle_cib( def fixture_report_unmanaged(resource_id): return ( severities.WARNING, - report_codes.RESOURCE_IS_UNMANAGED, + reports.codes.RESOURCE_IS_UNMANAGED, { "resource_id": resource_id, }, @@ -666,6 +667,202 @@ def test_unmanaged(self): self.env_assist.assert_reports([fixture_report_unmanaged("A")]) +@mock.patch.object( + settings, "pacemaker_api_result_schema", rc("pcmk_api_rng/api-result.rng") +) +class DisableStonith(TestCase): + resources_cib = """ + + + + + + """ + resources_cib_disabled = """ + + + + + + + + + + + + + + """ + resources_status = """ + + + + + + """ + + def fixture_config_sbd_calls(self, sbd_enabled): + node_name_list = ["node-1", "node-2"] + self.config.env.set_known_nodes(node_name_list) + self.config.corosync_conf.load(node_name_list=node_name_list) + self.config.http.sbd.check_sbd( + communication_list=[ + dict( + label=node, + param_list=[("watchdog", ""), ("device_list", "[]")], + output=json.dumps( + dict( + sbd=dict( + installed=True, + enabled=sbd_enabled, + running=sbd_enabled, + ) + ) + ), + ) + for node in node_name_list + ] + ) + + def setUp(self): + self.env_assist, self.config = get_env_tools(test_case=self) + + def test_useful_enabled_stonith_left(self): + resources_cib_disabled = """ + + + + + + + + + + """ + self.config.runner.cib.load(resources=self.resources_cib) + self.config.runner.pcmk.load_state(resources=self.resources_status) + self.config.env.push_cib(resources=resources_cib_disabled) + + resource.disable(self.env_assist.get_env(), ["S2"], False) + + def test_no_stonith_left_sbd_enabled(self): + self.config.runner.cib.load(resources=self.resources_cib) + self.fixture_config_sbd_calls(True) + self.config.runner.pcmk.load_state(resources=self.resources_status) + self.config.env.push_cib(resources=self.resources_cib_disabled) + + resource.disable(self.env_assist.get_env(), ["S1", "S2"], False) + + def test_no_stonith_left_not_live(self): + tmp_file = "/fake/tmp_file" + cmd_env = dict(CIB_file=tmp_file) + cib_xml_man = XmlManipulation.from_file(rc("cib-empty.xml")) + cib_xml_man.append_to_first_tag_name("resources", self.resources_cib) + self.config.env.set_cib_data(str(cib_xml_man), cib_tempfile=tmp_file) + self.config.runner.cib.load(resources=self.resources_cib, env=cmd_env) + self.config.runner.pcmk.load_state( + resources=self.resources_status, env=cmd_env + ) + # doesn't call other nodes to check sbd status + + self.env_assist.assert_raise_library_error( + lambda: resource.disable( + self.env_assist.get_env(), ["S1", "S2"], False + ) + ) + self.env_assist.assert_reports( + [ + fixture.error( + reports.codes.NO_STONITH_MEANS_WOULD_BE_LEFT, + force_code=reports.codes.FORCE, + ) + ] + ) + + def test_no_stonith_left_sbd_disabled(self): + self.config.runner.cib.load(resources=self.resources_cib) + self.fixture_config_sbd_calls(False) + self.config.runner.pcmk.load_state(resources=self.resources_status) + + self.env_assist.assert_raise_library_error( + lambda: resource.disable( + self.env_assist.get_env(), ["S1", "S2"], False + ) + ) + self.env_assist.assert_reports( + [ + fixture.error( + reports.codes.NO_STONITH_MEANS_WOULD_BE_LEFT, + force_code=reports.codes.FORCE, + ) + ] + ) + + def test_no_stonith_left_sbd_disabled_forced(self): + self.config.runner.cib.load(resources=self.resources_cib) + self.fixture_config_sbd_calls(False) + self.config.runner.pcmk.load_state(resources=self.resources_status) + self.config.env.push_cib(resources=self.resources_cib_disabled) + + resource.disable( + self.env_assist.get_env(), + ["S1", "S2"], + False, + force_flags={reports.codes.FORCE}, + ) + self.env_assist.assert_reports( + [ + fixture.warn( + reports.codes.NO_STONITH_MEANS_WOULD_BE_LEFT, + ) + ] + ) + + def test_no_useful_enabled_stonith_removed(self): + resources_cib = """ + + + + + + + + + """ + resources_cib_disabled = """ + + + + + + + + + + + + + """ + self.config.runner.cib.load(resources=resources_cib) + self.fixture_config_sbd_calls(False) + self.config.runner.pcmk.load_state(resources=self.resources_status) + self.config.env.push_cib(resources=resources_cib_disabled) + + resource.disable(self.env_assist.get_env(), ["S1", "S3"], False) + + @mock.patch.object( settings, "pacemaker_api_result_schema", rc("pcmk_api_rng/api-result.rng") ) @@ -1164,7 +1361,7 @@ def test_disable_clone(self): [ ( severities.INFO, - report_codes.RESOURCE_DOES_NOT_RUN, + reports.codes.RESOURCE_DOES_NOT_RUN, { "resource_id": "A-clone", }, @@ -1194,7 +1391,7 @@ def test_enable_clone(self): [ ( severities.INFO, - report_codes.RESOURCE_RUNNING_ON_NODES, + reports.codes.RESOURCE_RUNNING_ON_NODES, { "resource_id": "A-clone", "roles_with_nodes": {"Started": ["node1", "node2"]}, @@ -2150,7 +2347,7 @@ def test_not_live(self): ), [ fixture.error( - report_codes.LIVE_ENVIRONMENT_REQUIRED, + reports.codes.LIVE_ENVIRONMENT_REQUIRED, forbidden_options=["CIB"], ), ], @@ -2317,7 +2514,7 @@ def test_simulate_error(self): ), [ fixture.error( - report_codes.CIB_SIMULATE_ERROR, + reports.codes.CIB_SIMULATE_ERROR, reason="some stderr", ), ], @@ -2334,7 +2531,7 @@ def test_not_live(self): ), [ fixture.error( - report_codes.LIVE_ENVIRONMENT_REQUIRED, + reports.codes.LIVE_ENVIRONMENT_REQUIRED, forbidden_options=["CIB"], ), ], @@ -2372,7 +2569,7 @@ def test_simulate_error(self): ), [ fixture.error( - report_codes.CIB_SIMULATE_ERROR, + reports.codes.CIB_SIMULATE_ERROR, reason="some stderr", ), ], @@ -2453,12 +2650,12 @@ def test_other_resources_stopped(self): self.env_assist.assert_reports( [ fixture.error( - report_codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES, + reports.codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES, disabled_resource_list=["A"], affected_resource_list=["B"], ), fixture.info( - report_codes.PACEMAKER_SIMULATION_RESULT, + reports.codes.PACEMAKER_SIMULATION_RESULT, plaintext_output="simulate output", ), ], @@ -2499,12 +2696,12 @@ def test_master_demoted(self): self.env_assist.assert_reports( [ fixture.error( - report_codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES, + reports.codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES, disabled_resource_list=["A"], affected_resource_list=["B"], ), fixture.info( - report_codes.PACEMAKER_SIMULATION_RESULT, + reports.codes.PACEMAKER_SIMULATION_RESULT, plaintext_output="simulate output", ), ] @@ -2772,7 +2969,7 @@ def test_inner_resources(self): self.env_assist.assert_reports( [ fixture.error( - report_codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES, + reports.codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES, disabled_resource_list=[ "B-clone", "C-master", @@ -2784,7 +2981,7 @@ def test_inner_resources(self): affected_resource_list=["A"], ), fixture.info( - report_codes.PACEMAKER_SIMULATION_RESULT, + reports.codes.PACEMAKER_SIMULATION_RESULT, plaintext_output="simulate output", ), ], @@ -2873,12 +3070,12 @@ def test_resources_migrated(self): self.env_assist.assert_reports( [ fixture.error( - report_codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES, + reports.codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES, disabled_resource_list=["A"], affected_resource_list=["B"], ), fixture.info( - report_codes.PACEMAKER_SIMULATION_RESULT, + reports.codes.PACEMAKER_SIMULATION_RESULT, plaintext_output="simulate output", ), ] @@ -2919,12 +3116,12 @@ def test_master_migrated(self): self.env_assist.assert_reports( [ fixture.error( - report_codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES, + reports.codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES, disabled_resource_list=["A"], affected_resource_list=["B"], ), fixture.info( - report_codes.PACEMAKER_SIMULATION_RESULT, + reports.codes.PACEMAKER_SIMULATION_RESULT, plaintext_output="simulate output", ), ] diff --git a/pcs_test/tier0/lib/commands/sbd/test_disable_sbd.py b/pcs_test/tier0/lib/commands/sbd/test_disable_sbd.py index 3c353b2d9..7943c6af1 100644 --- a/pcs_test/tier0/lib/commands/sbd/test_disable_sbd.py +++ b/pcs_test/tier0/lib/commands/sbd/test_disable_sbd.py @@ -1,3 +1,4 @@ +import json from unittest import TestCase from pcs.common import reports @@ -20,6 +21,30 @@ def setUp(self): """ + def fixture_config_sbd_status_calls(self, sbd_enabled): + self.config.corosync_conf.load( + filename=self.corosync_conf_name, + name="corosync_conf.load.for_sbd_status", + ) + self.config.http.sbd.check_sbd( + communication_list=[ + dict( + label=node, + param_list=[("watchdog", ""), ("device_list", "[]")], + output=json.dumps( + dict( + sbd=dict( + installed=True, + enabled=sbd_enabled, + running=sbd_enabled, + ) + ) + ), + ) + for node in self.node_list + ] + ) + def test_success(self): self.config.corosync_conf.load(filename=self.corosync_conf_name) self.config.runner.cib.load(resources=self.cib_resources) @@ -67,6 +92,7 @@ def test_only_disabled_stonith_left(self): """ self.config.corosync_conf.load(filename=self.corosync_conf_name) self.config.runner.cib.load(resources=cib_resources) + self.fixture_config_sbd_status_calls(True) self.env_assist.assert_raise_library_error( lambda: disable_sbd(self.env_assist.get_env()) @@ -83,6 +109,7 @@ def test_only_disabled_stonith_left(self): def test_no_stonith_left(self): self.config.corosync_conf.load(filename=self.corosync_conf_name) self.config.runner.cib.load() + self.fixture_config_sbd_status_calls(True) self.env_assist.assert_raise_library_error( lambda: disable_sbd(self.env_assist.get_env()) @@ -99,6 +126,7 @@ def test_no_stonith_left(self): def test_no_stonith_left_forced(self): self.config.corosync_conf.load(filename=self.corosync_conf_name) self.config.runner.cib.load() + self.fixture_config_sbd_status_calls(True) self.config.http.host.check_auth(node_labels=self.node_list) self.config.http.pcmk.set_stonith_watchdog_timeout_to_zero( communication_list=[[dict(label=node)] for node in self.node_list], @@ -137,6 +165,43 @@ def test_no_stonith_left_forced(self): ] ) + def test_no_stonith_left_sbd_was_disabled(self): + self.config.corosync_conf.load(filename=self.corosync_conf_name) + self.config.runner.cib.load() + self.fixture_config_sbd_status_calls(False) + self.config.http.host.check_auth(node_labels=self.node_list) + self.config.http.pcmk.set_stonith_watchdog_timeout_to_zero( + communication_list=[[dict(label=node)] for node in self.node_list], + ) + self.config.http.sbd.disable_sbd(node_labels=self.node_list) + + disable_sbd(self.env_assist.get_env()) + self.env_assist.assert_reports( + [ + fixture.info( + reports.codes.SERVICE_ACTION_STARTED, + action=reports.const.SERVICE_ACTION_DISABLE, + service="sbd", + instance="", + ), + ] + + [ + fixture.info( + reports.codes.SERVICE_ACTION_SUCCEEDED, + action=reports.const.SERVICE_ACTION_DISABLE, + service="sbd", + node=node, + instance="", + ) + for node in self.node_list + ] + + [ + fixture.warn( + report_codes.CLUSTER_RESTART_REQUIRED_TO_APPLY_CHANGES + ) + ] + ) + def test_some_node_names_missing(self): self.corosync_conf_name = "corosync-some-node-names.conf" self.node_list = ["rh7-2"] diff --git a/pcs_test/tier0/lib/commands/test_cib.py b/pcs_test/tier0/lib/commands/test_cib.py index b12b9c830..7cc5a3130 100644 --- a/pcs_test/tier0/lib/commands/test_cib.py +++ b/pcs_test/tier0/lib/commands/test_cib.py @@ -1096,7 +1096,7 @@ def fixture_config_sbd_calls(self, sbd_enabled): ] ) - def fixture_remove_s1_s2(self): + def fixture_remove_s1_s2(self, s2_type="fence_any"): self.fixture_stop_resources_wait_calls( self.config.calls.get("runner.cib.load").stdout, initial_state_modifiers={ @@ -1108,7 +1108,7 @@ def fixture_remove_s1_s2(self): """ }, after_disable_cib_modifiers={ - "resources": """ + "resources": f""" @@ -1117,7 +1117,7 @@ def fixture_remove_s1_s2(self): /> - + - + """ self.config.runner.cib.load(resources=resources) @@ -1264,6 +1264,35 @@ def test_disabled_stonith_left_sbd_disabled(self): ] ) + def test_stonith_was_already_noneffective_and_sbd_disabled(self): + resources = """ + + + + + + + + + """ + self.fixture_init_tmp_file_mocker() + self.config.runner.cib.load(resources=resources) + self.fixture_config_sbd_calls(sbd_enabled=False) + self.fixture_remove_s1_s2(s2_type="fence_sbd") + + lib.remove_elements(self.env_assist.get_env(), ["S1", "S2"]) + self.env_assist.assert_reports( + [ + fixture.info( + reports.codes.STOPPING_RESOURCES_BEFORE_DELETING, + resource_id_list=["S1", "S2"], + ), + fixture.info(reports.codes.WAIT_FOR_IDLE_STARTED, timeout=0), + ] + ) + def test_no_stonith_left_sbd_disabled(self): self.config.runner.cib.load(resources=self.resources) self.fixture_config_sbd_calls(sbd_enabled=False) @@ -1335,11 +1364,11 @@ def test_no_stonith_left_sbd_disabled_forced(self): ] ) - def test_no_stonith_left_sbd_partially_disabled(self): - self.config.runner.cib.load(resources=self.resources) - + def test_no_stonith_left_sbd_partially_enabled(self): node_name_list = ["node-1", "node-2"] self.config.env.set_known_nodes(node_name_list) + self.fixture_init_tmp_file_mocker() + self.config.runner.cib.load(resources=self.resources) self.config.corosync_conf.load(node_name_list=node_name_list) self.config.http.sbd.check_sbd( communication_list=[ @@ -1365,16 +1394,16 @@ def test_no_stonith_left_sbd_partially_disabled(self): ), ] ) + self.fixture_remove_s1_s2() - self.env_assist.assert_raise_library_error( - lambda: lib.remove_elements(self.env_assist.get_env(), ["S1", "S2"]) - ) + lib.remove_elements(self.env_assist.get_env(), ["S1", "S2"]) self.env_assist.assert_reports( [ - fixture.error( - reports.codes.NO_STONITH_MEANS_WOULD_BE_LEFT, - force_code=reports.codes.FORCE, - ) + fixture.info( + reports.codes.STOPPING_RESOURCES_BEFORE_DELETING, + resource_id_list=["S1", "S2"], + ), + fixture.info(reports.codes.WAIT_FOR_IDLE_STARTED, timeout=0), ] ) diff --git a/pcs_test/tier1/cib_resource/test_stonith_enable_disable.py b/pcs_test/tier1/cib_resource/test_stonith_enable_disable.py index 25fb1d7bf..5f5e8e507 100644 --- a/pcs_test/tier1/cib_resource/test_stonith_enable_disable.py +++ b/pcs_test/tier1/cib_resource/test_stonith_enable_disable.py @@ -1,6 +1,10 @@ from pcs_test.tier1.cib_resource.common import ResourceTest from pcs_test.tools.bin_mock import get_mock_settings +ERRORS_HAVE_OCCURRED = ( + "Error: Errors have occurred, therefore pcs is unable to continue\n" +) + class Enable(ResourceTest): def setUp(self): @@ -74,8 +78,31 @@ def test_disable_enabled_stonith(self): """, ) - self.assert_effect( + self.assert_pcs_fail( "stonith disable S".split(), + ( + "Error: Requested action lefts the cluster with no enabled " + "means to fence nodes, resulting in the cluster not being able " + "to recover from certain failure conditions, use --force to " + "override\n" + ERRORS_HAVE_OCCURRED + ), + ) + + def test_disable_enabled_stonith_forced(self): + self.assert_effect( + "stonith create S fence_pcsmock_minimal".split(), + """ + + + + + + """, + ) + self.assert_effect( + "stonith disable S --force".split(), """ @@ -90,6 +117,11 @@ def test_disable_enabled_stonith(self): """, + stderr_full=( + "Warning: Requested action lefts the cluster with no enabled " + "means to fence nodes, resulting in the cluster not being able " + "to recover from certain failure conditions\n" + ), ) def test_keep_disabled_stonith(self): From 033da867852dbfda1640ca8030f6ae6fd788cc8d Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Fri, 14 Mar 2025 17:32:18 +0100 Subject: [PATCH 119/227] add tests --- pcs_test/tier0/cli/test_stonith.py | 34 ++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/pcs_test/tier0/cli/test_stonith.py b/pcs_test/tier0/cli/test_stonith.py index 50da98ca3..e5360358b 100644 --- a/pcs_test/tier0/cli/test_stonith.py +++ b/pcs_test/tier0/cli/test_stonith.py @@ -171,6 +171,40 @@ def test_modifiers(self): ) +class SbdDisable(TestCase): + def setUp(self): + self.lib = mock.Mock(spec_set=["sbd"]) + self.sbd = mock.Mock(spec_set=["disable_sbd"]) + self.lib.sbd = self.sbd + + def _call_cmd(self, argv=None, modifiers=None): + stonith.sbd_disable( + self.lib, argv or [], _dict_to_modifiers(modifiers or {}) + ) + + def test_no_options(self): + self._call_cmd() + self.lib.sbd.disable_sbd.assert_called_once_with(False, set()) + + def test_skip_offline(self): + self._call_cmd(modifiers={"skip-offline": ""}) + self.lib.sbd.disable_sbd.assert_called_once_with( + True, {reports.codes.SKIP_OFFLINE_NODES} + ) + + def test_force(self): + self._call_cmd(modifiers={"force": ""}) + self.lib.sbd.disable_sbd.assert_called_once_with( + False, {reports.codes.FORCE} + ) + + def test_all_modifiers(self): + self._call_cmd(modifiers={"skip-offline": "", "force": ""}) + self.lib.sbd.disable_sbd.assert_called_once_with( + True, {reports.codes.SKIP_OFFLINE_NODES, reports.codes.FORCE} + ) + + class SbdDeviceSetup(TestCase): def setUp(self): self.lib = mock.Mock(spec_set=["sbd"]) From d64de672863fbc9df2d91430b8ee8485046aaef6 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Wed, 19 Mar 2025 13:14:13 +0100 Subject: [PATCH 120/227] update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a36035ca7..79d507ff0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ - Command `pcs stonith sbd watchdog list` now prints watchdogs' identity and driver ([RHEL-76177]) - Command `pcs cluster rename` for changing cluster name ([RHEL-76055]) -- Removing or disabling stonith devices or disabling SBD fails if the cluster +- Prevent removing or disabling stonith devices or disabling SBD if the cluster would be left with disabled SBD and no stonith devices ([RHEL-76170]) ### Fixed From 6a8f9a66473ec2faee5a284d3d446d6e109b6aa0 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Fri, 21 Mar 2025 16:09:22 +0100 Subject: [PATCH 121/227] fix sbd check when considering available stonith --- pcs/lib/sbd_stonith.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/pcs/lib/sbd_stonith.py b/pcs/lib/sbd_stonith.py index 08ad20a48..cd047f604 100644 --- a/pcs/lib/sbd_stonith.py +++ b/pcs/lib/sbd_stonith.py @@ -65,14 +65,14 @@ def ensure_some_stonith_remains( return [] # No stonith in the cluster, need to check SBD. - current_sbd_enabled = _is_sbd_enabled_on_any_node(env) - sbd_left_enabled = current_sbd_enabled and not sbd_being_disabled + current_sbd_active = _is_sbd_active_on_any_node(env) + sbd_left_active = current_sbd_active and not sbd_being_disabled - if sbd_left_enabled: + if sbd_left_active: # SBD will be left enabled. return [] - if not current_stonith and not current_sbd_enabled: + if not current_stonith and not current_sbd_active: # Now we know that no enabled stonith will be left in the cluster and # sbd will also be disabled. However, if that already was the case, we # don't produce an error -> the cluster already cannot fence, saying @@ -89,7 +89,7 @@ def ensure_some_stonith_remains( ] -def _is_sbd_enabled_on_any_node(env: LibraryEnvironment) -> bool: +def _is_sbd_active_on_any_node(env: LibraryEnvironment) -> bool: # SBD can be enabled only partially in the cluster. Even when that is the # case, we warn the user when disabling it. For example, SBD can be enabled # for full stack nodes and disabled for remote / guest nodes. @@ -125,6 +125,11 @@ def _is_sbd_enabled_on_any_node(env: LibraryEnvironment) -> bool: # disabled), or None (== unknown, not connected). # We do not want to block removing resources just because we were # temporarily unable to connect to a node. - if response["status"]["enabled"] or response["status"]["running"]: + # If sbd is enabled and not running, then the cluster won't have any + # fencing after removing all stonith resources. If sbd is not enabled + # and running, then the cluster won't have any fencing after removing + # all stonith resources and rebooting nodes once. So we need both + # enabled and running to be true to consider sbd as active. + if response["status"]["enabled"] and response["status"]["running"]: return True return False From 8f3cb36330f3633816abad8dfb5de29abe9022eb Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Thu, 27 Mar 2025 14:02:45 +0100 Subject: [PATCH 122/227] add missing capability --- pcsd/capabilities.xml.in | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pcsd/capabilities.xml.in b/pcsd/capabilities.xml.in index 8fd4dc173..4034c34e2 100644 --- a/pcsd/capabilities.xml.in +++ b/pcsd/capabilities.xml.in @@ -2692,6 +2692,15 @@ /api/v1/sbd-enable-sbd/v1 + + + Command for disabling SBD accepts '--force' option / force-flags + + pcs commands: stonith sbd disable + daemon urls: /api/v1/sbd-disable-sbd/v1 + API v2: sbd.disable_sbd + + Allows to set SBD_TIMEOUT_ACTION option. From 09be0b3dd09f9d0c5556f437f6429131d3abff21 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Thu, 27 Mar 2025 16:53:17 +0100 Subject: [PATCH 123/227] fix stonith enabled check if stonith is cloned or grouped --- pcs/lib/commands/resource.py | 18 +- pcs/lib/sbd_stonith.py | 23 ++- .../resource/test_resource_enable_disable.py | 186 ++++++++++++++++++ 3 files changed, 214 insertions(+), 13 deletions(-) diff --git a/pcs/lib/commands/resource.py b/pcs/lib/commands/resource.py index a70acb320..8c6e1bc2a 100644 --- a/pcs/lib/commands/resource.py +++ b/pcs/lib/commands/resource.py @@ -1222,15 +1222,23 @@ def disable( ) env.report_processor.report_list(report_list) - if any( - resource.stonith.is_stonith(resource_el) - for resource_el in resource_el_list - ): + stonith_to_be_disabled = [] + for resource_el in resource_el_list: + stonith_to_be_disabled += [ + primitive_el + for primitive_el in resource.common.get_all_inner_resources( + resource_el + ) + | {resource_el} + if resource.stonith.is_stonith(primitive_el) + ] + + if stonith_to_be_disabled: env.report_processor.report_list( ensure_some_stonith_remains( env, get_resources(cib), - [str(res.attrib["id"]) for res in resource_el_list], + [str(res.attrib["id"]) for res in stonith_to_be_disabled], sbd_being_disabled=False, force_flags=force_flags, ) diff --git a/pcs/lib/sbd_stonith.py b/pcs/lib/sbd_stonith.py index cd047f604..42879971d 100644 --- a/pcs/lib/sbd_stonith.py +++ b/pcs/lib/sbd_stonith.py @@ -1,7 +1,10 @@ +from typing import Optional + from lxml.etree import _Element from pcs.common import reports from pcs.common.types import StringCollection +from pcs.lib.cib.resource.common import get_parent_resource from pcs.lib.cib.resource.common import is_disabled as is_resource_disabled from pcs.lib.cib.resource.stonith import get_all_node_isolating_resources from pcs.lib.communication.sbd import GetSbdStatus @@ -36,9 +39,8 @@ def ensure_some_stonith_remains( # capabilities and therefore nothing to report. return [] - current_stonith = [ - stonith_el - for stonith_el in get_all_node_isolating_resources(resources_el) + current_stonith = [] + for stonith_el in get_all_node_isolating_resources(resources_el): # If any nvset disables the resource, even with a rule to limit it to # specific time, then the resource wouldn't be able to fence all the # time and should be considered disabled. @@ -46,14 +48,19 @@ def ensure_some_stonith_remains( # so we only check that to be consistent. Checking all nvsets could # lead to a situation not resolvable by pcs, as pcs doesn't allow to # change other nvsets than the first one. - # Technically, stonith resources can be disabled by their parent clones - # or groups. However, pcs doesn't allow putting stonith to groups and - # clones, so we don't check that. + # Stonith resources can be disabled by their parent clones or groups, + # so check them as well. # The check is not perfect, but it is a reasonable effort, considering # that multiple nvsets are not supported for meta attributes by pcs now. # It can be improved when a need for it raises. - if not is_resource_disabled(stonith_el) - ] + resource_tree = [] + element: Optional[_Element] = stonith_el + while element is not None: + resource_tree.append(element) + element = get_parent_resource(element) + if all(not is_resource_disabled(res_el) for res_el in resource_tree): + current_stonith.append(stonith_el) + stonith_left = [ stonith_el for stonith_el in current_stonith diff --git a/pcs_test/tier0/lib/commands/resource/test_resource_enable_disable.py b/pcs_test/tier0/lib/commands/resource/test_resource_enable_disable.py index 840ba6abb..8c290c40f 100644 --- a/pcs_test/tier0/lib/commands/resource/test_resource_enable_disable.py +++ b/pcs_test/tier0/lib/commands/resource/test_resource_enable_disable.py @@ -863,6 +863,192 @@ def test_no_useful_enabled_stonith_removed(self): resource.disable(self.env_assist.get_env(), ["S1", "S3"], False) +class DisableStonithGroupsAndClones(TestCase): + # The point is to ensure that stonith in clones and groups are checked + # correctly. This is achieved by checking that "no stonith would be left" + # error is produced. Testing error overriding and successful cases bring + # little benefits for the effort spent. + def setUp(self): + self.env_assist, self.config = get_env_tools(test_case=self) + + def fixture_config_sbd_calls(self): + node_name_list = ["node-1", "node-2"] + self.config.env.set_known_nodes(node_name_list) + self.config.corosync_conf.load(node_name_list=node_name_list) + self.config.http.sbd.check_sbd( + communication_list=[ + dict( + label=node, + param_list=[("watchdog", ""), ("device_list", "[]")], + output=json.dumps( + dict( + sbd=dict( + installed=True, enabled=False, running=False + ) + ) + ), + ) + for node in node_name_list + ] + ) + + def _assert_no_stonith_left( + self, resources_cib, resources_status, resource_to_disable + ): + self.config.runner.cib.load(resources=resources_cib) + self.fixture_config_sbd_calls() + self.config.runner.pcmk.load_state(resources=resources_status) + + self.env_assist.assert_raise_library_error( + lambda: resource.disable( + self.env_assist.get_env(), [resource_to_disable], False + ) + ) + self.env_assist.assert_reports( + [ + fixture.error( + reports.codes.NO_STONITH_MEANS_WOULD_BE_LEFT, + force_code=reports.codes.FORCE, + ) + ] + ) + + def test_disable_group_with_stonith(self): + resources_cib = """ + + + + + + """ + resources_status = """ + + + + + + """ + self._assert_no_stonith_left(resources_cib, resources_status, "G") + + def test_disable_stonith_while_other_stonith_in_disabled_group(self): + resources_cib = """ + + + + + + + + + + """ + resources_status = """ + + + + + + + """ + self._assert_no_stonith_left(resources_cib, resources_status, "S2") + + def test_disable_clone_with_stonith(self): + resources_cib = """ + + + + + + """ + resources_status = """ + + + + + + """ + self._assert_no_stonith_left(resources_cib, resources_status, "C") + + def test_disable_stonith_while_other_stonith_in_disabled_clone(self): + resources_cib = """ + + + + + + + + + + """ + resources_status = """ + + + + + + + """ + self._assert_no_stonith_left(resources_cib, resources_status, "S2") + + def test_disable_cloned_group_with_stonith(self): + resources_cib = """ + + + + + + + + """ + resources_status = """ + + + + + + + + """ + self._assert_no_stonith_left(resources_cib, resources_status, "C") + + def test_disable_stonith_while_other_stonith_in_disabled_cloned_group(self): + resources_cib = """ + + + + + + + + + + + + """ + resources_status = """ + + + + + + + + + """ + self._assert_no_stonith_left(resources_cib, resources_status, "S2") + + @mock.patch.object( settings, "pacemaker_api_result_schema", rc("pcmk_api_rng/api-result.rng") ) From 1645d64587c277d8d0c5d7e986eeecda84027f9c Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Thu, 6 Mar 2025 16:59:46 +0100 Subject: [PATCH 124/227] add type hints --- mypy.ini | 27 +++++ pcs/alert.py | 14 +-- pcs/lib/cib/alert.py | 91 ++++++++++------- pcs/lib/cib/nvpair.py | 4 +- pcs/lib/cib/nvpair_multi.py | 5 +- pcs/lib/cib/resource/common.py | 5 +- pcs/lib/cib/sections.py | 6 +- pcs/lib/cib/tag.py | 10 +- pcs/lib/cib/tools.py | 137 +++++++++++++++----------- pcs/lib/commands/alert.py | 95 +++++++++++------- pcs_test/tier0/lib/cib/test_alert.py | 6 +- pcs_test/tier0/lib/cib/test_nvpair.py | 2 +- 12 files changed, 252 insertions(+), 150 deletions(-) diff --git a/mypy.ini b/mypy.ini index e995fc6c0..91e23fe5d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -33,6 +33,12 @@ disallow_untyped_calls = False disallow_untyped_defs = False +[mypy-pcs.alert] +disallow_untyped_defs = True +disallow_untyped_calls = True + + + [mypy-pcs.common.*] disallow_untyped_defs = True disallow_untyped_calls = True @@ -88,12 +94,17 @@ disallow_untyped_calls = True disallow_untyped_defs = True disallow_untyped_calls = True +[mypy-pcs.lib.cib.alert] +disallow_untyped_defs = True +disallow_untyped_calls = True + [mypy-pcs.lib.cib.fencing_topology] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.lib.cib.nvpair_multi] disallow_untyped_defs = True +disallow_untyped_calls = True [mypy-pcs.lib.cib.remove_elements] disallow_untyped_defs = True @@ -104,6 +115,7 @@ disallow_untyped_calls = True [mypy-pcs.lib.cib.resource.common] disallow_untyped_defs = True +disallow_untyped_calls = True [mypy-pcs.lib.cib.resource.group] disallow_untyped_defs = True @@ -132,8 +144,21 @@ disallow_untyped_calls = True disallow_untyped_defs = True disallow_untyped_calls = True +[mypy-pcs.lib.cib.sections] +disallow_untyped_defs = True +disallow_untyped_calls = True + [mypy-pcs.lib.cib.tag] disallow_untyped_defs = True +disallow_untyped_calls = True + +[mypy-pcs.lib.cib.tools] +disallow_untyped_defs = True +disallow_untyped_calls = True + +[mypy-pcs.lib.commands.alert] +disallow_untyped_defs = True +disallow_untyped_calls = True [mypy-pcs.lib.commands.booth] disallow_untyped_defs = True @@ -143,6 +168,7 @@ disallow_untyped_defs = True [mypy-pcs.lib.commands.cib_options] disallow_untyped_defs = True +disallow_untyped_calls = True [mypy-pcs.lib.commands.cluster_property] disallow_untyped_defs = True @@ -163,6 +189,7 @@ disallow_untyped_defs = True [mypy-pcs.lib.commands.tag] disallow_untyped_defs = True +disallow_untyped_calls = True [mypy-pcs.lib.corosync.*] disallow_untyped_defs = True diff --git a/pcs/alert.py b/pcs/alert.py index 3c83e3e78..14b793978 100644 --- a/pcs/alert.py +++ b/pcs/alert.py @@ -1,5 +1,5 @@ import json -from typing import Any +from typing import Any, Iterable, Mapping from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.parse_args import ( @@ -147,7 +147,7 @@ def recipient_remove(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: lib.alert.remove_recipient(argv) -def _nvset_to_str(nvset_obj): +def _nvset_to_str(nvset_obj: Iterable[Mapping[str, str]]) -> str: # TODO duplicite to pcs.resource._nvpairs_strings key_val = { nvpair_obj["name"]: nvpair_obj["value"] for nvpair_obj in nvset_obj @@ -159,7 +159,7 @@ def _nvset_to_str(nvset_obj): return " ".join(output) -def __description_attributes_to_str(obj): +def __description_attributes_to_str(obj: Mapping[str, Any]) -> list[str]: output = [] if obj.get("description"): output.append(f"Description: {obj['description']}") @@ -172,11 +172,11 @@ def __description_attributes_to_str(obj): return output -def _alert_to_str(alert): - content = [] +def _alert_to_str(alert: Mapping[str, Any]) -> list[str]: + content: list[str] = [] content.extend(__description_attributes_to_str(alert)) - recipients = [] + recipients: list[str] = [] for recipient in alert.get("recipient_list", []): recipients.extend(_recipient_to_str(recipient)) @@ -187,7 +187,7 @@ def _alert_to_str(alert): return [f"Alert: {alert['id']} (path={alert['path']})"] + indent(content, 1) -def _recipient_to_str(recipient): +def _recipient_to_str(recipient: Mapping[str, Any]) -> list[str]: return [ f"Recipient: {recipient['id']} (value={recipient['value']})" ] + indent(__description_attributes_to_str(recipient), 1) diff --git a/pcs/lib/cib/alert.py b/pcs/lib/cib/alert.py index b18048e80..ddcb42f87 100644 --- a/pcs/lib/cib/alert.py +++ b/pcs/lib/cib/alert.py @@ -1,8 +1,4 @@ -from functools import partial -from typing import ( - Optional, - cast, -) +from typing import Any, Optional, cast from lxml import etree from lxml.etree import _Element @@ -10,38 +6,52 @@ from pcs.common import reports from pcs.lib.cib.nvpair import get_nvset from pcs.lib.cib.tools import ( + ElementSearcher, IdProvider, create_subelement_id, - find_element_by_tag_and_id, get_alerts, ) +from pcs.lib.errors import LibraryError from pcs.lib.pacemaker.values import validate_id_reports -from pcs.lib.xml_tools import get_sub_element +from pcs.lib.xml_tools import ( + get_sub_element, + remove_one_element, + update_attribute_remove_empty, +) TAG_ALERT = "alert" TAG_RECIPIENT = "recipient" -find_alert = partial(find_element_by_tag_and_id, TAG_ALERT) -find_recipient = partial(find_element_by_tag_and_id, TAG_RECIPIENT) + +def find_alert(context_el: _Element, alert_id: str) -> _Element: + searcher = ElementSearcher(TAG_ALERT, alert_id, context_el) + found_element = searcher.get_element() + if found_element is not None: + return found_element + raise LibraryError(*searcher.get_errors()) + + +def find_recipient(context_el: _Element, recipient_id: str) -> _Element: + searcher = ElementSearcher(TAG_RECIPIENT, recipient_id, context_el) + found_element = searcher.get_element() + if found_element is not None: + return found_element + raise LibraryError(*searcher.get_errors()) def _update_optional_attribute( element: _Element, attribute: str, value: Optional[str] ) -> None: """ - Update optional attribute of element. Remove existing element if value - is empty. + Set value of an optional attribute, remove the attribute on empty value - element -- parent element of specified attribute + element -- element to be updated attribute -- attribute to be updated - value -- new value + value -- new value of the attribute """ if value is None: return - if value: - element.set(attribute, value) - elif attribute in element.attrib: - del element.attrib[attribute] + update_attribute_remove_empty(element, attribute, value) def _validate_recipient_value_is_unique( @@ -82,7 +92,8 @@ def _validate_recipient_value_is_unique( def validate_create_alert( id_provider: IdProvider, - path: str, + # should be str, see lib.commands.alert.create_alert + path: Optional[str], alert_id: Optional[str] = None, ) -> reports.ReportItemList: """ @@ -115,7 +126,7 @@ def create_alert( description: Optional[str] = None, ) -> _Element: """ - Create new alert element. Returns newly created element. + Create new alert element and return it tree -- cib etree node id_provider -- elements' ids generator @@ -135,7 +146,12 @@ def create_alert( return alert_el -def update_alert(tree, alert_id, path, description=None): +def update_alert( + tree: _Element, + alert_id: str, + path: Optional[str], + description: Optional[str] = None, +) -> _Element: """ Update existing alert. Return updated alert element. Raises LibraryError if alert with specified id doesn't exist. @@ -153,7 +169,7 @@ def update_alert(tree, alert_id, path, description=None): return alert -def remove_alert(tree, alert_id): +def remove_alert(tree: _Element, alert_id: str) -> None: """ Remove alert with specified id. Raises LibraryError if alert with specified id doesn't exist. @@ -161,14 +177,14 @@ def remove_alert(tree, alert_id): tree -- cib etree node alert_id -- id of alert which should be removed """ - alert = find_alert(get_alerts(tree), alert_id) - alert.getparent().remove(alert) + remove_one_element(find_alert(get_alerts(tree), alert_id)) def validate_add_recipient( id_provider: IdProvider, alert_el: _Element, - recipient_value: str, + # should be str, see lib.commands.alert.add_recipient + recipient_value: Optional[str], recipient_id: Optional[str] = None, allow_same_value: bool = False, ) -> reports.ReportItemList: @@ -189,6 +205,15 @@ def validate_add_recipient( reports.messages.RequiredOptionsAreMissing(["value"]) ) ) + else: + report_list.extend( + _validate_recipient_value_is_unique( + alert_el, + recipient_value, + recipient_id, + allow_duplicity=allow_same_value, + ) + ) if recipient_id: report_list.extend( @@ -196,15 +221,6 @@ def validate_add_recipient( ) report_list.extend(id_provider.book_ids(recipient_id)) - report_list.extend( - _validate_recipient_value_is_unique( - alert_el, - recipient_value, - recipient_id, - allow_duplicity=allow_same_value, - ) - ) - return report_list @@ -291,7 +307,7 @@ def update_recipient( return recipient_el -def remove_recipient(tree, recipient_id): +def remove_recipient(tree: _Element, recipient_id: str) -> None: """ Remove specified recipient. Raises LibraryError if recipient doesn't exist. @@ -299,11 +315,10 @@ def remove_recipient(tree, recipient_id): tree -- cib etree node recipient_id -- id of recipient to be removed """ - recipient = find_recipient(get_alerts(tree), recipient_id) - recipient.getparent().remove(recipient) + remove_one_element(find_recipient(get_alerts(tree), recipient_id)) -def get_all_recipients(alert): +def get_all_recipients(alert: _Element) -> list[dict[str, Any]]: """ Returns list of all recipient of specified alert. Format: [ @@ -334,7 +349,7 @@ def get_all_recipients(alert): ] -def get_all_alerts(tree): +def get_all_alerts(tree: _Element) -> list[dict[str, Any]]: """ Returns list of all alerts specified in tree. Format: [ diff --git a/pcs/lib/cib/nvpair.py b/pcs/lib/cib/nvpair.py index a5d7e6490..2ba770fc8 100644 --- a/pcs/lib/cib/nvpair.py +++ b/pcs/lib/cib/nvpair.py @@ -148,7 +148,7 @@ def update_nvset(nvset_element, nvpair_dict, id_provider): set_nvpair_in_nvset(nvset_element, name, value, id_provider) -def get_nvset(nvset): +def get_nvset(nvset: _Element) -> list[dict[str, Optional[str]]]: """ Returns nvset element as list of nvpairs with format: [ @@ -166,7 +166,7 @@ def get_nvset(nvset): { "id": nvpair.get("id"), "name": nvpair.get("name"), - "value": nvpair.get("value", ""), + "value": nvpair.get("value"), } for nvpair in nvset.findall("./nvpair") ] diff --git a/pcs/lib/cib/nvpair_multi.py b/pcs/lib/cib/nvpair_multi.py index efb78d75e..be262154c 100644 --- a/pcs/lib/cib/nvpair_multi.py +++ b/pcs/lib/cib/nvpair_multi.py @@ -119,8 +119,9 @@ def find_nvsets_by_ids( parent_element, element_type_desc="options set", ) - if searcher.element_found(): - element_list.append(searcher.get_element()) + found_element = searcher.get_element() + if found_element is not None: + element_list.append(found_element) else: report_list.extend(searcher.get_errors()) return element_list, report_list diff --git a/pcs/lib/cib/resource/common.py b/pcs/lib/cib/resource/common.py index 46cebff86..cf01834d2 100644 --- a/pcs/lib/cib/resource/common.py +++ b/pcs/lib/cib/resource/common.py @@ -66,8 +66,9 @@ def find_resources( resource_el_list = [] for res_id in resource_ids: searcher = ElementSearcher(resource_tags, res_id, context_element) - if searcher.element_found(): - resource_el_list.append(searcher.get_element()) + found_element = searcher.get_element() + if found_element is not None: + resource_el_list.append(found_element) else: report_list.extend(searcher.get_errors()) return resource_el_list, report_list diff --git a/pcs/lib/cib/sections.py b/pcs/lib/cib/sections.py index dc59e3480..32bbe02e9 100644 --- a/pcs/lib/cib/sections.py +++ b/pcs/lib/cib/sections.py @@ -3,6 +3,8 @@ for getting existing sections from the cib (lxml) tree. """ +from lxml.etree import _Element + from pcs.common import reports from pcs.common.reports.item import ReportItem from pcs.lib.errors import LibraryError @@ -39,7 +41,7 @@ ] -def get(tree, section_name): +def get(tree: _Element, section_name: str) -> _Element: """ Return the element which represents section 'section_name' in the tree. @@ -68,7 +70,7 @@ def get(tree, section_name): raise AssertionError(f"Unknown cib section '{section_name}'") -def exists(tree, section_name): +def exists(tree: _Element, section_name: str) -> bool: if section_name not in __MANDATORY_SECTIONS + __OPTIONAL_SECTIONS: raise AssertionError(f"Unknown cib section '{section_name}'") return tree.find(f".//{section_name}") is not None diff --git a/pcs/lib/cib/tag.py b/pcs/lib/cib/tag.py index 461003e29..3f5bb41da 100644 --- a/pcs/lib/cib/tag.py +++ b/pcs/lib/cib/tag.py @@ -541,8 +541,9 @@ def find_tag_elements_by_ids( report_list: ReportItemList = [] for tag_id in tag_id_list: searcher = ElementSearcher(TAG_TAG, tag_id, tags_section) - if searcher.element_found(): - element_list.append(searcher.get_element()) + found_element = searcher.get_element() + if found_element is not None: + element_list.append(found_element) else: report_list.extend(searcher.get_errors()) @@ -695,8 +696,9 @@ def expand_tag( searcher = ElementSearcher( only_expand_types, element_id, conf_section ) - if searcher.element_found(): - expanded_elements.append(searcher.get_element()) + found_element = searcher.get_element() + if found_element is not None: + expanded_elements.append(found_element) else: expanded_elements.extend( get_configuration_elements_by_id(conf_section, element_id) diff --git a/pcs/lib/cib/tools.py b/pcs/lib/cib/tools.py index f92d934b8..f45c1eee7 100644 --- a/pcs/lib/cib/tools.py +++ b/pcs/lib/cib/tools.py @@ -1,10 +1,8 @@ import contextlib import re from typing import ( - List, - Pattern, - Set, - Tuple, + Optional, + Union, cast, ) @@ -24,7 +22,7 @@ ReportItemList, ) from pcs.common.tools import Version -from pcs.common.types import StringIterable +from pcs.common.types import StringCollection, StringIterable from pcs.lib.cib import sections from pcs.lib.errors import LibraryError from pcs.lib.pacemaker.values import ( @@ -61,13 +59,13 @@ def __init__(self, cib_element: _Element): cib_element -- any element of the xml to check against """ self._cib = get_root(cib_element) - self._booked_ids: Set[str] = set() + self._booked_ids: set[str] = set() def allocate_id(self, proposed_id: str) -> str: """ Generate a new unique id based on the proposal and keep track of it - string proposed_id -- requested id + proposed_id -- requested id """ final_id = find_unique_id(self._cib, proposed_id, self._booked_ids) self._booked_ids.add(final_id) @@ -110,41 +108,48 @@ class ElementSearcher: """ def __init__( - self, tags, element_id, context_element, element_type_desc=None + self, + tags: Union[str, StringIterable], + element_id: str, + context_element: _Element, + element_type_desc: Union[None, str, StringIterable] = None, ): """ - string|iterable tags -- a tag (string) or tags (iterable) to look for - string element_id -- an id to look for - etree.Element context_element -- an element to look in - string|iterable element_type_desc -- element types for reports, tags - if not specified + tags -- a tag (string) or tags (iterable) to look for + element_id -- an id to look for + context_element -- an element to look in + element_type_desc -- element types for reports, tags if not specified """ self._executed = False - self._element = None + self._element: Optional[_Element] = None self._element_id = element_id self._context_element = context_element self._tag_list = [tags] if isinstance(tags, str) else tags self._expected_types = self._prepare_expected_types(element_type_desc) - self._book_errors = None + self._book_errors: Optional[ReportItemList] = None - def _prepare_expected_types(self, element_type_desc): + def _prepare_expected_types( + self, element_type_desc: Union[None, str, StringIterable] + ) -> list[str]: if element_type_desc is None: - return self._tag_list + return list(self._tag_list) if isinstance(element_type_desc, str): return [element_type_desc] - return element_type_desc + return list(element_type_desc) - def element_found(self): + def element_found(self) -> bool: if not self._executed: self._execute() return self._element is not None - def get_element(self): + def get_element(self) -> Optional[_Element]: if not self._executed: self._execute() return self._element - def validate_book_id(self, id_provider, id_description="id"): + def validate_book_id( + self, id_provider: IdProvider, id_description: str = "id" + ) -> bool: """ Book element_id in the id_provider, return True if success """ @@ -158,7 +163,7 @@ def validate_book_id(self, id_provider, id_description="id"): self._book_errors += id_provider.book_ids(self._element_id) return len(self._book_errors) < 1 - def get_errors(self): + def get_errors(self) -> ReportItemList: """ Report why the element has not been found or booking its id failed """ @@ -208,13 +213,16 @@ def get_errors(self): ] return self._book_errors - def _execute(self): + def _execute(self) -> None: self._executed = True for tag in self._tag_list: - element_list = self._context_element.xpath( - ".//*[local-name()=$tag_name and @id=$element_id]", - tag_name=tag, - element_id=self._element_id, + element_list = cast( + list[_Element], + self._context_element.xpath( + ".//*[local-name()=$tag_name and @id=$element_id]", + tag_name=tag, + element_id=self._element_id, + ), ) if element_list: self._element = element_list[0] @@ -223,7 +231,7 @@ def _execute(self): def get_configuration_elements_by_id( tree: _Element, check_id: str -) -> List[_Element]: +) -> list[_Element]: """ Return any configuration elements (not in status section of cib) with value of attribute id specified as 'check_id'; skip any and all elements having id @@ -241,7 +249,7 @@ def get_configuration_elements_by_id( # attribute of the explicit resource. So the value of nvpair named # "remote-node" is considered to be id return cast( - List[_Element], + list[_Element], get_root(tree).xpath( """ ( @@ -299,13 +307,12 @@ def get_element_by_id(cib: _Element, element_id: str) -> _Element: def get_elements_by_ids( cib: _Element, element_ids: StringIterable -) -> Tuple[List[_Element], List[str]]: +) -> tuple[list[_Element], list[str]]: """ - Returns a list of elements from CIB with the given IDs and a list of IDs - that weren't found + Return elements from CIB with the given IDs and IDs that weren't found cib -- the whole cib - element_ids -- iterable with element IDs to look for + element_ids -- element IDs to look for """ found_element_list = [] id_not_found_list = [] @@ -318,9 +325,10 @@ def get_elements_by_ids( # DEPRECATED, use IdProvider instead -def does_id_exist(tree, check_id): +def does_id_exist(tree: _Element, check_id: str) -> bool: """ Checks to see if id exists in the xml dom passed + tree cib -- etree node check_id -- id to check """ @@ -328,9 +336,9 @@ def does_id_exist(tree, check_id): # DEPRECATED, use IdProvider instead -def validate_id_does_not_exist(tree, _id): +def validate_id_does_not_exist(tree: _Element, _id: str) -> None: """ - tree cib etree node + Raise LibraryError if specified id exists in specified dom tree """ if does_id_exist(tree, _id): raise LibraryError( @@ -339,13 +347,18 @@ def validate_id_does_not_exist(tree, _id): # DEPRECATED, use IdProvider instead -def find_unique_id(tree, check_id, reserved_ids=None): +def find_unique_id( + tree: _Element, + check_id: str, + reserved_ids: Optional[StringCollection] = None, +) -> str: """ - Returns check_id if it doesn't exist in the dom, otherwise it adds - an integer to the end of the id and increments it until a unique id is found - etree tree -- cib etree node - string check_id -- id to check - iterable reserved_ids -- ids to think about as already used + Return check_id if it doesn't exist in the dom, otherwise add an integer to + the end of the id and increment it until a unique id is found + + tree -- cib etree node + check_id -- id to check + reserved_ids -- ids to think about as already used """ if not reserved_ids: reserved_ids = set() @@ -359,19 +372,23 @@ def find_unique_id(tree, check_id, reserved_ids=None): # DEPRECATED, use ElementSearcher instead def find_element_by_tag_and_id( - tag, context_element, element_id, none_if_id_unused=False, id_types=None -): + tag: Union[str, StringIterable], + context_element: _Element, + element_id: str, + none_if_id_unused: bool = False, + id_types: Optional[StringIterable] = None, +) -> Optional[_Element]: """ Return element with given tag and element_id under context_element. When element does not exists raises LibraryError or return None if specified in none_if_id_unused. - etree.Element(Tree) context_element is part of tree for element scan - string|list tag is expected tag (or list of tags) of search element - string element_id is id of search element - bool none_if_id_unused if the element is not found then return None if True + tag -- expected tag (or list of tags) of search element + context_element -- part of tree for element scan + element_id -- id of search element + none_if_id_unused -- if the element is not found then return None if True or raise a LibraryError if False - list id_types optional list of descriptions for id / expected types of id + id_types -- optional list of descriptions for id / expected types of id """ searcher = ElementSearcher( tag, element_id, context_element, element_type_desc=id_types @@ -402,30 +419,33 @@ def create_subelement_id( # DEPRECATED # use ElementSearcher, IdProvider or pcs.lib.validate.ValueId instead -def check_new_id_applicable(tree, description, _id): +def check_new_id_applicable(tree: _Element, description: str, _id: str) -> None: validate_id(_id, description) validate_id_does_not_exist(tree, _id) -def get_configuration(tree): +def get_configuration(tree: _Element) -> _Element: """ Return 'configuration' element from tree, raise LibraryError if missing + tree cib etree node """ return sections.get(tree, sections.CONFIGURATION) -def get_acls(tree): +def get_acls(tree: _Element) -> _Element: """ Return 'acls' element from tree, create a new one if missing + tree cib etree node """ return sections.get(tree, sections.ACLS) -def get_alerts(tree): +def get_alerts(tree: _Element) -> _Element: """ Return 'alerts' element from tree, create a new one if missing + tree -- cib etree node """ return sections.get(tree, sections.ALERTS) @@ -434,6 +454,7 @@ def get_alerts(tree): def get_constraints(tree: _Element) -> _Element: """ Return 'constraint' element from tree + tree cib etree node """ return sections.get(tree, sections.CONSTRAINTS) @@ -451,14 +472,16 @@ def get_crm_config(tree: _Element) -> _Element: def get_fencing_topology(tree: _Element) -> _Element: """ Return the 'fencing-topology' element from the tree + tree -- cib etree node """ return sections.get(tree, sections.FENCING_TOPOLOGY) -def get_nodes(tree): +def get_nodes(tree: _Element) -> _Element: """ Return 'nodes' element from the tree + tree cib etree node """ return sections.get(tree, sections.NODES) @@ -473,9 +496,10 @@ def get_resources(tree: _Element) -> _Element: return sections.get(tree, sections.RESOURCES) -def get_status(tree): +def get_status(tree: _Element) -> _Element: """ Return the 'status' element from the tree + tree -- cib etree node """ return get_sub_element(tree, "status") @@ -484,13 +508,14 @@ def get_status(tree): def get_tags(tree: _Element) -> _Element: """ Return 'tags' element from tree, create a new one if missing + tree -- cib etree node """ return sections.get(tree, sections.TAGS) def _get_cib_version( - cib: _ElementTree, attribute: str, regexp: Pattern + cib: _ElementTree, attribute: str, regexp: re.Pattern ) -> Version: version = cib.getroot().get(attribute) if version is None: diff --git a/pcs/lib/commands/alert.py b/pcs/lib/commands/alert.py index 714c7fd9d..e63c7729c 100644 --- a/pcs/lib/commands/alert.py +++ b/pcs/lib/commands/alert.py @@ -1,5 +1,6 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Mapping, Optional +from pcs.common.types import StringIterable from pcs.lib.cib import alert from pcs.lib.cib.nvpair import ( arrange_first_instance_attributes, @@ -17,13 +18,21 @@ def create_alert( + # Path is mandatory, so it should not be optional. However, the current + # code calling this function does not prevent it being None. The idea behind + # that was, that the validation would happen in the lib command. Since + # then, however, the paradigma got changed as we found out that a client + # should actually be responsible for providing all mandatory parameters. + # The interface cannot be simply changed, as backward compatibility must be + # maintained for lib.commands. We still want to change it, but it needs to + # be done in the proper way. lib_env: LibraryEnvironment, - alert_id, - path, - instance_attribute_dict, - meta_attribute_dict, - description=None, -): + alert_id: Optional[str], + path: Optional[str], + instance_attribute_dict: Mapping[str, str], + meta_attribute_dict: Mapping[str, str], + description: Optional[str] = None, +) -> None: """ Create new alert. Raises LibraryError if path is not specified, or any other failure. @@ -44,7 +53,13 @@ def create_alert( if lib_env.report_processor.has_errors: raise LibraryError() - alert_el = alert.create_alert(cib, id_provider, path, alert_id, description) + alert_el = alert.create_alert( + cib, + id_provider, + str(path), # if path were None, validation above would raise + alert_id, + description, + ) arrange_first_instance_attributes( alert_el, instance_attribute_dict, id_provider ) @@ -55,12 +70,12 @@ def create_alert( def update_alert( lib_env: LibraryEnvironment, - alert_id, - path, - instance_attribute_dict, - meta_attribute_dict, - description=None, -): + alert_id: str, + path: Optional[str], + instance_attribute_dict: Mapping[str, str], + meta_attribute_dict: Mapping[str, str], + description: Optional[str] = None, +) -> None: """ Update existing alert with specified id. @@ -84,7 +99,9 @@ def update_alert( lib_env.push_cib() -def remove_alert(lib_env: LibraryEnvironment, alert_id_list): +def remove_alert( + lib_env: LibraryEnvironment, alert_id_list: StringIterable +) -> None: """ Remove alerts with specified ids. @@ -105,15 +122,24 @@ def remove_alert(lib_env: LibraryEnvironment, alert_id_list): def add_recipient( + # Recipient value is mandatory, so it should not be optional. However, the + # current code calling this function does not prevent it being None. The + # idea behind that was, that the validation would happen in the lib + # command. Since then, however, the paradigma got changed as we found out + # that a client should actually be responsible for providing all mandatory + # parameters. + # The interface cannot be simply changed, as backward compatibility must be + # maintained for lib.commands. We still want to change it, but it needs to + # be done in the proper way. lib_env: LibraryEnvironment, - alert_id, - recipient_value, - instance_attribute_dict, - meta_attribute_dict, - recipient_id=None, - description=None, - allow_same_value=False, -): + alert_id: str, + recipient_value: Optional[str], + instance_attribute_dict: Mapping[str, str], + meta_attribute_dict: Mapping[str, str], + recipient_id: Optional[str] = None, + description: Optional[str] = None, + allow_same_value: bool = False, +) -> None: """ Add new recipient to alert witch id alert_id. @@ -145,7 +171,8 @@ def add_recipient( recipient_el = alert.add_recipient( id_provider, alert_el, - recipient_value, + # if recipient_value were None, validation above would raise + str(recipient_value), recipient_id, description=description, ) @@ -161,13 +188,13 @@ def add_recipient( def update_recipient( lib_env: LibraryEnvironment, - recipient_id, - instance_attribute_dict, - meta_attribute_dict, - recipient_value=None, - description=None, - allow_same_value=False, -): + recipient_id: str, + instance_attribute_dict: Mapping[str, str], + meta_attribute_dict: Mapping[str, str], + recipient_value: Optional[str] = None, + description: Optional[str] = None, + allow_same_value: bool = False, +) -> None: """ Update existing recipient. @@ -208,7 +235,9 @@ def update_recipient( lib_env.push_cib() -def remove_recipient(lib_env: LibraryEnvironment, recipient_id_list): +def remove_recipient( + lib_env: LibraryEnvironment, recipient_id_list: StringIterable +) -> None: """ Remove specified recipients. @@ -227,7 +256,7 @@ def remove_recipient(lib_env: LibraryEnvironment, recipient_id_list): lib_env.push_cib() -def get_all_alerts(lib_env: LibraryEnvironment): +def get_all_alerts(lib_env: LibraryEnvironment) -> list[dict[str, Any]]: """ Returns list of all alerts. See docs of pcs.lib.cib.alert.get_all_alerts for description of data format. diff --git a/pcs_test/tier0/lib/cib/test_alert.py b/pcs_test/tier0/lib/cib/test_alert.py index 393363894..50a0de88c 100644 --- a/pcs_test/tier0/lib/cib/test_alert.py +++ b/pcs_test/tier0/lib/cib/test_alert.py @@ -920,7 +920,7 @@ def test_success(self): }, ], "meta_attributes": [ - {"id": "nvset-name3", "name": "name3", "value": ""} + {"id": "nvset-name3", "name": "name3", "value": None} ], }, { @@ -1011,7 +1011,7 @@ def test_success(self): { "id": "meta_attributes-name3", "name": "name3", - "value": "", + "value": None, } ], }, @@ -1041,7 +1041,7 @@ def test_success(self): }, ], "meta_attributes": [ - {"id": "alert1-name3", "name": "name3", "value": ""} + {"id": "alert1-name3", "name": "name3", "value": None} ], "recipient_list": [], }, diff --git a/pcs_test/tier0/lib/cib/test_nvpair.py b/pcs_test/tier0/lib/cib/test_nvpair.py index 98f37b7e2..19056e9a6 100644 --- a/pcs_test/tier0/lib/cib/test_nvpair.py +++ b/pcs_test/tier0/lib/cib/test_nvpair.py @@ -323,7 +323,7 @@ def test_success(self): [ {"id": "nvset-name1", "name": "name1", "value": "value1"}, {"id": "nvset-name2", "name": "name2", "value": "value2"}, - {"id": "nvset-name3", "name": "name3", "value": ""}, + {"id": "nvset-name3", "name": "name3", "value": None}, ], nvpair.get_nvset(nvset), ) From e668e378677442546748c7f187cf45f8b2a37b41 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Fri, 28 Mar 2025 17:01:23 +0100 Subject: [PATCH 125/227] export alerts as json and commands --- CHANGELOG.md | 2 + pcs/Makefile.am | 4 + pcs/alert.py | 31 +- pcs/cli/alert/__init__.py | 0 pcs/cli/alert/command.py | 42 ++ pcs/cli/alert/output.py | 141 ++++++ pcs/cli/common/lib_wrapper.py | 1 + pcs/cli/routing/alert.py | 3 +- pcs/common/pacemaker/alert.py | 45 ++ pcs/config.py | 5 +- .../async_tasks/worker/command_mapping.py | 4 + pcs/lib/cib/alert.py | 90 +++- pcs/lib/commands/alert.py | 14 +- pcs/pcs.8.in | 4 +- pcs/usage.py | 8 +- pcs_test/Makefile.am | 3 + pcs_test/resources/cib-all.xml | 30 ++ pcs_test/resources/commands-alert | 7 + pcs_test/tier0/cli/alert/__init__.py | 0 pcs_test/tier0/cli/alert/test_output.py | 223 +++++++++ pcs_test/tier0/lib/cib/test_alert.py | 461 ++++++++++++++---- pcs_test/tier0/lib/commands/test_alert.py | 154 +++++- pcs_test/tier1/legacy/test_alert.py | 420 +++++++++------- pcs_test/tier1/test_alert.py | 300 ++++++++++++ pcsd/capabilities.xml.in | 12 + 25 files changed, 1721 insertions(+), 283 deletions(-) create mode 100644 pcs/cli/alert/__init__.py create mode 100644 pcs/cli/alert/command.py create mode 100644 pcs/cli/alert/output.py create mode 100644 pcs/common/pacemaker/alert.py create mode 100644 pcs_test/resources/commands-alert create mode 100644 pcs_test/tier0/cli/alert/__init__.py create mode 100644 pcs_test/tier0/cli/alert/test_output.py create mode 100644 pcs_test/tier1/test_alert.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 79d507ff0..2268c7056 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Command `pcs cluster rename` for changing cluster name ([RHEL-76055]) - Prevent removing or disabling stonith devices or disabling SBD if the cluster would be left with disabled SBD and no stonith devices ([RHEL-76170]) +- Support for exporting alerts in `json` and `cmd` formats ([RHEL-76153]) ### Fixed - Command `pcs resource restart` allows restarting bundle instances (broken @@ -23,6 +24,7 @@ [RHEL-76055]: https://issues.redhat.com/browse/RHEL-76055 [RHEL-76059]: https://issues.redhat.com/browse/RHEL-76059 +[RHEL-76153]: https://issues.redhat.com/browse/RHEL-76153 [RHEL-76170]: https://issues.redhat.com/browse/RHEL-76170 [RHEL-76177]: https://issues.redhat.com/browse/RHEL-76177 [RHEL-79055]: https://issues.redhat.com/browse/RHEL-79055 diff --git a/pcs/Makefile.am b/pcs/Makefile.am index bf7dfb5b4..c41d7d0d9 100644 --- a/pcs/Makefile.am +++ b/pcs/Makefile.am @@ -20,6 +20,9 @@ EXTRA_DIST = \ acl.py \ alert.py \ app.py \ + cli/alert/__init__.py \ + cli/alert/command.py \ + cli/alert/output.py \ cli/booth/command.py \ cli/booth/env.py \ cli/booth/__init__.py \ @@ -132,6 +135,7 @@ EXTRA_DIST = \ common/interface/__init__.py \ common/node_communicator.py \ common/pacemaker/__init__.py \ + common/pacemaker/alert.py \ common/pacemaker/cluster_property.py \ common/pacemaker/constraint/__init__.py \ common/pacemaker/constraint/all.py \ diff --git a/pcs/alert.py b/pcs/alert.py index 14b793978..1c3d207b5 100644 --- a/pcs/alert.py +++ b/pcs/alert.py @@ -1,7 +1,9 @@ import json from typing import Any, Iterable, Mapping +from pcs.cli.alert.output import config_dto_to_lines from pcs.cli.common.errors import CmdLineInputError +from pcs.cli.common.output import lines_to_str from pcs.cli.common.parse_args import ( Argv, InputModifiers, @@ -159,7 +161,7 @@ def _nvset_to_str(nvset_obj: Iterable[Mapping[str, str]]) -> str: return " ".join(output) -def __description_attributes_to_str(obj: Mapping[str, Any]) -> list[str]: +def _old__description_attributes_to_str(obj: Mapping[str, Any]) -> list[str]: output = [] if obj.get("description"): output.append(f"Description: {obj['description']}") @@ -172,13 +174,13 @@ def __description_attributes_to_str(obj: Mapping[str, Any]) -> list[str]: return output -def _alert_to_str(alert: Mapping[str, Any]) -> list[str]: +def _old_alert_to_str(alert: Mapping[str, Any]) -> list[str]: content: list[str] = [] - content.extend(__description_attributes_to_str(alert)) + content.extend(_old__description_attributes_to_str(alert)) recipients: list[str] = [] for recipient in alert.get("recipient_list", []): - recipients.extend(_recipient_to_str(recipient)) + recipients.extend(_old_recipient_to_str(recipient)) if recipients: content.append("Recipients:") @@ -187,10 +189,10 @@ def _alert_to_str(alert: Mapping[str, Any]) -> list[str]: return [f"Alert: {alert['id']} (path={alert['path']})"] + indent(content, 1) -def _recipient_to_str(recipient: Mapping[str, Any]) -> list[str]: +def _old_recipient_to_str(recipient: Mapping[str, Any]) -> list[str]: return [ f"Recipient: {recipient['id']} (value={recipient['value']})" - ] + indent(__description_attributes_to_str(recipient), 1) + ] + indent(_old__description_attributes_to_str(recipient), 1) def print_alert_show(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: @@ -198,10 +200,17 @@ def print_alert_show(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: "This command is deprecated and will be removed. " "Please use 'pcs alert config' instead." ) - return print_alert_config(lib, argv, modifiers) + modifiers.ensure_only_supported("-f") + if argv: + raise CmdLineInputError() + result_text = lines_to_str(config_dto_to_lines(lib.alert.get_config_dto())) + if result_text: + print(result_text) -def print_alert_config(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: +def old_print_alert_config( + lib: Any, argv: Argv, modifiers: InputModifiers +) -> None: """ Options: * -f - CIB file (in lib wrapper) @@ -209,18 +218,18 @@ def print_alert_config(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: modifiers.ensure_only_supported("-f") if argv: raise CmdLineInputError() - lines = alert_config_lines(lib) + lines = old_alert_config_lines(lib) if lines: print("\n".join(lines)) -def alert_config_lines(lib: Any) -> list[str]: +def old_alert_config_lines(lib: Any) -> list[str]: lines = [] alert_list = lib.alert.get_all_alerts() if alert_list: lines.append("Alerts:") for alert in alert_list: - lines.extend(indent(_alert_to_str(alert), 1)) + lines.extend(indent(_old_alert_to_str(alert), 1)) return lines diff --git a/pcs/cli/alert/__init__.py b/pcs/cli/alert/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pcs/cli/alert/command.py b/pcs/cli/alert/command.py new file mode 100644 index 000000000..546825789 --- /dev/null +++ b/pcs/cli/alert/command.py @@ -0,0 +1,42 @@ +import json +from typing import Any + +from pcs.cli.common.errors import CmdLineInputError +from pcs.cli.common.output import lines_to_str +from pcs.cli.common.parse_args import ( + OUTPUT_FORMAT_VALUE_CMD, + OUTPUT_FORMAT_VALUE_JSON, + Argv, + InputModifiers, +) +from pcs.common.interface.dto import to_dict + +from .output import config_dto_to_cmd, config_dto_to_lines + + +def alert_config(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: + """ + Options: + * -f - CIB file + * --output-format - supported formats: text, cmd, json + """ + modifiers.ensure_only_supported("-f", output_format_supported=True) + output_format = modifiers.get_output_format() + if argv: + raise CmdLineInputError + + config_dto = lib.alert.get_config_dto() + + if output_format == OUTPUT_FORMAT_VALUE_JSON: + print(json.dumps(to_dict(config_dto), indent=2)) + return + + if output_format == OUTPUT_FORMAT_VALUE_CMD: + result_cmd = config_dto_to_cmd(config_dto) + if result_cmd: + print(";\n".join(result_cmd)) + return + + result_text = lines_to_str(config_dto_to_lines(config_dto)) + if result_text: + print(result_text) diff --git a/pcs/cli/alert/output.py b/pcs/cli/alert/output.py new file mode 100644 index 000000000..5ee55351e --- /dev/null +++ b/pcs/cli/alert/output.py @@ -0,0 +1,141 @@ +import shlex +from typing import Optional, Sequence, Union + +from pcs.cli.common.output import ( + INDENT_STEP, + pairs_to_cmd, +) +from pcs.cli.nvset import nvset_dto_to_lines +from pcs.common.pacemaker.alert import ( + CibAlertDto, + CibAlertListDto, + CibAlertRecipientDto, + CibAlertSelectDto, +) +from pcs.common.pacemaker.nvset import CibNvsetDto +from pcs.common.str_tools import ( + format_list, + format_optional, + indent, +) + + +def _description_to_lines(desc: Optional[str]) -> list[str]: + return [f"Description: {desc}"] if desc else [] + + +def _nvsets_to_lines(label: str, nvsets: Sequence[CibNvsetDto]) -> list[str]: + if nvsets and nvsets[0].nvpairs: + return nvset_dto_to_lines(nvset=nvsets[0], nvset_label=label) + return [] + + +def _recipient_dto_to_lines(recipient_dto: CibAlertRecipientDto) -> list[str]: + lines = ( + _description_to_lines(recipient_dto.description) + + [f"Value: {recipient_dto.value}"] + + _nvsets_to_lines("Attributes", recipient_dto.instance_attributes) + + _nvsets_to_lines("Meta Attributes", recipient_dto.meta_attributes) + ) + return [f"Recipient: {recipient_dto.id}"] + indent( + lines, indent_step=INDENT_STEP + ) + + +def _recipients_to_lines( + recipient_dto_list: Sequence[CibAlertRecipientDto], +) -> list[str]: + if not recipient_dto_list: + return [] + lines = [] + for recipient_dto in recipient_dto_list: + lines.extend(_recipient_dto_to_lines(recipient_dto)) + return ["Recipients:"] + indent(lines, indent_step=INDENT_STEP) + + +def _select_dto_to_lines(select_dto: Optional[CibAlertSelectDto]) -> list[str]: + if not select_dto: + return [] + lines = [] + if select_dto.nodes: + lines.append("nodes") + if select_dto.fencing: + lines.append("fencing") + if select_dto.resources: + lines.append("resources") + if select_dto.attributes: + attr_names = format_list( + attr.name for attr in select_dto.attributes_select + ) + lines.append("attributes" + format_optional(attr_names, ": {}")) + return ["Receives:"] + indent(lines, indent_step=INDENT_STEP) + + +def alert_dto_to_lines(alert_dto: CibAlertDto) -> list[str]: + lines = ( + _description_to_lines(alert_dto.description) + + [f"Path: {alert_dto.path}"] + + _recipients_to_lines(alert_dto.recipients) + + _select_dto_to_lines(alert_dto.select) + + _nvsets_to_lines("Attributes", alert_dto.instance_attributes) + + _nvsets_to_lines("Meta Attributes", alert_dto.meta_attributes) + ) + return [f"Alert: {alert_dto.id}"] + indent(lines, indent_step=INDENT_STEP) + + +def config_dto_to_lines(config_dto: CibAlertListDto) -> list[str]: + result = [] + for alert_dto in config_dto.alerts: + result.extend(alert_dto_to_lines(alert_dto)) + return result + + +def config_dto_to_cmd(config_dto: CibAlertListDto) -> list[str]: + commands = [] + for alert_dto in config_dto.alerts: + # alert + alert_parts = [ + "pcs -- alert create path={path} id={id}".format( + path=shlex.quote(alert_dto.path), id=shlex.quote(alert_dto.id) + ) + ] + _desc_instance_meta_to_cmd(alert_dto) + # TODO export select, once it is supported by pcs + commands.append(" ".join(alert_parts)) + # recipients + for recipient_dto in alert_dto.recipients: + recipient_parts = [ + "pcs -- alert recipient add {alert_id} value={value} id={id}".format( + alert_id=shlex.quote(alert_dto.id), + value=shlex.quote(recipient_dto.value), + id=shlex.quote(recipient_dto.id), + ) + ] + _desc_instance_meta_to_cmd(recipient_dto) + commands.append(" ".join(recipient_parts)) + return commands + + +def _nvset_to_cmd( + label: Optional[str], + nvsets: Sequence[CibNvsetDto], +) -> list[str]: + if nvsets and nvsets[0].nvpairs: + options = pairs_to_cmd( + (nvpair.name, nvpair.value) for nvpair in nvsets[0].nvpairs + ) + if label: + options = f"{label} {options}" + return [options] + return [] + + +def _desc_instance_meta_to_cmd( + dto: Union[CibAlertDto, CibAlertRecipientDto], +) -> list[str]: + parts = [] + if dto.description: + parts.append( + "description={desc}".format(desc=shlex.quote(dto.description)) + ) + parts.extend(_nvset_to_cmd("options", dto.instance_attributes)) + parts.extend(_nvset_to_cmd("meta", dto.meta_attributes)) + return parts diff --git a/pcs/cli/common/lib_wrapper.py b/pcs/cli/common/lib_wrapper.py index ef2f389a2..e2864b728 100644 --- a/pcs/cli/common/lib_wrapper.py +++ b/pcs/cli/common/lib_wrapper.py @@ -139,6 +139,7 @@ def load_module(env, middleware_factory, name): # noqa: PLR0911, PLR0912 "update_recipient": alert.update_recipient, "remove_recipient": alert.remove_recipient, "get_all_alerts": alert.get_all_alerts, + "get_config_dto": alert.get_config_dto, }, ) diff --git a/pcs/cli/routing/alert.py b/pcs/cli/routing/alert.py index f93f3d72b..f6ca71d2e 100644 --- a/pcs/cli/routing/alert.py +++ b/pcs/cli/routing/alert.py @@ -2,6 +2,7 @@ alert, usage, ) +from pcs.cli.alert import command as alert_command from pcs.cli.common.routing import create_router alert_cmd = create_router( @@ -11,7 +12,7 @@ "update": alert.alert_update, "delete": alert.alert_remove, "remove": alert.alert_remove, - "config": alert.print_alert_config, + "config": alert_command.alert_config, # TODO remove, deprecated command # replaced with 'config' "show": alert.print_alert_show, diff --git a/pcs/common/pacemaker/alert.py b/pcs/common/pacemaker/alert.py new file mode 100644 index 000000000..7ae9929be --- /dev/null +++ b/pcs/common/pacemaker/alert.py @@ -0,0 +1,45 @@ +from dataclasses import dataclass +from typing import Optional, Sequence + +from pcs.common.interface.dto import DataTransferObject +from pcs.common.pacemaker.nvset import CibNvsetDto + + +@dataclass(frozen=True) +class CibAlertRecipientDto(DataTransferObject): + id: str + value: str + description: Optional[str] + meta_attributes: Sequence[CibNvsetDto] + instance_attributes: Sequence[CibNvsetDto] + + +@dataclass(frozen=True) +class CibAlertSelectAttributeDto(DataTransferObject): + id: str + name: str + + +@dataclass(frozen=True) +class CibAlertSelectDto(DataTransferObject): + nodes: bool + fencing: bool + resources: bool + attributes: bool + attributes_select: Sequence[CibAlertSelectAttributeDto] + + +@dataclass(frozen=True) +class CibAlertDto(DataTransferObject): + id: str + path: str + description: Optional[str] + recipients: Sequence[CibAlertRecipientDto] + select: Optional[CibAlertSelectDto] + meta_attributes: Sequence[CibNvsetDto] + instance_attributes: Sequence[CibNvsetDto] + + +@dataclass(frozen=True) +class CibAlertListDto(DataTransferObject): + alerts: Sequence[CibAlertDto] diff --git a/pcs/config.py b/pcs/config.py index bf5beb710..fac5cc2f3 100644 --- a/pcs/config.py +++ b/pcs/config.py @@ -16,7 +16,6 @@ from xml.dom.minidom import parse from pcs import ( - alert, cluster, quorum, settings, @@ -24,6 +23,7 @@ usage, utils, ) +from pcs.cli.alert.output import config_dto_to_lines as alerts_to_lines from pcs.cli.cluster_property.output import ( PropertyConfigurationFacade, properties_to_text, @@ -175,10 +175,11 @@ def _config_show_cib_lines(lib, properties_facade=None): # noqa: PLR0912, PLR09 all_lines.append("") all_lines.extend(constraints_lines) - alert_lines = alert.alert_config_lines(lib) + alert_lines = indent(alerts_to_lines(lib.alert.get_config_dto())) if alert_lines: if all_lines: all_lines.append("") + all_lines.append("Alerts:") all_lines.extend(alert_lines) resources_defaults_lines = indent( diff --git a/pcs/daemon/async_tasks/worker/command_mapping.py b/pcs/daemon/async_tasks/worker/command_mapping.py index 394b341bc..bab46af50 100644 --- a/pcs/daemon/async_tasks/worker/command_mapping.py +++ b/pcs/daemon/async_tasks/worker/command_mapping.py @@ -93,6 +93,10 @@ class _Cmd: cmd=alert.create_alert, required_permission=p.WRITE, ), + "alert.get_config_dto": _Cmd( + cmd=alert.get_config_dto, + required_permission=p.READ, + ), "alert.remove_alert": _Cmd( cmd=alert.remove_alert, required_permission=p.WRITE, diff --git a/pcs/lib/cib/alert.py b/pcs/lib/cib/alert.py index ddcb42f87..9d72cacae 100644 --- a/pcs/lib/cib/alert.py +++ b/pcs/lib/cib/alert.py @@ -4,6 +4,13 @@ from lxml.etree import _Element from pcs.common import reports +from pcs.common.pacemaker.alert import ( + CibAlertDto, + CibAlertRecipientDto, + CibAlertSelectAttributeDto, + CibAlertSelectDto, +) +from pcs.lib.cib import nvpair_multi, rule from pcs.lib.cib.nvpair import get_nvset from pcs.lib.cib.tools import ( ElementSearcher, @@ -23,6 +30,10 @@ TAG_RECIPIENT = "recipient" +def get_all_alert_elements(tree: _Element) -> list[_Element]: + return tree.findall(TAG_ALERT) + + def find_alert(context_el: _Element, alert_id: str) -> _Element: searcher = ElementSearcher(TAG_ALERT, alert_id, context_el) found_element = searcher.get_element() @@ -318,7 +329,79 @@ def remove_recipient(tree: _Element, recipient_id: str) -> None: remove_one_element(find_recipient(get_alerts(tree), recipient_id)) -def get_all_recipients(alert: _Element) -> list[dict[str, Any]]: +def _recipient_el_to_dto( + recipient_el: _Element, + rule_eval: Optional[rule.RuleInEffectEval] = None, +) -> CibAlertRecipientDto: + if rule_eval is None: + rule_eval = rule.RuleInEffectEvalDummy() + return CibAlertRecipientDto( + id=str(recipient_el.attrib["id"]), + value=str(recipient_el.attrib["value"]), + description=recipient_el.get("description"), + meta_attributes=[ + nvpair_multi.nvset_element_to_dto(nvset, rule_eval) + for nvset in nvpair_multi.find_nvsets( + recipient_el, nvpair_multi.NVSET_META + ) + ], + instance_attributes=[ + nvpair_multi.nvset_element_to_dto(nvset, rule_eval) + for nvset in nvpair_multi.find_nvsets( + recipient_el, nvpair_multi.NVSET_INSTANCE + ) + ], + ) + + +def _select_el_to_dto(select_el: _Element) -> CibAlertSelectDto: + return CibAlertSelectDto( + nodes=(select_el.find("select_nodes") is not None), + fencing=(select_el.find("select_fencing") is not None), + resources=(select_el.find("select_resources") is not None), + attributes=(select_el.find("select_attributes") is not None), + attributes_select=[ + CibAlertSelectAttributeDto( + str(attr_el.attrib["id"]), str(attr_el.attrib["name"]) + ) + for attr_el in select_el.iterfind("select_attributes/attribute") + ], + ) + + +def alert_el_to_dto( + alert_el: _Element, + rule_eval: Optional[rule.RuleInEffectEval] = None, +) -> CibAlertDto: + if rule_eval is None: + rule_eval = rule.RuleInEffectEvalDummy() + select_el = alert_el.find("select") + return CibAlertDto( + id=str(alert_el.attrib["id"]), + path=str(alert_el.attrib["path"]), + description=alert_el.get("description"), + recipients=[ + _recipient_el_to_dto(recipient_el) + for recipient_el in alert_el.iterfind(TAG_RECIPIENT) + ], + select=_select_el_to_dto(select_el) if select_el is not None else None, + meta_attributes=[ + nvpair_multi.nvset_element_to_dto(nvset, rule_eval) + for nvset in nvpair_multi.find_nvsets( + alert_el, nvpair_multi.NVSET_META + ) + ], + instance_attributes=[ + nvpair_multi.nvset_element_to_dto(nvset, rule_eval) + for nvset in nvpair_multi.find_nvsets( + alert_el, nvpair_multi.NVSET_INSTANCE + ) + ], + ) + + +# DEPRECATED, used only in get_all_alerts_dict +def get_all_recipients_dict(alert: _Element) -> list[dict[str, Any]]: """ Returns list of all recipient of specified alert. Format: [ @@ -349,7 +432,8 @@ def get_all_recipients(alert: _Element) -> list[dict[str, Any]]: ] -def get_all_alerts(tree: _Element) -> list[dict[str, Any]]: +# DEPRECATED, use alert_el_to_dto + get_all_alert_elements +def get_all_alerts_dict(tree: _Element) -> list[dict[str, Any]]: """ Returns list of all alerts specified in tree. Format: [ @@ -376,7 +460,7 @@ def get_all_alerts(tree: _Element) -> list[dict[str, Any]]: "meta_attributes": get_nvset( get_sub_element(alert, "meta_attributes") ), - "recipient_list": get_all_recipients(alert), + "recipient_list": get_all_recipients_dict(alert), } for alert in get_alerts(tree).findall("./alert") ] diff --git a/pcs/lib/commands/alert.py b/pcs/lib/commands/alert.py index e63c7729c..ff6b36948 100644 --- a/pcs/lib/commands/alert.py +++ b/pcs/lib/commands/alert.py @@ -1,5 +1,6 @@ from typing import TYPE_CHECKING, Any, Mapping, Optional +from pcs.common.pacemaker.alert import CibAlertListDto from pcs.common.types import StringIterable from pcs.lib.cib import alert from pcs.lib.cib.nvpair import ( @@ -256,6 +257,17 @@ def remove_recipient( lib_env.push_cib() +def get_config_dto(lib_env: LibraryEnvironment) -> CibAlertListDto: + cib = lib_env.get_cib() + return CibAlertListDto( + [ + alert.alert_el_to_dto(alert_el) + for alert_el in alert.get_all_alert_elements(get_alerts(cib)) + ] + ) + + +# DEPRECATED, use get_config_dto def get_all_alerts(lib_env: LibraryEnvironment) -> list[dict[str, Any]]: """ Returns list of all alerts. See docs of pcs.lib.cib.alert.get_all_alerts for @@ -263,4 +275,4 @@ def get_all_alerts(lib_env: LibraryEnvironment) -> list[dict[str, Any]]: lib_env -- LibraryEnvironment """ - return alert.get_all_alerts(lib_env.get_cib()) + return alert.get_all_alerts_dict(lib_env.get_cib()) diff --git a/pcs/pcs.8.in b/pcs/pcs.8.in index b4a70b6ac..0bc649a1e 100644 --- a/pcs/pcs.8.in +++ b/pcs/pcs.8.in @@ -1564,8 +1564,8 @@ utilization [[] [\fB\-\-name\fR ] | = ...] Add specified utilization options to specified node. If node is not specified, shows utilization of all nodes. If \fB\-\-name\fR is specified, shows specified utilization value from 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= For the utilization configuration to be in effect, cluster property 'placement-strategy' must be configured accordingly. .SS "alert" .TP -[config] -Show all configured alerts. +[config] [@OUTPUT_FORMAT_SYNTAX_DOC@] +Show all configured alerts. @OUTPUT_FORMAT_DESC_DOC@ .TP create path= [id=] [description=] [options [ + + + Show / export alerts in various formats. + + pcs commands: alert [config] --output-format=text|json|cmd + + + + + API v2: alert.get_config_dto + + From 9ae3f7f76f3af373c785df813025e701f78e0146 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Fri, 28 Mar 2025 17:05:09 +0100 Subject: [PATCH 126/227] remove old code for printing alerts config --- pcs/alert.py | 74 +--------------------------------------------------- 1 file changed, 1 insertion(+), 73 deletions(-) diff --git a/pcs/alert.py b/pcs/alert.py index 1c3d207b5..db9411010 100644 --- a/pcs/alert.py +++ b/pcs/alert.py @@ -1,5 +1,5 @@ import json -from typing import Any, Iterable, Mapping +from typing import Any from pcs.cli.alert.output import config_dto_to_lines from pcs.cli.common.errors import CmdLineInputError @@ -11,7 +11,6 @@ group_by_keywords, ) from pcs.cli.reports.output import deprecation_warning -from pcs.common.str_tools import indent def alert_add(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: @@ -149,52 +148,6 @@ def recipient_remove(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: lib.alert.remove_recipient(argv) -def _nvset_to_str(nvset_obj: Iterable[Mapping[str, str]]) -> str: - # TODO duplicite to pcs.resource._nvpairs_strings - key_val = { - nvpair_obj["name"]: nvpair_obj["value"] for nvpair_obj in nvset_obj - } - output = [] - for name, value in sorted(key_val.items()): - safe_value = f'"{value}"' if " " in value else value - output.append(f"{name}={safe_value}") - return " ".join(output) - - -def _old__description_attributes_to_str(obj: Mapping[str, Any]) -> list[str]: - output = [] - if obj.get("description"): - output.append(f"Description: {obj['description']}") - if obj.get("instance_attributes"): - attributes = _nvset_to_str(obj["instance_attributes"]) - output.append(f"Options: {attributes}") - if obj.get("meta_attributes"): - attributes = _nvset_to_str(obj["meta_attributes"]) - output.append(f"Meta options: {attributes}") - return output - - -def _old_alert_to_str(alert: Mapping[str, Any]) -> list[str]: - content: list[str] = [] - content.extend(_old__description_attributes_to_str(alert)) - - recipients: list[str] = [] - for recipient in alert.get("recipient_list", []): - recipients.extend(_old_recipient_to_str(recipient)) - - if recipients: - content.append("Recipients:") - content.extend(indent(recipients, 1)) - - return [f"Alert: {alert['id']} (path={alert['path']})"] + indent(content, 1) - - -def _old_recipient_to_str(recipient: Mapping[str, Any]) -> list[str]: - return [ - f"Recipient: {recipient['id']} (value={recipient['value']})" - ] + indent(_old__description_attributes_to_str(recipient), 1) - - def print_alert_show(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: deprecation_warning( "This command is deprecated and will be removed. " @@ -208,31 +161,6 @@ def print_alert_show(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: print(result_text) -def old_print_alert_config( - lib: Any, argv: Argv, modifiers: InputModifiers -) -> None: - """ - Options: - * -f - CIB file (in lib wrapper) - """ - modifiers.ensure_only_supported("-f") - if argv: - raise CmdLineInputError() - lines = old_alert_config_lines(lib) - if lines: - print("\n".join(lines)) - - -def old_alert_config_lines(lib: Any) -> list[str]: - lines = [] - alert_list = lib.alert.get_all_alerts() - if alert_list: - lines.append("Alerts:") - for alert in alert_list: - lines.extend(indent(_old_alert_to_str(alert), 1)) - return lines - - def print_alerts_in_json( lib: Any, argv: Argv, modifiers: InputModifiers ) -> None: From 0dbad5d72ff4d06e2d702cc6d1ac4ceecc14e370 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Wed, 9 Apr 2025 10:42:44 +0200 Subject: [PATCH 127/227] fix lib command alert.get_config_dto * allow to evaluate expired rules in nvsets --- pcs/lib/cib/alert.py | 2 +- pcs/lib/commands/alert.py | 10 +- pcs_test/tier0/lib/commands/test_alert.py | 377 +++++++++++++++------- 3 files changed, 275 insertions(+), 114 deletions(-) diff --git a/pcs/lib/cib/alert.py b/pcs/lib/cib/alert.py index 9d72cacae..d33fd0ee9 100644 --- a/pcs/lib/cib/alert.py +++ b/pcs/lib/cib/alert.py @@ -381,7 +381,7 @@ def alert_el_to_dto( path=str(alert_el.attrib["path"]), description=alert_el.get("description"), recipients=[ - _recipient_el_to_dto(recipient_el) + _recipient_el_to_dto(recipient_el, rule_eval) for recipient_el in alert_el.iterfind(TAG_RECIPIENT) ], select=_select_el_to_dto(select_el) if select_el is not None else None, diff --git a/pcs/lib/commands/alert.py b/pcs/lib/commands/alert.py index ff6b36948..321d3b79d 100644 --- a/pcs/lib/commands/alert.py +++ b/pcs/lib/commands/alert.py @@ -7,6 +7,7 @@ arrange_first_instance_attributes, arrange_first_meta_attributes, ) +from pcs.lib.cib.rule.in_effect import get_rule_evaluator from pcs.lib.cib.tools import ( IdProvider, get_alerts, @@ -257,11 +258,16 @@ def remove_recipient( lib_env.push_cib() -def get_config_dto(lib_env: LibraryEnvironment) -> CibAlertListDto: +def get_config_dto( + lib_env: LibraryEnvironment, evaluate_expired: bool = False +) -> CibAlertListDto: cib = lib_env.get_cib() + rule_in_effect_eval = get_rule_evaluator( + cib, lib_env.cmd_runner(), lib_env.report_processor, evaluate_expired + ) return CibAlertListDto( [ - alert.alert_el_to_dto(alert_el) + alert.alert_el_to_dto(alert_el, rule_eval=rule_in_effect_eval) for alert_el in alert.get_all_alert_elements(get_alerts(cib)) ] ) diff --git a/pcs_test/tier0/lib/commands/test_alert.py b/pcs_test/tier0/lib/commands/test_alert.py index 0efaf996c..02344e2eb 100644 --- a/pcs_test/tier0/lib/commands/test_alert.py +++ b/pcs_test/tier0/lib/commands/test_alert.py @@ -9,13 +9,19 @@ CibAlertSelectDto, ) from pcs.common.pacemaker.nvset import CibNvpairDto, CibNvsetDto +from pcs.common.pacemaker.rule import CibRuleExpressionDto from pcs.common.reports import ReportItemSeverity as Severities from pcs.common.reports import codes as report_codes +from pcs.common.types import CibRuleExpressionType, CibRuleInEffectStatus +from pcs.lib.cib.rule.in_effect import RuleInEffectEval from pcs.lib.env import LibraryEnvironment from pcs_test.tools import fixture from pcs_test.tools.command_env import get_env_tools -from pcs_test.tools.custom_mock import MockLibraryReportProcessor +from pcs_test.tools.custom_mock import ( + MockLibraryReportProcessor, + RuleInEffectEvalMock, +) class CreateAlertTest(TestCase): @@ -728,133 +734,282 @@ def test_no_recipient(self): class GetConfigDto(TestCase): - def setUp(self): - self.env_assist, self.config = get_env_tools(self) - - def test_success_no_alerts(self): - self.config.runner.cib.load() - self.assertEqual( - cmd_alert.get_config_dto(self.env_assist.get_env()), - CibAlertListDto([]), - ) - - def test_success(self): - self.config.runner.cib.load( - optional_in_conf=""" - - - - - - - - - - + fixture_alerts = """ + + + - + + + - + + + - - - - """ - ) - self.assertEqual( - cmd_alert.get_config_dto(self.env_assist.get_env()), - CibAlertListDto( - [ - CibAlertDto( - id="alert-all", - path="/path/all", - description="all options", - recipients=[ - CibAlertRecipientDto( - id="alert-all-recipient", - value="value-all", - description="all options recipient", - meta_attributes=[ - CibNvsetDto( - id="", - options={}, - rule=None, - nvpairs=[ - CibNvpairDto( - id="alert-all-recipient-ma", - name="all-mar1-name", - value="all-mar1-value", + + + + + + + + + + + + + + + + + """ + + def setUp(self): + self.env_assist, self.config = get_env_tools(self) + + def get_alerts_dto(self, rule_eval: RuleInEffectEval) -> CibAlertListDto: + return CibAlertListDto( + [ + CibAlertDto( + id="alert-all", + path="/path/all", + description="all options", + recipients=[ + CibAlertRecipientDto( + id="alert-all-recipient", + value="value-all", + description="all options recipient", + meta_attributes=[ + CibNvsetDto( + id="", + options={}, + rule=CibRuleExpressionDto( + id="alert-all-recipient-ma-rule", + type=CibRuleExpressionType.RULE, + in_effect=rule_eval.get_rule_status( + "alert-all-recipient-ma-rule" + ), + options={"boolean-op": "and"}, + date_spec=None, + duration=None, + expressions=[ + CibRuleExpressionDto( + id="alert-all-recipient-ma-rule-de", + type=CibRuleExpressionType.DATE_EXPRESSION, + in_effect=CibRuleInEffectStatus.UNKNOWN, + options={ + "operation": "lt", + "end": "2000-01-01", + }, + date_spec=None, + duration=None, + expressions=[], + as_string="date lt 2000-01-01", ) ], - ) - ], - instance_attributes=[ - CibNvsetDto( - id="", + as_string="date lt 2000-01-01", + ), + nvpairs=[ + CibNvpairDto( + id="alert-all-recipient-ma", + name="all-mar1-name", + value="all-mar1-value", + ) + ], + ) + ], + instance_attributes=[ + CibNvsetDto( + id="", + options={}, + rule=CibRuleExpressionDto( + id="alert-all-recipient-ia-rule", + type=CibRuleExpressionType.RULE, + in_effect=rule_eval.get_rule_status( + "alert-all-recipient-ia-rule" + ), options={}, - rule=None, - nvpairs=[ - CibNvpairDto( - id="alert-all-recipient-ia", - name="all-iar1-name", - value="all-iar1-value", + date_spec=None, + duration=None, + expressions=[ + CibRuleExpressionDto( + id="alert-all-recipient-ia-rule-de", + type=CibRuleExpressionType.DATE_EXPRESSION, + in_effect=CibRuleInEffectStatus.UNKNOWN, + options={ + "operation": "gt", + "end": "2000-01-01", + }, + date_spec=None, + duration=None, + expressions=[], + as_string="date gt 2000-01-01", ) ], + as_string="date gt 2000-01-01", + ), + nvpairs=[ + CibNvpairDto( + id="alert-all-recipient-ia", + name="all-iar1-name", + value="all-iar1-value", + ) + ], + ) + ], + ) + ], + select=CibAlertSelectDto( + nodes=True, + fencing=False, + resources=False, + attributes=True, + attributes_select=[], + ), + meta_attributes=[ + CibNvsetDto( + id="", + options={}, + rule=CibRuleExpressionDto( + id="alert-all-ma-rule", + type=CibRuleExpressionType.RULE, + in_effect=rule_eval.get_rule_status( + "alert-all-ma-rule" + ), + options={"boolean-op": "and"}, + date_spec=None, + duration=None, + expressions=[ + CibRuleExpressionDto( + id="alert-all-ma-rule-de", + type=CibRuleExpressionType.DATE_EXPRESSION, + in_effect=CibRuleInEffectStatus.UNKNOWN, + options={ + "operation": "lt", + "end": "2000-01-01", + }, + date_spec=None, + duration=None, + expressions=[], + as_string="date lt 2000-01-01", ) ], - ) - ], - select=CibAlertSelectDto( - nodes=True, - fencing=False, - resources=False, - attributes=True, - attributes_select=[], - ), - meta_attributes=[ - CibNvsetDto( - id="", - options={}, - rule=None, - nvpairs=[ - CibNvpairDto( - id="alert-all-ma", - name="all-maa1-name", - value="all-maa1-value", - ) - ], - ) - ], - instance_attributes=[ - CibNvsetDto( - id="", + as_string="date lt 2000-01-01", + ), + nvpairs=[ + CibNvpairDto( + id="alert-all-ma", + name="all-maa1-name", + value="all-maa1-value", + ) + ], + ) + ], + instance_attributes=[ + CibNvsetDto( + id="", + options={}, + rule=CibRuleExpressionDto( + id="alert-all-ia-rule", + type=CibRuleExpressionType.RULE, + in_effect=rule_eval.get_rule_status( + "alert-all-ia-rule" + ), options={}, - rule=None, - nvpairs=[ - CibNvpairDto( - id="alert-all-ia", - name="all-iaa1-name", - value="all-iaa1-value", + date_spec=None, + duration=None, + expressions=[ + CibRuleExpressionDto( + id="alert-all-ia-rule-de", + type=CibRuleExpressionType.DATE_EXPRESSION, + in_effect=CibRuleInEffectStatus.UNKNOWN, + options={ + "operation": "gt", + "end": "2000-01-01", + }, + date_spec=None, + duration=None, + expressions=[], + as_string="date gt 2000-01-01", ) ], - ) - ], - ), - ] + as_string="date gt 2000-01-01", + ), + nvpairs=[ + CibNvpairDto( + id="alert-all-ia", + name="all-iaa1-name", + value="all-iaa1-value", + ) + ], + ) + ], + ), + ] + ) + + def test_success_no_alerts(self): + self.config.runner.cib.load() + self.assertEqual( + cmd_alert.get_config_dto(self.env_assist.get_env()), + CibAlertListDto([]), + ) + + @mock.patch("pcs.lib.commands.alert.get_rule_evaluator") + def test_success(self, mock_get_rule_evaluator): + self.config.runner.cib.load(optional_in_conf=self.fixture_alerts) + rule_evaluator = RuleInEffectEvalMock( + { + "alert-all-recipient-ia-rule": CibRuleInEffectStatus.IN_EFFECT, + "alert-all-recipient-ma-rule": CibRuleInEffectStatus.EXPIRED, + "alert-all-ma-rule": CibRuleInEffectStatus.EXPIRED, + "alert-all-ia-rule": CibRuleInEffectStatus.IN_EFFECT, + } + ) + mock_get_rule_evaluator.return_value = rule_evaluator + self.assertEqual( + cmd_alert.get_config_dto( + self.env_assist.get_env(), evaluate_expired=True + ), + self.get_alerts_dto(rule_evaluator), + ) + mock_get_rule_evaluator.assert_called_once() + + @mock.patch("pcs.lib.cib.rule.in_effect.has_rule_in_effect_status_tool") + def test_success_no_rule_evaluation(self, mock_has_rule_tool): + mock_has_rule_tool.side_effect = AssertionError( + "has_rule_in_effect_status_tool should not be called" + ) + self.config.runner.cib.load(optional_in_conf=self.fixture_alerts) + self.assertEqual( + cmd_alert.get_config_dto( + self.env_assist.get_env(), evaluate_expired=False ), + self.get_alerts_dto(RuleInEffectEvalMock({})), ) From b2479c8e2e01f0ea538ea9856d5a5de4d179e7d5 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Thu, 10 Apr 2025 14:49:16 +0200 Subject: [PATCH 128/227] fix force_flags type hints --- pcs/lib/commands/booth.py | 9 ++------ pcs/lib/commands/cib_options.py | 14 ++++-------- pcs/lib/commands/cluster.py | 32 +++++++++++----------------- pcs/lib/commands/cluster_property.py | 8 ++----- pcs/lib/commands/dr.py | 12 ++--------- pcs/lib/commands/remote_node.py | 11 ++-------- pcs/lib/commands/stonith.py | 15 ++++--------- 7 files changed, 28 insertions(+), 73 deletions(-) diff --git a/pcs/lib/commands/booth.py b/pcs/lib/commands/booth.py index d71c55294..220edc832 100644 --- a/pcs/lib/commands/booth.py +++ b/pcs/lib/commands/booth.py @@ -1,12 +1,7 @@ import base64 import os.path from functools import partial -from typing import ( - Collection, - Mapping, - Optional, - cast, -) +from typing import Mapping, Optional, cast from lxml.etree import _Element @@ -553,7 +548,7 @@ def create_in_cluster( def remove_from_cluster( env: LibraryEnvironment, instance_name: Optional[str] = None, - force_flags: Collection[reports.types.ForceCode] = (), + force_flags: reports.types.ForceFlags = (), ) -> None: """ Remove group with ip resource and booth resource diff --git a/pcs/lib/commands/cib_options.py b/pcs/lib/commands/cib_options.py index 8630ace01..e997f75e4 100644 --- a/pcs/lib/commands/cib_options.py +++ b/pcs/lib/commands/cib_options.py @@ -1,10 +1,4 @@ -from typing import ( - Any, - Collection, - Container, - Mapping, - Optional, -) +from typing import Any, Mapping, Optional from lxml.etree import _Element @@ -41,7 +35,7 @@ def resource_defaults_create( nvpairs: Mapping[str, str], nvset_options: Mapping[str, str], nvset_rule: Optional[str] = None, - force_flags: Collection[reports.types.ForceCode] = (), + force_flags: reports.types.ForceFlags = (), ) -> None: """ Create new resource defaults nvset @@ -72,7 +66,7 @@ def operation_defaults_create( nvpairs: Mapping[str, str], nvset_options: Mapping[str, str], nvset_rule: Optional[str] = None, - force_flags: Collection[reports.types.ForceCode] = (), + force_flags: reports.types.ForceFlags = (), ) -> None: """ Create new operation defaults nvset @@ -105,7 +99,7 @@ def _defaults_create( nvpairs: Mapping[str, str], nvset_options: Mapping[str, str], nvset_rule: Optional[str] = None, - force_flags: Container[reports.types.ForceCode] = (), + force_flags: reports.types.ForceFlags = (), ) -> None: required_cib_version = None nice_to_have_cib_version = None diff --git a/pcs/lib/commands/cluster.py b/pcs/lib/commands/cluster.py index 6919b9af5..560de38ee 100644 --- a/pcs/lib/commands/cluster.py +++ b/pcs/lib/commands/cluster.py @@ -2,15 +2,7 @@ import math import os.path import time -from typing import ( - Any, - Collection, - Mapping, - Optional, - Sequence, - Tuple, - cast, -) +from typing import Any, Mapping, Optional, Sequence, Tuple, cast from lxml.etree import _Element @@ -215,7 +207,7 @@ def setup( # noqa: PLR0913, PLR0915 enable: bool = False, no_keys_sync: bool = False, no_cluster_uuid: bool = False, - force_flags: Collection[reports.types.ForceCode] = (), + force_flags: reports.types.ForceFlags = (), ): # pylint: disable=too-many-arguments # pylint: disable=too-many-positional-arguments @@ -473,7 +465,7 @@ def setup_local( # noqa: PLR0913 totem_options: Mapping[str, str], quorum_options: Mapping[str, str], no_cluster_uuid: bool = False, - force_flags: Collection[reports.types.ForceCode] = (), + force_flags: reports.types.ForceFlags = (), ) -> bytes: """ Return corosync.conf text based on specified parameters. @@ -858,7 +850,7 @@ def add_nodes( # noqa: PLR0912, PLR0915 start=False, enable=False, no_watchdog_validation=False, - force_flags: Collection[reports.types.ForceCode] = (), + force_flags: reports.types.ForceFlags = (), ): # pylint: disable=too-many-branches # pylint: disable=too-many-locals @@ -1735,7 +1727,7 @@ def _verify_corosync_conf(corosync_conf_facade): def remove_nodes( # noqa: PLR0912, PLR0915 env: LibraryEnvironment, node_list, - force_flags: Collection[reports.types.ForceCode] = (), + force_flags: reports.types.ForceFlags = (), ): # pylint: disable=too-many-branches # pylint: disable=too-many-locals @@ -1994,7 +1986,7 @@ def add_link( env: LibraryEnvironment, node_addr_map, link_options=None, - force_flags: Collection[reports.types.ForceCode] = (), + force_flags: reports.types.ForceFlags = (), ): """ Add a corosync link to a cluster @@ -2061,7 +2053,7 @@ def add_link( def remove_links( env: LibraryEnvironment, linknumber_list, - force_flags: Collection[reports.types.ForceCode] = (), + force_flags: reports.types.ForceFlags = (), ): """ Remove corosync links from a cluster @@ -2104,7 +2096,7 @@ def update_link( linknumber, node_addr_map=None, link_options=None, - force_flags: Collection[reports.types.ForceCode] = (), + force_flags: reports.types.ForceFlags = (), ): """ Change an existing corosync link @@ -2168,7 +2160,7 @@ def update_link( def corosync_authkey_change( env: LibraryEnvironment, corosync_authkey: Optional[bytes] = None, - force_flags: Collection[reports.types.ForceCode] = (), + force_flags: reports.types.ForceFlags = (), ) -> None: """ Distribute new corosync authkey to all cluster nodes. @@ -2268,7 +2260,7 @@ def _generate_cluster_uuid( def generate_cluster_uuid( env: LibraryEnvironment, - force_flags: Collection[reports.types.ForceCode] = (), + force_flags: reports.types.ForceFlags = (), ) -> None: """ Add or update cluster UUID in live cluster @@ -2288,7 +2280,7 @@ def generate_cluster_uuid( def generate_cluster_uuid_local( env: LibraryEnvironment, corosync_conf_content: bytes, - force_flags: Collection[reports.types.ForceCode] = (), + force_flags: reports.types.ForceFlags = (), ) -> bytes: """ Add or update cluster UUID in corosync.conf passed as an argument and return @@ -2339,7 +2331,7 @@ def wait_for_pcmk_idle(env: LibraryEnvironment, wait_value: WaitType) -> None: def rename( env: LibraryEnvironment, new_name: str, - force_flags: Collection[reports.types.ForceCode] = (), + force_flags: reports.types.ForceFlags = (), ) -> None: """ Change the name of the local cluster diff --git a/pcs/lib/commands/cluster_property.py b/pcs/lib/commands/cluster_property.py index 71b0523ea..ef9aaf202 100644 --- a/pcs/lib/commands/cluster_property.py +++ b/pcs/lib/commands/cluster_property.py @@ -1,8 +1,4 @@ -from typing import ( - Collection, - Mapping, - Union, -) +from typing import Mapping, Union from pcs import settings from pcs.common import reports @@ -127,7 +123,7 @@ def get_cluster_properties_definition_legacy( def set_properties( env: LibraryEnvironment, cluster_properties: Mapping[str, str], - force_flags: Collection[reports.types.ForceCode] = (), + force_flags: reports.types.ForceFlags = (), ) -> None: """ Set specified pacemaker cluster properties, remove those with empty values. diff --git a/pcs/lib/commands/dr.py b/pcs/lib/commands/dr.py index 948b7df02..881c46167 100644 --- a/pcs/lib/commands/dr.py +++ b/pcs/lib/commands/dr.py @@ -1,12 +1,4 @@ -from typing import ( - Any, - Container, - Iterable, - List, - Mapping, - Tuple, - cast, -) +from typing import Any, Iterable, List, Mapping, Tuple, cast from pcs.common import ( file_type_codes, @@ -311,7 +303,7 @@ def _load_dr_config( def destroy( env: LibraryEnvironment, - force_flags: Container[reports.types.ForceCode] = (), + force_flags: reports.types.ForceFlags = (), ) -> None: """ Destroy disaster-recovery configuration on all sites diff --git a/pcs/lib/commands/remote_node.py b/pcs/lib/commands/remote_node.py index 064f403f5..83865ea9f 100644 --- a/pcs/lib/commands/remote_node.py +++ b/pcs/lib/commands/remote_node.py @@ -1,11 +1,4 @@ -from typing import ( - TYPE_CHECKING, - Callable, - Collection, - Iterable, - Mapping, - Optional, -) +from typing import TYPE_CHECKING, Callable, Iterable, Mapping, Optional from lxml.etree import _Element @@ -709,7 +702,7 @@ def _report_skip_live_parts_in_remove(node_names_list): def node_remove_remote( env: LibraryEnvironment, node_identifier: str, - force_flags: Collection[reports.types.ForceCode] = (), + force_flags: reports.types.ForceFlags = (), ): """ remove a resource representing remote node and destroy remote node diff --git a/pcs/lib/commands/stonith.py b/pcs/lib/commands/stonith.py index 021b2fc2b..5d94834b7 100644 --- a/pcs/lib/commands/stonith.py +++ b/pcs/lib/commands/stonith.py @@ -1,11 +1,4 @@ -from typing import ( - Collection, - Container, - List, - Mapping, - Optional, - Tuple, -) +from typing import Collection, List, Mapping, Optional, Tuple from lxml.etree import _Element @@ -437,7 +430,7 @@ def _unfencing_scsi_devices( stonith_el: _Element, original_devices: StringCollection, updated_devices: StringCollection, - force_flags: Container[reports.types.ForceCode] = (), + force_flags: reports.types.ForceFlags = (), ) -> None: """ Unfence scsi devices provided in device_list if it is possible to connect @@ -498,7 +491,7 @@ def update_scsi_devices( env: LibraryEnvironment, stonith_id: str, set_device_list: StringCollection, - force_flags: Container[reports.types.ForceCode] = (), + force_flags: reports.types.ForceFlags = (), ) -> None: """ Update scsi fencing devices without restart and affecting other resources. @@ -544,7 +537,7 @@ def update_scsi_devices_add_remove( stonith_id: str, add_device_list: StringCollection, remove_device_list: StringCollection, - force_flags: Container[reports.types.ForceCode] = (), + force_flags: reports.types.ForceFlags = (), ) -> None: """ Update scsi fencing devices without restart and affecting other resources. From fb6c96143672fb1384a91e0d3e3fb351ee64b0aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Posp=C3=AD=C5=A1il?= Date: Tue, 15 Apr 2025 12:32:06 +0200 Subject: [PATCH 129/227] fix changelog after special release A special release with regression fixes was made from these commits: 437a441 fix restarting bundle instances 9034688 fix deletion of misconfigured bundles --- CHANGELOG.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2268c7056..98ad42c39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,6 @@ - Support for exporting alerts in `json` and `cmd` formats ([RHEL-76153]) ### Fixed -- Command `pcs resource restart` allows restarting bundle instances (broken - since pcs-0.11.9) ([RHEL-79055]) -- Do not end with traceback when using `pcs resource delete` to remove bundle - resources when the bundle has no IP address specified ([RHEL-79160]) - Fixed a traceback when removing a resource fails in web UI - It is now possible to override errors when editing cluster properties in web UI @@ -27,9 +23,19 @@ [RHEL-76153]: https://issues.redhat.com/browse/RHEL-76153 [RHEL-76170]: https://issues.redhat.com/browse/RHEL-76170 [RHEL-76177]: https://issues.redhat.com/browse/RHEL-76177 +[RHEL-82894]: https://issues.redhat.com/browse/RHEL-82894 + + +## [0.11.9.1] - 2025-04-14 + +### Fixed +- Command `pcs resource restart` allows restarting bundle instances (broken + since pcs-0.11.9) ([RHEL-79055]) +- Do not end with traceback when using `pcs resource delete` to remove bundle + resources when the bundle has no IP address specified ([RHEL-79160]) + [RHEL-79055]: https://issues.redhat.com/browse/RHEL-79055 [RHEL-79160]: https://issues.redhat.com/browse/RHEL-79160 -[RHEL-82894]: https://issues.redhat.com/browse/RHEL-82894 ## [0.11.9] - 2025-01-10 From b60e8880d39d7f3dadb5bf3f6177fa5fb41827ab Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Wed, 23 Apr 2025 16:37:39 +0200 Subject: [PATCH 130/227] readme: document web ui --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 56750bfbd..972c88d24 100644 --- a/README.md +++ b/README.md @@ -63,11 +63,14 @@ installation: During the installation, all required rubygems are automatically downloaded and compiled. +Web UI frontend is no longer part of pcs sources. You can get it at +[https://github.com/ClusterLabs/pcs-web-ui](https://github.com/ClusterLabs/pcs-web-ui). + To install pcs and pcsd run the following in terminal: ```shell ./autogen.sh ./configure -# alternatively './configure --enable-local-build' can be used to also download +# alternatively, './configure --enable-local-build' can be used to also download # missing dependencies make make install From d16bd2ad5713d867291e0e28b45010ab16f8fc61 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Fri, 11 Apr 2025 14:53:22 +0200 Subject: [PATCH 131/227] code formatting --- pcs_test/tier0/lib/commands/test_status.py | 576 ++++++++++----------- pcs_test/tier0/lib/pacemaker/test_live.py | 125 ++--- 2 files changed, 327 insertions(+), 374 deletions(-) diff --git a/pcs_test/tier0/lib/commands/test_status.py b/pcs_test/tier0/lib/commands/test_status.py index 9bfb3f7bc..a9aded803 100644 --- a/pcs_test/tier0/lib/commands/test_status.py +++ b/pcs_test/tier0/lib/commands/test_status.py @@ -2,10 +2,7 @@ import os from textwrap import dedent from typing import Optional -from unittest import ( - TestCase, - mock, -) +from unittest import TestCase, mock from pcs import settings from pcs.common import file_type_codes @@ -27,10 +24,7 @@ from pcs.lib.commands import status from pcs.lib.errors import LibraryError -from pcs_test.tools import ( - fixture, - fixture_crm_mon, -) +from pcs_test.tools import fixture, fixture_crm_mon from pcs_test.tools.assertions import assert_xml_equal from pcs_test.tools.command_env import get_env_tools from pcs_test.tools.command_env.config_runner_pcmk import ( @@ -103,41 +97,37 @@ def _fixture_xml_clustername(name): """.format(name=name) def _fixture_config_live_minimal(self): - ( - self.config.runner.pcmk.load_state_plaintext( - stdout="crm_mon cluster status", - ) - .fs.exists(settings.corosync_conf_file, return_value=True) - .corosync_conf.load() - .runner.cib.load( - resources=""" + self.config.runner.pcmk.load_state_plaintext( + stdout="crm_mon cluster status" + ) + self.config.fs.exists(settings.corosync_conf_file, return_value=True) + self.config.corosync_conf.load() + self.config.runner.cib.load( + resources=""" """ - ) - .services.is_running( - "sbd", return_value=False, name="services.is_running.sbd" - ) + ) + self.config.services.is_running( + "sbd", return_value=False, name="services.is_running.sbd" ) def _fixture_config_live_remote_minimal(self): - ( - self.config.runner.pcmk.load_state_plaintext( - stdout="crm_mon cluster status", - ) - .fs.exists(settings.corosync_conf_file, return_value=False) - .runner.cib.load( - optional_in_conf=self._fixture_xml_clustername("test-cib"), - resources=""" + self.config.runner.pcmk.load_state_plaintext( + stdout="crm_mon cluster status" + ) + self.config.fs.exists(settings.corosync_conf_file, return_value=False) + self.config.runner.cib.load( + optional_in_conf=self._fixture_xml_clustername("test-cib"), + resources=""" """, - ) - .services.is_running( - "sbd", return_value=False, name="services.is_running.sbd" - ) + ) + self.config.services.is_running( + "sbd", return_value=False, name="services.is_running.sbd" ) def _fixture_config_local_daemons( # noqa: PLR0913 @@ -155,57 +145,55 @@ def _fixture_config_local_daemons( # noqa: PLR0913 sbd_active=False, ): # pylint: disable=too-many-arguments - ( - self.config.services.is_enabled( - "corosync", - name="services.is_enabled.corosync", - return_value=corosync_enabled, - ) - .services.is_running( - "corosync", - name="services.is_running.corosync", - return_value=corosync_active, - ) - .services.is_enabled( - "pacemaker", - name="services.is_enabled.pacemaker", - return_value=pacemaker_enabled, - ) - .services.is_running( - "pacemaker", - name="services.is_running.pacemaker", - return_value=pacemaker_active, - ) - .services.is_enabled( - "pacemaker_remote", - name="services.is_enabled.pacemaker_remote", - return_value=pacemaker_remote_enabled, - ) - .services.is_running( - "pacemaker_remote", - name="services.is_running.pacemaker_remote", - return_value=pacemaker_remote_active, - ) - .services.is_enabled( - "pcsd", - name="services.is_enabled.pcsd", - return_value=pcsd_enabled, - ) - .services.is_running( - "pcsd", - name="services.is_running.pcsd", - return_value=pcsd_active, - ) - .services.is_enabled( - "sbd", - name="services.is_enabled.sbd_2", - return_value=sbd_enabled, - ) - .services.is_running( - "sbd", - name="services.is_running.sbd_2", - return_value=sbd_active, - ) + self.config.services.is_enabled( + "corosync", + name="services.is_enabled.corosync", + return_value=corosync_enabled, + ) + self.config.services.is_running( + "corosync", + name="services.is_running.corosync", + return_value=corosync_active, + ) + self.config.services.is_enabled( + "pacemaker", + name="services.is_enabled.pacemaker", + return_value=pacemaker_enabled, + ) + self.config.services.is_running( + "pacemaker", + name="services.is_running.pacemaker", + return_value=pacemaker_active, + ) + self.config.services.is_enabled( + "pacemaker_remote", + name="services.is_enabled.pacemaker_remote", + return_value=pacemaker_remote_enabled, + ) + self.config.services.is_running( + "pacemaker_remote", + name="services.is_running.pacemaker_remote", + return_value=pacemaker_remote_active, + ) + self.config.services.is_enabled( + "pcsd", + name="services.is_enabled.pcsd", + return_value=pcsd_enabled, + ) + self.config.services.is_running( + "pcsd", + name="services.is_running.pcsd", + return_value=pcsd_active, + ) + self.config.services.is_enabled( + "sbd", + name="services.is_enabled.sbd_2", + return_value=sbd_enabled, + ) + self.config.services.is_running( + "sbd", + name="services.is_running.sbd_2", + return_value=sbd_active, ) @@ -215,6 +203,7 @@ class FullClusterStatusPlaintext(FullClusterStatusPlaintextBase): # pylint: disable=too-many-public-methods def test_life_cib_mocked_corosync(self): self.config.env.set_corosync_conf_data("corosync conf data") + self.env_assist.assert_raise_library_error( lambda: status.full_cluster_status_plaintext( self.env_assist.get_env() @@ -231,6 +220,7 @@ def test_life_cib_mocked_corosync(self): def test_mocked_cib_life_corosync(self): self.config.env.set_cib_data("") + self.env_assist.assert_raise_library_error( lambda: status.full_cluster_status_plaintext( self.env_assist.get_env() @@ -246,13 +236,10 @@ def test_mocked_cib_life_corosync(self): ) def test_fail_getting_cluster_status(self): - ( - self.config.runner.pcmk.load_state_plaintext( - stdout="some stdout", - stderr="some stderr", - returncode=1, - ) + self.config.runner.pcmk.load_state_plaintext( + stdout="some stdout", stderr="some stderr", returncode=1 ) + self.env_assist.assert_raise_library_error( lambda: status.full_cluster_status_plaintext( self.env_assist.get_env() @@ -267,13 +254,12 @@ def test_fail_getting_cluster_status(self): ) def test_fail_getting_corosync_conf(self): - ( - self.config.runner.pcmk.load_state_plaintext( - stdout="crm_mon cluster status", - ) - .fs.exists(settings.corosync_conf_file, return_value=True) - .corosync_conf.load_content("invalid corosync conf") + self.config.runner.pcmk.load_state_plaintext( + stdout="crm_mon cluster status" ) + self.config.fs.exists(settings.corosync_conf_file, return_value=True) + self.config.corosync_conf.load_content("invalid corosync conf") + self.env_assist.assert_raise_library_error( lambda: status.full_cluster_status_plaintext( self.env_assist.get_env() @@ -288,16 +274,15 @@ def test_fail_getting_corosync_conf(self): ) def test_fail_getting_cib(self): - ( - self.config.runner.pcmk.load_state_plaintext( - stdout="crm_mon cluster status", - ) - .fs.exists(settings.corosync_conf_file, return_value=True) - .corosync_conf.load() - .runner.cib.load_content( - "some stdout", stderr="cib load error", returncode=1 - ) + self.config.runner.pcmk.load_state_plaintext( + stdout="crm_mon cluster status", + ) + self.config.fs.exists(settings.corosync_conf_file, return_value=True) + self.config.corosync_conf.load() + self.config.runner.cib.load_content( + "some stdout", stderr="cib load error", returncode=1 ) + self.env_assist.assert_raise_library_error( lambda: status.full_cluster_status_plaintext( self.env_assist.get_env() @@ -315,6 +300,7 @@ def test_success_live(self): self._fixture_config_live_minimal() self._fixture_config_local_daemons() self.config.fs.isfile(settings.crm_rule_exec, return_value=True) + self.assertEqual( status.full_cluster_status_plaintext(self.env_assist.get_env()), dedent( @@ -330,32 +316,29 @@ def test_success_live(self): ) def test_success_live_verbose(self): - ( - self.config.env.set_known_nodes(self.node_name_list) - .runner.pcmk.can_fence_history_status(stderr="not supported") - .runner.pcmk.load_state_plaintext( - verbose=True, - stdout="crm_mon cluster status", - ) - .fs.exists(settings.corosync_conf_file, return_value=True) - .corosync_conf.load(node_name_list=self.node_name_list) - .runner.cib.load( - resources=""" + self.config.env.set_known_nodes(self.node_name_list) + self.config.runner.pcmk.can_fence_history_status(stderr="not supported") + self.config.runner.pcmk.load_state_plaintext( + verbose=True, stdout="crm_mon cluster status" + ) + self.config.fs.exists(settings.corosync_conf_file, return_value=True) + self.config.corosync_conf.load(node_name_list=self.node_name_list) + self.config.runner.cib.load( + resources=""" """ - ) - .runner.pcmk.load_ticket_state_plaintext(stdout="ticket status") - .services.is_running( - "sbd", return_value=False, name="services.is_running.sbd" - ) + ) + self.config.runner.pcmk.load_ticket_state_plaintext( + stdout="ticket status" + ) + self.config.services.is_running( + "sbd", return_value=False, name="services.is_running.sbd" ) self._fixture_config_local_daemons() - ( - self.config.http.host.check_reachability( - node_labels=self.node_name_list - ) + self.config.http.host.check_reachability( + node_labels=self.node_name_list ) self.config.fs.isfile(settings.crm_rule_exec, return_value=True) @@ -394,6 +377,7 @@ def test_success_live_remote_node(self): pacemaker_remote_active=True, ) self.config.fs.isfile(settings.crm_rule_exec, return_value=True) + self.assertEqual( status.full_cluster_status_plaintext(self.env_assist.get_env()), dedent( @@ -410,27 +394,24 @@ def test_success_live_remote_node(self): ) def test_success_live_remote_node_verbose(self): - ( - self.config.runner.pcmk.can_fence_history_status( - stderr="not supported" - ) - .runner.pcmk.load_state_plaintext( - verbose=True, - stdout="crm_mon cluster status", - ) - .fs.exists(settings.corosync_conf_file, return_value=False) - .runner.cib.load( - optional_in_conf=self._fixture_xml_clustername("test-cib"), - resources=""" + self.config.runner.pcmk.can_fence_history_status(stderr="not supported") + self.config.runner.pcmk.load_state_plaintext( + verbose=True, stdout="crm_mon cluster status" + ) + self.config.fs.exists(settings.corosync_conf_file, return_value=False) + self.config.runner.cib.load( + optional_in_conf=self._fixture_xml_clustername("test-cib"), + resources=""" """, - ) - .runner.pcmk.load_ticket_state_plaintext(stdout="ticket status") - .services.is_running( - "sbd", return_value=False, name="services.is_running.sbd" - ) + ) + self.config.runner.pcmk.load_ticket_state_plaintext( + stdout="ticket status" + ) + self.config.services.is_running( + "sbd", return_value=False, name="services.is_running.sbd" ) self._fixture_config_local_daemons( corosync_enabled=False, @@ -465,23 +446,21 @@ def test_success_live_remote_node_verbose(self): def test_success_mocked(self): tmp_file = "/fake/tmp_file" env = dict(CIB_file=tmp_file) - ( - self.config.env.set_corosync_conf_data(rc_read("corosync.conf")) - .env.set_cib_data("", cib_tempfile=tmp_file) - .runner.pcmk.load_state_plaintext( - stdout="crm_mon cluster status", - env=env, - ) - .runner.cib.load( - resources=""" + self.config.env.set_corosync_conf_data(rc_read("corosync.conf")) + self.config.env.set_cib_data("", cib_tempfile=tmp_file) + self.config.runner.pcmk.load_state_plaintext( + stdout="crm_mon cluster status", env=env + ) + self.config.runner.cib.load( + resources=""" """, - env=env, - ) - .fs.isfile(settings.crm_rule_exec, return_value=True) + env=env, ) + self.config.fs.isfile(settings.crm_rule_exec, return_value=True) + self.assertEqual( status.full_cluster_status_plaintext(self.env_assist.get_env()), dedent( @@ -494,31 +473,26 @@ def test_success_mocked(self): def test_success_mocked_verbose(self): tmp_file = "/fake/tmp_file" env = dict(CIB_file=tmp_file) - ( - self.config.env.set_corosync_conf_data(rc_read("corosync.conf")) - .env.set_cib_data("", cib_tempfile=tmp_file) - .runner.pcmk.can_fence_history_status( - stderr="not supported", - env=env, - ) - .runner.pcmk.load_state_plaintext( - verbose=True, - stdout="crm_mon cluster status", - env=env, - ) - .runner.cib.load( - resources=""" + self.config.env.set_corosync_conf_data(rc_read("corosync.conf")) + self.config.env.set_cib_data("", cib_tempfile=tmp_file) + self.config.runner.pcmk.can_fence_history_status( + stderr="not supported", env=env + ) + self.config.runner.pcmk.load_state_plaintext( + verbose=True, stdout="crm_mon cluster status", env=env + ) + self.config.runner.cib.load( + resources=""" """, - env=env, - ) - .runner.pcmk.load_ticket_state_plaintext( - stdout="ticket status", env=env - ) - .fs.isfile(settings.crm_rule_exec, return_value=True) + env=env, ) + self.config.runner.pcmk.load_ticket_state_plaintext( + stdout="ticket status", env=env + ) + self.config.fs.isfile(settings.crm_rule_exec, return_value=True) self.assertEqual( status.full_cluster_status_plaintext( self.env_assist.get_env(), verbose=True @@ -534,34 +508,32 @@ def test_success_mocked_verbose(self): ) def test_success_verbose_inactive_and_fence_history(self): - ( - self.config.env.set_known_nodes(self.node_name_list) - .runner.pcmk.can_fence_history_status() - .runner.pcmk.load_state_plaintext( - verbose=True, - inactive=False, - fence_history=True, - stdout="crm_mon cluster status", - ) - .fs.exists(settings.corosync_conf_file, return_value=True) - .corosync_conf.load(node_name_list=self.node_name_list) - .runner.cib.load( - resources=""" + self.config.env.set_known_nodes(self.node_name_list) + self.config.runner.pcmk.can_fence_history_status() + self.config.runner.pcmk.load_state_plaintext( + verbose=True, + inactive=False, + fence_history=True, + stdout="crm_mon cluster status", + ) + self.config.fs.exists(settings.corosync_conf_file, return_value=True) + self.config.corosync_conf.load(node_name_list=self.node_name_list) + self.config.runner.cib.load( + resources=""" """ - ) - .runner.pcmk.load_ticket_state_plaintext(stdout="ticket status") - .services.is_running( - "sbd", return_value=False, name="services.is_running.sbd" - ) + ) + self.config.runner.pcmk.load_ticket_state_plaintext( + stdout="ticket status" + ) + self.config.services.is_running( + "sbd", return_value=False, name="services.is_running.sbd" ) self._fixture_config_local_daemons() - ( - self.config.http.host.check_reachability( - node_labels=self.node_name_list - ) + self.config.http.host.check_reachability( + node_labels=self.node_name_list ) self.config.fs.isfile(settings.crm_rule_exec, return_value=True) @@ -592,34 +564,29 @@ def test_success_verbose_inactive_and_fence_history(self): ) def _assert_success_with_ticket_status_failure(self, stderr="", msg=""): - ( - self.config.env.set_known_nodes(self.node_name_list) - .runner.pcmk.can_fence_history_status(stderr="not supported") - .runner.pcmk.load_state_plaintext( - verbose=True, - stdout="crm_mon cluster status", - ) - .fs.exists(settings.corosync_conf_file, return_value=True) - .corosync_conf.load(node_name_list=self.node_name_list) - .runner.cib.load( - resources=""" + self.config.env.set_known_nodes(self.node_name_list) + self.config.runner.pcmk.can_fence_history_status(stderr="not supported") + self.config.runner.pcmk.load_state_plaintext( + verbose=True, stdout="crm_mon cluster status" + ) + self.config.fs.exists(settings.corosync_conf_file, return_value=True) + self.config.corosync_conf.load(node_name_list=self.node_name_list) + self.config.runner.cib.load( + resources=""" """ - ) - .runner.pcmk.load_ticket_state_plaintext( - stdout="ticket stdout", stderr=stderr, returncode=1 - ) - .services.is_running( - "sbd", return_value=False, name="services.is_running.sbd" - ) + ) + self.config.runner.pcmk.load_ticket_state_plaintext( + stdout="ticket stdout", stderr=stderr, returncode=1 + ) + self.config.services.is_running( + "sbd", return_value=False, name="services.is_running.sbd" ) self._fixture_config_local_daemons() - ( - self.config.http.host.check_reachability( - node_labels=self.node_name_list - ) + self.config.http.host.check_reachability( + node_labels=self.node_name_list ) self.config.fs.isfile(settings.crm_rule_exec, return_value=True) @@ -657,16 +624,14 @@ def test_success_with_ticket_status_failure_with_message(self): ) def test_stonith_warning_no_devices(self): - ( - self.config.runner.pcmk.load_state_plaintext( - stdout="crm_mon cluster status", - ) - .fs.exists(settings.corosync_conf_file, return_value=True) - .corosync_conf.load() - .runner.cib.load() - .services.is_running( - "sbd", return_value=False, name="services.is_running.sbd" - ) + self.config.runner.pcmk.load_state_plaintext( + stdout="crm_mon cluster status" + ) + self.config.fs.exists(settings.corosync_conf_file, return_value=True) + self.config.corosync_conf.load() + self.config.runner.cib.load() + self.config.services.is_running( + "sbd", return_value=False, name="services.is_running.sbd" ) self._fixture_config_local_daemons() self.config.fs.isfile(settings.crm_rule_exec, return_value=True) @@ -690,16 +655,14 @@ def test_stonith_warning_no_devices(self): ) def test_stonith_warning_no_devices_sbd_enabled(self): - ( - self.config.runner.pcmk.load_state_plaintext( - stdout="crm_mon cluster status", - ) - .fs.exists(settings.corosync_conf_file, return_value=True) - .corosync_conf.load() - .runner.cib.load() - .services.is_running( - "sbd", return_value=True, name="services.is_running.sbd" - ) + self.config.runner.pcmk.load_state_plaintext( + stdout="crm_mon cluster status" + ) + self.config.fs.exists(settings.corosync_conf_file, return_value=True) + self.config.corosync_conf.load() + self.config.runner.cib.load() + self.config.services.is_running( + "sbd", return_value=True, name="services.is_running.sbd" ) self._fixture_config_local_daemons() self.config.fs.isfile(settings.crm_rule_exec, return_value=True) @@ -719,14 +682,13 @@ def test_stonith_warning_no_devices_sbd_enabled(self): ) def test_stonith_warnings_regarding_devices_configuration(self): - ( - self.config.runner.pcmk.load_state_plaintext( - stdout="crm_mon cluster status", - ) - .fs.exists(settings.corosync_conf_file, return_value=True) - .corosync_conf.load() - .runner.cib.load( - resources=""" + self.config.runner.pcmk.load_state_plaintext( + stdout="crm_mon cluster status" + ) + self.config.fs.exists(settings.corosync_conf_file, return_value=True) + self.config.corosync_conf.load() + self.config.runner.cib.load( + resources=""" @@ -752,10 +714,9 @@ def test_stonith_warnings_regarding_devices_configuration(self): """ - ) - .services.is_running( - "sbd", return_value=False, name="services.is_running.sbd" - ) + ) + self.config.services.is_running( + "sbd", return_value=False, name="services.is_running.sbd" ) self._fixture_config_local_daemons() self.config.fs.isfile(settings.crm_rule_exec, return_value=True) @@ -781,54 +742,40 @@ def test_stonith_warnings_regarding_devices_configuration(self): def test_pcsd_status_issues(self): self.node_name_list = ["node1", "node2", "node3", "node4", "node5"] - - ( - self.config.env.set_known_nodes(self.node_name_list[1:]) - .runner.pcmk.can_fence_history_status(stderr="not supported") - .runner.pcmk.load_state_plaintext( - verbose=True, - stdout="crm_mon cluster status", - ) - .fs.exists(settings.corosync_conf_file, return_value=True) - .corosync_conf.load(node_name_list=self.node_name_list) - .runner.cib.load( - resources=""" + self.config.env.set_known_nodes(self.node_name_list[1:]) + self.config.runner.pcmk.can_fence_history_status(stderr="not supported") + self.config.runner.pcmk.load_state_plaintext( + verbose=True, stdout="crm_mon cluster status" + ) + self.config.fs.exists(settings.corosync_conf_file, return_value=True) + self.config.corosync_conf.load(node_name_list=self.node_name_list) + self.config.runner.cib.load( + resources=""" """ - ) - .runner.pcmk.load_ticket_state_plaintext(stdout="ticket status") - .services.is_running( - "sbd", return_value=False, name="services.is_running.sbd" - ) + ) + self.config.runner.pcmk.load_ticket_state_plaintext( + stdout="ticket status" + ) + self.config.services.is_running( + "sbd", return_value=False, name="services.is_running.sbd" ) self._fixture_config_local_daemons() - ( - self.config.http.host.check_reachability( - communication_list=[ - # node1 has no record in known-hosts - dict( - label="node2", - was_connected=False, - errno=7, - error_msg="node2 error", - ), - dict( - label="node3", - response_code=401, - output="node3 output", - ), - dict( - label="node4", - response_code=500, - output="node4 output", - ), - dict( - label="node5", - ), - ] - ) + self.config.http.host.check_reachability( + communication_list=[ + # node1 has no record in known-hosts + dict( + label="node2", + was_connected=False, + errno=7, + error_msg="node2 error", + ), + dict(label="node3", response_code=401, output="node3 output"), + dict(label="node4", response_code=500, output="node4 output"), + dict(label="node5"), + ] ) self.config.fs.isfile(settings.crm_rule_exec, return_value=True) @@ -938,7 +885,7 @@ def test_move_constrains_warnings(self): - """, + """, resources=""" - """, + """, ) self.config.services.is_running( "sbd", return_value=True, name="services.is_running.sbd" @@ -997,7 +944,7 @@ def test_expired_move_constraints_warnings(self): - """, + """, resources=""" - """, + """, ) self.config.services.is_running( "sbd", return_value=True, name="services.is_running.sbd" @@ -1066,7 +1013,7 @@ def test_expired_and_in_effect_move_constraints_warnings(self): - """, + """, resources=""" - """, + """, ) self.config.services.is_running( "sbd", return_value=True, name="services.is_running.sbd" @@ -1338,7 +1285,12 @@ def test_bad_xml(self): self.config.runner.pcmk.load_state( resources=""" - + """, ) @@ -1525,11 +1477,31 @@ def test_bundle_skip(self): self.config.runner.pcmk.load_state( resources=""" - + - - - + + + diff --git a/pcs_test/tier0/lib/pacemaker/test_live.py b/pcs_test/tier0/lib/pacemaker/test_live.py index fab9f8291..688b9c202 100644 --- a/pcs_test/tier0/lib/pacemaker/test_live.py +++ b/pcs_test/tier0/lib/pacemaker/test_live.py @@ -1,14 +1,10 @@ # pylint: disable=too-many-lines -from unittest import ( - TestCase, - mock, -) +from unittest import TestCase, mock from lxml import etree import pcs.lib.pacemaker.live as lib from pcs import settings -from pcs.common.reports import ReportItemSeverity as Severity from pcs.common.reports import codes as report_codes from pcs.common.tools import Version from pcs.common.types import CibRuleInEffectStatus @@ -16,10 +12,7 @@ from pcs.lib.pacemaker import api_result from pcs.lib.resource_agent import ResourceAgentName -from pcs_test.tools import ( - fixture, - fixture_crm_mon, -) +from pcs_test.tools import fixture, fixture_crm_mon from pcs_test.tools.assertions import ( assert_raise_library_error, assert_report_item_list_equal, @@ -27,16 +20,10 @@ start_tag_error_text, ) from pcs_test.tools.command_env import get_env_tools -from pcs_test.tools.custom_mock import ( - TmpFileCall, - TmpFileMock, -) +from pcs_test.tools.custom_mock import TmpFileCall, TmpFileMock from pcs_test.tools.custom_mock import get_runner_mock as get_runner from pcs_test.tools.misc import get_test_resource as rc -from pcs_test.tools.xml import ( - XmlManipulation, - etree_to_str, -) +from pcs_test.tools.xml import XmlManipulation, etree_to_str _EXITCODE_NOT_CONNECTED = 102 @@ -380,11 +367,10 @@ def test_error(self): assert_raise_library_error( lambda: lib.get_cib_xml(mock_runner), ( - Severity.ERROR, - report_codes.CIB_LOAD_ERROR, - { - "reason": expected_stderr + "\n" + expected_stdout, - }, + fixture.error( + report_codes.CIB_LOAD_ERROR, + reason=expected_stderr + "\n" + expected_stdout, + ) ), ) @@ -430,12 +416,11 @@ def test_scope_error(self): assert_raise_library_error( lambda: lib.get_cib_xml(mock_runner, scope=scope), ( - Severity.ERROR, - report_codes.CIB_LOAD_ERROR_SCOPE_MISSING, - { - "scope": scope, - "reason": expected_stderr + "\n" + expected_stdout, - }, + fixture.error( + report_codes.CIB_LOAD_ERROR_SCOPE_MISSING, + scope=scope, + reason=expected_stderr + "\n" + expected_stdout, + ) ), ) @@ -632,12 +617,11 @@ def test_error(self): mock_runner, XmlManipulation.from_str(xml).tree ), ( - Severity.ERROR, - report_codes.CIB_PUSH_ERROR, - { - "reason": expected_stderr, - "pushed_cib": expected_stdout, - }, + fixture.error( + report_codes.CIB_PUSH_ERROR, + reason=expected_stderr, + pushed_cib=expected_stdout, + ) ), ) @@ -674,11 +658,10 @@ def test_error(self): assert_raise_library_error( lambda: lib._upgrade_cib(mock_runner), ( - Severity.ERROR, - report_codes.CIB_UPGRADE_FAILED, - { - "reason": expected_stderr + "\n" + expected_stdout, - }, + fixture.error( + report_codes.CIB_UPGRADE_FAILED, + reason=expected_stderr + "\n" + expected_stdout, + ) ), ) mock_runner.run.assert_called_once_with( @@ -740,9 +723,11 @@ def test_upgraded_lower_version(self, mock_upgrade, mock_get_cib): self.mock_runner, self.cib, Version(2, 3, 5) ), ( - Severity.ERROR, - report_codes.CIB_UPGRADE_FAILED_TO_MINIMAL_REQUIRED_VERSION, - {"required_version": "2.3.5", "current_version": "2.3.4"}, + fixture.error( + report_codes.CIB_UPGRADE_FAILED_TO_MINIMAL_REQUIRED_VERSION, + required_version="2.3.5", + current_version="2.3.4", + ) ), ) mock_upgrade.assert_called_once_with(self.mock_runner) @@ -769,11 +754,10 @@ def test_cib_parse_error(self, mock_upgrade, mock_get_cib): self.mock_runner, self.cib, Version(2, 3, 5) ), ( - Severity.ERROR, - report_codes.CIB_UPGRADE_FAILED, - { - "reason": start_tag_error_text(), - }, + fixture.error( + report_codes.CIB_UPGRADE_FAILED, + reason=start_tag_error_text(), + ) ), ) mock_upgrade.assert_called_once_with(self.mock_runner) @@ -1036,11 +1020,10 @@ def test_error_not_connected(self): cm.exception.args, [ ( - Severity.ERROR, - report_codes.PACEMAKER_LOCAL_NODE_NAME_NOT_FOUND, - { - "reason": stderr.strip(), - }, + fixture.error( + report_codes.PACEMAKER_LOCAL_NODE_NAME_NOT_FOUND, + reason=stderr.strip(), + ) ), ], ) @@ -1163,13 +1146,12 @@ def test_error(self): assert_raise_library_error( lambda: lib.remove_node(mock_runner, "NODE_NAME"), ( - Severity.ERROR, - report_codes.NODE_REMOVE_IN_PACEMAKER_FAILED, - { - "node": "", - "node_list_to_remove": ["NODE_NAME"], - "reason": expected_stderr, - }, + fixture.error( + report_codes.NODE_REMOVE_IN_PACEMAKER_FAILED, + node="", + node_list_to_remove=["NODE_NAME"], + reason=expected_stderr, + ) ), ) @@ -1523,11 +1505,10 @@ def test_wait_error(self): assert_raise_library_error( lambda: lib.wait_for_idle(mock_runner, 0), ( - Severity.ERROR, - report_codes.WAIT_FOR_IDLE_ERROR, - { - "reason": expected_stderr + "\n" + expected_stdout, - }, + fixture.error( + report_codes.WAIT_FOR_IDLE_ERROR, + reason=expected_stderr + "\n" + expected_stdout, + ) ), ) @@ -1546,11 +1527,10 @@ def test_wait_error_timeout(self): assert_raise_library_error( lambda: lib.wait_for_idle(mock_runner, 0), ( - Severity.ERROR, - report_codes.WAIT_FOR_IDLE_TIMED_OUT, - { - "reason": expected_stderr + "\n" + expected_stdout, - }, + fixture.error( + report_codes.WAIT_FOR_IDLE_TIMED_OUT, + reason=expected_stderr + "\n" + expected_stdout, + ) ), ) @@ -1661,9 +1641,10 @@ def assert_command_failure( self.CRM_ATTRS, ), ( - Severity.ERROR, - report_codes.UNABLE_TO_GET_RESOURCE_OPERATION_DIGESTS, - {"output": report_output}, + fixture.error( + report_codes.UNABLE_TO_GET_RESOURCE_OPERATION_DIGESTS, + output=report_output, + ) ), ) runner.run.assert_called_once_with(self.CALL_ARGS) From 64592050ed97e79265651385a33520a8abf7ecda Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Fri, 11 Apr 2025 15:37:58 +0200 Subject: [PATCH 132/227] fix test framework --- pcs_test/tools/command_env/calls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pcs_test/tools/command_env/calls.py b/pcs_test/tools/command_env/calls.py index caae7f6d1..d44febbd8 100644 --- a/pcs_test/tools/command_env/calls.py +++ b/pcs_test/tools/command_env/calls.py @@ -63,7 +63,7 @@ def __unexpected_type(self, call, real_type, real_call_info): "\nHint: check call compatibility: for example if you use" " env.push_cib() then runner.cib.push() will be never launched" ).format( - self.__index + 1, + self.__index, call.type, real_type, call, From 86d32ac814643da46c0bb0257ad9e3659af062c5 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Tue, 15 Apr 2025 12:24:53 +0200 Subject: [PATCH 133/227] remove tests for unsupported pacemaker version --- pcs_test/tier1/test_status.py | 305 +++++++++------------------------- 1 file changed, 78 insertions(+), 227 deletions(-) diff --git a/pcs_test/tier1/test_status.py b/pcs_test/tier1/test_status.py index 27e24dc35..fd02ad9f3 100644 --- a/pcs_test/tier1/test_status.py +++ b/pcs_test/tier1/test_status.py @@ -9,15 +9,11 @@ from pcs_test.tools.misc import get_test_resource as rc from pcs_test.tools.misc import ( get_tmp_file, - is_minimum_pacemaker_version, - is_pacemaker_21_without_20_compatibility, outdent, write_file_to_tmpfile, ) from pcs_test.tools.pcs_runner import PcsRunner -PCMK_2_0_3_PLUS = is_minimum_pacemaker_version(2, 0, 3) - class StonithWarningTest(TestCase, AssertPcsMixin): empty_cib = rc("cib-empty.xml") @@ -64,138 +60,73 @@ def test_warning_stonith_action(self): self.fixture_stonith_action() self.fixture_resource() self.pcs_runner.corosync_conf_opt = self.corosync_conf - if PCMK_2_0_3_PLUS: - self.assert_pcs_success( - ["status"], - stdout_start=dedent( - """\ - Cluster name: test99 + self.assert_pcs_success( + ["status"], + stdout_start=dedent( + """\ + Cluster name: test99 - WARNINGS: - Following stonith devices have the 'action' option set, it is recommended to set 'pcmk_off_action', 'pcmk_reboot_action' instead: 'Sa' + WARNINGS: + Following stonith devices have the 'action' option set, it is recommended to set 'pcmk_off_action', 'pcmk_reboot_action' instead: 'Sa' - Cluster Summary: - """ - ), - ) - else: - self.assert_pcs_success( - ["status"], - stdout_start=dedent( - """\ - Cluster name: test99 - - WARNINGS: - Following stonith devices have the 'action' option set, it is recommended to set 'pcmk_off_action', 'pcmk_reboot_action' instead: 'Sa' - - Stack: unknown - Current DC: NONE + Cluster Summary: """ - ), - ) + ), + ) def test_warning_stonith_method_cycle(self): self.fixture_stonith_cycle() self.fixture_resource() self.pcs_runner.corosync_conf_opt = self.corosync_conf - if PCMK_2_0_3_PLUS: - self.assert_pcs_success( - ["status"], - stdout_start=dedent( - """\ - Cluster name: test99 + self.assert_pcs_success( + ["status"], + stdout_start=dedent( + """\ + Cluster name: test99 - WARNINGS: - Following stonith devices have the 'method' option set to 'cycle' which is potentially dangerous, please consider using 'onoff': 'Sc' + WARNINGS: + Following stonith devices have the 'method' option set to 'cycle' which is potentially dangerous, please consider using 'onoff': 'Sc' - Cluster Summary: + Cluster Summary: """ - ), - ) - else: - self.assert_pcs_success( - ["status"], - stdout_start=dedent( - """\ - Cluster name: test99 - - WARNINGS: - Following stonith devices have the 'method' option set to 'cycle' which is potentially dangerous, please consider using 'onoff': 'Sc' - - Stack: unknown - Current DC: NONE - """ - ), - ) + ), + ) def test_stonith_warnings(self): self.fixture_stonith_action() self.fixture_stonith_cycle() self.fixture_resource() self.pcs_runner.corosync_conf_opt = self.corosync_conf - if PCMK_2_0_3_PLUS: - self.assert_pcs_success( - ["status"], - stdout_start=dedent( - """\ - Cluster name: test99 - - WARNINGS: - Following stonith devices have the 'action' option set, it is recommended to set 'pcmk_off_action', 'pcmk_reboot_action' instead: 'Sa' - Following stonith devices have the 'method' option set to 'cycle' which is potentially dangerous, please consider using 'onoff': 'Sc' - - Cluster Summary: - """ - ), - ) - else: - self.assert_pcs_success( - ["status"], - stdout_start=dedent( - """\ - Cluster name: test99 - - WARNINGS: - Following stonith devices have the 'action' option set, it is recommended to set 'pcmk_off_action', 'pcmk_reboot_action' instead: 'Sa' - Following stonith devices have the 'method' option set to 'cycle' which is potentially dangerous, please consider using 'onoff': 'Sc' - - Stack: unknown - Current DC: NONE + self.assert_pcs_success( + ["status"], + stdout_start=dedent( + """\ + Cluster name: test99 + + WARNINGS: + Following stonith devices have the 'action' option set, it is recommended to set 'pcmk_off_action', 'pcmk_reboot_action' instead: 'Sa' + Following stonith devices have the 'method' option set to 'cycle' which is potentially dangerous, please consider using 'onoff': 'Sc' + + Cluster Summary: """ - ), - ) + ), + ) def test_warn_when_no_stonith(self): self.pcs_runner.corosync_conf_opt = self.corosync_conf - if PCMK_2_0_3_PLUS: - self.assert_pcs_success( - ["status"], - stdout_start=dedent( - """\ - Cluster name: test99 + self.assert_pcs_success( + ["status"], + stdout_start=dedent( + """\ + Cluster name: test99 - WARNINGS: - No stonith devices and stonith-enabled is not false + WARNINGS: + No stonith devices and stonith-enabled is not false - Cluster Summary: + Cluster Summary: """ - ), - ) - else: - self.assert_pcs_success( - ["status"], - stdout_start=dedent( - """\ - Cluster name: test99 - - WARNINGS: - No stonith devices and stonith-enabled is not false - - Stack: unknown - Current DC: NONE - """ - ), - ) + ), + ) def test_no_stonith_warning_when_stonith_in_group(self): self.assert_pcs_success( @@ -206,53 +137,28 @@ def test_no_stonith_warning_when_stonith_in_group(self): ), ) self.pcs_runner.corosync_conf_opt = self.corosync_conf - if PCMK_2_0_3_PLUS: - self.assert_pcs_success( - ["status"], - stdout_start=dedent( - """\ - Cluster name: test99 - Cluster Summary: - """ - ), - ) - else: - self.assert_pcs_success( - ["status"], - stdout_start=dedent( - """\ - Cluster name: test99 - - Stack: unknown - Current DC: NONE + self.assert_pcs_success( + ["status"], + stdout_start=dedent( + """\ + Cluster name: test99 + Cluster Summary: """ - ), - ) + ), + ) def test_disabled_stonith_does_not_care_about_missing_devices(self): self.assert_pcs_success("property set stonith-enabled=false".split()) self.pcs_runner.corosync_conf_opt = self.corosync_conf - if PCMK_2_0_3_PLUS: - self.assert_pcs_success( - ["status"], - stdout_start=dedent( - """\ - Cluster name: test99 - Cluster Summary: - """ - ), - ) - else: - self.assert_pcs_success( - ["status"], - stdout_start=dedent( - """\ - Cluster name: test99 - Stack: unknown - Current DC: NONE + self.assert_pcs_success( + ["status"], + stdout_start=dedent( + """\ + Cluster name: test99 + Cluster Summary: """ - ), - ) + ), + ) class ResourceStonithStatusBase(AssertPcsMixin): @@ -315,13 +221,9 @@ def test_more_no_node_option(self): ) def test_resource_id(self): - if is_pacemaker_21_without_20_compatibility(): - stdout_full = " * x1 (ocf:pcsmock:minimal): Started rh-1\n" - else: - stdout_full = " * x1 (ocf::pcsmock:minimal): Started rh-1\n" self.assert_pcs_success( self.command + ["x1"], - stdout_full=stdout_full, + stdout_full=" * x1 (ocf:pcsmock:minimal): Started rh-1\n", ) def test_resource_id_hide_inactive(self): @@ -337,23 +239,15 @@ def test_resource_id_with_node_hide_inactive(self): ) def test_resource_id_with_node_started(self): - if is_pacemaker_21_without_20_compatibility(): - stdout_full = " * x1 (ocf:pcsmock:minimal): Started rh-1\n" - else: - stdout_full = " * x1 (ocf::pcsmock:minimal): Started rh-1\n" self.assert_pcs_success( self.command + ["x1", "node=rh-1"], - stdout_full=stdout_full, + stdout_full=" * x1 (ocf:pcsmock:minimal): Started rh-1\n", ) def test_resource_id_with_node_stopped(self): - if is_pacemaker_21_without_20_compatibility(): - stdout_full = " * x2 (ocf:pcsmock:minimal): Stopped\n" - else: - stdout_full = " * x2 (ocf::pcsmock:minimal): Stopped\n" self.assert_pcs_success( self.command + ["x2", "node=rh-1"], - stdout_full=stdout_full, + stdout_full=" * x2 (ocf:pcsmock:minimal): Stopped\n", ) def test_resource_id_with_node_without_status(self): @@ -363,13 +257,9 @@ def test_resource_id_with_node_without_status(self): ) def test_resource_id_with_node_changed_arg_order(self): - if is_pacemaker_21_without_20_compatibility(): - stdout_full = " * x1 (ocf:pcsmock:minimal): Started rh-1\n" - else: - stdout_full = " * x1 (ocf::pcsmock:minimal): Started rh-1\n" self.assert_pcs_success( self.command + ["node=rh-1", "x1"], - stdout_full=stdout_full, + stdout_full=" * x1 (ocf:pcsmock:minimal): Started rh-1\n", ) def test_stonith_id(self): @@ -409,27 +299,16 @@ def test_stonith_id_with_node_without_status(self): ) def test_tag_id(self): - if is_pacemaker_21_without_20_compatibility(): - stdout_full = outdent( + self.assert_pcs_success( + self.command + ["tag-mixed-stonith-devices-and-resources"], + stdout_full=outdent( """\ * fence-rh-1 (stonith:fence_pcsmock_minimal): Started rh-1 * fence-rh-2 (stonith:fence_pcsmock_minimal): Stopped * x3 (ocf:pcsmock:minimal): Stopped * y1 (ocf:pcsmock:minimal): Stopped """ - ) - else: - stdout_full = outdent( - """\ - * fence-rh-1 (stonith:fence_pcsmock_minimal): Started rh-1 - * fence-rh-2 (stonith:fence_pcsmock_minimal): Stopped - * x3 (ocf::pcsmock:minimal): Stopped - * y1 (ocf::pcsmock:minimal): Stopped - """ - ) - self.assert_pcs_success( - self.command + ["tag-mixed-stonith-devices-and-resources"], - stdout_full=stdout_full, + ), ) def test_tag_id_hide_inactive(self): @@ -444,26 +323,16 @@ def test_tag_id_hide_inactive(self): ) def test_tag_id_with_node(self): - if is_pacemaker_21_without_20_compatibility(): - stdout_full = outdent( + self.assert_pcs_success( + self.command + + ["tag-mixed-stonith-devices-and-resources", "node=rh-2"], + stdout_full=outdent( """\ * fence-rh-2 (stonith:fence_pcsmock_minimal): Stopped * x3 (ocf:pcsmock:minimal): Stopped * y1 (ocf:pcsmock:minimal): Stopped """ - ) - else: - stdout_full = outdent( - """\ - * fence-rh-2 (stonith:fence_pcsmock_minimal): Stopped - * x3 (ocf::pcsmock:minimal): Stopped - * y1 (ocf::pcsmock:minimal): Stopped - """ - ) - self.assert_pcs_success( - self.command - + ["tag-mixed-stonith-devices-and-resources", "node=rh-2"], - stdout_full=stdout_full, + ), ) def test_tag_id_with_node_hide_inactive(self): @@ -600,37 +469,19 @@ class StonithStatus(ResourceStonithStatusBase, TestCase): def fixture_resources_status_output(nodes="rh-1 rh-2", inactive=True): if not inactive: - if is_pacemaker_21_without_20_compatibility(): - return outdent( - """\ - * x1 (ocf:pcsmock:minimal): Started rh-1 - """ - ) return outdent( """\ - * x1 (ocf::pcsmock:minimal): Started rh-1 - """ - ) - - if is_pacemaker_21_without_20_compatibility(): - return outdent( - f"""\ - * not-in-tags (ocf:pcsmock:minimal): Stopped * x1 (ocf:pcsmock:minimal): Started rh-1 - * x2 (ocf:pcsmock:minimal): Stopped - * x3 (ocf:pcsmock:minimal): Stopped - * y1 (ocf:pcsmock:minimal): Stopped - * Clone Set: y2-clone [y2]: - * Stopped: [ {nodes} ] """ ) + return outdent( f"""\ - * not-in-tags (ocf::pcsmock:minimal): Stopped - * x1 (ocf::pcsmock:minimal): Started rh-1 - * x2 (ocf::pcsmock:minimal): Stopped - * x3 (ocf::pcsmock:minimal): Stopped - * y1 (ocf::pcsmock:minimal): Stopped + * not-in-tags (ocf:pcsmock:minimal): Stopped + * x1 (ocf:pcsmock:minimal): Started rh-1 + * x2 (ocf:pcsmock:minimal): Stopped + * x3 (ocf:pcsmock:minimal): Stopped + * y1 (ocf:pcsmock:minimal): Stopped * Clone Set: y2-clone [y2]: * Stopped: [ {nodes} ] """ From cf5d3401e0cc8b2c05407893fa35a3ba2c4b83e5 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Tue, 15 Apr 2025 13:42:10 +0200 Subject: [PATCH 134/227] add crm_verify messages in pcs status --- CHANGELOG.md | 3 + pcs/common/reports/codes.py | 1 + pcs/common/reports/messages.py | 18 ++ pcs/lib/commands/status.py | 17 ++ pcs/lib/pacemaker/live.py | 53 +++++- .../tier0/common/reports/test_messages.py | 14 ++ pcs_test/tier0/lib/commands/test_status.py | 170 ++++++++++++++++++ pcs_test/tier0/lib/pacemaker/test_live.py | 77 ++++++++ pcs_test/tier1/test_status.py | 10 ++ .../tools/command_env/config_runner_pcmk.py | 59 ++++-- 10 files changed, 403 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98ad42c39..5b496eff1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ - Prevent removing or disabling stonith devices or disabling SBD if the cluster would be left with disabled SBD and no stonith devices ([RHEL-76170]) - Support for exporting alerts in `json` and `cmd` formats ([RHEL-76153]) +- Output of `pcs status` now contains messages about CIB misconfiguration + provided by `crm_verify` pacemaker tool ([RHEL-76060]) ### Fixed - Fixed a traceback when removing a resource fails in web UI @@ -20,6 +22,7 @@ [RHEL-76055]: https://issues.redhat.com/browse/RHEL-76055 [RHEL-76059]: https://issues.redhat.com/browse/RHEL-76059 +[RHEL-76060]: https://issues.redhat.com/browse/RHEL-76060 [RHEL-76153]: https://issues.redhat.com/browse/RHEL-76153 [RHEL-76170]: https://issues.redhat.com/browse/RHEL-76170 [RHEL-76177]: https://issues.redhat.com/browse/RHEL-76177 diff --git a/pcs/common/reports/codes.py b/pcs/common/reports/codes.py index 393fceb76..fed7d4cb0 100644 --- a/pcs/common/reports/codes.py +++ b/pcs/common/reports/codes.py @@ -51,6 +51,7 @@ AGENT_SELF_VALIDATION_RESULT = M("AGENT_SELF_VALIDATION_RESULT") BAD_CLUSTER_STATE_FORMAT = M("BAD_CLUSTER_STATE_FORMAT") BAD_CLUSTER_STATE_DATA = M("BAD_CLUSTER_STATE_DATA") +BAD_PCMK_API_RESPONSE_FORMAT = M("BAD_PCMK_API_RESPONSE_FORMAT") BOOTH_ADDRESS_DUPLICATION = M("BOOTH_ADDRESS_DUPLICATION") BOOTH_ALREADY_IN_CIB = M("BOOTH_ALREADY_IN_CIB") BOOTH_AUTHFILE_NOT_USED = M("BOOTH_AUTHFILE_NOT_USED") diff --git a/pcs/common/reports/messages.py b/pcs/common/reports/messages.py index e67f48df0..e8e262987 100644 --- a/pcs/common/reports/messages.py +++ b/pcs/common/reports/messages.py @@ -3261,6 +3261,24 @@ def message(self) -> str: ) +@dataclass(frozen=True) +class BadPcmkApiResponseFormat(ReportItemMessage): + """ + Structured response from pacemaker doesn't match expected format / schema + """ + + _code = codes.BAD_PCMK_API_RESPONSE_FORMAT + reason: str + api_response: str + + @property + def message(self) -> str: + return ( + "Cannot process pacemaker response due to a parse error: " + f"{self.reason}\n{self.api_response}" + ) + + @dataclass(frozen=True) class BadClusterStateFormat(ReportItemMessage): """ diff --git a/pcs/lib/commands/status.py b/pcs/lib/commands/status.py index 650dc9693..05b1052ea 100644 --- a/pcs/lib/commands/status.py +++ b/pcs/lib/commands/status.py @@ -46,6 +46,8 @@ from pcs.lib.node import get_existing_nodes_names from pcs.lib.node_communication import NodeTargetLibFactory from pcs.lib.pacemaker.live import ( + BadApiResultFormat, + get_cib_verification_errors, get_cluster_status_text, get_cluster_status_xml_raw, get_ticket_status_text, @@ -147,6 +149,20 @@ def full_cluster_status_plaintext( # noqa: PLR0912, PLR0915 if not live or os.path.exists(settings.corosync_conf_file): corosync_conf = env.get_corosync_conf() cib = env.get_cib() + # get messages from crm_verify + crm_verify_messages = [] + try: + crm_verify_messages = get_cib_verification_errors(runner) + except BadApiResultFormat as e: + # do not fail the whole command just because we cannot load this + report_processor.report( + reports.ReportItem.debug( + reports.messages.BadPcmkApiResponseFormat( + str(e.original_exception), e.pacemaker_response + ) + ) + ) + # get extra info for verbose output if verbose: ( ticket_status_text, @@ -182,6 +198,7 @@ def full_cluster_status_plaintext( # noqa: PLR0912, PLR0915 warning_list.extend( _booth_authfile_warning(env.report_processor, env.get_booth_env(None)) ) + warning_list.extend(crm_verify_messages) # put it all together if report_processor.has_errors: diff --git a/pcs/lib/pacemaker/live.py b/pcs/lib/pacemaker/live.py index b96467661..da1154f53 100644 --- a/pcs/lib/pacemaker/live.py +++ b/pcs/lib/pacemaker/live.py @@ -34,6 +34,7 @@ from pcs.lib.resource_agent import ResourceAgentName from pcs.lib.xml_tools import etree_to_str +__EXITCODE_INVALID_CIB = 78 __EXITCODE_NOT_CONNECTED = 102 __EXITCODE_CIB_SCOPE_VALID_BUT_NOT_PRESENT = 105 __EXITCODE_WAIT_TIMEOUT = 124 @@ -48,6 +49,12 @@ class FenceHistoryCommandErrorException(Exception): pass +class BadApiResultFormat(Exception): + def __init__(self, original_exception: Exception, pacemaker_response: str): + self.original_exception = original_exception + self.pacemaker_response = pacemaker_response + + ### status @@ -195,25 +202,35 @@ def get_cib(xml: str) -> _Element: ) from e -def verify( - runner: CommandRunner, verbose: bool = False -) -> tuple[str, str, int, bool]: +def _run_crm_verify( + runner: CommandRunner, xml_output: bool = False, verbose: bool = False +) -> tuple[str, str, int]: crm_verify_cmd = [settings.crm_verify_exec] # Currently, crm_verify can suggest up to two -V options but it accepts # more than two. We stick with two -V options if verbose mode was enabled. if verbose: crm_verify_cmd.extend(["-V", "-V"]) - # With the `crm_verify` command it is not possible simply use the - # environment variable CIB_file because `crm_verify` simply tries to - # connect to cib file via tool that can fail because: Update does not - # conform to the configured schema + if xml_output: + crm_verify_cmd.extend(["--output-as", "xml"]) + # With the `crm_verify` command it is not possible to simply use the + # environment variable CIB_file because `crm_verify` tries to connect to + # cib file via tool that can fail because: Update does not conform to the + # configured schema # So we use the explicit flag `--xml-file`. cib_tmp_file = runner.env_vars.get("CIB_file", None) if cib_tmp_file is None: crm_verify_cmd.append("--live-check") else: crm_verify_cmd.extend(["--xml-file", cib_tmp_file]) - stdout, stderr, returncode = runner.run(crm_verify_cmd) + return runner.run(crm_verify_cmd) + + +def verify( + runner: CommandRunner, verbose: bool = False +) -> tuple[str, str, int, bool]: + stdout, stderr, returncode = _run_crm_verify( + runner, xml_output=False, verbose=verbose + ) can_be_more_verbose = False if returncode != 0: # remove lines with -V options @@ -233,6 +250,26 @@ def verify( return stdout, stderr, returncode, can_be_more_verbose +def get_cib_verification_errors(runner: CommandRunner) -> list[str]: + # Uses XML output of crm_verify which is easier to work with. Verbose mode + # is not needed, it only adds debug messages outside of the XML. We don't + # need to filter out hints to add more -V to increase verbosity, as they + # are not printed by crm_verify in XML output mode. + + # in case of invalid configuration, returncode != 0 - it cannot be used to + # determine whether the command succeeded or failed + stdout, stderr, returncode = _run_crm_verify( + runner, xml_output=True, verbose=False + ) + try: + api_status = _get_status_from_api_result(_get_api_result_dom(stdout)) + if api_status.code == __EXITCODE_INVALID_CIB: + return list(api_status.errors) + return [] + except (etree.XMLSyntaxError, etree.DocumentInvalid) as e: + raise BadApiResultFormat(e, join_multilines([stderr, stdout])) from e + + def replace_cib_configuration_xml(runner: CommandRunner, xml: str) -> None: cmd = [ settings.cibadmin_exec, diff --git a/pcs_test/tier0/common/reports/test_messages.py b/pcs_test/tier0/common/reports/test_messages.py index 14adcc085..7c6481342 100644 --- a/pcs_test/tier0/common/reports/test_messages.py +++ b/pcs_test/tier0/common/reports/test_messages.py @@ -2185,6 +2185,20 @@ def test_with_reason(self): ) +class BadPcmkApiResponseFormat(NameBuildTest): + def test_all(self): + self.assert_message_from_report( + ( + "Cannot process pacemaker response due to a parse error: " + "detailed parse or xml error\n" + "pacemaker tool output" + ), + reports.BadPcmkApiResponseFormat( + "detailed parse or xml error", "pacemaker tool output" + ), + ) + + class BadClusterStateFormat(NameBuildTest): def test_all(self): self.assert_message_from_report( diff --git a/pcs_test/tier0/lib/commands/test_status.py b/pcs_test/tier0/lib/commands/test_status.py index a9aded803..c4dd98d6d 100644 --- a/pcs_test/tier0/lib/commands/test_status.py +++ b/pcs_test/tier0/lib/commands/test_status.py @@ -34,6 +34,8 @@ from pcs_test.tools.misc import get_test_resource as rc from pcs_test.tools.misc import read_test_resource as rc_read +EXITCODE_INVALID_CIB = 78 + def _booth_config_path_fixture(instance_name="booth"): return os.path.join(settings.booth_config_dir, f"{instance_name}.conf") @@ -96,6 +98,46 @@ def _fixture_xml_clustername(name): """.format(name=name) + @staticmethod + def _fixture_crm_verify_success(): + return """ + + + + """ + + @staticmethod + def _fixture_crm_verify_invalid_cib(error_list): + errors = "\n".join(f"{error}" for error in error_list) + return f""" + + + {errors} + + + """ + + def _fixture_config_crm_verify( + self, stdout, stderr="", retval=0, cib_file=None + ): + self.config.runner.pcmk.verify_xml( + cib_tempfile=cib_file, + stdout=stdout, + stderr=stderr, + returncode=retval, + env=({"CIB_file": cib_file} if cib_file else None), + ) + self.config.fs.isfile( + settings.pacemaker_api_result_schema, + name="fs.exists.crm_verify_xml_schema", + ) + def _fixture_config_live_minimal(self): self.config.runner.pcmk.load_state_plaintext( stdout="crm_mon cluster status" @@ -109,6 +151,7 @@ def _fixture_config_live_minimal(self): """ ) + self._fixture_config_crm_verify(self._fixture_crm_verify_success()) self.config.services.is_running( "sbd", return_value=False, name="services.is_running.sbd" ) @@ -126,6 +169,7 @@ def _fixture_config_live_remote_minimal(self): """, ) + self._fixture_config_crm_verify(self._fixture_crm_verify_success()) self.config.services.is_running( "sbd", return_value=False, name="services.is_running.sbd" ) @@ -199,6 +243,9 @@ def _fixture_config_local_daemons( # noqa: PLR0913 @mock.patch("pcs.settings.booth_enable_authfile_set_enabled", False) @mock.patch("pcs.settings.booth_enable_authfile_unset_enabled", False) +@mock.patch.object( + settings, "pacemaker_api_result_schema", rc("pcmk_api_rng/api-result.rng") +) class FullClusterStatusPlaintext(FullClusterStatusPlaintextBase): # pylint: disable=too-many-public-methods def test_life_cib_mocked_corosync(self): @@ -330,6 +377,7 @@ def test_success_live_verbose(self): """ ) + self._fixture_config_crm_verify(self._fixture_crm_verify_success()) self.config.runner.pcmk.load_ticket_state_plaintext( stdout="ticket status" ) @@ -407,6 +455,7 @@ def test_success_live_remote_node_verbose(self): """, ) + self._fixture_config_crm_verify(self._fixture_crm_verify_success()) self.config.runner.pcmk.load_ticket_state_plaintext( stdout="ticket status" ) @@ -459,6 +508,9 @@ def test_success_mocked(self): """, env=env, ) + self._fixture_config_crm_verify( + self._fixture_crm_verify_success(), cib_file=tmp_file + ) self.config.fs.isfile(settings.crm_rule_exec, return_value=True) self.assertEqual( @@ -489,6 +541,9 @@ def test_success_mocked_verbose(self): """, env=env, ) + self._fixture_config_crm_verify( + self._fixture_crm_verify_success(), cib_file=tmp_file + ) self.config.runner.pcmk.load_ticket_state_plaintext( stdout="ticket status", env=env ) @@ -525,6 +580,7 @@ def test_success_verbose_inactive_and_fence_history(self): """ ) + self._fixture_config_crm_verify(self._fixture_crm_verify_success()) self.config.runner.pcmk.load_ticket_state_plaintext( stdout="ticket status" ) @@ -578,6 +634,7 @@ def _assert_success_with_ticket_status_failure(self, stderr="", msg=""): """ ) + self._fixture_config_crm_verify(self._fixture_crm_verify_success()) self.config.runner.pcmk.load_ticket_state_plaintext( stdout="ticket stdout", stderr=stderr, returncode=1 ) @@ -623,6 +680,104 @@ def test_success_with_ticket_status_failure_with_message(self): msg="\n ticket status error\n multiline", ) + def test_crm_verify_messages(self): + errors = [ + "error: Resource chr:0 is of type systemd and therefore cannot be used as a promotable clone resource", + "error: Ignoring <clone> resource 'chr-clone' because configuration is invalid", + "error: CIB did not pass schema validation", + "Configuration invalid (with errors)", + ] + self.config.runner.pcmk.load_state_plaintext( + stdout="crm_mon cluster status" + ) + self.config.fs.exists(settings.corosync_conf_file, return_value=True) + self.config.corosync_conf.load() + self.config.runner.cib.load( + resources=""" + + + + """ + ) + self._fixture_config_crm_verify( + self._fixture_crm_verify_invalid_cib(errors), + retval=EXITCODE_INVALID_CIB, + ) + self.config.services.is_running( + "sbd", return_value=False, name="services.is_running.sbd" + ) + self._fixture_config_local_daemons() + self.config.fs.isfile(settings.crm_rule_exec, return_value=True) + + self.assertEqual( + status.full_cluster_status_plaintext(self.env_assist.get_env()), + dedent( + """\ + Cluster name: test99 + + WARNINGS: + error: Resource chr:0 is of type systemd and therefore cannot be used as a promotable clone resource + error: Ignoring resource 'chr-clone' because configuration is invalid + error: CIB did not pass schema validation + Configuration invalid (with errors) + + crm_mon cluster status + + Daemon Status: + corosync: active/enabled + pacemaker: active/enabled + pcsd: active/enabled""" + ), + ) + + def test_crm_verify_error(self): + self.config.runner.pcmk.load_state_plaintext( + stdout="crm_mon cluster status" + ) + self.config.fs.exists(settings.corosync_conf_file, return_value=True) + self.config.corosync_conf.load() + self.config.runner.cib.load( + resources=""" + + + + """ + ) + self.config.runner.pcmk.verify_xml( + stdout="not a xml", stderr="some message" + ) + self.config.services.is_running( + "sbd", return_value=False, name="services.is_running.sbd" + ) + self._fixture_config_local_daemons() + self.config.fs.isfile(settings.crm_rule_exec, return_value=True) + + self.assertEqual( + status.full_cluster_status_plaintext(self.env_assist.get_env()), + dedent( + """\ + Cluster name: test99 + crm_mon cluster status + + Daemon Status: + corosync: active/enabled + pacemaker: active/enabled + pcsd: active/enabled""" + ), + ) + self.env_assist.assert_reports( + [ + fixture.debug( + report_codes.BAD_PCMK_API_RESPONSE_FORMAT, + reason=( + "Start tag expected, '<' not found, line 1, column 1 " + "(, line 1)" + ), + api_response="some message\nnot a xml", + ) + ] + ) + def test_stonith_warning_no_devices(self): self.config.runner.pcmk.load_state_plaintext( stdout="crm_mon cluster status" @@ -630,6 +785,7 @@ def test_stonith_warning_no_devices(self): self.config.fs.exists(settings.corosync_conf_file, return_value=True) self.config.corosync_conf.load() self.config.runner.cib.load() + self._fixture_config_crm_verify(self._fixture_crm_verify_success()) self.config.services.is_running( "sbd", return_value=False, name="services.is_running.sbd" ) @@ -661,6 +817,7 @@ def test_stonith_warning_no_devices_sbd_enabled(self): self.config.fs.exists(settings.corosync_conf_file, return_value=True) self.config.corosync_conf.load() self.config.runner.cib.load() + self._fixture_config_crm_verify(self._fixture_crm_verify_success()) self.config.services.is_running( "sbd", return_value=True, name="services.is_running.sbd" ) @@ -715,6 +872,7 @@ def test_stonith_warnings_regarding_devices_configuration(self): """ ) + self._fixture_config_crm_verify(self._fixture_crm_verify_success()) self.config.services.is_running( "sbd", return_value=False, name="services.is_running.sbd" ) @@ -756,6 +914,7 @@ def test_pcsd_status_issues(self): """ ) + self._fixture_config_crm_verify(self._fixture_crm_verify_success()) self.config.runner.pcmk.load_ticket_state_plaintext( stdout="ticket status" ) @@ -897,6 +1056,7 @@ def test_move_constrains_warnings(self): """, ) + self._fixture_config_crm_verify(self._fixture_crm_verify_success()) self.config.services.is_running( "sbd", return_value=True, name="services.is_running.sbd" ) @@ -956,6 +1116,7 @@ def test_expired_move_constraints_warnings(self): """, ) + self._fixture_config_crm_verify(self._fixture_crm_verify_success()) self.config.services.is_running( "sbd", return_value=True, name="services.is_running.sbd" ) @@ -1025,6 +1186,7 @@ def test_expired_and_in_effect_move_constraints_warnings(self): """, ) + self._fixture_config_crm_verify(self._fixture_crm_verify_success()) self.config.services.is_running( "sbd", return_value=True, name="services.is_running.sbd" ) @@ -1065,9 +1227,17 @@ def test_expired_and_in_effect_move_constraints_warnings(self): class FullClusterStatusPlaintextBoothWarning(FullClusterStatusPlaintextBase): def setUp(self): super().setUp() + self.settings_patcher = mock.patch( + "pcs.settings.pacemaker_api_result_schema", + rc("pcmk_api_rng/api-result.rng"), + ) + self.settings_patcher.start() self._fixture_config_live_minimal() self._fixture_config_local_daemons() + def tearDown(self): + self.settings_patcher.stop() + def _assert_status_output(self, warning=None): warning_str = "" if warning: diff --git a/pcs_test/tier0/lib/pacemaker/test_live.py b/pcs_test/tier0/lib/pacemaker/test_live.py index 688b9c202..2db12354d 100644 --- a/pcs_test/tier0/lib/pacemaker/test_live.py +++ b/pcs_test/tier0/lib/pacemaker/test_live.py @@ -576,6 +576,83 @@ def test_error_cannot_be_more_verbose_in_verbose_mode(self): ) +@mock.patch.object( + settings, "pacemaker_api_result_schema", rc("pcmk_api_rng/api-result.rng") +) +class GetCibVerificationErrors(TestCase): + fixture_ok = """ + + + + """ + + def test_run_on_live_cib(self): + runner = get_runner(self.fixture_ok) + self.assertEqual(lib.get_cib_verification_errors(runner), []) + runner.run.assert_called_once_with( + [settings.crm_verify_exec, "--output-as", "xml", "--live-check"], + ) + + def test_run_on_mocked_cib(self): + fake_tmp_file = "/fake/tmp/file" + runner = get_runner( + self.fixture_ok, env_vars={"CIB_file": fake_tmp_file} + ) + + self.assertEqual(lib.get_cib_verification_errors(runner), []) + runner.run.assert_called_once_with( + [ + settings.crm_verify_exec, + "--output-as", + "xml", + "--xml-file", + fake_tmp_file, + ], + ) + + def test_errors_present(self): + fixture_errors = """ + + + + error: Somewthing wrong with <clone> bad-clone + error: CIB did not pass schema validation + Configuration invalid (with errors) + + + + """ + runner = get_runner(fixture_errors) + self.assertEqual( + lib.get_cib_verification_errors(runner), + [ + "error: Somewthing wrong with bad-clone", + "error: CIB did not pass schema validation", + "Configuration invalid (with errors)", + ], + ) + runner.run.assert_called_once_with( + [settings.crm_verify_exec, "--output-as", "xml", "--live-check"], + ) + + def test_not_xml_response(self): + runner = get_runner("not xml output") + with self.assertRaises(lib.BadApiResultFormat) as cm: + lib.get_cib_verification_errors(runner) + self.assertEqual(cm.exception.pacemaker_response, "not xml output") + self.assertEqual( + type(cm.exception.original_exception), etree.XMLSyntaxError + ) + + runner.run.assert_called_once_with( + [settings.crm_verify_exec, "--output-as", "xml", "--live-check"], + ) + + class ReplaceCibConfigurationTest(TestCase): # pylint: disable=no-self-use def test_success(self): diff --git a/pcs_test/tier1/test_status.py b/pcs_test/tier1/test_status.py index fd02ad9f3..d7e3034be 100644 --- a/pcs_test/tier1/test_status.py +++ b/pcs_test/tier1/test_status.py @@ -122,6 +122,11 @@ def test_warn_when_no_stonith(self): WARNINGS: No stonith devices and stonith-enabled is not false + error: Resource start-up disabled since no STONITH resources have been defined + error: Either configure some or disable STONITH with the stonith-enabled option + error: NOTE: Clusters with shared data need STONITH to ensure data integrity + error: CIB did not pass schema validation + Errors found during check: config not valid Cluster Summary: """ @@ -510,6 +515,11 @@ class StatusResources(ResourceStonithStatusBase, TestCase): WARNINGS: No stonith devices and stonith-enabled is not false + error: Resource start-up disabled since no STONITH resources have been defined + error: Either configure some or disable STONITH with the stonith-enabled option + error: NOTE: Clusters with shared data need STONITH to ensure data integrity + error: CIB did not pass schema validation + Errors found during check: config not valid Cluster Summary: * Stack: unknown diff --git a/pcs_test/tools/command_env/config_runner_pcmk.py b/pcs_test/tools/command_env/config_runner_pcmk.py index 9e003c3a8..778c5b67e 100644 --- a/pcs_test/tools/command_env/config_runner_pcmk.py +++ b/pcs_test/tools/command_env/config_runner_pcmk.py @@ -1,5 +1,5 @@ import os -from typing import Optional +from typing import Mapping, Optional from pcs import settings @@ -931,18 +931,20 @@ def resource_digests( def verify( self, - name="runner.pcmk.verify", - cib_tempfile=None, - stderr=None, - verbose=False, - env=None, - ): + name: str = "runner.pcmk.verify", + cib_tempfile: Optional[str] = None, + stderr: Optional[str] = None, + verbose: bool = False, + env: Optional[Mapping[str, str]] = None, + ) -> None: """ - Create call that checks that wait for idle is supported + Create a call for running crm_verify with plaintext output - string name -- key of the call - string before -- key of call before which this new call is to be placed - dict env -- CommandRunner environment variables + name -- key of the call + cib_tempfile -- path to a file with CIB + stderr -- output of crm_verify + verbose -- run crm_verify in verbose mode + env -- CommandRunner environment variables """ cmd = ["crm_verify"] if verbose: @@ -961,6 +963,41 @@ def verify( ), ) + def verify_xml( + self, + name="runner.pcmk.verify_xml", + cib_tempfile: Optional[str] = None, + stdout: str = "", + stderr: str = "", + returncode: int = 0, + env: Optional[Mapping[str, str]] = None, + ) -> None: + """ + Create a call for running crm_verify with xml output + + name -- key of the call + cib_tempfile -- path to a file with CIB + stdout -- usually XML output of crm_verify + stderr -- error output of crm_verify, debug_messages + returncode -- crm_verify return code + env -- CommandRunner environment variables + """ + cmd = ["crm_verify", "--output-as", "xml"] + if cib_tempfile: + cmd.extend(["--xml-file", cib_tempfile]) + else: + cmd.append("--live-check") + self.__calls.place( + name, + RunnerCall( + cmd, + stdout=stdout, + stderr=stderr, + returncode=returncode, + env=env, + ), + ) + def remove_node( self, node_name, From 21dbc1830b265bb3b30cc1b381092a34cd5c26b1 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Tue, 15 Apr 2025 14:55:24 +0200 Subject: [PATCH 135/227] run crm_verify on successfull manual cib push/edit --- CHANGELOG.md | 2 ++ pcs/cluster.py | 11 ++++++++++- pcs_test/tier1/cluster/test_cib_push.py | 18 +++++++++++++----- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b496eff1..4b807272c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### Added - Commands `pcs cluster cib-push` and `pcs cluster edit` now print more info when new CIB does not conform to the CIB schema ([RHEL-76059]) +- Commands `pcs cluster cib-push` and `pcs cluster edit` now print info about + problems in pushed CIB even if it conforms to the CIB schema ([RHEL-76060]) - Command `pcs stonith sbd watchdog list` now prints watchdogs' identity and driver ([RHEL-76177]) - Command `pcs cluster rename` for changing cluster name ([RHEL-76055]) diff --git a/pcs/cluster.py b/pcs/cluster.py index d27d1c6d7..b091f438f 100644 --- a/pcs/cluster.py +++ b/pcs/cluster.py @@ -867,9 +867,9 @@ def get_details_from_crm_verify(): utils.err("unable to parse new cib: %s" % e) EXITCODE_INVALID_CIB = 78 + runner = utils.cmd_runner() if diff_against: - runner = utils.cmd_runner() command = [ settings.crm_diff_exec, "--original", @@ -940,6 +940,15 @@ def get_details_from_crm_verify(): utils.err("unable to push cib\n" + error_text) print_to_stderr("CIB updated") + try: + cib_errors = lib_pacemaker.get_cib_verification_errors(runner) + if cib_errors: + print_to_stderr("\n".join(cib_errors)) + except lib_pacemaker.BadApiResultFormat as e: + print_to_stderr( + f"Unable to verify CIB: {e.original_exception}\n" + f"crm_verify output:\n{e.pacemaker_response}" + ) if not modifiers.is_specified("--wait"): return diff --git a/pcs_test/tier1/cluster/test_cib_push.py b/pcs_test/tier1/cluster/test_cib_push.py index 3707067d3..e8e145e6f 100644 --- a/pcs_test/tier1/cluster/test_cib_push.py +++ b/pcs_test/tier1/cluster/test_cib_push.py @@ -1,10 +1,8 @@ +from textwrap import dedent from unittest import TestCase from pcs_test.tools.assertions import AssertPcsMixin -from pcs_test.tools.misc import ( - get_tmp_file, - write_data_to_tmpfile, -) +from pcs_test.tools.misc import get_tmp_file, write_data_to_tmpfile from pcs_test.tools.pcs_runner import PcsRunner CIB_EPOCH_TEMPLATE = """ @@ -127,4 +125,14 @@ def test_diff_no_difference(self): def test_cib_updated(self): write_data_to_tmpfile(CIB_EPOCH_NEWER, self.updated_cib) - self.assert_pcs_success(self.cib_push_cmd, stderr_full="CIB updated\n") + self.assert_pcs_success( + self.cib_push_cmd, + stderr_full=dedent("""\ + CIB updated + error: Resource start-up disabled since no STONITH resources have been defined + error: Either configure some or disable STONITH with the stonith-enabled option + error: NOTE: Clusters with shared data need STONITH to ensure data integrity + error: CIB did not pass schema validation + Errors found during check: config not valid + """), + ) From e314d56a2a65f3102798b4790c6e1da67580a82d Mon Sep 17 00:00:00 2001 From: Michal Pospisil Date: Fri, 2 May 2025 19:23:06 +0200 Subject: [PATCH 136/227] resource meta command overhaul --- CHANGELOG.md | 4 + pcs/Makefile.am | 2 + pcs/cli/common/lib_wrapper.py | 1 + pcs/cli/reports/messages.py | 13 + pcs/cli/resource/command.py | 34 + pcs/cli/resource/common.py | 122 +++ pcs/cli/routing/resource.py | 2 +- pcs/cli/routing/stonith.py | 2 +- pcs/cli/stonith/command.py | 32 + pcs/cli/stonith/common.py | 22 + pcs/common/reports/__init__.py | 1 + pcs/common/reports/codes.py | 4 + pcs/common/reports/item.py | 22 + pcs/common/reports/messages.py | 60 +- .../async_tasks/worker/command_mapping.py | 4 + pcs/lib/cib/nvpair_multi.py | 24 + pcs/lib/cib/resource/clone.py | 51 ++ pcs/lib/cib/resource/common.py | 8 +- pcs/lib/cib/resource/guest_node.py | 191 ++++- pcs/lib/cib/resource/operations.py | 11 +- pcs/lib/cib/resource/primitive.py | 10 + pcs/lib/cib/resource/remote_node.py | 4 +- pcs/lib/commands/resource.py | 343 ++++++-- pcs/lib/node.py | 7 +- pcs/lib/resource_agent/types.py | 4 + pcs/pcs.8.in | 4 +- pcs/resource.py | 130 +-- pcs/stonith.py | 41 +- pcs/usage.py | 8 +- pcs_test/Makefile.am | 7 +- pcs_test/tier0/cli/reports/test_messages.py | 11 + pcs_test/tier0/cli/resource/test_common.py | 409 ++++++++++ pcs_test/tier0/cli/resource/test_update.py | 153 ++++ pcs_test/tier0/cli/stonith/test_update.py | 65 ++ pcs_test/tier0/cli/test_resource.py | 2 +- .../tier0/common/reports/test_messages.py | 23 +- pcs_test/tier0/lib/cib/test_nvpair_multi.py | 86 +- pcs_test/tier0/lib/cib/test_resource_clone.py | 200 ++++- .../tier0/lib/cib/test_resource_guest_node.py | 446 ++++++++++- .../commands/resource/test_resource_update.py | 754 ++++++++++++++++++ .../tier0/lib/resource_agent/test_types.py | 10 + pcs_test/tier1/cib_resource/test_create.py | 17 +- pcs_test/tier1/cib_resource/test_update.py | 204 +++++ pcs_test/tier1/legacy/test_resource.py | 241 ------ pcs_test/tier1/test_cluster_pcmk_remote.py | 15 +- pcsd/capabilities.xml.in | 15 + pcsd/pcs.rb | 10 +- 47 files changed, 3268 insertions(+), 561 deletions(-) create mode 100644 pcs/cli/resource/common.py create mode 100644 pcs/cli/stonith/common.py create mode 100644 pcs_test/tier0/cli/resource/test_common.py create mode 100644 pcs_test/tier0/cli/resource/test_update.py create mode 100644 pcs_test/tier0/cli/stonith/test_update.py create mode 100644 pcs_test/tier0/lib/commands/resource/test_resource_update.py create mode 100644 pcs_test/tier1/cib_resource/test_update.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b807272c..798cdfc10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ - Support for exporting alerts in `json` and `cmd` formats ([RHEL-76153]) - Output of `pcs status` now contains messages about CIB misconfiguration provided by `crm_verify` pacemaker tool ([RHEL-76060]) +- Support for bundle resources in `pcs resource meta`, disallow updating + `remote-node` and `remote-addr` without `--force`, add lib command + `resource.update_meta` to API v2 ([RHEL-35420]) ### Fixed - Fixed a traceback when removing a resource fails in web UI @@ -22,6 +25,7 @@ UI - Display node-attribute in colocation constraints configuration ([RHEL-82894]) +[RHEL-35420]: https://issues.redhat.com/browse/RHEL-35420 [RHEL-76055]: https://issues.redhat.com/browse/RHEL-76055 [RHEL-76059]: https://issues.redhat.com/browse/RHEL-76059 [RHEL-76060]: https://issues.redhat.com/browse/RHEL-76060 diff --git a/pcs/Makefile.am b/pcs/Makefile.am index c41d7d0d9..eb0b40887 100644 --- a/pcs/Makefile.am +++ b/pcs/Makefile.am @@ -79,6 +79,7 @@ EXTRA_DIST = \ cli/reports/processor.py \ cli/resource/__init__.py \ cli/resource/command.py \ + cli/resource/common.py \ cli/resource/parse_args.py \ cli/resource/output.py \ cli/resource/relations.py \ @@ -108,6 +109,7 @@ EXTRA_DIST = \ cli/status/__init__.py \ cli/stonith/__init__.py \ cli/stonith/command.py \ + cli/stonith/common.py \ cli/stonith/levels/__init__.py \ cli/stonith/levels/command.py \ cli/stonith/levels/output.py \ diff --git a/pcs/cli/common/lib_wrapper.py b/pcs/cli/common/lib_wrapper.py index e2864b728..b5745e35c 100644 --- a/pcs/cli/common/lib_wrapper.py +++ b/pcs/cli/common/lib_wrapper.py @@ -418,6 +418,7 @@ def load_module(env, middleware_factory, name): # noqa: PLR0911, PLR0912 "is_any_resource_except_stonith": resource.is_any_resource_except_stonith, "is_any_stonith": resource.is_any_stonith, "manage": resource.manage, + "update_meta": resource.update_meta, "move": resource.move, "move_autoclean": resource.move_autoclean, "restart": resource.restart, diff --git a/pcs/cli/reports/messages.py b/pcs/cli/reports/messages.py index 1cf6847c4..2b587581b 100644 --- a/pcs/cli/reports/messages.py +++ b/pcs/cli/reports/messages.py @@ -304,6 +304,19 @@ def message(self) -> str: ) +class UseCommandRemoveAndAddGuestNode(CliReportMessageCustom): + _obj: messages.UseCommandRemoveAndAddGuestNode + + @property + def message(self) -> str: + return ( + "Changing connection parameters of an existing guest node is not " + "sufficient for connecting to a different guest node, remove the " + "existing guest node with 'pcs cluster node remove-guest' and add " + "a new one with 'pcs cluster node add-guest'" + ) + + class CorosyncNodeConflictCheckSkipped(CliReportMessageCustom): _obj: messages.CorosyncNodeConflictCheckSkipped diff --git a/pcs/cli/resource/command.py b/pcs/cli/resource/command.py index 0dcb595cb..a2355b8a9 100644 --- a/pcs/cli/resource/command.py +++ b/pcs/cli/resource/command.py @@ -1,6 +1,7 @@ import json from typing import Any +from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.output import ( format_cmd_list, lines_to_str, @@ -11,12 +12,19 @@ OUTPUT_FORMAT_VALUE_JSON, Argv, InputModifiers, + KeyValueParser, + wait_to_timeout, +) +from pcs.cli.resource.common import ( + check_is_not_stonith, + get_resource_status_msg, ) from pcs.cli.resource.output import ( ResourcesConfigurationFacade, resources_to_cmd, resources_to_text, ) +from pcs.common import reports from pcs.common.interface import dto from pcs.common.pacemaker.resource.list import CibResourcesDto @@ -66,3 +74,29 @@ def config_common( smart_wrap_text(resources_to_text(resources_facade)) ) return output + + +def meta(lib: Any, argv: list[str], modifiers: InputModifiers) -> None: + """ + Options: + * --force - override editing connection attributes for guest nodes + * --wait - wait for cluster to reach steady state + * -f + """ + modifiers.ensure_only_supported("-f", "--force", "--wait") + modifiers.ensure_not_mutually_exclusive("-f", "--wait") + wait_timeout = wait_to_timeout(modifiers.get("--wait")) + force_flags = [] + if modifiers.get("--force"): + force_flags.append(reports.codes.FORCE) + if not argv: + raise CmdLineInputError() + resource_id = argv.pop(0) + check_is_not_stonith(lib, [resource_id], "pcs stonith meta") + meta_attrs_dict = KeyValueParser(argv).get_unique() + + lib.resource.update_meta(resource_id, meta_attrs_dict, force_flags) + + if wait_timeout >= 0: + lib.cluster.wait_for_pcmk_idle(wait_timeout) + print(get_resource_status_msg(lib, resource_id)) diff --git a/pcs/cli/resource/common.py b/pcs/cli/resource/common.py new file mode 100644 index 000000000..3748491ad --- /dev/null +++ b/pcs/cli/resource/common.py @@ -0,0 +1,122 @@ +from collections import defaultdict +from typing import ( + Any, + Optional, + Sequence, + Union, +) + +from pcs.cli.reports.output import ( + deprecation_warning, +) +from pcs.common import ( + const, + reports, +) +from pcs.common.status_dto import ( + AnyResourceStatusDto, + BundleStatusDto, + CloneStatusDto, + GroupStatusDto, + PrimitiveStatusDto, +) +from pcs.common.str_tools import ( + format_list, + format_optional, + format_plural, +) + +RESOURCE_NOT_RUNNING = "Resource '{resource_id}' is not running on any nodes" + + +def check_is_not_stonith( + lib: Any, resource_id_list: list[str], cmd_to_use: Optional[str] = None +) -> None: + if lib.resource.is_any_stonith(resource_id_list): + deprecation_warning( + reports.messages.ResourceStonithCommandsMismatch( + "stonith resources" + ).message + + format_optional(cmd_to_use, " Please use '{}' instead.") + ) + + +def _get_primitive_instance_list_dto( + resource_dto: Union[AnyResourceStatusDto], +) -> Sequence[PrimitiveStatusDto]: + """ + Return a list of primitive instances from any resource status DTO. + """ + if isinstance(resource_dto, GroupStatusDto): + return resource_dto.members + if isinstance(resource_dto, CloneStatusDto): + if isinstance(resource_dto.instances[0], GroupStatusDto): + instance_list: list[PrimitiveStatusDto] = [] + for group_dto in resource_dto.instances: + # There is a check for the type in the if above + instance_list.extend(group_dto.members) # type: ignore + return instance_list + # There is a check for the type in the if above + return resource_dto.instances # type: ignore + if isinstance(resource_dto, BundleStatusDto): + return [replica_dto.container for replica_dto in resource_dto.replicas] + return [resource_dto] + + +def get_resource_status_msg(lib: Any, resource_id: str) -> str: + """ + Get where resources are running, typically used after waiting for cluster + is finished. Returns a text string similar to utils.resource_running_on. + Examples: + Resource 'r1' is not running on any nodes + Resource 'r2-clone' is promoted on node 'n1'; unpromoted on nodes 'n2', + 'n3' + """ + resource_dto = None + for resource in lib.status.resources_status().resources: + if resource.resource_id == resource_id: + resource_dto = resource + if resource_dto is None: + # Resource is configured but Pacemaker is ignoring it + return RESOURCE_NOT_RUNNING.format(resource_id=resource_id) + + # Using set in case more instances are running on the same node + role_and_location: dict[const.PcmkStatusRoleType, set[str]] = defaultdict( + set + ) + instance_list_dto = _get_primitive_instance_list_dto(resource_dto) + for instance_dto in instance_list_dto: + role_and_location[instance_dto.role].update(instance_dto.node_names) + + # The old function (utils.resource_running_on) this is replicating only + # supported states Started, Promoted and Unpromoted. Therefore we have to + # filter out only these states since others were ignored previously. + role_and_location = { + role: loc + for role, loc in role_and_location.items() + if role + in [ + const.PCMK_STATUS_ROLE_STARTED, + const.PCMK_STATUS_ROLE_PROMOTED, + const.PCMK_STATUS_ROLE_UNPROMOTED, + ] + } + + if not role_and_location: + return RESOURCE_NOT_RUNNING.format(resource_id=resource_id) + + state_parts = [] + for state_name, node_list in role_and_location.items(): + state_parts.append( + "{state_name} on {node_pl} {node_list}".format( + state_name=( + state_name.lower() + if state_name != const.PCMK_STATUS_ROLE_STARTED + else "running" + ), + node_pl=format_plural(depends_on=node_list, singular="node"), + node_list=format_list(node_list), + ) + ) + state_info = "; ".join(state_parts) + return f"Resource '{resource_id}' is {state_info}" diff --git a/pcs/cli/routing/resource.py b/pcs/cli/routing/resource.py index 3775dae7c..eae794e46 100644 --- a/pcs/cli/routing/resource.py +++ b/pcs/cli/routing/resource.py @@ -27,7 +27,7 @@ "providers": resource.resource_providers, "agents": resource.resource_agents, "update": resource.update_cmd, - "meta": resource.meta_cmd, + "meta": resource_cli.meta, "delete": resource.resource_remove_cmd, "remove": resource.resource_remove_cmd, # TODO remove, deprecated command diff --git a/pcs/cli/routing/stonith.py b/pcs/cli/routing/stonith.py index 370d033e8..753c99991 100644 --- a/pcs/cli/routing/stonith.py +++ b/pcs/cli/routing/stonith.py @@ -27,7 +27,7 @@ # replaced with 'stonith status' and 'stonith config' "show": stonith.stonith_show_cmd, "status": stonith.stonith_status_cmd, - "meta": stonith.meta_cmd, + "meta": stonith_cli.meta, "op": create_router( { "defaults": resource_op_defaults_cmd( diff --git a/pcs/cli/stonith/command.py b/pcs/cli/stonith/command.py index ac0dd5ddb..0c0ec1f45 100644 --- a/pcs/cli/stonith/command.py +++ b/pcs/cli/stonith/command.py @@ -1,5 +1,6 @@ from typing import Any +from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.output import ( format_cmd_list, lines_to_str, @@ -10,13 +11,18 @@ OUTPUT_FORMAT_VALUE_JSON, Argv, InputModifiers, + KeyValueParser, + wait_to_timeout, ) from pcs.cli.reports.output import warn from pcs.cli.resource import command as resource_cmd +from pcs.cli.resource.common import get_resource_status_msg +from pcs.cli.stonith.common import check_is_stonith from pcs.cli.stonith.levels.output import ( stonith_level_config_to_cmd, stonith_level_config_to_text, ) +from pcs.common import reports from pcs.common.str_tools import indent @@ -56,3 +62,29 @@ def config(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: if output: print(output) + + +def meta(lib: Any, argv: list[str], modifiers: InputModifiers) -> None: + """ + Options: + * --force - override editing connection attributes for guest nodes + * --wait - wait for cluster to reach steady state + * -f + """ + modifiers.ensure_only_supported("-f", "--force", "--wait") + modifiers.ensure_not_mutually_exclusive("-f", "--wait") + wait_timeout = wait_to_timeout(modifiers.get("--wait")) + force_flags = [] + if modifiers.get("--force"): + force_flags.append(reports.codes.FORCE) + if not argv: + raise CmdLineInputError() + resource_id = argv.pop(0) + check_is_stonith(lib, [resource_id], "pcs resource meta") + meta_attrs_dict = KeyValueParser(argv).get_unique() + + lib.resource.update_meta(resource_id, meta_attrs_dict, force_flags) + + if wait_timeout >= 0: + lib.cluster.wait_for_pcmk_idle(wait_timeout) + print(get_resource_status_msg(lib, resource_id)) diff --git a/pcs/cli/stonith/common.py b/pcs/cli/stonith/common.py new file mode 100644 index 000000000..f96f9ec93 --- /dev/null +++ b/pcs/cli/stonith/common.py @@ -0,0 +1,22 @@ +from typing import ( + Any, + Optional, +) + +from pcs.cli.reports.output import deprecation_warning +from pcs.common import reports +from pcs.common.str_tools import format_optional + + +def check_is_stonith( + lib: Any, + resource_id_list: list[str], + cmd_to_use: Optional[str] = None, +) -> None: + if lib.resource.is_any_resource_except_stonith(resource_id_list): + deprecation_warning( + reports.messages.ResourceStonithCommandsMismatch( + "resources" + ).message + + format_optional(cmd_to_use, " Please use '{}' instead.") + ) diff --git a/pcs/common/reports/__init__.py b/pcs/common/reports/__init__.py index 1b16ad1b6..0d2a38586 100644 --- a/pcs/common/reports/__init__.py +++ b/pcs/common/reports/__init__.py @@ -14,6 +14,7 @@ ReportItemMessage, ReportItemSeverity, get_severity, + get_severity_from_flags, ) from .processor import ( ReportProcessor, diff --git a/pcs/common/reports/codes.py b/pcs/common/reports/codes.py index fed7d4cb0..ebf53c006 100644 --- a/pcs/common/reports/codes.py +++ b/pcs/common/reports/codes.py @@ -302,6 +302,7 @@ FILE_REMOVE_FROM_NODE_ERROR = M("FILE_REMOVE_FROM_NODE_ERROR") FILE_REMOVE_FROM_NODE_SUCCESS = M("FILE_REMOVE_FROM_NODE_SUCCESS") GFS2_LOCK_TABLE_RENAME_NEEDED = M("GFS2_LOCK_TABLE_RENAME_NEEDED") +GUEST_NODE_NAME_ALREADY_EXISTS = M("GUEST_NODE_NAME_ALREADY_EXISTS") HOST_NOT_FOUND = M("HOST_NOT_FOUND") HOST_ALREADY_AUTHORIZED = M("HOST_ALREADY_AUTHORIZED") HOST_ALREADY_IN_CLUSTER_CONFIG = M("HOST_ALREADY_IN_CLUSTER_CONFIG") @@ -635,6 +636,9 @@ USE_COMMAND_NODE_ADD_GUEST = M("USE_COMMAND_NODE_ADD_GUEST") USE_COMMAND_NODE_REMOVE_REMOTE = M("USE_COMMAND_NODE_REMOVE_REMOTE") USE_COMMAND_NODE_REMOVE_GUEST = M("USE_COMMAND_NODE_REMOVE_GUEST") +USE_COMMAND_REMOVE_AND_ADD_GUEST_NODE = M( + "USE_COMMAND_REMOVE_AND_ADD_GUEST_NODE" +) REMOTE_NODE_REMOVAL_INCOMPLETE = M("REMOTE_NODE_REMOVAL_INCOMPLETE") GUEST_NODE_REMOVAL_INCOMPLETE = M("GUEST_NODE_REMOVAL_INCOMPLETE") USING_DEFAULT_ADDRESS_FOR_HOST = M("USING_DEFAULT_ADDRESS_FOR_HOST") diff --git a/pcs/common/reports/item.py b/pcs/common/reports/item.py index 7bb7447a3..b3d946958 100644 --- a/pcs/common/reports/item.py +++ b/pcs/common/reports/item.py @@ -19,6 +19,7 @@ ) from .types import ( ForceCode, + ForceFlags, MessageCode, SeverityLevel, ) @@ -79,6 +80,27 @@ def get_severity( return ReportItemSeverity(ReportItemSeverity.ERROR, force_code) +def get_severity_from_flags( + force_code: Optional[ForceCode], force_flags: ForceFlags +) -> ReportItemSeverity: + """ + Returns warning/error severity for report creation depending on whether the + force_code is in force_flags. + + force_code -- the force code by which the report can be overridden + force_flags -- force flags specified to the command + + TODO: When pcs starts using other force codes than all-mighty force, this + function can be expanded to allow for checking the weaker force code with + automatic override by the all-mighty force. For example, if force_code is + weak_force, and force_flags contain force but not weak_force, the function + would return warning severity. + """ + if force_code in force_flags: + return ReportItemSeverity(ReportItemSeverity.WARNING) + return ReportItemSeverity(ReportItemSeverity.ERROR, force_code) + + @dataclass(frozen=True, init=False) class ReportItemMessage(ImplementsToDto): _code = MessageCode("") diff --git a/pcs/common/reports/messages.py b/pcs/common/reports/messages.py index e8e262987..49a324b3a 100644 --- a/pcs/common/reports/messages.py +++ b/pcs/common/reports/messages.py @@ -2677,10 +2677,17 @@ class IdNotFound(ReportItemMessage): """ Specified id does not exist in CIB, user referenced a nonexisting id + Context provides info about what CIB subsection was searched - group for + example. + + Examples: + resource 'r1' does not exist + there is no resource 'r1' in the group 'g1' + id -- specified id expected_types -- list of id's roles - expected types with the id - context_type -- context_id's role / type - context_id -- specifies the search area + context_type -- subsection role / type + context_id -- id of the subsection """ id: str # pylint: disable=invalid-name @@ -3408,7 +3415,7 @@ class WaitForIdleNotLiveCluster(ReportItemMessage): @property def message(self) -> str: - return "Cannot use 'mocked CIB' together with 'wait'" + return "Cannot pass CIB together with 'wait'" @dataclass(frozen=True) @@ -4937,10 +4944,12 @@ class LiveEnvironmentRequired(ReportItemMessage): @property def message(self) -> str: - return "This command does not support {forbidden_options}".format( - forbidden_options=format_list( - [str(item) for item in self.forbidden_options] - ), + return ( + "This command does not support passing {forbidden_options}".format( + forbidden_options=format_list( + [str(item) for item in self.forbidden_options] + ), + ) ) @@ -5320,6 +5329,24 @@ def message(self) -> str: ).format(name=self.node_name) +@dataclass(frozen=True) +class UseCommandRemoveAndAddGuestNode(ReportItemMessage): + """ + Changing connection parameters of an existing guest node is not recommended, + new guest nodes should be readded. + """ + + _code = codes.USE_COMMAND_REMOVE_AND_ADD_GUEST_NODE + + @property + def message(self) -> str: + return ( + "Changing connection parameters of an existing guest node is not " + "sufficient for connecting to a different guest node, remove the " + "existing guest node and add a new one instead" + ) + + @dataclass(frozen=True) class GuestNodeRemovalIncomplete(ReportItemMessage): """ @@ -5340,6 +5367,25 @@ def message(self) -> str: ).format(name=self.node_name) +@dataclass(frozen=True) +class GuestNodeNameAlreadyExists(ReportItemMessage): + """ + Cannot set a guest node name that overlaps with another id in the CIB. + + node_name -- node name conflicting with another id + """ + + node_name: str + _code = codes.GUEST_NODE_NAME_ALREADY_EXISTS + + @property + def message(self) -> str: + return ( + f"Cannot set name of the guest node to '{self.node_name}' because " + "that ID already exists in the cluster configuration." + ) + + @dataclass(frozen=True) class TmpFileWrite(ReportItemMessage): """ diff --git a/pcs/daemon/async_tasks/worker/command_mapping.py b/pcs/daemon/async_tasks/worker/command_mapping.py index bab46af50..8834df3b0 100644 --- a/pcs/daemon/async_tasks/worker/command_mapping.py +++ b/pcs/daemon/async_tasks/worker/command_mapping.py @@ -363,6 +363,10 @@ class _Cmd: cmd=resource.manage, required_permission=p.WRITE, ), + "resource.update_meta": _Cmd( + cmd=resource.update_meta, + required_permission=p.WRITE, + ), "resource.move": _Cmd( cmd=resource.move, required_permission=p.WRITE, diff --git a/pcs/lib/cib/nvpair_multi.py b/pcs/lib/cib/nvpair_multi.py index be262154c..e569c245e 100644 --- a/pcs/lib/cib/nvpair_multi.py +++ b/pcs/lib/cib/nvpair_multi.py @@ -85,6 +85,30 @@ def nvset_element_to_dto( ) +def nvset_to_dict(nvset_el: _Element) -> dict[str, Optional[str]]: + """ + Export only nvpairs from an nvset xml element into a dictionary + """ + return { + str(nvpair_el.attrib["name"]): nvpair_el.get("value") + for nvpair_el in nvset_el.iterfind("./nvpair") + } + + +def nvset_to_dict_except_without_values( + nvset_el: _Element, +) -> dict[str, str]: + """ + Value in a nvpair is not mandatory. This function ignores such entries in + the same way as Pacemaker does. + """ + return { + key: value + for key, value in nvset_to_dict(nvset_el).items() + if value is not None + } + + def find_nvsets(parent_element: _Element, tag: NvsetTag) -> List[_Element]: """ Get all nvset xml elements in the given parent element diff --git a/pcs/lib/cib/resource/clone.py b/pcs/lib/cib/resource/clone.py index a1c006db9..3facebd1b 100644 --- a/pcs/lib/cib/resource/clone.py +++ b/pcs/lib/cib/resource/clone.py @@ -19,9 +19,13 @@ from lxml import etree from lxml.etree import _Element +from pcs.common import ( + const, +) from pcs.common.pacemaker import nvset from pcs.common.pacemaker.resource.clone import CibResourceCloneDto from pcs.common.reports import ReportItemList +from pcs.common.tools import Version from pcs.lib.cib import ( nvpair, nvpair_multi, @@ -29,6 +33,13 @@ ) from pcs.lib.cib.const import TAG_RESOURCE_CLONE as TAG_CLONE from pcs.lib.cib.const import TAG_RESOURCE_MASTER as TAG_MASTER +from pcs.lib.cib.nvpair_multi import ( + NVSET_META, + find_nvsets, + nvset_append_new, + nvset_update, +) +from pcs.lib.cib.resource.operations import get_resource_operations from pcs.lib.cib.tools import IdProvider from pcs.lib.pacemaker.values import ( is_true, @@ -37,6 +48,9 @@ ALL_TAGS = [TAG_CLONE, TAG_MASTER] +META_GLOBALLY_UNIQUE = "globally-unique" +META_PROMOTABLE = "promotable" + def is_clone(resource_el: _Element) -> bool: return resource_el.tag == TAG_CLONE @@ -182,6 +196,13 @@ def get_inner_resource(clone_el: _Element) -> _Element: return cast(List[_Element], clone_el.xpath("./primitive | ./group"))[0] +def get_inner_primitives(clone_el: _Element) -> list[_Element]: + """ + Also returns primitives inside cloned groups. + """ + return cast(List[_Element], clone_el.xpath(".//primitive")) + + def validate_clone_id(clone_id: str, id_provider: IdProvider) -> ReportItemList: """ Validate that clone_id is a valid xml id and it is unique in the cib. @@ -193,3 +214,33 @@ def validate_clone_id(clone_id: str, id_provider: IdProvider) -> ReportItemList: validate_id(clone_id, reporter=report_list) report_list.extend(id_provider.book_ids(clone_id)) return report_list + + +def convert_master_to_promotable( + id_provider: IdProvider, cib_validate_with: Version, master_el: _Element +) -> None: + master_el.tag = TAG_CLONE + meta_attrs = {META_PROMOTABLE: "true"} + meta_attrs_nvset_list = find_nvsets(master_el, NVSET_META) + if meta_attrs_nvset_list: + nvset_update(meta_attrs_nvset_list[0], id_provider, meta_attrs) + else: + nvset_append_new( + master_el, + id_provider, + cib_validate_with, + NVSET_META, + nvpair_dict=meta_attrs, + nvset_options={}, + ) + + clone_primitives = get_inner_primitives(master_el) + + for primitive_el in clone_primitives: + ops_monitor = get_resource_operations(primitive_el, names=["monitor"]) + for op_monitor in ops_monitor: + role = op_monitor.get("role", "") + if role == const.PCMK_ROLE_PROMOTED_LEGACY: + op_monitor.set("role", const.PCMK_ROLE_PROMOTED) + if role == const.PCMK_ROLE_UNPROMOTED_LEGACY: + op_monitor.set("role", const.PCMK_ROLE_UNPROMOTED) diff --git a/pcs/lib/cib/resource/common.py b/pcs/lib/cib/resource/common.py index cf01834d2..44634dced 100644 --- a/pcs/lib/cib/resource/common.py +++ b/pcs/lib/cib/resource/common.py @@ -20,6 +20,9 @@ from .bundle import get_inner_resource as get_bundle_inner_resource from .bundle import is_bundle +from .clone import ( + get_inner_primitives as get_clone_inner_primitive_resources, +) from .clone import get_inner_resource as get_clone_inner_resource from .clone import is_any_clone from .group import get_inner_resources as get_group_inner_resources @@ -27,6 +30,7 @@ from .primitive import is_primitive +# DEPRECATED, use get_element_by_id def find_one_resource( context_element: _Element, resource_id: str, @@ -48,6 +52,8 @@ def find_one_resource( return resource, report_list +# DEPRECATED, use get_elements_by_ids +# Issue: produces report with CIB tags when wrong element type was found def find_resources( context_element: _Element, resource_ids: StringCollection, @@ -84,7 +90,7 @@ def find_primitives(resource_el: _Element) -> List[_Element]: in_bundle = get_bundle_inner_resource(resource_el) return [in_bundle] if in_bundle is not None else [] if is_any_clone(resource_el): - resource_el = get_clone_inner_resource(resource_el) + return get_clone_inner_primitive_resources(resource_el) if is_group(resource_el): return get_group_inner_resources(resource_el) if is_primitive(resource_el): diff --git a/pcs/lib/cib/resource/guest_node.py b/pcs/lib/cib/resource/guest_node.py index ef8c03acd..5a9f9078e 100644 --- a/pcs/lib/cib/resource/guest_node.py +++ b/pcs/lib/cib/resource/guest_node.py @@ -6,7 +6,10 @@ from lxml.etree import _Element from pcs.common import reports -from pcs.common.reports.item import ReportItem +from pcs.common.reports.item import ( + ReportItem, + ReportItemList, +) from pcs.common.types import StringCollection from pcs.lib import validate from pcs.lib.cib.node import PacemakerNode @@ -17,15 +20,150 @@ ) from pcs.lib.cib.tools import does_id_exist +OPTION_REMOTE_NODE = "remote-node" +_OPTION_REMOTE_ADDR = "remote-addr" +_OPTION_REMOTE_PORT = "remote-port" +_OPTION_REMOTE_CONN_TIMEOUT = "remote-connect-timeout" + + # TODO pcs currently does not care about multiple meta_attributes and here # we don't care as well GUEST_OPTIONS = [ - "remote-port", - "remote-addr", - "remote-connect-timeout", + _OPTION_REMOTE_ADDR, + _OPTION_REMOTE_PORT, + _OPTION_REMOTE_CONN_TIMEOUT, ] +def validate_updating_guest_attributes( + cib: _Element, + existing_nodes_names: list[str], + existing_nodes_addrs: list[str], + new_meta_attrs: Mapping[str, str], + existing_meta_attrs: Mapping[str, str], + force_flags: reports.types.ForceFlags, +) -> ReportItemList: + """ + Guest nodes have an implicit connection resource created by Pacemaker with + attributes remote-node and remode-addr that defaults to remote-node. + Updating these attributes doesn't make sense because Pacemaker Remote also + needs authkey, so changing the address to another host will not work as + expected. However it can still be forced (in case of a networking change) + and additional check for IDs in CIB is performed. + + TODO: needs to be consolidated with checks in resource create during its + overhaul + + existing_nodes_names -- list of existing guest and remote node names to + check for name conflicts + existing_nodes_addrs -- list of existing guest and remote node addresses to + check for name conflicts, since address is used if name is not defined + new_meta_attrs -- meta attributes that are being updated with their new + values + existing_meta_attrs -- currently defined meta attributes with their values + force_flags -- force flags + """ + + validator_list = [ + validate.ValueTimeInterval(_OPTION_REMOTE_CONN_TIMEOUT), + validate.ValuePortNumber(_OPTION_REMOTE_PORT), + ] + for validator in validator_list: + validator.empty_string_valid = True + report_list = validate.ValidatorAll(validator_list).validate(new_meta_attrs) + + # Validate remote-node collision with other CIB IDs + report_list.extend( + validate_conflicts( + cib, + existing_nodes_names, + existing_nodes_addrs, + new_meta_attrs.get(OPTION_REMOTE_NODE, ""), + new_meta_attrs, + ) + ) + + # Validating previously undefined meta attributes + new_meta_attrs_set = set(new_meta_attrs.keys()) + existing_meta_attrs_set = set(existing_meta_attrs.keys()) + + # Only addition of remote-node constitutes creating of a guest node, just + # adding remote-addr when remote-node is not defined is fine + added_guest_conn_keys = set(new_meta_attrs_set - existing_meta_attrs_set) + if OPTION_REMOTE_NODE in added_guest_conn_keys: + # Suggesting remove is not needed, this is only triggered when the + # attributes weren't defined previously + report_list.append( + ReportItem( + severity=reports.item.get_severity_from_flags( + reports.codes.FORCE, + force_flags, + ), + message=reports.messages.UseCommandNodeAddGuest(), + ) + ) + # Never return reports with contradictory guidance + return report_list + + # Validating previously defined meta attributes + updated_guest_conn_keys = existing_meta_attrs_set.intersection( + new_meta_attrs_set + ).intersection( + # These keys are crucial for defining the connection to the guest node + # and should only be changed by recreating the guest node in most cases + {OPTION_REMOTE_NODE, _OPTION_REMOTE_ADDR} + ) + if any( + new_meta_attrs[key] != existing_meta_attrs[key] + for key in updated_guest_conn_keys + ): + # If all new values are empty, this is a delete operation + if not all(new_meta_attrs[key] for key in updated_guest_conn_keys): + report_list.append( + ReportItem( + severity=reports.item.get_severity_from_flags( + reports.codes.FORCE, + force_flags, + ), + message=reports.messages.UseCommandNodeRemoveGuest(), + ) + ) + elif OPTION_REMOTE_NODE in existing_meta_attrs: + # Otherwise, this is an update, suggest readding resource but only + # if remote-node is defined - if it isn't, it's not a dangerous + # operation + report_list.append( + ReportItem( + severity=reports.item.get_severity_from_flags( + reports.codes.FORCE, + force_flags, + ), + message=reports.messages.UseCommandRemoveAndAddGuestNode(), + ) + ) + + # Special case - when remote-node is set up without node-addr, it is used as + # an address, so adding remote-addr counts as an address change too + if ( + _OPTION_REMOTE_ADDR not in existing_meta_attrs + and OPTION_REMOTE_NODE in existing_meta_attrs + and existing_meta_attrs[OPTION_REMOTE_NODE] + and _OPTION_REMOTE_ADDR in new_meta_attrs + and new_meta_attrs[_OPTION_REMOTE_ADDR] + ): + report_list.append( + ReportItem( + severity=reports.item.get_severity_from_flags( + reports.codes.FORCE, + force_flags, + ), + message=reports.messages.UseCommandRemoveAndAddGuestNode(), + ) + ) + + return report_list + + def validate_conflicts( tree: _Element, existing_nodes_names: StringCollection, @@ -37,33 +175,40 @@ def validate_conflicts( if ( does_id_exist(tree, node_name) or node_name in existing_nodes_names - or ("remote-addr" not in options and node_name in existing_nodes_addrs) + or ( + _OPTION_REMOTE_ADDR not in options + and node_name in existing_nodes_addrs + ) ): report_list.append( - ReportItem.error(reports.messages.IdAlreadyExists(node_name)) + ReportItem.error( + reports.messages.GuestNodeNameAlreadyExists(node_name) + ) ) if ( - "remote-addr" in options - and options["remote-addr"] in existing_nodes_addrs + _OPTION_REMOTE_ADDR in options + and options[_OPTION_REMOTE_ADDR] in existing_nodes_addrs ): report_list.append( ReportItem.error( - reports.messages.IdAlreadyExists(options["remote-addr"]) + reports.messages.NodeAddressesAlreadyExist( + [options[_OPTION_REMOTE_ADDR]] + ) ) ) return report_list def is_node_name_in_options(options): - return "remote-node" in options + return OPTION_REMOTE_NODE in options def get_guest_option_value(options, default=None): """ Commandline options: no options """ - return options.get("remote-node", default) + return options.get(OPTION_REMOTE_NODE, default) def validate_set_as_guest( @@ -71,8 +216,8 @@ def validate_set_as_guest( ): validator_list = [ validate.NamesIn(GUEST_OPTIONS, option_type="guest"), - validate.ValueTimeInterval("remote-connect-timeout"), - validate.ValuePortNumber("remote-port"), + validate.ValueTimeInterval(_OPTION_REMOTE_CONN_TIMEOUT), + validate.ValuePortNumber(_OPTION_REMOTE_PORT), ] return ( validate.ValidatorAll(validator_list).validate(options) @@ -91,7 +236,7 @@ def is_guest_node(resource_element): etree.Element resource_element is a search element """ - return has_meta_attribute(resource_element, "remote-node") + return has_meta_attribute(resource_element, OPTION_REMOTE_NODE) def validate_is_not_guest(resource_element): @@ -124,13 +269,13 @@ def set_as_guest( etree.Element resource_element """ - meta_options = {"remote-node": str(node)} + meta_options = {OPTION_REMOTE_NODE: str(node)} if addr: - meta_options["remote-addr"] = str(addr) + meta_options[_OPTION_REMOTE_ADDR] = str(addr) if port: - meta_options["remote-port"] = str(port) + meta_options[_OPTION_REMOTE_PORT] = str(port) if connect_timeout: - meta_options["remote-connect-timeout"] = str(connect_timeout) + meta_options[_OPTION_REMOTE_CONN_TIMEOUT] = str(connect_timeout) arrange_first_meta_attributes(resource_element, meta_options, id_provider) @@ -152,7 +297,7 @@ def unset_guest(resource_element): " or ".join( [ f'@name="{option}"' - for option in (GUEST_OPTIONS + ["remote-node"]) + for option in (GUEST_OPTIONS + [OPTION_REMOTE_NODE]) ] ) ) @@ -167,7 +312,7 @@ def get_node_name_from_options(meta_options, default=None): Return node_name from meta options. dict meta_options """ - return meta_options.get("remote-node", default) + return meta_options.get(OPTION_REMOTE_NODE, default) def get_node_name_from_resource(resource_element): @@ -176,7 +321,7 @@ def get_node_name_from_resource(resource_element): etree.Element resource_element """ - return get_meta_attribute_value(resource_element, "remote-node") + return get_meta_attribute_value(resource_element, OPTION_REMOTE_NODE) def find_node_list(tree): @@ -201,9 +346,9 @@ def find_node_list(tree): host = None name = None for nvpair in meta_attrs: - if nvpair.attrib.get("name", "") == "remote-addr": + if nvpair.attrib.get("name", "") == _OPTION_REMOTE_ADDR: host = nvpair.attrib["value"] - if nvpair.attrib.get("name", "") == "remote-node": + if nvpair.attrib.get("name", "") == OPTION_REMOTE_NODE: name = nvpair.attrib["value"] if host is None: host = name diff --git a/pcs/lib/cib/resource/operations.py b/pcs/lib/cib/resource/operations.py index 68a4ea3d7..a9b9ed382 100644 --- a/pcs/lib/cib/resource/operations.py +++ b/pcs/lib/cib/resource/operations.py @@ -5,6 +5,7 @@ List, Optional, Tuple, + cast, ) from lxml import etree @@ -449,15 +450,17 @@ def append_new_operation(operations_element, id_provider, options): return op_element -def get_resource_operations(resource_el, names=None): +def get_resource_operations( + resource_el: _Element, names: Optional[StringCollection] = None +) -> list[_Element]: """ Get operations of a given resource, optionally filtered by name - etree resource_el -- resource element - iterable names -- return only operations of these names if specified + resource_el -- resource element + names -- return only operations of these names if specified """ return [ op_el - for op_el in resource_el.xpath("./operations/op") + for op_el in cast(list[_Element], resource_el.xpath("./operations/op")) if not names or op_el.attrib.get("name", "") in names ] diff --git a/pcs/lib/cib/resource/primitive.py b/pcs/lib/cib/resource/primitive.py index a82bd92fa..b401014f9 100644 --- a/pcs/lib/cib/resource/primitive.py +++ b/pcs/lib/cib/resource/primitive.py @@ -57,6 +57,16 @@ def is_primitive(resource_el: _Element) -> bool: return resource_el.tag == TAG +def resource_agent_name_from_primitive( + primitive_el: _Element, +) -> ResourceAgentName: + return ResourceAgentName( + standard=str(primitive_el.attrib["class"]), + provider=primitive_el.get("provider"), + type=str(primitive_el.attrib["type"]), + ) + + def primitive_element_to_dto( primitive_element: _Element, rule_eval: Optional[rule.RuleInEffectEval] = None, diff --git a/pcs/lib/cib/resource/remote_node.py b/pcs/lib/cib/resource/remote_node.py index b1425ca3e..c58ef224d 100644 --- a/pcs/lib/cib/resource/remote_node.py +++ b/pcs/lib/cib/resource/remote_node.py @@ -145,7 +145,9 @@ def validate_host_not_conflicts( host = instance_attributes.get("server", node_name) if host in existing_nodes_addrs: return [ - reports.ReportItem.error(reports.messages.IdAlreadyExists(host)) + reports.ReportItem.error( + reports.messages.NodeAddressesAlreadyExist([host]) + ) ] return [] diff --git a/pcs/lib/commands/resource.py b/pcs/lib/commands/resource.py index 8c6e1bc2a..cd50fafe3 100644 --- a/pcs/lib/commands/resource.py +++ b/pcs/lib/commands/resource.py @@ -26,7 +26,7 @@ ) from pcs.common.interface import dto from pcs.common.pacemaker.resource.list import CibResourcesDto -from pcs.common.reports import ReportItemList +from pcs.common.reports import ReportItemList, ReportProcessor from pcs.common.reports.item import ReportItem from pcs.common.tools import ( Version, @@ -36,6 +36,13 @@ from pcs.lib.cib import const as cib_const from pcs.lib.cib import resource from pcs.lib.cib import status as cib_status +from pcs.lib.cib.nvpair_multi import ( + NVSET_META, + find_nvsets, + nvset_append_new, + nvset_to_dict_except_without_values, + nvset_update, +) from pcs.lib.cib.tag import expand_tag from pcs.lib.cib.tools import ( ElementNotFound, @@ -43,6 +50,7 @@ find_element_by_tag_and_id, get_element_by_id, get_elements_by_ids, + get_pacemaker_version_by_which_cib_was_validated, get_resources, get_status, ) @@ -64,6 +72,7 @@ get_cluster_status_dom, has_resource_unmove_unban_expired_support, push_cib_diff_xml, + remove_node, resource_ban, resource_move, resource_restart, @@ -92,6 +101,7 @@ resource_agent_error_to_report_item, split_resource_agent_name, ) +from pcs.lib.resource_agent.const import OCF_1_1 from pcs.lib.sbd_stonith import ensure_some_stonith_remains from pcs.lib.tools import get_tmp_cib from pcs.lib.validate import ValueTimeInterval @@ -158,60 +168,66 @@ def inner(state, resource_id): return inner -def _get_agent_facade( - report_processor: reports.ReportProcessor, - runner: CommandRunner, - factory: ResourceAgentFacadeFactory, - name: str, - allow_absent_agent: bool, -) -> ResourceAgentFacade: +def _get_resource_agent_name( + runner: CommandRunner, report_processor: reports.ReportProcessor, name: str +) -> ResourceAgentName: try: - split_name = ( + agent_name = ( split_resource_agent_name(name) if ":" in name else find_one_resource_agent_by_type(runner, report_processor, name) ) - if split_name.is_stonith: - report_processor.report( - reports.ReportItem.deprecation( - reports.messages.ResourceStonithCommandsMismatch( - "fence agent", reports.const.PCS_COMMAND_STONITH_CREATE - ) - ) - ) - if split_name.standard in ("nagios", "upstart"): - # TODO deprecated in pacemaker 2, to be removed in pacemaker 3 - # added to pcs after 0.11.7 - report_processor.report( - reports.ReportItem.deprecation( - reports.messages.DeprecatedOptionValue( - "standard", split_name.standard - ) - ) + except ResourceAgentError as e: + report_processor.report( + resource_agent_error_to_report_item( + e, reports.ReportItemSeverity.error() ) + ) + raise LibraryError() from e - return factory.facade_from_parsed_name(split_name) - except (UnableToGetAgentMetadata, UnsupportedOcfVersion) as e: - if allow_absent_agent: - report_processor.report( - resource_agent_error_to_report_item( - e, reports.ReportItemSeverity.warning() + if agent_name.is_stonith: + report_processor.report( + reports.ReportItem.deprecation( + reports.messages.ResourceStonithCommandsMismatch( + "fence agent", reports.const.PCS_COMMAND_STONITH_CREATE ) ) - return factory.void_facade_from_parsed_name(split_name) + ) + + if agent_name.standard in ("nagios", "upstart"): + # TODO deprecated in pacemaker 2, to be removed in pacemaker 3 + # added to pcs after 0.11.7 report_processor.report( - resource_agent_error_to_report_item( - e, reports.ReportItemSeverity.error(reports.codes.FORCE) + reports.ReportItem.deprecation( + reports.messages.DeprecatedOptionValue( + "standard", agent_name.standard + ) ) ) - raise LibraryError() from e - except ResourceAgentError as e: + + return agent_name + + +def _get_resource_agent_facade( + report_processor: reports.ReportProcessor, + factory: ResourceAgentFacadeFactory, + agent_name: ResourceAgentName, + force_flags: reports.types.ForceFlags, +) -> ResourceAgentFacade: + try: + return factory.facade_from_parsed_name(agent_name) + except (UnableToGetAgentMetadata, UnsupportedOcfVersion) as e: report_processor.report( resource_agent_error_to_report_item( - e, reports.ReportItemSeverity.error() + e, + reports.get_severity_from_flags( + reports.codes.FORCE, force_flags + ), ) ) - raise LibraryError() from e + if report_processor.has_errors: + raise LibraryError() from e + return factory.void_facade_from_parsed_name(agent_name) def _validate_remote_connection( @@ -356,6 +372,79 @@ def _check_special_cases( raise LibraryError() +def _validate_clone_meta_attributes( + report_processor: ReportProcessor, + agent_facade_factory: ResourceAgentFacadeFactory, + resource_el: _Element, + meta_attrs: Mapping[str, str], + force_flags: reports.types.ForceFlags, +) -> None: + clone_child_el = resource.clone.get_inner_resource(resource_el) + if clone_child_el is None: + return + + group_id = None + if resource.group.is_group(clone_child_el): + group_id = str(clone_child_el.attrib["id"]) + + inner_primitives = resource.clone.get_inner_primitives(resource_el) + + facade_cache: dict[ResourceAgentName, ResourceAgentFacade] = {} + for primitive_el in inner_primitives: + agent_name = resource.primitive.resource_agent_name_from_primitive( + primitive_el + ) + if agent_name.is_ocf: + if agent_name in facade_cache: + agent_facade = facade_cache[agent_name] + else: + agent_facade = _get_resource_agent_facade( + report_processor, + agent_facade_factory, + agent_name, + force_flags, + ) + facade_cache[agent_name] = agent_facade + + if ( + agent_facade.metadata.ocf_version == OCF_1_1 + and is_true(meta_attrs.get(resource.clone.META_PROMOTABLE, "0")) + and not agent_facade.metadata.provides_promotability + ): + report_processor.report( + reports.ReportItem( + reports.get_severity_from_flags( + reports.codes.FORCE, + force_flags, + ), + reports.messages.ResourceCloneIncompatibleMetaAttributes( + resource.clone.META_PROMOTABLE, + agent_name.to_dto(), + resource_id=primitive_el.get("id"), + group_id=group_id, + ), + ) + ) + else: + report_processor.report_list( + [ + reports.ReportItem.error( + reports.messages.ResourceCloneIncompatibleMetaAttributes( + incompatible_attr, + agent_name.to_dto(), + resource_id=primitive_el.get("id"), + group_id=group_id, + ) + ) + for incompatible_attr in [ + resource.clone.META_GLOBALLY_UNIQUE, + resource.clone.META_PROMOTABLE, + ] + if is_true(meta_attrs.get(incompatible_attr, "0")) + ] + ) + + _find_bundle = partial( find_element_by_tag_and_id, cib_const.TAG_RESOURCE_BUNDLE ) @@ -434,12 +523,14 @@ def create( # noqa: PLR0913 """ runner = env.cmd_runner() agent_factory = ResourceAgentFacadeFactory(runner, env.report_processor) - resource_agent = _get_agent_facade( + agent_name = _get_resource_agent_name( + runner, env.report_processor, resource_agent_name + ) + resource_agent = _get_resource_agent_facade( env.report_processor, - runner, agent_factory, - resource_agent_name, - allow_absent_agent, + agent_name, + [reports.codes.FORCE] if allow_absent_agent else [], ) with resource_environment( env, @@ -540,12 +631,14 @@ def create_as_clone( # noqa: PLR0913 """ runner = env.cmd_runner() agent_factory = ResourceAgentFacadeFactory(runner, env.report_processor) - resource_agent = _get_agent_facade( + agent_name = _get_resource_agent_name( + runner, env.report_processor, resource_agent_name + ) + resource_agent = _get_resource_agent_facade( env.report_processor, - runner, agent_factory, - resource_agent_name, - allow_absent_agent, + agent_name, + [reports.codes.FORCE] if allow_absent_agent else [], ) if resource_agent.metadata.name.standard != "ocf": for incompatible_attr in ("globally-unique", "promotable"): @@ -688,12 +781,14 @@ def create_in_group( # noqa: PLR0913 """ runner = env.cmd_runner() agent_factory = ResourceAgentFacadeFactory(runner, env.report_processor) - resource_agent = _get_agent_facade( + agent_name = _get_resource_agent_name( + runner, env.report_processor, resource_agent_name + ) + resource_agent = _get_resource_agent_facade( env.report_processor, - runner, agent_factory, - resource_agent_name, - allow_absent_agent, + agent_name, + [reports.codes.FORCE] if allow_absent_agent else [], ) with resource_environment( env, @@ -832,12 +927,14 @@ def create_into_bundle( # noqa: PLR0913 """ runner = env.cmd_runner() agent_factory = ResourceAgentFacadeFactory(runner, env.report_processor) - resource_agent = _get_agent_facade( + agent_name = _get_resource_agent_name( + runner, env.report_processor, resource_agent_name + ) + resource_agent = _get_resource_agent_facade( env.report_processor, - runner, agent_factory, - resource_agent_name, - allow_absent_agent, + agent_name, + [reports.codes.FORCE] if allow_absent_agent else [], ) required_cib_version = get_required_cib_version_for_primitive( operation_list @@ -2463,30 +2560,26 @@ def unmove_unban( raise LibraryError() -def get_resource_relations_tree( - env: LibraryEnvironment, +def _find_resource_elem( + cib: _Element, resource_id: str, -) -> Mapping[str, Any]: +) -> _Element: """ - Return a dict representing tree-like structure of resources and their - relations. + Find a resource element in CIB and handle errors. - env -- library environment - resource_id -- id of a resource which should be the root of the relation - tree + cib -- CIB + resource_id -- name of the resource """ - cib = env.get_cib() - try: resource_el = get_element_by_id(cib, resource_id) - except ElementNotFound as e: + except ElementNotFound as exc: raise LibraryError( ReportItem.error( reports.messages.IdNotFound( resource_id, expected_types=["resource"] ) ) - ) from e + ) from exc if not resource.common.is_resource(resource_el): raise LibraryError( ReportItem.error( @@ -2497,6 +2590,23 @@ def get_resource_relations_tree( ) ) ) + return resource_el + + +def get_resource_relations_tree( + env: LibraryEnvironment, + resource_id: str, +) -> Mapping[str, Any]: + """ + Return a dict representing tree-like structure of resources and their + relations. + + env -- library environment + resource_id -- id of a resource which should be the root of the relation + tree + """ + cib = env.get_cib() + resource_el = _find_resource_elem(cib, resource_id) if resource.stonith.is_stonith(resource_el): env.report_processor.report( reports.ReportItem.deprecation( @@ -2708,3 +2818,102 @@ def restart( node=node, timeout=timeout, ) + + +def update_meta( + env: LibraryEnvironment, + resource_id: str, + meta_attrs: Mapping[str, str], + force_flags: reports.types.ForceFlags, +) -> None: + """ + Update meta attributes of all resource types without stonith check + + env -- library environment + resource_id -- id of resource to update + meta_attrs -- meta attributes to update with desired values + force_flags -- force flags + """ + cib = env.get_cib() + resource_el = _find_resource_elem(cib, resource_id) + id_provider = IdProvider(cib) + cib_validate_with = get_pacemaker_version_by_which_cib_was_validated(cib) + if resource.clone.is_master(resource_el): + resource.clone.convert_master_to_promotable( + id_provider, cib_validate_with, resource_el + ) + + meta_attrs_nvset_list = find_nvsets(resource_el, NVSET_META) + meta_attrs_nvset = ( + meta_attrs_nvset_list[0] if meta_attrs_nvset_list else None + ) + + ( + existing_nodes_names, + existing_nodes_addrs, + report_list, + ) = get_existing_nodes_names_addrs( + env.get_corosync_conf() if env.is_cib_live else None, + cib=cib, + ) + env.report_processor.report_list(report_list) + + existing_meta_attrs = ( + nvset_to_dict_except_without_values(meta_attrs_nvset) + if meta_attrs_nvset is not None + else {} + ) + if not resource.stonith.is_stonith(resource_el): + env.report_processor.report_list( + resource.guest_node.validate_updating_guest_attributes( + cib, + existing_nodes_names, + existing_nodes_addrs, + meta_attrs, + existing_meta_attrs, + force_flags, + ) + ) + + cmd_runner = env.cmd_runner() + + if resource.clone.is_any_clone(resource_el): + _validate_clone_meta_attributes( + env.report_processor, + ResourceAgentFacadeFactory(cmd_runner, env.report_processor), + resource_el, + meta_attrs, + force_flags, + ) + + if env.report_processor.has_errors: + raise LibraryError() + + # Do not add element if user didn't provide any value + if meta_attrs_nvset is None and any(meta_attrs.values()): + nvset_append_new( + resource_el, + id_provider, + cib_validate_with, + NVSET_META, + nvpair_dict=meta_attrs, + nvset_options={}, + ) + elif meta_attrs_nvset is not None: + nvset_update(meta_attrs_nvset, id_provider, meta_attrs) + + env.push_cib() + + # If remote node was removed or its name changed, it needs to be removed + # from pacemaker + if ( + reports.codes.FORCE in force_flags + and resource.guest_node.OPTION_REMOTE_NODE in meta_attrs + and resource.guest_node.OPTION_REMOTE_NODE in existing_meta_attrs + and existing_meta_attrs[resource.guest_node.OPTION_REMOTE_NODE] + != meta_attrs[resource.guest_node.OPTION_REMOTE_NODE] + ): + remove_node( + cmd_runner, + existing_meta_attrs[resource.guest_node.OPTION_REMOTE_NODE], + ) diff --git a/pcs/lib/node.py b/pcs/lib/node.py index aa40a59bd..3dd215fb7 100644 --- a/pcs/lib/node.py +++ b/pcs/lib/node.py @@ -43,7 +43,7 @@ def get_pacemaker_node_names(cib: _Element) -> Set[str]: def get_existing_nodes_names_addrs( corosync_conf=None, cib=None, error_on_missing_name=False -): +) -> tuple[list[str], list[str], ReportItemList]: corosync_nodes, remote_and_guest_nodes = __get_nodes(corosync_conf, cib) names, report_list = __get_nodes_names( corosync_nodes, remote_and_guest_nodes, error_on_missing_name @@ -106,7 +106,10 @@ def __get_nodes_names( ) -def __get_nodes_addrs(corosync_nodes, remote_and_guest_nodes): +def __get_nodes_addrs( + corosync_nodes: Iterable[CorosyncNode], + remote_and_guest_nodes: Iterable[PacemakerNode], +) -> list[str]: nodes_addrs = [node.addr for node in remote_and_guest_nodes] for node in corosync_nodes: nodes_addrs += node.addrs_plain() diff --git a/pcs/lib/resource_agent/types.py b/pcs/lib/resource_agent/types.py index 9b1bd421a..53ec08081 100644 --- a/pcs/lib/resource_agent/types.py +++ b/pcs/lib/resource_agent/types.py @@ -38,6 +38,10 @@ def is_pcmk_fake_agent(self) -> bool: def is_stonith(self) -> bool: return self.standard == "stonith" + @property + def is_ocf(self) -> bool: + return self.standard == "ocf" + def to_dto(self) -> ResourceAgentNameDto: return ResourceAgentNameDto( standard=self.standard, diff --git a/pcs/pcs.8.in b/pcs/pcs.8.in index 0bc649a1e..c6acf3b01 100644 --- a/pcs/pcs.8.in +++ b/pcs/pcs.8.in @@ -309,8 +309,8 @@ Add, remove or change default values for operations. This is a simplified comman NOTE: Defaults do not apply to resources / stonith devices which override them with their own defined values. .TP -meta [\fB\-\-wait\fR[=n]] -Add specified options to the specified resource, group or clone. Meta options should be in the format of name=value, options may be removed by setting an option without a value. If \fB\-\-wait\fR is specified, pcs will wait up to 'n' seconds for the changes to take effect and then return 0 if the changes have been processed or 1 otherwise. If 'n' is not specified it defaults to 60 minutes. +meta [\fB\-\-wait\fR[=n]] +Add specified options to the specified resource. Meta options should be in the format of name=value, options may be removed by setting an option without a value. If \fB\-\-wait\fR is specified, pcs will wait up to 'n' seconds for the changes to take effect and then return 0 if the changes have been processed or 1 otherwise. If 'n' is not specified it defaults to 60 minutes. .br Example: pcs resource meta TestResource failure\-timeout=50 resource\-stickiness= .TP diff --git a/pcs/resource.py b/pcs/resource.py index 357789b3b..01254db2a 100644 --- a/pcs/resource.py +++ b/pcs/resource.py @@ -51,6 +51,7 @@ deprecation_warning, warn, ) +from pcs.cli.resource.common import check_is_not_stonith from pcs.cli.resource.output import ( operation_defaults_to_cmd, resource_agent_metadata_to_text, @@ -82,7 +83,6 @@ from pcs.common.str_tools import ( format_list, format_list_custom_last_separator, - format_optional, format_plural, ) from pcs.lib.cib.resource import ( @@ -109,18 +109,6 @@ RESOURCE_RELOCATE_CONSTRAINT_PREFIX = "pcs-relocate-" -def _check_is_not_stonith( - lib: Any, resource_id_list: list[str], cmd_to_use: Optional[str] = None -) -> None: - if lib.resource.is_any_stonith(resource_id_list): - deprecation_warning( - reports.messages.ResourceStonithCommandsMismatch( - "stonith resources" - ).message - + format_optional(cmd_to_use, " Please use '{}' instead.") - ) - - def _detect_guest_change( meta_attributes: Mapping[str, str], allow_not_suitable_command: bool ) -> None: @@ -170,7 +158,7 @@ def resource_utilization_cmd( print_resources_utilization() return resource_id = argv.pop(0) - _check_is_not_stonith(lib, [resource_id]) + check_is_not_stonith(lib, [resource_id]) if argv: set_resource_utilization(resource_id, argv) else: @@ -460,7 +448,7 @@ def op_add_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ if not argv: raise CmdLineInputError() - _check_is_not_stonith(lib, [argv[0]], "pcs stonith op add") + check_is_not_stonith(lib, [argv[0]], "pcs stonith op add") resource_op_add(argv, modifiers) @@ -506,7 +494,7 @@ def op_delete_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: if not argv: raise CmdLineInputError() resource_id = argv.pop(0) - _check_is_not_stonith(lib, [resource_id], "pcs stonith op delete") + check_is_not_stonith(lib, [resource_id], "pcs stonith op delete") resource_operation_remove(resource_id, argv) @@ -1010,7 +998,7 @@ def update_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ if not argv: raise CmdLineInputError() - _check_is_not_stonith(lib, [argv[0]], "pcs stonith update") + check_is_not_stonith(lib, [argv[0]], "pcs stonith update") resource_update(argv, modifiers) @@ -1508,96 +1496,6 @@ def resource_operation_remove(res_id: str, argv: Argv) -> None: # noqa: PLR0912 utils.replace_cib_configuration(dom) -def meta_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: - """ - Options: - * --force - allow not suitable command - * --wait - * -f - CIB file - """ - if not argv: - raise CmdLineInputError() - _check_is_not_stonith(lib, [argv[0]], "pcs stonith meta") - resource_meta(argv, modifiers) - - -def resource_meta(argv: Argv, modifiers: InputModifiers) -> None: # noqa: PLR0912 - """ - Commandline options: - * --force - allow not suitable command - * --wait - * -f - CIB file - """ - # pylint: disable=too-many-branches - modifiers.ensure_only_supported("--force", "--wait", "-f") - if len(argv) < 2: - raise CmdLineInputError() - res_id = argv.pop(0) - _detect_guest_change( - KeyValueParser(argv).get_unique(), - bool(modifiers.get("--force")), - ) - - dom = utils.get_cib_dom() - - master = utils.dom_get_master(dom, res_id) - if master: - resource_el = transform_master_to_clone(master) - else: - resource_el = utils.dom_get_any_resource(dom, res_id) - if resource_el is None: - raise CmdLineInputError( - f"unable to find a resource/clone/group: {res_id}" - ) - - if modifiers.is_specified("--wait"): - wait_timeout = utils.validate_wait_get_timeout() - - attr_tuples = utils.convert_args_to_tuples(argv) - - if resource_el.tagName == "clone": - clone_child = utils.dom_elem_get_clone_ms_resource(resource_el) - if clone_child: - _check_clone_incompatible_options_child( - clone_child, - dict(attr_tuples), - force=bool(modifiers.get("--force")), - ) - - remote_node_name = utils.dom_get_resource_remote_node_name(resource_el) - utils.dom_update_meta_attr(resource_el, attr_tuples) - - utils.replace_cib_configuration(dom) - - if ( - remote_node_name - and remote_node_name - != utils.dom_get_resource_remote_node_name(resource_el) - ): - # if the resource was a remote node and it is not anymore, (or its name - # changed) we need to tell pacemaker about it - output, retval = utils.run( - ["crm_node", "--force", "--remove", remote_node_name] - ) - - if modifiers.is_specified("--wait"): - args = ["crm_resource", "--wait"] - if wait_timeout: - args.extend(["--timeout=%s" % wait_timeout]) - output, retval = utils.run(args) - running_on = utils.resource_running_on(res_id) - if retval == 0: - print_to_stderr(running_on["message"]) - else: - msg = [] - if retval == PACEMAKER_WAIT_TIMEOUT_STATUS: - msg.append("waiting timeout") - msg.append(running_on["message"]) - if retval != 0 and output: - msg.append("\n" + output) - utils.err("\n".join(msg).strip()) - - def resource_group_rm_cmd( lib: Any, argv: Argv, modifiers: InputModifiers ) -> None: @@ -1686,7 +1584,7 @@ def resource_clone( raise CmdLineInputError() res = argv[0] - _check_is_not_stonith(lib, [res]) + check_is_not_stonith(lib, [res]) cib_dom = utils.get_cib_dom() if modifiers.is_specified("--wait"): @@ -2321,7 +2219,7 @@ def resource_disable_cmd( """ if not argv: raise CmdLineInputError("You must specify resource(s) to disable") - _check_is_not_stonith(lib, argv, "pcs stonith disable") + check_is_not_stonith(lib, argv, "pcs stonith disable") resource_disable_common(lib, argv, modifiers) @@ -2435,7 +2333,7 @@ def resource_enable_cmd( if not argv: raise CmdLineInputError("You must specify resource(s) to enable") resources = argv - _check_is_not_stonith(lib, resources, "pcs stonith enable") + check_is_not_stonith(lib, resources, "pcs stonith enable") lib.resource.enable(resources, modifiers.get("--wait")) @@ -2457,7 +2355,7 @@ def resource_restart_cmd( if argv: raise CmdLineInputError() - _check_is_not_stonith(lib, [resource]) + check_is_not_stonith(lib, [resource]) timeout = ( modifiers.get("--wait") if modifiers.is_specified("--wait") else None ) @@ -2493,7 +2391,7 @@ def resource_force_action( # noqa: PLR0912 raise CmdLineInputError() resource = argv[0] - _check_is_not_stonith(lib, [resource]) + check_is_not_stonith(lib, [resource]) dom = utils.get_cib_dom() if not ( @@ -2563,7 +2461,7 @@ def resource_manage_cmd( if not argv: raise CmdLineInputError("You must specify resource(s) to manage") resources = argv - _check_is_not_stonith(lib, resources) + check_is_not_stonith(lib, resources) lib.resource.manage(resources, with_monitor=modifiers.get("--monitor")) @@ -2579,7 +2477,7 @@ def resource_unmanage_cmd( if not argv: raise CmdLineInputError("You must specify resource(s) to unmanage") resources = argv - _check_is_not_stonith(lib, resources) + check_is_not_stonith(lib, resources) lib.resource.unmanage(resources, with_monitor=modifiers.get("--monitor")) @@ -2804,7 +2702,7 @@ def resource_relocate_dry_run_cmd( """ modifiers.ensure_only_supported("-f") if argv: - _check_is_not_stonith(lib, argv) + check_is_not_stonith(lib, argv) resource_relocate_run(utils.get_cib_dom(), argv, dry=True) @@ -2816,7 +2714,7 @@ def resource_relocate_run_cmd( """ modifiers.ensure_only_supported() if argv: - _check_is_not_stonith(lib, argv) + check_is_not_stonith(lib, argv) resource_relocate_run(utils.get_cib_dom(), argv, dry=False) diff --git a/pcs/stonith.py b/pcs/stonith.py index 2e15d6bad..dfddf0cf8 100644 --- a/pcs/stonith.py +++ b/pcs/stonith.py @@ -27,6 +27,7 @@ from pcs.cli.resource.parse_args import ( parse_primitive as parse_primitive_resource, ) +from pcs.cli.stonith.common import check_is_stonith from pcs.cli.stonith.levels.output import stonith_level_config_to_text from pcs.common import reports from pcs.common.fencing_topology import ( @@ -40,7 +41,6 @@ from pcs.common.resource_agent.dto import ResourceAgentNameDto from pcs.common.str_tools import ( format_list, - format_optional, format_plural, indent, ) @@ -133,20 +133,6 @@ def stonith_list_options( ) -def _check_is_stonith( - lib: Any, - resource_id_list: list[str], - cmd_to_use: Optional[str] = None, -) -> None: - if lib.resource.is_any_resource_except_stonith(resource_id_list): - deprecation_warning( - reports.messages.ResourceStonithCommandsMismatch( - "resources" - ).message - + format_optional(cmd_to_use, " Please use '{}' instead.") - ) - - def stonith_create(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: @@ -325,7 +311,7 @@ def stonith_level_add_cmd( level_id = parser.get_unique().get("id") target_type, target_value = _stonith_level_parse_node(argv[1]) stonith_devices = _stonith_level_normalize_devices(argv[2:]) - _check_is_stonith(lib, stonith_devices) + check_is_stonith(lib, stonith_devices) lib.fencing_topology.add_level( argv[0], @@ -1004,7 +990,7 @@ def enable_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: "You must specify stonith resource(s) to enable" ) resources = argv - _check_is_stonith(lib, resources, "pcs resource enable") + check_is_stonith(lib, resources, "pcs resource enable") lib.resource.enable(resources, modifiers.get("--wait")) @@ -1023,7 +1009,7 @@ def disable_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: raise CmdLineInputError( "You must specify stonith resource(s) to disable" ) - _check_is_stonith(lib, argv, "pcs resource disable") + check_is_stonith(lib, argv, "pcs resource disable") if modifiers.is_specified_any(("--safe", "--no-strict", "--simulate")): deprecation_warning( "Options '--safe', '--no-strict' and '--simulate' are deprecated " @@ -1042,7 +1028,7 @@ def update_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ if not argv: raise CmdLineInputError() - _check_is_stonith(lib, [argv[0]], "pcs resource update") + check_is_stonith(lib, [argv[0]], "pcs resource update") resource.resource_update(argv, modifiers) @@ -1054,7 +1040,7 @@ def op_add_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ if not argv: raise CmdLineInputError() - _check_is_stonith(lib, [argv[0]], "pcs resource op add") + check_is_stonith(lib, [argv[0]], "pcs resource op add") resource.resource_op_add(argv, modifiers) @@ -1066,18 +1052,5 @@ def op_delete_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: modifiers.ensure_only_supported("-f") if not argv: raise CmdLineInputError() - _check_is_stonith(lib, [argv[0]], "pcs resource op delete") + check_is_stonith(lib, [argv[0]], "pcs resource op delete") resource.resource_operation_remove(argv[0], argv[1:]) - - -def meta_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: - """ - Options: - * --force - allow not suitable command - * --wait - * -f - CIB file - """ - if not argv: - raise CmdLineInputError() - _check_is_stonith(lib, [argv[0]], "pcs resource meta") - resource.resource_meta(argv, modifiers) diff --git a/pcs/usage.py b/pcs/usage.py index 611c92699..bcea6c645 100644 --- a/pcs/usage.py +++ b/pcs/usage.py @@ -1400,15 +1400,11 @@ def resource(args: Argv) -> str: meta_syntax=_format_syntax( "{} {}".format( _RESOURCE_META_CMD, - _RESOURCE_META_SYNTAX.format( - obj="resource id | group id | clone id" - ), + _RESOURCE_META_SYNTAX.format(obj="resource id"), ) ), meta_desc=_format_desc( - _resource_meta_desc_fn( - obj="resource, group or clone", parent_cmd="resource" - ) + _resource_meta_desc_fn(obj="resource", parent_cmd="resource") ), defaults_config_syntax=_format_syntax( f"{_RESOURCE_DEFAULTS_CONFIG_CMD} {_RESOURCE_DEFAULTS_CONFIG_SYNTAX}" diff --git a/pcs_test/Makefile.am b/pcs_test/Makefile.am index 7b88c77ab..535cdb790 100644 --- a/pcs_test/Makefile.am +++ b/pcs_test/Makefile.am @@ -102,10 +102,12 @@ EXTRA_DIST = \ tier0/cli/reports/__init__.py \ tier0/cli/reports/test_messages.py \ tier0/cli/resource/__init__.py \ + tier0/cli/resource/test_common.py \ tier0/cli/resource/test_defaults.py \ tier0/cli/resource/test_parse_args.py \ tier0/cli/resource/test_relations.py \ tier0/cli/resource/test_remove.py \ + tier0/cli/resource/test_update.py \ tier0/cli/tag/__init__.py \ tier0/cli/tag/test_command.py \ tier0/cli/test_booth.py \ @@ -120,6 +122,7 @@ EXTRA_DIST = \ tier0/cli/stonith/__init__.py \ tier0/cli/stonith/levels/__init__.py \ tier0/cli/stonith/levels/test_output.py \ + tier0/cli/stonith/test_update.py \ tier0/common/__init__.py \ tier0/common/interface/__init__.py \ tier0/common/interface/test_dto.py \ @@ -267,6 +270,7 @@ EXTRA_DIST = \ tier0/lib/commands/resource/test_resource_move_autoclean.py \ tier0/lib/commands/resource/test_resource_move_ban.py \ tier0/lib/commands/resource/test_resource_relations.py \ + tier0/lib/commands/resource/test_resource_update.py \ tier0/lib/commands/resource/test_restart.py \ tier0/lib/commands/sbd/__init__.py \ tier0/lib/commands/sbd/test_disable_sbd.py \ @@ -297,9 +301,9 @@ EXTRA_DIST = \ tier0/lib/commands/test_sbd.py \ tier0/lib/commands/test_scsi.py \ tier0/lib/commands/test_status.py \ + tier0/lib/commands/test_stonith.py \ tier0/lib/commands/test_stonith_agent.py \ tier0/lib/commands/test_stonith_history.py \ - tier0/lib/commands/test_stonith.py \ tier0/lib/commands/test_stonith_update_scsi_devices.py \ tier0/lib/commands/test_ticket.py \ tier0/lib/communication/__init__.py \ @@ -382,6 +386,7 @@ EXTRA_DIST = \ tier1/cib_resource/test_operation_add.py \ tier1/cib_resource/test_stonith_create.py \ tier1/cib_resource/test_stonith_enable_disable.py \ + tier1/cib_resource/test_update.py \ tier1/cluster/common.py \ tier1/cluster/__init__.py \ tier1/cluster/test_cib_push.py \ diff --git a/pcs_test/tier0/cli/reports/test_messages.py b/pcs_test/tier0/cli/reports/test_messages.py index 188964954..bcd6158af 100644 --- a/pcs_test/tier0/cli/reports/test_messages.py +++ b/pcs_test/tier0/cli/reports/test_messages.py @@ -366,6 +366,17 @@ def test_success(self): ) +class UseCommandRemoveAndAddGuestNode(CliReportMessageTestBase): + def test_success(self): + self.assert_message( + messages.UseCommandRemoveAndAddGuestNode(), + "Changing connection parameters of an existing guest node is not " + "sufficient for connecting to a different guest node, remove the " + "existing guest node with 'pcs cluster node remove-guest' and add " + "a new one with 'pcs cluster node add-guest'", + ) + + class CorosyncNodeConflictCheckSkipped(CliReportMessageTestBase): def test_not_live_cib(self): self.assert_message( diff --git a/pcs_test/tier0/cli/resource/test_common.py b/pcs_test/tier0/cli/resource/test_common.py new file mode 100644 index 000000000..3d1121ad5 --- /dev/null +++ b/pcs_test/tier0/cli/resource/test_common.py @@ -0,0 +1,409 @@ +from typing import Optional +from unittest import ( + TestCase, + mock, +) + +from pcs.cli.resource import command +from pcs.common import const +from pcs.common.const import ( + PcmkRoleType, + PcmkStatusRoleType, +) +from pcs.common.status_dto import ( + BundleReplicaStatusDto, + BundleStatusDto, + CloneStatusDto, + GroupStatusDto, + PrimitiveStatusDto, + ResourcesStatusDto, +) + + +def _fixture_primitive_status_dto( + resource_id: str, + resource_agent: str = "ocf:pacemaker:Dummy", + target_role: Optional[PcmkRoleType] = None, + role: PcmkStatusRoleType = const.PCMK_STATUS_ROLE_STARTED, + managed: bool = True, + node_names: Optional[list[str]] = None, +) -> PrimitiveStatusDto: + return PrimitiveStatusDto( + resource_id=resource_id, + instance_id=None, + resource_agent=resource_agent, + role=role, + target_role=target_role, + active=False, + orphaned=False, + blocked=False, + maintenance=False, + description=None, + failed=False, + managed=managed, + failure_ignored=False, + node_names=node_names if node_names else [], + pending=None, + locked_to=None, + ) + + +def _fixture_group_status_dto( + group_size, + running_on_nodes, + instance_id=None, + agent_name="ocf:pacemaker:Stateful", +): + return GroupStatusDto( + resource_id="G1", + instance_id=instance_id, + maintenance=False, + description=None, + managed=True, + disabled=False, + members=[ + _fixture_primitive_status_dto( + f"R{i}", + agent_name, + node_names=running_on_nodes, + ) + for i in range(1, group_size) + ], + ) + + +def _fixture_bundle_status_dto(replicas): + return BundleStatusDto( + resource_id="B1", + type="docker", + image="pcs:test", + unique=True, + maintenance=False, + description=None, + managed=False, + failed=False, + replicas=replicas, + ) + + +def _fixture_bundle_replica_dto(replica_id, container): + return BundleReplicaStatusDto( + replica_id=replica_id, + member=None, + remote=None, + ip_address=None, + container=container, + ) + + +def _fixture_clone_status_dto(multi_state, instances): + return CloneStatusDto( + resource_id="clone", + multi_state=multi_state, + unique=False, + maintenance=False, + description=None, + managed=True, + disabled=False, + failed=False, + failure_ignored=False, + target_role=None, + instances=instances, + ) + + +class GetResourceMessage(TestCase): + def setUp(self): + self.lib = mock.Mock(spec_set=["status"]) + + self.lib.status = mock.Mock(spec_set=["resources_status"]) + self.lib.status.resources_status = self.mock_resources_status = ( + mock.Mock() + ) + + def test_bundle_not_running(self): + self.mock_resources_status.return_value = ResourcesStatusDto( + [ + _fixture_bundle_status_dto( + replicas=[ + _fixture_bundle_replica_dto( + replica_id=0, + container=_fixture_primitive_status_dto( + "B1-docker-0", + "ocf:heartbeat:docker", + role=const.PCMK_STATUS_ROLE_STOPPED, + managed=False, + node_names=["node1"], + ), + ), + ], + ) + ] + ) + self.assertEqual( + command.get_resource_status_msg(self.lib, "B1"), + "Resource 'B1' is not running on any nodes", + ) + + def test_bundle_running_one_replica(self): + self.mock_resources_status.return_value = ResourcesStatusDto( + [ + _fixture_bundle_status_dto( + replicas=[ + _fixture_bundle_replica_dto( + replica_id="0", + container=_fixture_primitive_status_dto( + "B1-docker-0", + "ocf:heartbeat:docker", + node_names=["node1"], + ), + ), + ], + ) + ] + ) + self.assertEqual( + command.get_resource_status_msg(self.lib, "B1"), + "Resource 'B1' is running on node 'node1'", + ) + + def test_bundle_running_and_failed(self): + self.mock_resources_status.return_value = ResourcesStatusDto( + [ + _fixture_bundle_status_dto( + replicas=[ + _fixture_bundle_replica_dto( + replica_id="0", + container=_fixture_primitive_status_dto( + "B1-docker-0", + "ocf:heartbeat:docker", + node_names=["node1"], + ), + ), + _fixture_bundle_replica_dto( + replica_id="1", + container=_fixture_primitive_status_dto( + "B1-docker-1", + "ocf:heartbeat:docker", + node_names=["node2"], + ), + ), + _fixture_bundle_replica_dto( + replica_id="2", + container=_fixture_primitive_status_dto( + "B1-docker-2", + "ocf:heartbeat:docker", + role=const.PCMK_STATUS_ROLE_STOPPED, + node_names=[], + ), + ), + ], + ) + ] + ) + self.assertEqual( + command.get_resource_status_msg(self.lib, "B1"), + "Resource 'B1' is running on nodes 'node1', 'node2'", + ) + + def test_bundle_running_multiple_replicas_one_node(self): + self.mock_resources_status.return_value = ResourcesStatusDto( + [ + _fixture_bundle_status_dto( + replicas=[ + _fixture_bundle_replica_dto( + replica_id="0", + container=_fixture_primitive_status_dto( + "B1-docker-0", + "ocf:heartbeat:docker", + node_names=["node1"], + ), + ), + _fixture_bundle_replica_dto( + replica_id="1", + container=_fixture_primitive_status_dto( + "B1-docker-1", + "ocf:heartbeat:docker", + node_names=["node1"], + ), + ), + ], + ) + ] + ) + self.assertEqual( + command.get_resource_status_msg(self.lib, "B1"), + "Resource 'B1' is running on node 'node1'", + ) + + def test_primitive(self): + status_expected_msg = { + const.PCMK_STATUS_ROLE_STARTED: "Resource 'R1' is running on node 'node1'" + } + status_expected_msg.update( + { + status: "Resource 'R1' is not running on any nodes" + for status in [ + const.PCMK_STATUS_ROLE_STARTING, + const.PCMK_STATUS_ROLE_STOPPING, + const.PCMK_STATUS_ROLE_STOPPED, + ] + } + ) + for status, expected_msg in status_expected_msg.items(): + with self.subTest(status=status, expected_msg=expected_msg): + self.mock_resources_status.return_value = ResourcesStatusDto( + [ + _fixture_primitive_status_dto( + "R1", + role=status, + node_names=["node1"], + ), + ] + ) + self.assertEqual( + command.get_resource_status_msg(self.lib, "R1"), + expected_msg, + ) + + def test_cloned_group_one_instance(self): + self.mock_resources_status.return_value = ResourcesStatusDto( + [ + _fixture_clone_status_dto( + multi_state=True, + instances=[ + _fixture_group_status_dto( + instance_id=0, + group_size=3, + running_on_nodes=["node1"], + ) + ], + ), + ] + ) + self.assertEqual( + command.get_resource_status_msg(self.lib, "clone"), + "Resource 'clone' is running on node 'node1'", + ) + + def test_cloned_group_more_instances(self): + self.mock_resources_status.return_value = ResourcesStatusDto( + [ + _fixture_clone_status_dto( + multi_state=True, + instances=[ + _fixture_group_status_dto( + instance_id=0, + group_size=3, + running_on_nodes=["node1"], + ), + _fixture_group_status_dto( + instance_id=1, + group_size=3, + running_on_nodes=["node2"], + ), + ], + ), + ] + ) + self.assertEqual( + command.get_resource_status_msg(self.lib, "clone"), + "Resource 'clone' is running on nodes 'node1', 'node2'", + ) + + def test_clone_one_instance(self): + self.mock_resources_status.return_value = ResourcesStatusDto( + [ + _fixture_clone_status_dto( + multi_state=False, + instances=[ + _fixture_primitive_status_dto( + "clone", + node_names=["node1"], + ) + ], + ), + ] + ) + self.assertEqual( + command.get_resource_status_msg(self.lib, "clone"), + "Resource 'clone' is running on node 'node1'", + ) + + def test_clone_more_instances(self): + self.mock_resources_status.return_value = ResourcesStatusDto( + [ + _fixture_clone_status_dto( + multi_state=False, + instances=[ + _fixture_primitive_status_dto( + "R6", node_names=["node1"] + ), + _fixture_primitive_status_dto( + "R6", node_names=["node1"] + ), + _fixture_primitive_status_dto( + "R6", node_names=["node2"] + ), + _fixture_primitive_status_dto( + "R6", node_names=["node2"] + ), + ], + ), + ] + ) + self.assertEqual( + command.get_resource_status_msg(self.lib, "clone"), + "Resource 'clone' is running on nodes 'node1', 'node2'", + ) + + def test_clone_promotable(self): + self.mock_resources_status.return_value = ResourcesStatusDto( + [ + _fixture_clone_status_dto( + multi_state=False, + instances=[ + _fixture_primitive_status_dto( + "R6", + node_names=["node1"], + role=const.PCMK_STATUS_ROLE_PROMOTED, + ), + _fixture_primitive_status_dto( + "R6", + node_names=["node2"], + role=const.PCMK_STATUS_ROLE_UNPROMOTED, + ), + _fixture_primitive_status_dto( + "R6", + node_names=["node3"], + role=const.PCMK_STATUS_ROLE_DEMOTING, + ), + _fixture_primitive_status_dto( + "R6", + node_names=["node3"], + role=const.PCMK_STATUS_ROLE_PROMOTING, + ), + ], + ), + ] + ) + self.assertEqual( + command.get_resource_status_msg(self.lib, "clone"), + "Resource 'clone' is promoted on node 'node1'; unpromoted on " + "node 'node2'", + ) + + def test_group(self): + self.mock_resources_status.return_value = ResourcesStatusDto( + [ + _fixture_group_status_dto( + group_size=2, + running_on_nodes=["node1", "node2"], + agent_name="ocf:pacemaker:Dummy", + ), + ] + ) + self.assertEqual( + command.get_resource_status_msg(self.lib, "G1"), + "Resource 'G1' is running on nodes 'node1', 'node2'", + ) diff --git a/pcs_test/tier0/cli/resource/test_update.py b/pcs_test/tier0/cli/resource/test_update.py new file mode 100644 index 000000000..f2b27a953 --- /dev/null +++ b/pcs_test/tier0/cli/resource/test_update.py @@ -0,0 +1,153 @@ +from unittest import ( + TestCase, + mock, +) + +from pcs.cli.common.errors import CmdLineInputError +from pcs.cli.resource import command +from pcs.common import reports + +from pcs_test.tools.misc import dict_to_modifiers + +RESOURCE_ID = "resource-id" + + +class ResourceMeta(TestCase): + command = staticmethod(command.meta) + + def setUp(self): + self.lib = mock.Mock(spec_set=["resource", "cluster"]) + + self.lib.resource = mock.Mock( + spec_set=[ + "update_meta", + "is_any_stonith", + "is_any_resource_except_stonith", + ] + ) + self.lib.resource.update_meta = self.update_meta = mock.Mock() + + mock_get_resource_status_msg_patcher = mock.patch( + "pcs.cli.resource.command.get_resource_status_msg" + ) + self.addCleanup(mock_get_resource_status_msg_patcher.stop) + self.mock_get_resource_status_msg = ( + mock_get_resource_status_msg_patcher.start() + ) + + self.lib.resource.is_any_stonith = mock.Mock() + self.lib.resource.is_any_stonith.return_value = False + + self.lib.cluster = mock.Mock(spec_set=["wait_for_pcmk_idle"]) + self.lib.cluster.wait_for_pcmk_idle = self.wait_for_pcmk_idle = ( + mock.Mock() + ) + + def _assert_no_wait_or_stonith(self): + self.lib.resource.is_any_stonith.assert_called_once_with([RESOURCE_ID]) + self.wait_for_pcmk_idle.assert_not_called() + self.mock_get_resource_status_msg.assert_not_called() + + def test_no_args(self): + with self.assertRaises(CmdLineInputError) as cm: + self.command(self.lib, [], dict_to_modifiers({})) + self.assertIsNone(cm.exception.message) + self.lib.resource.is_any_stonith.assert_not_called() + self.update_meta.assert_not_called() + self.wait_for_pcmk_idle.assert_not_called() + + def test_duplicate_attrs(self): + with self.assertRaises(CmdLineInputError) as cm: + self.command( + self.lib, + [RESOURCE_ID, "meta-attr1=value1", "meta-attr1=value2"], + dict_to_modifiers({}), + ) + self.assertEqual( + cm.exception.message, + "duplicate option 'meta-attr1' with different values 'value1' and " + "'value2'", + ) + self.update_meta.assert_not_called() + self._assert_no_wait_or_stonith() + + def test_one_attr(self): + self.command( + self.lib, + [RESOURCE_ID, "meta-attr1=value1"], + dict_to_modifiers({}), + ) + self.update_meta.assert_called_once_with( + RESOURCE_ID, {"meta-attr1": "value1"}, [] + ) + self._assert_no_wait_or_stonith() + + @mock.patch("pcs.cli.resource.common.deprecation_warning") + def test_stonith_deprecation(self, mock_warning): + self.lib.resource.is_any_stonith.return_value = True + self.command( + self.lib, + [RESOURCE_ID, "meta-attr1=value1"], + dict_to_modifiers({}), + ) + self.update_meta.assert_called_once_with( + RESOURCE_ID, {"meta-attr1": "value1"}, [] + ) + mock_warning.assert_called_once_with( + "Ability of this command to accept stonith resources is deprecated " + "and will be removed in a future release. Please use 'pcs stonith " + "meta' instead." + ) + self._assert_no_wait_or_stonith() + + def test_multiple_attrs(self): + self.command( + self.lib, + [RESOURCE_ID, "meta-attr1=value1", "meta-attr2=value2"], + dict_to_modifiers({}), + ) + self.update_meta.assert_called_once_with( + RESOURCE_ID, {"meta-attr1": "value1", "meta-attr2": "value2"}, [] + ) + self._assert_no_wait_or_stonith() + + @mock.patch("pcs.cli.resource.command.print") + def test_with_wait_zero(self, _): + self.command( + self.lib, + [RESOURCE_ID, "meta-attr1=value1"], + dict_to_modifiers({"wait": "0"}), + ) + self.update_meta.assert_called_once_with( + RESOURCE_ID, {"meta-attr1": "value1"}, [] + ) + self.lib.resource.is_any_stonith.assert_called_once_with([RESOURCE_ID]) + self.wait_for_pcmk_idle.assert_called_once_with(0) + + @mock.patch("pcs.cli.resource.command.print") + @mock.patch("pcs.cli.stonith.command.print") + def test_with_wait_timeout(self, _a, _b): + self.command( + self.lib, + [RESOURCE_ID, "meta-attr1=value1"], + dict_to_modifiers({"wait": "30"}), + ) + self.update_meta.assert_called_once_with( + RESOURCE_ID, {"meta-attr1": "value1"}, [] + ) + self.wait_for_pcmk_idle.assert_called_once_with(30) + self.mock_get_resource_status_msg.assert_called_once_with( + mock.ANY, RESOURCE_ID + ) + + def test_with_force(self): + self.command( + self.lib, + [RESOURCE_ID, "meta-attr1=value1"], + dict_to_modifiers({"force": True}), + ) + self.update_meta.assert_called_once_with( + RESOURCE_ID, {"meta-attr1": "value1"}, [reports.codes.FORCE] + ) + self.mock_get_resource_status_msg.assert_not_called() + self._assert_no_wait_or_stonith() diff --git a/pcs_test/tier0/cli/stonith/test_update.py b/pcs_test/tier0/cli/stonith/test_update.py new file mode 100644 index 000000000..c8193ba29 --- /dev/null +++ b/pcs_test/tier0/cli/stonith/test_update.py @@ -0,0 +1,65 @@ +from unittest import mock + +from pcs.cli.stonith import command + +from pcs_test.tier0.cli.resource.test_update import ResourceMeta +from pcs_test.tools.misc import dict_to_modifiers + +RESOURCE_ID = "resource-id" + + +class StonithMeta(ResourceMeta): + command = staticmethod(command.meta) + + def setUp(self): + super().setUp() + self.lib.resource.is_any_resource_except_stonith = mock.Mock() + self.lib.resource.is_any_resource_except_stonith.return_value = False + + mock_get_resource_status_msg_patcher = mock.patch( + "pcs.cli.stonith.command.get_resource_status_msg" + ) + self.addCleanup(mock_get_resource_status_msg_patcher.stop) + self.mock_get_resource_status_msg = ( + mock_get_resource_status_msg_patcher.start() + ) + + def _assert_no_wait_or_stonith(self): + self.lib.resource.is_any_resource_except_stonith.assert_called_once_with( + [RESOURCE_ID] + ) + self.wait_for_pcmk_idle.assert_not_called() + self.mock_get_resource_status_msg.assert_not_called() + + @mock.patch("pcs.cli.stonith.common.deprecation_warning") + def test_stonith_deprecation(self, mock_warning): + self.lib.resource.is_any_resource_except_stonith.return_value = True + self.command( + self.lib, + [RESOURCE_ID, "meta-attr1=value1"], + dict_to_modifiers({}), + ) + self.update_meta.assert_called_once_with( + RESOURCE_ID, {"meta-attr1": "value1"}, [] + ) + mock_warning.assert_called_once_with( + "Ability of this command to accept resources is deprecated and " + "will be removed in a future release. Please use 'pcs resource " + "meta' instead." + ) + self._assert_no_wait_or_stonith() + + @mock.patch("pcs.cli.stonith.command.print") + def test_with_wait_zero(self, _): + self.command( + self.lib, + [RESOURCE_ID, "meta-attr1=value1"], + dict_to_modifiers({"wait": "0"}), + ) + self.update_meta.assert_called_once_with( + RESOURCE_ID, {"meta-attr1": "value1"}, [] + ) + self.lib.resource.is_any_resource_except_stonith.assert_called_once_with( + [RESOURCE_ID] + ) + self.wait_for_pcmk_idle.assert_called_once_with(0) diff --git a/pcs_test/tier0/cli/test_resource.py b/pcs_test/tier0/cli/test_resource.py index 6be4f740e..0ea04c994 100644 --- a/pcs_test/tier0/cli/test_resource.py +++ b/pcs_test/tier0/cli/test_resource.py @@ -1304,7 +1304,7 @@ def test_all_options(self, mock_print): self.resource.restart.assert_called_once_with("resource", "node", "10s") mock_print.assert_called_once_with("resource successfully restarted") - @mock.patch("pcs.resource.deprecation_warning") + @mock.patch("pcs.cli.resource.common.deprecation_warning") def test_stonith(self, mock_deprecation, mock_print): self.resource.is_any_stonith.return_value = True resource.resource_restart_cmd( diff --git a/pcs_test/tier0/common/reports/test_messages.py b/pcs_test/tier0/common/reports/test_messages.py index 7c6481342..23df30bd7 100644 --- a/pcs_test/tier0/common/reports/test_messages.py +++ b/pcs_test/tier0/common/reports/test_messages.py @@ -2278,7 +2278,7 @@ def test_all(self): class WaitForIdleNotLiveCluster(NameBuildTest): def test_all(self): self.assert_message_from_report( - "Cannot use 'mocked CIB' together with 'wait'", + "Cannot pass CIB together with 'wait'", reports.WaitForIdleNotLiveCluster(), ) @@ -3581,7 +3581,7 @@ def test_all(self): class LiveEnvironmentRequired(NameBuildTest): def test_build_messages_transformable_codes(self): self.assert_message_from_report( - "This command does not support '{}', '{}'".format( + "This command does not support passing '{}', '{}'".format( str(file_type_codes.CIB), str(file_type_codes.COROSYNC_CONF), ), @@ -3925,6 +3925,25 @@ def test_build_messages(self): ) +class UseCommandRemoveAndAddGuestNode(NameBuildTest): + def test_message(self): + self.assert_message_from_report( + "Changing connection parameters of an existing guest node is not " + "sufficient for connecting to a different guest node, remove the " + "existing guest node and add a new one instead", + reports.UseCommandRemoveAndAddGuestNode(), + ) + + +class GuestNodeNameAlreadyExists(NameBuildTest): + def test_message(self): + self.assert_message_from_report( + "Cannot set name of the guest node to 'N' because that ID already " + "exists in the cluster configuration.", + reports.GuestNodeNameAlreadyExists("N"), + ) + + class TmpFileWrite(NameBuildTest): def test_success(self): self.assert_message_from_report( diff --git a/pcs_test/tier0/lib/cib/test_nvpair_multi.py b/pcs_test/tier0/lib/cib/test_nvpair_multi.py index ea141d8bf..99bf6e120 100644 --- a/pcs_test/tier0/lib/cib/test_nvpair_multi.py +++ b/pcs_test/tier0/lib/cib/test_nvpair_multi.py @@ -38,6 +38,12 @@ ) from pcs_test.tools.xml import etree_to_str +ALL_NVSETS = ( + nvpair_multi.NVSET_INSTANCE, + nvpair_multi.NVSET_META, + nvpair_multi.NVSET_UTILIZATION, +) + class RuleInEffectEvalMock(RuleInEffectEval): def __init__(self, mock_data=None): @@ -65,14 +71,8 @@ def test_success(self): class NvsetElementToDto(TestCase): - all_nvsets = ( - nvpair_multi.NVSET_INSTANCE, - nvpair_multi.NVSET_META, - nvpair_multi.NVSET_UTILIZATION, - ) - def test_minimal(self): - for tag in self.all_nvsets: + for tag in ALL_NVSETS: with self.subTest(tag=tag): xml = etree.fromstring(f"""<{tag} id="my-id" />""") self.assertEqual( @@ -83,7 +83,7 @@ def test_minimal(self): ) def test_expired(self): - for tag in self.all_nvsets: + for tag in ALL_NVSETS: with self.subTest(tag=tag): xml = etree.fromstring( f""" @@ -138,7 +138,7 @@ def test_expired(self): ) def test_full(self): - for tag in self.all_nvsets: + for tag in ALL_NVSETS: with self.subTest(tag=tag): xml = etree.fromstring( f""" @@ -357,6 +357,74 @@ def test_full(self): ) +class NvsetToDict(TestCase): + command = staticmethod(nvpair_multi.nvset_to_dict) + + def test_empty(self): + for tag in ALL_NVSETS: + with self.subTest(tag=tag): + xml = etree.fromstring(f"""<{tag} id="my-id" />""") + self.assertEqual( + self.command(xml), + {}, + ) + + def _empty_value(self, result): + for tag in ALL_NVSETS: + with self.subTest(tag=tag): + xml = etree.fromstring( + f""" + <{tag} id="my-id"> + + + + + """ + ) + self.assertEqual( + self.command(xml), + result, + ) + + def test_empty_value(self): + result = { + "name1": "value1", + "name2": None, + "name3": "", + } + self._empty_value(result) + + def test_multiple(self): + for tag in ALL_NVSETS: + with self.subTest(tag=tag): + xml = etree.fromstring( + f""" + <{tag} id="my-id"> + + + + """ + ) + self.assertEqual( + self.command(xml), + { + "name1": "value1", + "name2": "value2", + }, + ) + + +class NvsetToDictExceptWithoutValues(NvsetToDict): + command = staticmethod(nvpair_multi.nvset_to_dict_except_without_values) + + def test_empty_value(self): + result = { + "name1": "value1", + "name3": "", + } + self._empty_value(result) + + class FindNvsets(TestCase): def setUp(self): self.xml = etree.fromstring( diff --git a/pcs_test/tier0/lib/cib/test_resource_clone.py b/pcs_test/tier0/lib/cib/test_resource_clone.py index 71d56f1c6..967cef971 100644 --- a/pcs_test/tier0/lib/cib/test_resource_clone.py +++ b/pcs_test/tier0/lib/cib/test_resource_clone.py @@ -1,7 +1,10 @@ -from unittest import TestCase +from unittest import ( + TestCase, +) from lxml import etree +from pcs.common.tools import Version from pcs.lib.cib.resource import clone from pcs.lib.cib.tools import IdProvider @@ -10,6 +13,81 @@ assert_report_item_list_equal, assert_xml_equal, ) +from pcs_test.tools.xml import etree_to_str + + +def fixture_resource_meta_stateful( + meta_nvpairs="", + use_legacy_roles=False, + is_grouped=False, +): + clone_el_tag = "clone" + role_promoted = "Promoted" + role_unpromoted = "Unpromoted" + meta_attributes_xml = "" + + if use_legacy_roles: + clone_el_tag = "master" + role_promoted = "Master" + role_unpromoted = "Slave" + if meta_nvpairs: + meta_attributes_xml = f""" + + {meta_nvpairs} + + """ + else: + meta_attributes_xml = f""" + + {meta_nvpairs} + + + """ + + group_start = group_end = "" + if is_grouped: + group_start = """""" + group_end = "" + + return f""" + <{clone_el_tag} id="custom-clone"> + {group_start} + + + + + + + + + + + + + {group_end} + {meta_attributes_xml} + + """ class AppendNewCommon(TestCase): @@ -308,6 +386,61 @@ def test_group(self): ) +class GetInnerPrimitiveResources(TestCase): + def assert_inner_resource(self, resource_id_list, xml): + self.assertListEqual( + resource_id_list, + [ + inner_el.get("id", "") + for inner_el in clone.get_inner_primitives( + etree.fromstring(xml) + ) + ], + ) + + def test_primitive(self): + self.assert_inner_resource( + ["A"], + """ + + + + + + """, + ) + + def test_group_single(self): + self.assert_inner_resource( + ["A"], + """ + + + + + + + + """, + ) + + def test_group_multiple(self): + self.assert_inner_resource( + ["A", "B", "C"], + """ + + + + + + + + + + """, + ) + + class ValidateCloneId(TestCase): def setUp(self): self.cib = etree.fromstring( @@ -344,3 +477,68 @@ def test_clone_id_exist(self): "CloneId-meta_attributes", [fixture.report_id_already_exist("CloneId-meta_attributes")], ) + + +class ConvertLegacyPromotableElement(TestCase): + _EXISTING_NVPAIR = ( + '' + ) + + def test_update_primitive(self): + legacy_el_str = fixture_resource_meta_stateful( + use_legacy_roles=True, + ) + legacy_el = etree.fromstring(legacy_el_str) + clone.convert_master_to_promotable( + IdProvider(legacy_el), Version(3, 7), legacy_el + ) + assert_xml_equal( + etree_to_str(legacy_el), + fixture_resource_meta_stateful(), + ) + + def test_update_primitive_with_existing_meta(self): + legacy_el_str = fixture_resource_meta_stateful( + meta_nvpairs=self._EXISTING_NVPAIR, + use_legacy_roles=True, + ) + legacy_el = etree.fromstring(legacy_el_str) + clone.convert_master_to_promotable( + IdProvider(legacy_el), Version(3, 7), legacy_el + ) + assert_xml_equal( + etree_to_str(legacy_el), + fixture_resource_meta_stateful(meta_nvpairs=self._EXISTING_NVPAIR), + ) + + def test_update_group(self): + legacy_el_str = fixture_resource_meta_stateful( + use_legacy_roles=True, + is_grouped=True, + ) + legacy_el = etree.fromstring(legacy_el_str) + clone.convert_master_to_promotable( + IdProvider(legacy_el), Version(3, 7), legacy_el + ) + assert_xml_equal( + etree_to_str(legacy_el), + fixture_resource_meta_stateful(is_grouped=True), + ) + + def test_update_group_with_existing_meta(self): + legacy_el_str = fixture_resource_meta_stateful( + meta_nvpairs=self._EXISTING_NVPAIR, + use_legacy_roles=True, + is_grouped=True, + ) + legacy_el = etree.fromstring(legacy_el_str) + clone.convert_master_to_promotable( + IdProvider(legacy_el), Version(3, 7), legacy_el + ) + assert_xml_equal( + etree_to_str(legacy_el), + fixture_resource_meta_stateful( + meta_nvpairs=self._EXISTING_NVPAIR, + is_grouped=True, + ), + ) diff --git a/pcs_test/tier0/lib/cib/test_resource_guest_node.py b/pcs_test/tier0/lib/cib/test_resource_guest_node.py index 073a42857..22073f7a5 100644 --- a/pcs_test/tier0/lib/cib/test_resource_guest_node.py +++ b/pcs_test/tier0/lib/cib/test_resource_guest_node.py @@ -1,4 +1,7 @@ -from unittest import TestCase +from unittest import ( + TestCase, + mock, +) from lxml import etree @@ -8,6 +11,7 @@ from pcs.lib.cib.resource import guest_node from pcs.lib.cib.tools import IdProvider +from pcs_test.tools import fixture from pcs_test.tools.assertions import ( assert_report_item_list_equal, assert_xml_equal, @@ -17,6 +21,390 @@ SetupPatchMixin = create_setup_patch_mixin(guest_node) +class ValidateUpdatingGuestAttributes(TestCase): + def setUp(self): + self.validate_conflicts_mock = mock.patch( + "pcs.lib.cib.resource.guest_node.validate_conflicts" + ) + self.validate_conflicts_mock.return_value = [] + self.validate_conflicts_mock.start() + self.cib = etree.fromstring("") + + def tearDown(self): + self.validate_conflicts_mock.stop() + + def test_no_existing_no_new(self): + assert_report_item_list_equal( + guest_node.validate_updating_guest_attributes( + self.cib, [], [], {}, {}, [] + ), + [], + ) + + def test_no_existing_add_addr(self): + assert_report_item_list_equal( + guest_node.validate_updating_guest_attributes( + self.cib, [], [], {"node-addr": "192.168.1.100"}, {}, [] + ), + [], + ) + + def test_fake_guest_conn_update(self): + assert_report_item_list_equal( + guest_node.validate_updating_guest_attributes( + self.cib, + [], + [], + { + "is_managed": "true", + "remote-node": "remote1", + }, + { + "is_managed": "false", + "remote-node": "remote1", + }, + force_flags=[], + ), + [], + ) + + def test_existing_guest_add_other(self): + assert_report_item_list_equal( + guest_node.validate_updating_guest_attributes( + self.cib, + [], + [], + { + "is-managed": "false", + }, + { + "remote-node": "remote-1", + }, + force_flags=[], + ), + [], + ) + + def test_existing_guest_update_all(self): + assert_report_item_list_equal( + guest_node.validate_updating_guest_attributes( + self.cib, + [], + [], + { + "remote-node": "remote-2", + }, + { + "remote-node": "remote-1", + }, + force_flags=[], + ), + [ + fixture.error( + report_codes.USE_COMMAND_REMOVE_AND_ADD_GUEST_NODE, + force_code=report_codes.FORCE, + ) + ], + ) + + def test_existing_guest_update_all_force(self): + assert_report_item_list_equal( + guest_node.validate_updating_guest_attributes( + self.cib, + [], + [], + { + "remote-node": "remote-2", + }, + { + "remote-node": "remote-1", + }, + force_flags=[report_codes.FORCE], + ), + [fixture.warn(report_codes.USE_COMMAND_REMOVE_AND_ADD_GUEST_NODE)], + ) + + def test_existing_guest_update_addr(self): + assert_report_item_list_equal( + guest_node.validate_updating_guest_attributes( + self.cib, + [], + [], + { + "remote-addr": "192.168.1.100", + }, + { + "remote-node": "remote-1", + }, + force_flags=[], + ), + [ + fixture.error( + report_codes.USE_COMMAND_REMOVE_AND_ADD_GUEST_NODE, + force_code=report_codes.FORCE, + ) + ], + ) + + def test_existing_guest_update_addr_force(self): + assert_report_item_list_equal( + guest_node.validate_updating_guest_attributes( + self.cib, + [], + [], + { + "remote-addr": "192.168.1.100", + }, + { + "remote-node": "remote-1", + }, + force_flags=[report_codes.FORCE], + ), + [fixture.warn(report_codes.USE_COMMAND_REMOVE_AND_ADD_GUEST_NODE)], + ) + + def test_existing_guest_update_some(self): + assert_report_item_list_equal( + guest_node.validate_updating_guest_attributes( + self.cib, + [], + [], + { + "remote-addr": "192.168.1.100", + }, + { + "remote-node": "remote-1", + "remote-addr": "10.0.0.10", + "remote-connect-timeout": "30s", + }, + force_flags=[], + ), + [ + fixture.error( + report_codes.USE_COMMAND_REMOVE_AND_ADD_GUEST_NODE, + force_code=report_codes.FORCE, + ) + ], + ) + + def test_remote_addr_exists_add_remote_node(self): + assert_report_item_list_equal( + guest_node.validate_updating_guest_attributes( + self.cib, + [], + [], + { + "remote-node": "remote-1", + }, + { + "remote-addr": "192.168.1.100", + }, + force_flags=[], + ), + [ + fixture.error( + report_codes.USE_COMMAND_NODE_ADD_GUEST, + force_code=report_codes.FORCE, + ), + ], + ) + + def test_remote_addr_exists_add_remote_node_force(self): + assert_report_item_list_equal( + guest_node.validate_updating_guest_attributes( + self.cib, + [], + [], + { + "remote-node": "remote-1", + }, + { + "remote-addr": "192.168.1.100", + }, + force_flags=[report_codes.FORCE], + ), + [ + fixture.warn(report_codes.USE_COMMAND_NODE_ADD_GUEST), + ], + ) + + def test_remote_addr_exists_update_and_add_remote_node(self): + assert_report_item_list_equal( + guest_node.validate_updating_guest_attributes( + self.cib, + [], + [], + { + "remote-node": "remote-1", + "remote-addr": "10.0.0.10", + }, + { + "remote-addr": "192.168.1.100", + }, + force_flags=[], + ), + [ + fixture.error( + report_codes.USE_COMMAND_NODE_ADD_GUEST, + force_code=report_codes.FORCE, + ), + ], + ) + + def test_existing_guest_remove_guest_some(self): + assert_report_item_list_equal( + guest_node.validate_updating_guest_attributes( + self.cib, + [], + [], + { + "remote-addr": "", + }, + { + "remote-node": "remote-1", + "remote-addr": "10.0.0.10", + "remote-connect-timeout": "30s", + }, + force_flags=[], + ), + [ + fixture.error( + report_codes.USE_COMMAND_NODE_REMOVE_GUEST, + force_code=report_codes.FORCE, + ) + ], + ) + + def test_existing_guest_remove_guest_some_force(self): + assert_report_item_list_equal( + guest_node.validate_updating_guest_attributes( + self.cib, + [], + [], + { + "remote-addr": "", + }, + { + "remote-node": "remote-1", + "remote-addr": "10.0.0.10", + "remote-connect-timeout": "30s", + }, + force_flags=[report_codes.FORCE], + ), + [fixture.warn(report_codes.USE_COMMAND_NODE_REMOVE_GUEST)], + ) + + def test_existing_guest_remove_guest_all(self): + assert_report_item_list_equal( + guest_node.validate_updating_guest_attributes( + self.cib, + [], + [], + { + "remote-node": "", + "remote-addr": "", + "remote_port": "", + "remote-connect-timeout": "", + }, + { + "remote-node": "remote-1", + "remote-addr": "10.0.0.10", + "remote_port": "50000", + "remote-connect-timeout": "30s", + }, + force_flags=[], + ), + [ + fixture.error( + report_codes.USE_COMMAND_NODE_REMOVE_GUEST, + force_code=report_codes.FORCE, + ) + ], + ) + + def test_invalid_remote_port(self): + assert_report_item_list_equal( + guest_node.validate_updating_guest_attributes( + self.cib, + [], + [], + { + "remote-port": "808080", + }, + {}, + force_flags=[], + ), + [ + fixture.error( + report_codes.INVALID_OPTION_VALUE, + option_name="remote-port", + option_value="808080", + allowed_values="a port number (1..65535)", + cannot_be_empty=False, + forbidden_characters=None, + ) + ], + ) + + def test_remove_remote_port(self): + assert_report_item_list_equal( + guest_node.validate_updating_guest_attributes( + self.cib, + [], + [], + { + "remote-port": "", + }, + { + "remote-port": "50000", + }, + force_flags=[], + ), + [], + ) + + def test_invalid_remote_timeout(self): + assert_report_item_list_equal( + guest_node.validate_updating_guest_attributes( + self.cib, + [], + [], + { + "remote-connect-timeout": "abc", + }, + {}, + force_flags=[], + ), + [ + fixture.error( + report_codes.INVALID_OPTION_VALUE, + option_name="remote-connect-timeout", + option_value="abc", + allowed_values="time interval (e.g. 1, 2s, 3m, 4h, ...)", + cannot_be_empty=False, + forbidden_characters=None, + ) + ], + ) + + def test_remove_remote_timeout(self): + assert_report_item_list_equal( + guest_node.validate_updating_guest_attributes( + self.cib, + [], + [], + { + "remote-connect-timeout": "", + }, + { + "remote-connect-timeout": "30s", + }, + force_flags=[], + ), + [], + ) + + class ValidateHostConflicts(TestCase): @staticmethod def validate(node_name, options): @@ -65,37 +453,46 @@ def validate(node_name, options): tree, existing_nodes_names, existing_nodes_addrs, node_name, options ) - def assert_already_exists_error( - self, conflict_name, node_name, options=None + def assert_node_name_already_exists_error( + self, report_node_name, node_name, options=None ): assert_report_item_list_equal( self.validate(node_name, options if options else {}), [ - ( - severities.ERROR, - report_codes.ID_ALREADY_EXISTS, - { - "id": conflict_name, - }, - None, - ), + fixture.error( + report_codes.GUEST_NODE_NAME_ALREADY_EXISTS, + node_name=report_node_name, + ) + ], + ) + + def assert_remote_addr_already_exists_error(self, node_name, options): + assert_report_item_list_equal( + self.validate(node_name, options), + [ + fixture.error( + report_codes.NODE_ADDRESSES_ALREADY_EXIST, + address_list=[options["remote-addr"]], + ) ], ) def test_report_conflict_with_id(self): - self.assert_already_exists_error("CONFLICT", "CONFLICT") + self.assert_node_name_already_exists_error("CONFLICT", "CONFLICT") def test_report_conflict_guest_node(self): - self.assert_already_exists_error("GUEST_CONFLICT", "GUEST_CONFLICT") + self.assert_node_name_already_exists_error( + "GUEST_CONFLICT", "GUEST_CONFLICT" + ) def test_report_conflict_guest_addr(self): - self.assert_already_exists_error( + self.assert_node_name_already_exists_error( "GUEST_ADDR_CONFLICT", "GUEST_ADDR_CONFLICT", ) def test_report_conflict_guest_addr_by_addr(self): - self.assert_already_exists_error( + self.assert_node_name_already_exists_error( "GUEST_ADDR_CONFLICT", "GUEST_ADDR_CONFLICT", ) @@ -112,7 +509,9 @@ def test_no_conflict_guest_node_when_addr_is_different(self): ) def test_report_conflict_remote_node(self): - self.assert_already_exists_error("REMOTE_CONFLICT", "REMOTE_CONFLICT") + self.assert_node_name_already_exists_error( + "REMOTE_CONFLICT", "REMOTE_CONFLICT" + ) def test_no_conflict_remote_node_when_addr_is_different(self): self.assertEqual( @@ -126,9 +525,8 @@ def test_no_conflict_remote_node_when_addr_is_different(self): ) def test_report_conflict_remote_node_by_addr(self): - self.assert_already_exists_error( + self.assert_remote_addr_already_exists_error( "REMOTE_CONFLICT", - "different", { "remote-addr": "REMOTE_CONFLICT", }, @@ -192,14 +590,10 @@ def test_report_invalid_node_name(self): assert_report_item_list_equal( self.validate({}, "EXISTING-HOST-NAME"), [ - ( - severities.ERROR, - report_codes.ID_ALREADY_EXISTS, - { - "id": "EXISTING-HOST-NAME", - }, - None, - ), + fixture.error( + report_codes.GUEST_NODE_NAME_ALREADY_EXISTS, + node_name="EXISTING-HOST-NAME", + ) ], ) diff --git a/pcs_test/tier0/lib/commands/resource/test_resource_update.py b/pcs_test/tier0/lib/commands/resource/test_resource_update.py new file mode 100644 index 000000000..54d8e45a2 --- /dev/null +++ b/pcs_test/tier0/lib/commands/resource/test_resource_update.py @@ -0,0 +1,754 @@ +from unittest import ( + TestCase, +) + +from pcs.common import ( + reports, +) +from pcs.lib.commands import resource +from pcs.lib.resource_agent.types import ResourceAgentName + +from pcs_test.tools import fixture +from pcs_test.tools.assertions import assert_raise_library_error +from pcs_test.tools.command_env import get_env_tools + +_AGENT_NAME_PCMK_DUMMY = ResourceAgentName("ocf", "pacemaker", "Dummy") +_AGENT_NAME_PCMK_STATEFUL = ResourceAgentName("ocf", "pacemaker", "Stateful") + + +def fixture_primitive( + resource_agent=_AGENT_NAME_PCMK_DUMMY, + is_cloned=False, + is_grouped=False, + meta_nvpairs_xml="", + primitive_inner="", +): + meta_attributes_xml = meta_attributes_xml_clone = "" + if meta_nvpairs_xml: + if is_cloned: + meta_attributes_xml_clone = f""" + + {meta_nvpairs_xml} + + """ + else: + meta_attributes_xml = f""" + + {meta_nvpairs_xml} + + """ + + clone_start = clone_end = "" + if is_cloned: + clone_start = """""" + clone_end = "" + + group_start = group_end = "" + if is_grouped: + group_start = """""" + group_end = "" + + return f""" + + {clone_start} + {group_start} + + {meta_attributes_xml} + {primitive_inner} + + {group_end} + {meta_attributes_xml_clone} + {clone_end} + + """ + + +def fixture_resource_meta_stateful( + use_legacy_roles=False, + meta_nvpairs="", + clone_inner="", + is_grouped=False, + is_promotable=True, +): + clone_el_tag = "clone" + role_promoted = "Promoted" + role_unpromoted = "Unpromoted" + + if use_legacy_roles: + clone_el_tag = "master" if is_promotable else "clone" + role_promoted = "Master" + role_unpromoted = "Slave" + elif is_promotable: + meta_nvpairs += """ + + """ + + meta_attributes_xml = "" + if meta_nvpairs: + meta_attributes_xml = f""" + + {meta_nvpairs} + + """ + + group_start = group_end = "" + if is_grouped: + group_start = """""" + group_end = "" + + return f""" + + <{clone_el_tag} id="A-clone"> + {group_start} + + + + + + + + + + + + + {group_end} + {meta_attributes_xml} + {clone_inner} + + + """ + + +class UpdateMeta(TestCase): + def setUp(self): + self.env_assist, self.config = get_env_tools(test_case=self) + + def test_update_nonexistent_resource(self): + self.config.runner.cib.load(filename="cib-resources.xml") + self.env_assist.assert_raise_library_error( + lambda: resource.update_meta( + self.env_assist.get_env(), "Rx", {"priority": "1"}, [] + ), + reports=[ + fixture.error( + reports.codes.ID_NOT_FOUND, + id="Rx", + expected_types=["resource"], + context_type="", + context_id="", + ), + ], + expected_in_processor=False, + ) + + def test_wrong_id_type_found(self): + self.config.runner.cib.load( + tags=""" + + + + + + + """ + ) + self.env_assist.assert_raise_library_error( + lambda: resource.update_meta( + self.env_assist.get_env(), "all-vms", {"priority": "1"}, [] + ), + reports=[ + fixture.error( + reports.codes.ID_BELONGS_TO_UNEXPECTED_TYPE, + id="all-vms", + expected_types=["resource"], + current_type="tag", + ), + ], + expected_in_processor=False, + ) + + def test_meta_attr_elem_missing(self): + self.config.runner.cib.load(resources=fixture_primitive()) + self.config.corosync_conf.load() + self.config.env.push_cib( + resources=fixture_primitive( + meta_nvpairs_xml=""" + + """ + ) + ) + resource.update_meta( + self.env_assist.get_env(), "A", {"priority": "1"}, [] + ) + + def test_no_new_attrs_element_not_added(self): + self.config.runner.cib.load(resources=fixture_primitive()) + self.config.corosync_conf.load() + self.config.env.push_cib(resources=fixture_primitive()) + resource.update_meta( + self.env_assist.get_env(), "A", {"priority": ""}, [] + ) + + def test_existing_meta_attr_elem_add_remove(self): + self.config.runner.cib.load( + resources=fixture_primitive( + meta_nvpairs_xml=""" + + + """, + ) + ) + self.config.corosync_conf.load() + self.config.env.push_cib( + resources=fixture_primitive( + meta_nvpairs_xml=""" + + + """, + ) + ) + resource.update_meta( + self.env_assist.get_env(), + "A", + {"priority": "1", "failure-timeout": ""}, + [], + ) + + def test_conflict_existing_node_name(self): + self.config.runner.cib.load( + resources=fixture_primitive(), + ) + self.config.corosync_conf.load(node_name_list=["node1"]) + self.env_assist.assert_raise_library_error( + lambda: resource.update_meta( + self.env_assist.get_env(), + "A", + {"remote-node": "node1"}, + [], + ) + ) + self.env_assist.assert_reports( + [ + fixture.error( + reports.codes.GUEST_NODE_NAME_ALREADY_EXISTS, + node_name="node1", + ), + fixture.error( + reports.codes.USE_COMMAND_NODE_ADD_GUEST, + reports.codes.FORCE, + ), + ] + ) + + def test_update_guest_attr_protected_force(self): + self.config.runner.cib.load( + resources=fixture_primitive(), + ) + self.config.corosync_conf.load() + self.config.env.push_cib( + resources=fixture_primitive( + meta_nvpairs_xml=""" + + """ + ) + ) + resource.update_meta( + self.env_assist.get_env(), + "A", + {"remote-node": "node1"}, + [reports.codes.FORCE], + ) + + self.env_assist.assert_reports( + [ + fixture.warn(reports.codes.USE_COMMAND_NODE_ADD_GUEST), + ] + ) + + def test_update_legacy_clone_element_tag_and_reset_promotable(self): + self.config.runner.cib.load( + resources=fixture_resource_meta_stateful( + use_legacy_roles=True, + ) + ) + self.config.corosync_conf.load() + agent = ResourceAgentName("ocf", "pacemaker", "Stateful") + self.config.runner.pcmk.load_agent(agent_name=agent.full_name) + self.config.env.push_cib( + resources=fixture_resource_meta_stateful( + meta_nvpairs=""" + + """, + is_promotable=False, + ) + ) + resource.update_meta( + self.env_assist.get_env(), + "A-clone", + {"priority": "1", "promotable": ""}, + [], + ) + + +class UpdateMetaCheckCloneIncompatibleMetaAttrs(TestCase): + def setUp(self): + self.env_assist, self.config = get_env_tools(test_case=self) + + def test_success_cloned_primitive_priority(self): + self.config.runner.cib.load(resources=fixture_primitive(is_cloned=True)) + self.config.corosync_conf.load() + self.config.runner.pcmk.load_agent( + agent_name=_AGENT_NAME_PCMK_DUMMY.full_name + ) + self.config.env.push_cib( + resources=fixture_primitive( + is_cloned=True, + meta_nvpairs_xml=""" + + """, + ) + ) + resource.update_meta( + self.env_assist.get_env(), "A-clone", {"priority": "1"}, [] + ) + + def test_success_cloned_primitive_promotable(self): + self.config.runner.cib.load( + resources=fixture_primitive( + resource_agent=_AGENT_NAME_PCMK_STATEFUL, + is_cloned=True, + ) + ) + self.config.corosync_conf.load() + self.config.runner.pcmk.load_agent( + agent_name=_AGENT_NAME_PCMK_STATEFUL.full_name + ) + self.config.env.push_cib( + resources=fixture_primitive( + resource_agent=_AGENT_NAME_PCMK_STATEFUL, + is_cloned=True, + meta_nvpairs_xml=""" + + """, + ) + ) + resource.update_meta( + self.env_assist.get_env(), "A-clone", {"promotable": "true"}, [] + ) + + def test_success_cloned_primitive_promotable_ocf_old(self): + # Despite the test name, agent older than OCF 1.1 can be promoted + # because the old standard doesn't allow for promotability checks + agent_name = ResourceAgentName("ocf", "heartbeat", "Dummy") + self.config.runner.cib.load( + resources=fixture_primitive( + resource_agent=agent_name, + is_cloned=True, + ) + ) + self.config.corosync_conf.load() + self.config.runner.pcmk.load_agent(agent_name=agent_name.full_name) + self.config.env.push_cib( + resources=fixture_primitive( + resource_agent=agent_name, + is_cloned=True, + meta_nvpairs_xml=""" + + """, + ) + ) + resource.update_meta( + self.env_assist.get_env(), "A-clone", {"promotable": "true"}, [] + ) + + def test_fail_cloned_primitive_promotable_incompatible(self): + self.config.runner.cib.load( + resources=fixture_primitive(is_cloned=True), + ) + self.config.corosync_conf.load() + self.config.runner.pcmk.load_agent( + agent_name=_AGENT_NAME_PCMK_DUMMY.full_name + ) + self.env_assist.assert_raise_library_error( + lambda: resource.update_meta( + self.env_assist.get_env(), + "A-clone", + {"promotable": "true"}, + [], + ) + ) + + self.env_assist.assert_reports( + [ + fixture.error( + reports.codes.RESOURCE_CLONE_INCOMPATIBLE_META_ATTRIBUTES, + attribute="promotable", + resource_agent=_AGENT_NAME_PCMK_DUMMY.to_dto(), + resource_id="A", + group_id=None, + force_code=reports.codes.FORCE, + ) + ] + ) + + def test_fail_cloned_primitive_promotable_incompatible_force(self): + self.config.runner.cib.load( + resources=fixture_primitive(is_cloned=True), + ) + self.config.corosync_conf.load() + self.config.runner.pcmk.load_agent( + agent_name=_AGENT_NAME_PCMK_DUMMY.full_name + ) + self.config.env.push_cib( + resources=fixture_primitive( + is_cloned=True, + meta_nvpairs_xml=""" + + """, + ) + ) + resource.update_meta( + self.env_assist.get_env(), + "A-clone", + {"promotable": "true"}, + [reports.codes.FORCE], + ) + self.env_assist.assert_reports( + [ + fixture.warn( + reports.codes.RESOURCE_CLONE_INCOMPATIBLE_META_ATTRIBUTES, + attribute="promotable", + resource_agent=_AGENT_NAME_PCMK_DUMMY.to_dto(), + resource_id="A", + group_id=None, + ) + ] + ) + + def _subtest_fail_cloned_primitive_non_ocf_incompatible_attrs(self, attr): + # Cannot use real subtest because of env_assist call stack, it needs + # initialization and teardown between test runs + self.config.runner.cib.load( + resources=""" + + + + + + """, + ) + self.config.corosync_conf.load() + agent = ResourceAgentName("systemd", None, "pcsmock") + self.env_assist.assert_raise_library_error( + lambda: resource.update_meta( + self.env_assist.get_env(), + "non-ocf-clone", + {attr: "true"}, # noqa: B023 + [], + ), + ) + self.env_assist.assert_reports( + [ + fixture.error( + reports.codes.RESOURCE_CLONE_INCOMPATIBLE_META_ATTRIBUTES, + attribute=attr, + resource_agent=agent.to_dto(), + resource_id="non-ocf", + group_id=None, + ) + ] + ) + + def test_fail_cloned_primitive_non_ocf_promotable(self): + self._subtest_fail_cloned_primitive_non_ocf_incompatible_attrs( + "promotable" + ) + + def test_fail_cloned_primitive_non_ocf_globally_unique(self): + self._subtest_fail_cloned_primitive_non_ocf_incompatible_attrs( + "globally-unique" + ) + + def test_fail_cloned_group_promotable(self): + self.config.runner.cib.load( + resources=fixture_primitive(is_cloned=True, is_grouped=True), + ) + self.config.corosync_conf.load() + self.config.runner.pcmk.load_agent( + agent_name=_AGENT_NAME_PCMK_DUMMY.full_name + ) + self.env_assist.assert_raise_library_error( + lambda: resource.update_meta( + self.env_assist.get_env(), + "A-clone", + {"promotable": "true"}, + [], + ) + ) + + self.env_assist.assert_reports( + [ + fixture.error( + reports.codes.RESOURCE_CLONE_INCOMPATIBLE_META_ATTRIBUTES, + attribute="promotable", + resource_agent=_AGENT_NAME_PCMK_DUMMY.to_dto(), + resource_id="A", + group_id="G", + force_code=reports.codes.FORCE, + ) + ] + ) + + def test_fail_cloned_group_promotable_force(self): + self.config.runner.cib.load( + resources=fixture_primitive(is_cloned=True, is_grouped=True), + ) + self.config.corosync_conf.load() + self.config.runner.pcmk.load_agent( + agent_name=_AGENT_NAME_PCMK_DUMMY.full_name + ) + self.config.env.push_cib( + resources=fixture_primitive( + is_cloned=True, + is_grouped=True, + meta_nvpairs_xml=""" + + """, + ) + ) + resource.update_meta( + self.env_assist.get_env(), + "A-clone", + {"promotable": "true"}, + [reports.codes.FORCE], + ) + self.env_assist.assert_reports( + [ + fixture.warn( + reports.codes.RESOURCE_CLONE_INCOMPATIBLE_META_ATTRIBUTES, + attribute="promotable", + resource_agent=_AGENT_NAME_PCMK_DUMMY.to_dto(), + resource_id="A", + group_id="G", + ) + ] + ) + + +class UpdateMetaRemoveUpdatedGuestNode(TestCase): + def setUp(self): + self.env_assist, self.config = get_env_tools(test_case=self) + + def _update_guest_attr_success( + self, + old_node_addr="", + new_node_addr="", + is_nvset_empty=False, + is_node_removed=False, + is_forced=False, + report_list=None, + ): + self.config.runner.cib.load( + resources=fixture_primitive( + meta_nvpairs_xml=f""" + + """ + if old_node_addr + else "", + ) + ) + self.config.corosync_conf.load() + self.config.env.push_cib( + resources=fixture_primitive( + # For removeal, we want to hack the fixture to get the meta + # attributes element without nvpairs + meta_nvpairs_xml=" " + if is_nvset_empty + else f""" + + """ + ) + ) + if is_node_removed: + self.config.runner.pcmk.remove_node(old_node_addr) + env = self.env_assist.get_env() + resource.update_meta( + env, + "A", + {"remote-node": new_node_addr}, + [reports.codes.FORCE] if is_forced else [], + ) + self.env_assist.assert_reports(report_list if report_list else []) + + def _update_guest_attr_fail( + self, report_list, old_node_addr="", new_node_addr="", is_forced=False + ): + self.config.runner.cib.load( + resources=fixture_primitive( + meta_nvpairs_xml=f""" + + """ + if old_node_addr + else "", + ) + ) + self.config.corosync_conf.load() + env = self.env_assist.get_env() + assert_raise_library_error( + lambda: resource.update_meta( + env, + "A", + {"remote-node": new_node_addr}, + [reports.codes.FORCE] if is_forced else [], + ) + ) + self.env_assist.assert_reports(report_list) + + def test_not_called_add_remote_node(self): + self._update_guest_attr_fail( + new_node_addr="rnode", + report_list=[ + fixture.error( + reports.codes.USE_COMMAND_NODE_ADD_GUEST, + reports.codes.FORCE, + ), + ], + ) + + def test_not_called_add_remote_node_force(self): + self._update_guest_attr_success( + new_node_addr="rnode", + is_forced=True, + report_list=[ + fixture.warn(reports.codes.USE_COMMAND_NODE_ADD_GUEST), + ], + ) + + def test_not_called_change_remote_node(self): + self._update_guest_attr_fail( + old_node_addr="rnode2", + new_node_addr="rnode", + report_list=[ + fixture.error( + reports.codes.USE_COMMAND_REMOVE_AND_ADD_GUEST_NODE, + reports.codes.FORCE, + ), + ], + ) + + def test_called_change_remote_node_force(self): + self._update_guest_attr_success( + old_node_addr="rnode2", + new_node_addr="rnode", + is_forced=True, + is_node_removed=True, + report_list=[ + fixture.warn( + reports.codes.USE_COMMAND_REMOVE_AND_ADD_GUEST_NODE + ), + ], + ) + + def test_not_called_fake_change_remote_node(self): + self._update_guest_attr_fail( + old_node_addr="rnode", + new_node_addr="rnode", + report_list=[ + fixture.error( + reports.codes.GUEST_NODE_NAME_ALREADY_EXISTS, + node_name="rnode", + ) + ], + ) + + def test_not_called_fake_change_remote_node_force(self): + self._update_guest_attr_fail( + old_node_addr="rnode", + new_node_addr="rnode", + is_forced=True, + report_list=[ + fixture.error( + reports.codes.GUEST_NODE_NAME_ALREADY_EXISTS, + node_name="rnode", + ) + ], + ) + + def test_not_called_remove_remote_node(self): + self._update_guest_attr_fail( + old_node_addr="rnode2", + new_node_addr="", + report_list=[ + fixture.error( + reports.codes.USE_COMMAND_NODE_REMOVE_GUEST, + reports.codes.FORCE, + ), + ], + ) + + def test_called_remove_remote_node_force(self): + self._update_guest_attr_success( + old_node_addr="rnode2", + new_node_addr="", + is_nvset_empty=True, + is_node_removed=True, + is_forced=True, + report_list=[ + fixture.warn(reports.codes.USE_COMMAND_NODE_REMOVE_GUEST), + ], + ) diff --git a/pcs_test/tier0/lib/resource_agent/test_types.py b/pcs_test/tier0/lib/resource_agent/test_types.py index adfc65943..31124695b 100644 --- a/pcs_test/tier0/lib/resource_agent/test_types.py +++ b/pcs_test/tier0/lib/resource_agent/test_types.py @@ -22,6 +22,16 @@ def test_full_name_2_parts_empty(self): "standard:type", ) + def test_is_ocf_yes(self): + self.assertTrue( + ra.ResourceAgentName("ocf", "pacemaker", "Dummy").is_ocf + ) + + def test_is_ocf_no(self): + self.assertFalse( + ra.ResourceAgentName("systemd", None, "chronyd").is_ocf + ) + def test_is_stonith_yes(self): self.assertTrue( ra.ResourceAgentName("stonith", "pacemaker", "Dummy").is_stonith diff --git a/pcs_test/tier1/cib_resource/test_create.py b/pcs_test/tier1/cib_resource/test_create.py index 241a0a569..2e78b7bc5 100644 --- a/pcs_test/tier1/cib_resource/test_create.py +++ b/pcs_test/tier1/cib_resource/test_create.py @@ -1817,7 +1817,8 @@ def test_fail_when_on_pacemaker_remote_conflict_with_existing_node(self): ( "Warning: this command is not sufficient for creating a " "remote connection, use 'pcs cluster node add-remote'\n" - "Error: 'R' already exists\n" + ERRORS_HAVE_OCCURRED + "Error: Node address 'R' is already used by existing nodes; " + "please, use other address\n" + ERRORS_HAVE_OCCURRED ), ) @@ -1835,7 +1836,8 @@ def test_fail_when_on_pacemaker_remote_conflict_with_existing_id(self): ( "Warning: this command is not sufficient for creating a " "remote connection, use 'pcs cluster node add-remote'\n" - "Error: 'R2' already exists\n" + ERRORS_HAVE_OCCURRED + "Error: Node address 'R2' is already used by existing nodes; " + "please, use other address\n" + ERRORS_HAVE_OCCURRED ), ) @@ -1856,7 +1858,9 @@ def test_fail_when_on_guest_conflict_with_existing_node(self): ( "Warning: this command is not sufficient for creating a " "guest node, use 'pcs cluster node add-guest'\n" - "Error: 'R' already exists\n" + ERRORS_HAVE_OCCURRED + "Error: Cannot set name of the guest node to 'R' because that " + "ID already exists in the cluster configuration.\n" + + ERRORS_HAVE_OCCURRED ), ) @@ -1877,7 +1881,9 @@ def test_fail_when_on_guest_conflict_with_existing_node_host(self): ( "Warning: this command is not sufficient for creating a " "guest node, use 'pcs cluster node add-guest'\n" - "Error: 'HOST' already exists\n" + ERRORS_HAVE_OCCURRED + "Error: Cannot set name of the guest node to 'HOST' because " + "that ID already exists in the cluster configuration.\n" + + ERRORS_HAVE_OCCURRED ), ) @@ -1898,7 +1904,8 @@ def test_fail_when_on_guest_conflict_with_existing_node_host_addr(self): ( "Warning: this command is not sufficient for creating a " "guest node, use 'pcs cluster node add-guest'\n" - "Error: 'HOST' already exists\n" + ERRORS_HAVE_OCCURRED + "Error: Node address 'HOST' is already used by existing nodes; " + "please, use other address\n" + ERRORS_HAVE_OCCURRED ), ) diff --git a/pcs_test/tier1/cib_resource/test_update.py b/pcs_test/tier1/cib_resource/test_update.py new file mode 100644 index 000000000..7a836c5f6 --- /dev/null +++ b/pcs_test/tier1/cib_resource/test_update.py @@ -0,0 +1,204 @@ +from textwrap import dedent +from unittest import TestCase + +from pcs_test.tier1.cib_resource.common import get_cib_resources +from pcs_test.tools.bin_mock import get_mock_settings +from pcs_test.tools.cib import get_assert_pcs_effect_mixin +from pcs_test.tools.fixture_cib import modify_cib_file +from pcs_test.tools.misc import ( + get_test_resource, + get_tmp_file, + write_data_to_tmpfile, +) +from pcs_test.tools.pcs_runner import PcsRunner + + +def fixture_primitive( + rsc_id, + agent_class="ocf", + agent_provider="pcsmock", + agent_type="minimal", + inner_xml="", +): + return f""" + + {inner_xml} + + """ + + +def fixture_clone(clone_id, inner_xml=""): + clone_id_split = clone_id.split("-") + assert clone_id_split[-1] == "clone" + rsc_id = "-".join(clone_id_split[:-1]) + primitive_xml = fixture_primitive( + rsc_id, + agent_class="ocf", + agent_provider="pcsmock", + agent_type="stateful", + ) + return dedent( + f""" + + {primitive_xml} + {inner_xml} + + """ + ) + + +def fixture_group(group_id, inner_xml=""): + group_id_split = group_id.split("-") + assert group_id_split[-1] == "group" + rsc_id = "-".join(group_id_split[:-1]) + return dedent( + f""" + + {fixture_primitive(rsc_id)} + {inner_xml} + + """ + ) + + +def fixture_bundle(bundle_id, inner_xml=""): + return dedent( + f""" + + + {inner_xml} + + """ + ) + + +def fixture_resources(resources_xml=""): + return f"\n {resources_xml}\n\n" + + +def fixture_meta_attrs(rsc_id, nvpairs_xml=""): + return dedent( + f""" + + {nvpairs_xml} + """ + ) + + +class ResourceMetaPrimitive( + TestCase, get_assert_pcs_effect_mixin(get_cib_resources) +): + rsc_id = "R" + resource_fixture = staticmethod(fixture_primitive) + + def setUp(self): + self.temp_cib = get_tmp_file("tier1_test_resource_meta") + self.pcs_runner = PcsRunner(self.temp_cib.name) + self.pcs_runner.mock_settings = get_mock_settings("crm_resource_exec") + + def tearDown(self): + self.temp_cib.close() + + def _fixture_nvpair_priority(self, value): + return dedent( + f""" + """ + ) + + def test_add(self): + write_data_to_tmpfile( + modify_cib_file( + get_test_resource("cib-empty.xml"), + resources=fixture_resources(self.resource_fixture(self.rsc_id)), + ), + self.temp_cib, + ) + self.assert_effect( + ["resource", "meta", self.rsc_id, "priority=2"], + fixture_resources( + self.resource_fixture( + self.rsc_id, + inner_xml=fixture_meta_attrs( + self.rsc_id, + nvpairs_xml=self._fixture_nvpair_priority(2), + ), + ), + ), + ) + + def test_modify(self): + write_data_to_tmpfile( + modify_cib_file( + get_test_resource("cib-empty.xml"), + resources=fixture_resources( + self.resource_fixture( + self.rsc_id, + inner_xml=fixture_meta_attrs( + self.rsc_id, + nvpairs_xml=self._fixture_nvpair_priority(2), + ), + ) + ), + ), + self.temp_cib, + ) + self.assert_effect( + ["resource", "meta", self.rsc_id, "priority=0"], + fixture_resources( + self.resource_fixture( + self.rsc_id, + inner_xml=fixture_meta_attrs( + self.rsc_id, + nvpairs_xml=self._fixture_nvpair_priority(0), + ), + ) + ), + ) + + def test_remove(self): + write_data_to_tmpfile( + modify_cib_file( + get_test_resource("cib-empty.xml"), + resources=fixture_resources( + self.resource_fixture( + self.rsc_id, + inner_xml=fixture_meta_attrs( + self.rsc_id, + nvpairs_xml=self._fixture_nvpair_priority(2), + ), + ) + ), + ), + self.temp_cib, + ) + self.assert_effect( + ["resource", "meta", self.rsc_id, "priority="], + fixture_resources( + self.resource_fixture( + self.rsc_id, + inner_xml=fixture_meta_attrs(self.rsc_id), + ) + ), + ) + + +class ResourceMetaGroup(ResourceMetaPrimitive): + rsc_id = "R-group" + resource_fixture = staticmethod(fixture_group) + + +class ResourceMetaClone(ResourceMetaPrimitive): + rsc_id = "R-clone" + resource_fixture = staticmethod(fixture_clone) + + +class ResourceMetaBundle(ResourceMetaPrimitive): + rsc_id = "B" + resource_fixture = staticmethod(fixture_bundle) diff --git a/pcs_test/tier1/legacy/test_resource.py b/pcs_test/tier1/legacy/test_resource.py index 06a2a8f58..e02a7763b 100644 --- a/pcs_test/tier1/legacy/test_resource.py +++ b/pcs_test/tier1/legacy/test_resource.py @@ -4023,13 +4023,6 @@ def test_meta_attrs(self): ), ) - def test_resource_meta_keep_empty_meta(self): - self.fixture_resource_meta() - self.assert_effect( - "resource meta R a=".split(), - self.fixture_xml_resource_empty_meta(), - ) - def test_resource_update_keep_empty_meta(self): self.fixture_resource_meta() self.assert_effect( @@ -4037,13 +4030,6 @@ def test_resource_update_keep_empty_meta(self): self.fixture_xml_resource_empty_meta(), ) - def test_resource_meta_dont_create_meta_on_removal(self): - self.fixture_resource() - self.assert_effect( - "resource meta R a=".split(), - self.fixture_xml_resource_no_meta(), - ) - def test_resource_update_dont_create_meta_on_removal(self): self.fixture_resource() self.assert_effect( @@ -4066,78 +4052,6 @@ def fixture_not_ocf_clone(): """ - def test_clone_promotable_not_ocf(self): - self.set_cib_file(self.fixture_not_ocf_clone()) - self.assert_pcs_fail( - "resource meta clone-R promotable=1".split(), - ( - "Error: Clone option 'promotable' is not compatible with " - "'systemd:pcsmock' resource agent of resource 'R'\n" - ), - ) - - def test_clone_globally_unique_not_ocf(self): - self.set_cib_file(self.fixture_not_ocf_clone()) - self.assert_pcs_fail( - "resource meta clone-R globally-unique=1".split(), - ( - "Error: Clone option 'globally-unique' is not compatible with " - "'systemd:pcsmock' resource agent of resource 'R'\n" - ), - ) - - def test_clone_promotable_unsupported(self): - self.set_cib_file( - f""" - - {self._fixture_xml_resource_no_meta()} - - """ - ) - self.assert_pcs_fail( - "resource meta clone-R promotable=1".split(), - ( - "Error: Clone option 'promotable' is not compatible with " - "'ocf:pcsmock:minimal' resource agent of resource 'R', use --force to override\n" - ), - ) - - def test_clone_promotable_unsupported_force(self): - self.set_cib_file( - f""" - - {self._fixture_xml_resource_no_meta()} - - """ - ) - self.assert_effect( - "resource meta clone-R promotable=1 --force".split(), - """ - - - - - - - - - - - - - """, - stderr_full=( - "Warning: Clone option 'promotable' is not compatible with " - "'ocf:pcsmock:minimal' resource agent of resource 'R'\n" - ), - ) - class UpdateInstanceAttrs( TestCase, @@ -4559,94 +4473,6 @@ def setUp(self): super().setUp() self.pcs_runner.mock_settings = get_mock_settings() - def test_transform_master_without_meta_on_meta(self): - # pcs no longer allows creating masters but supports existing ones. In - # order to test it, we need to put a master in the CIB without pcs. - fixture_to_cib(self.temp_cib.name, fixture_master_xml("dummy")) - self.assert_effect( - "resource meta dummy-master a=b".split(), - """ - - - - - - - - - - - - - - - - """, - ) - - def test_transform_master_with_meta_on_meta(self): - # pcs no longer allows creating masters but supports existing ones. In - # order to test it, we need to put a master in the CIB without pcs. - fixture_to_cib( - self.temp_cib.name, - fixture_master_xml("dummy", meta_dict=dict(a="A", b="B", c="C")), - ) - self.assert_effect( - "resource meta dummy-master a=AA b= d=D promotable=".split(), - """ - - - - - - - - - - - - - - - - - """, - ) - def test_transform_master_without_meta_on_update(self): # pcs no longer allows creating masters but supports existing ones. In # order to test it, we need to put a master in the CIB without pcs. @@ -4900,13 +4726,6 @@ def test_update(self): "Error: Unable to find resource: B\n", ) - def test_meta(self): - self.fixture_bundle("B") - self.assert_pcs_fail_regardless_of_force( - "resource meta B aaa=bbb".split(), - "Error: unable to find a resource/clone/group: B\n", - ) - def test_utilization(self): self.fixture_bundle("B") self.assert_pcs_fail( @@ -5103,66 +4922,6 @@ def test_update_warn_on_pacemaker_guest_attempt_remove(self): ), ) - def test_meta_fail_on_pacemaker_guest_attempt(self): - self.assert_pcs_success("resource create R ocf:pcsmock:minimal".split()) - self.assert_pcs_fail( - "resource meta R remote-node=HOST".split(), - ( - "Error: this command is not sufficient for creating a guest " - "node, use 'pcs cluster node add-guest', use --force to " - "override\n" + ERRORS_HAVE_OCCURRED - ), - ) - - def test_meta_warn_on_pacemaker_guest_attempt(self): - self.assert_pcs_success("resource create R ocf:pcsmock:minimal".split()) - self.assert_pcs_success( - "resource meta R remote-node=HOST --force".split(), - stderr_full=( - "Warning: this command is not sufficient for creating a guest node," - " use 'pcs cluster node add-guest'\n" - ), - ) - - def test_meta_fail_on_pacemaker_guest_attempt_remove(self): - self.assert_pcs_success( - ( - "resource create R ocf:pcsmock:minimal meta remote-node=HOST" - " --force" - ).split(), - stderr_full=( - "Warning: this command is not sufficient for creating a guest node," - " use 'pcs cluster node add-guest'\n" - ), - ) - self.assert_pcs_fail( - "resource meta R remote-node=".split(), - ( - "Error: this command is not sufficient for removing a guest " - "node, use 'pcs cluster node remove-guest', use --force to " - "override\n" + ERRORS_HAVE_OCCURRED - ), - ) - - def test_meta_warn_on_pacemaker_guest_attempt_remove(self): - self.assert_pcs_success( - ( - "resource create R ocf:pcsmock:minimal meta remote-node=HOST" - " --force" - ).split(), - stderr_full=( - "Warning: this command is not sufficient for creating a guest node," - " use 'pcs cluster node add-guest'\n" - ), - ) - self.assert_pcs_success( - "resource meta R remote-node= --force".split(), - stderr_full=( - "Warning: this command is not sufficient for removing a guest node," - " use 'pcs cluster node remove-guest'\n" - ), - ) - class ResourceUpdateUniqueAttrChecks(TestCase, AssertPcsMixin): def setUp(self): diff --git a/pcs_test/tier1/test_cluster_pcmk_remote.py b/pcs_test/tier1/test_cluster_pcmk_remote.py index 2241d1ec6..42bb9aec4 100644 --- a/pcs_test/tier1/test_cluster_pcmk_remote.py +++ b/pcs_test/tier1/test_cluster_pcmk_remote.py @@ -296,7 +296,9 @@ def test_fail_when_guest_node_conflicts_with_existing_id(self): "Unable to check if there is a conflict with nodes set in corosync " "because the command does not run on a live cluster (e.g. -f " "was used)\n" - "Error: 'CONFLICT' already exists\n" + ERRORS_HAVE_OCCURRED, + "Error: Cannot set name of the guest node to 'CONFLICT' because " + "that ID already exists in the cluster configuration.\n" + + ERRORS_HAVE_OCCURRED, ) def test_fail_when_guest_node_conflicts_with_existing_guest(self): @@ -313,7 +315,9 @@ def test_fail_when_guest_node_conflicts_with_existing_guest(self): "Unable to check if there is a conflict with nodes set in corosync " "because the command does not run on a live cluster (e.g. -f " "was used)\n" - "Error: 'node-name' already exists\n" + ERRORS_HAVE_OCCURRED, + "Error: Cannot set name of the guest node to 'node-name' because " + "that ID already exists in the cluster configuration.\n" + + ERRORS_HAVE_OCCURRED, ) def test_fail_when_guest_node_conflicts_with_existing_remote(self): @@ -332,7 +336,8 @@ def test_fail_when_guest_node_conflicts_with_existing_remote(self): "Unable to check if there is a conflict with nodes set in corosync " "because the command does not run on a live cluster (e.g. -f " "was used)\n" - "Error: 'node-addr' already exists\n" + ERRORS_HAVE_OCCURRED, + "Error: Node address 'node-addr' is already used by existing " + "nodes; please, use other address\n" + ERRORS_HAVE_OCCURRED, ) def test_fail_when_guest_node_name_conflicts_with_existing_remote(self): @@ -351,7 +356,9 @@ def test_fail_when_guest_node_name_conflicts_with_existing_remote(self): "Unable to check if there is a conflict with nodes set in corosync " "because the command does not run on a live cluster (e.g. -f " "was used)\n" - "Error: 'R' already exists\n" + ERRORS_HAVE_OCCURRED, + "Error: Cannot set name of the guest node to 'R' because that ID " + "already exists in the cluster configuration.\n" + + ERRORS_HAVE_OCCURRED, ) def test_success(self): diff --git a/pcsd/capabilities.xml.in b/pcsd/capabilities.xml.in index 11198cbc6..a9e707932 100644 --- a/pcsd/capabilities.xml.in +++ b/pcsd/capabilities.xml.in @@ -1563,6 +1563,14 @@ daemon urls: add_meta_attr_remote + + + The resource meta command also supports bundle resources. + + pcs commands: resource meta + daemon urls: add_meta_attr_remote + + Update several meta attributes of a resource at once. @@ -1577,6 +1585,13 @@ pcs commands: resource update --wait, resource meta --wait + + + Add, update or remove meta attributes of any resource. + + API v2: resource.update_meta + + Create and delete an operation of an existing resource. An operation can diff --git a/pcsd/pcs.rb b/pcsd/pcs.rb index ffc0d3554..78ea34a3d 100644 --- a/pcsd/pcs.rb +++ b/pcsd/pcs.rb @@ -66,12 +66,10 @@ def add_node_attr(auth_user, node, key, value) def add_meta_attr(auth_user, resource, key, value) cmd = ["resource", "meta", resource, key.to_s + "=" + value.to_s] flags = [] - if key.to_s == "remote-node" - # --force is a workaround for: - # 1) Error: this command is not sufficient for create guest node, use 'pcs - # cluster node add-guest', use --force to override - # 2) Error: this command is not sufficient for remove guest node, use 'pcs - # cluster node remove-guest', use --force to override + if ["remote-node", "remote-addr"].include?(key.to_s) + # --force is a workaround for missing guest node management in the web ui + # The reports generated are to prevent adding guest nodes, removing guest + # nodes and changing their connection parameters. flags << "--force" end stdout, stderr, retval = run_cmd(auth_user, PCS, *flags, "--", *cmd) From 32a0dedc9ab82db6131a7a0fac8c8a9ced6ead30 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Tue, 8 Apr 2025 14:28:39 +0200 Subject: [PATCH 137/227] export node attribute/utilization as json and commands --- pcs/Makefile.am | 4 ++ pcs/cli/common/lib_wrapper.py | 1 + pcs/cli/node/__init__.py | 0 pcs/cli/node/command.py | 120 ++++++++++++++++++++++++++++++++ pcs/cli/node/output.py | 124 ++++++++++++++++++++++++++++++++++ pcs/cli/nvset.py | 20 +++++- pcs/cli/routing/node.py | 30 +++++++- pcs/common/pacemaker/node.py | 21 ++++++ pcs/lib/cib/node.py | 36 +++++++++- pcs/lib/commands/node.py | 23 ++++++- pcs/node.py | 40 +++++------ pcs/utils.py | 2 +- 12 files changed, 395 insertions(+), 26 deletions(-) create mode 100644 pcs/cli/node/__init__.py create mode 100644 pcs/cli/node/command.py create mode 100644 pcs/cli/node/output.py create mode 100644 pcs/common/pacemaker/node.py diff --git a/pcs/Makefile.am b/pcs/Makefile.am index eb0b40887..113e1e9e6 100644 --- a/pcs/Makefile.am +++ b/pcs/Makefile.am @@ -69,6 +69,9 @@ EXTRA_DIST = \ cli/file/__init__.py \ cli/file/metadata.py \ cli/__init__.py \ + cli/node/__init__.py \ + cli/node/command.py \ + cli/node/output.py \ cli/nvset.py \ cli/query/__init__.py \ cli/query/resource.py \ @@ -148,6 +151,7 @@ EXTRA_DIST = \ common/pacemaker/constraint/ticket.py \ common/pacemaker/defaults.py \ common/pacemaker/fencing_topology.py \ + common/pacemaker/node.py \ common/pacemaker/nvset.py \ common/pacemaker/resource/__init__.py \ common/pacemaker/resource/bundle.py \ diff --git a/pcs/cli/common/lib_wrapper.py b/pcs/cli/common/lib_wrapper.py index b5745e35c..8abacf691 100644 --- a/pcs/cli/common/lib_wrapper.py +++ b/pcs/cli/common/lib_wrapper.py @@ -303,6 +303,7 @@ def load_module(env, middleware_factory, name): # noqa: PLR0911, PLR0912 env, middleware.build(middleware_factory.cib), { + "get_config_dto": node.get_config_dto, "maintenance_unmaintenance_all": ( node.maintenance_unmaintenance_all ), diff --git a/pcs/cli/node/__init__.py b/pcs/cli/node/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pcs/cli/node/command.py b/pcs/cli/node/command.py new file mode 100644 index 000000000..7591e8772 --- /dev/null +++ b/pcs/cli/node/command.py @@ -0,0 +1,120 @@ +import json +from typing import Any, Callable + +from pcs.cli.cluster_property.output import PropertyConfigurationFacade +from pcs.cli.common.errors import CmdLineInputError +from pcs.cli.common.output import lines_to_str +from pcs.cli.common.parse_args import ( + OUTPUT_FORMAT_OPTION, + OUTPUT_FORMAT_VALUE_CMD, + OUTPUT_FORMAT_VALUE_JSON, + OUTPUT_FORMAT_VALUE_TEXT, + Argv, + InputModifiers, +) +from pcs.common.interface.dto import to_dict +from pcs.common.pacemaker.node import CibNodeListDto +from pcs.utils import print_warning_if_utilization_attrs_has_no_effect + +from .output import ( + config_dto_to_attribute_cmd, + config_dto_to_utilization_cmd, + filter_nodes_by_node_name, + filter_nodes_by_nvpair_name, + node_attribute_to_lines, + node_utilization_to_lines, +) + + +def _node_output_cmd( + lib: Any, + argv: Argv, + modifiers: InputModifiers, + supported_options: list[str], + to_cmd: Callable[[CibNodeListDto], list[str]], + to_lines: Callable[[CibNodeListDto], list[str]], + utilization_warning: bool = False, +) -> None: + """ + Options: + * -f - CIB file + * --name - specify attribute name for output filter + * --output-format - supported formats: text, cmd, json + """ + modifiers.ensure_only_supported( + *supported_options, output_format_supported=True + ) + if len(argv) > 1 or modifiers.is_specified("--force"): + raise CmdLineInputError() + output_format = modifiers.get_output_format() + if output_format != OUTPUT_FORMAT_VALUE_TEXT and ( + argv or modifiers.is_specified("--name") + ): + raise CmdLineInputError( + f"filtering is not supported with {OUTPUT_FORMAT_OPTION}=" + f"{OUTPUT_FORMAT_VALUE_CMD}|{OUTPUT_FORMAT_VALUE_JSON}" + ) + if utilization_warning: + print_warning_if_utilization_attrs_has_no_effect( + PropertyConfigurationFacade.from_properties_dtos( + lib.cluster_property.get_properties(), + lib.cluster_property.get_properties_metadata(), + ) + ) + config_dto = lib.node.get_config_dto() + if argv: + node_name = argv[0] + config_dto = filter_nodes_by_node_name(config_dto, node_name) + if not config_dto.nodes: + raise CmdLineInputError(f"Unable to find a node: {node_name}") + if modifiers.is_specified("--name"): + config_dto = filter_nodes_by_nvpair_name( + config_dto, str(modifiers.get("--name")) + ) + if output_format == OUTPUT_FORMAT_VALUE_CMD: + output = ";\n".join(to_cmd(config_dto)) + elif output_format == OUTPUT_FORMAT_VALUE_JSON: + output = json.dumps(to_dict(config_dto)) + else: + output = lines_to_str(to_lines(config_dto)) + if output: + print(output) + + +def node_attribute_output_cmd( + lib: Any, argv: Argv, modifiers: InputModifiers +) -> None: + """ + Options: + * -f - CIB file + * --name - specify attribute name for output filter + * --output-format - supported formats: text, cmd, json + """ + _node_output_cmd( + lib, + argv, + modifiers, + ["-f", "--force", "--name"], + config_dto_to_attribute_cmd, + node_attribute_to_lines, + ) + + +def node_utilization_output_cmd( + lib: Any, argv: Argv, modifiers: InputModifiers +) -> None: + """ + Options: + * -f - CIB file + * --name - specify attribute name for output filter + * --output-format - supported formats: text, cmd, json + """ + _node_output_cmd( + lib, + argv, + modifiers, + ["-f", "--name"], + config_dto_to_utilization_cmd, + node_utilization_to_lines, + utilization_warning=True, + ) diff --git a/pcs/cli/node/output.py b/pcs/cli/node/output.py new file mode 100644 index 000000000..0a9857fcb --- /dev/null +++ b/pcs/cli/node/output.py @@ -0,0 +1,124 @@ +import shlex +from typing import Optional, Sequence + +from pcs.cli.common.output import ( + INDENT_STEP, + pairs_to_cmd, +) +from pcs.cli.nvset import filter_nvpairs_by_names, nvset_dto_to_lines +from pcs.common.pacemaker.node import CibNodeDto, CibNodeListDto +from pcs.common.pacemaker.nvset import CibNvsetDto +from pcs.common.str_tools import ( + indent, +) + + +def _description_to_lines(desc: Optional[str]) -> list[str]: + return [f"Description: {desc}"] if desc else [] + + +def _nvsets_to_lines(label: str, nvsets: Sequence[CibNvsetDto]) -> list[str]: + if nvsets and nvsets[0].nvpairs: + return nvset_dto_to_lines(nvset=nvsets[0], nvset_label=label) + return [] + + +def _node_dto_to_nvset_lines( + node_dto: CibNodeDto, label: str, nvsets: Sequence[CibNvsetDto] +) -> list[str]: + nvsets_lines = _nvsets_to_lines(label, nvsets) + if not nvsets_lines: + return [] + lines = _description_to_lines(node_dto.description) + lines.extend(nvsets_lines) + return [f"Node: {node_dto.uname}"] + indent(lines, indent_step=INDENT_STEP) + + +def node_utilization_to_lines(config_dto: CibNodeListDto) -> list[str]: + result = [] + for node_dto in config_dto.nodes: + result.extend( + _node_dto_to_nvset_lines( + node_dto, "Utilization", node_dto.utilization + ) + ) + return result + + +def node_attribute_to_lines(config_dto: CibNodeListDto) -> list[str]: + result = [] + for node_dto in config_dto.nodes: + result.extend( + _node_dto_to_nvset_lines( + node_dto, "Attributes", node_dto.instance_attributes + ) + ) + return result + + +def _nvsets_to_cmd( + nvset_cmd: str, node_name: str, nvsets: Sequence[CibNvsetDto] +) -> list[str]: + if nvsets and nvsets[0].nvpairs: + nvset_cmd = shlex.quote(nvset_cmd) + node = shlex.quote(node_name) + options = pairs_to_cmd( + (nvpair.name, nvpair.value) for nvpair in nvsets[0].nvpairs + ) + return [f"pcs -- node {nvset_cmd} {node} {options}"] + return [] + + +def config_dto_to_attribute_cmd(config_dto: CibNodeListDto) -> list[str]: + commands = [] + for node_dto in config_dto.nodes: + commands.extend( + _nvsets_to_cmd( + "attribute", node_dto.uname, node_dto.instance_attributes + ) + ) + return commands + + +def config_dto_to_utilization_cmd(config_dto: CibNodeListDto) -> list[str]: + commands = [] + for node_dto in config_dto.nodes: + commands.extend( + _nvsets_to_cmd("utilization", node_dto.uname, node_dto.utilization) + ) + return commands + + +def filter_nodes_by_node_name( + config_dto: CibNodeListDto, node_name: str +) -> CibNodeListDto: + return CibNodeListDto( + nodes=[ + node_dto + for node_dto in config_dto.nodes + if node_dto.uname == node_name + ] + ) + + +def filter_nodes_by_nvpair_name( + config_dto: CibNodeListDto, name: str +) -> CibNodeListDto: + return CibNodeListDto( + [ + CibNodeDto( + id=node_dto.id, + uname=node_dto.uname, + description=node_dto.description, + score=node_dto.score, + type=node_dto.type, + instance_attributes=filter_nvpairs_by_names( + node_dto.instance_attributes, [name] + ), + utilization=filter_nvpairs_by_names( + node_dto.utilization, [name] + ), + ) + for node_dto in config_dto.nodes + ] + ) diff --git a/pcs/cli/nvset.py b/pcs/cli/nvset.py index 3ae9a079f..e93248d8d 100644 --- a/pcs/cli/nvset.py +++ b/pcs/cli/nvset.py @@ -14,7 +14,7 @@ format_optional, indent, ) -from pcs.common.types import CibRuleInEffectStatus +from pcs.common.types import CibRuleInEffectStatus, StringSequence def filter_out_expired_nvset( @@ -28,6 +28,24 @@ def filter_out_expired_nvset( ] +def filter_nvpairs_by_names( + nvsets: Iterable[CibNvsetDto], nvpair_names: StringSequence +) -> list[CibNvsetDto]: + return [ + CibNvsetDto( + id=nvset_dto.id, + options=nvset_dto.options, + rule=nvset_dto.rule, + nvpairs=[ + nvpair_dto + for nvpair_dto in nvset_dto.nvpairs + if nvpair_dto.name in nvpair_names + ], + ) + for nvset_dto in nvsets + ] + + def nvset_dto_list_to_lines( nvset_dto_list: Iterable[CibNvsetDto], nvset_label: str, diff --git a/pcs/cli/routing/node.py b/pcs/cli/routing/node.py index 4b2320eb9..cc1ee0331 100644 --- a/pcs/cli/routing/node.py +++ b/pcs/cli/routing/node.py @@ -1,10 +1,36 @@ from functools import partial +from typing import Any from pcs import ( node, usage, ) +from pcs.cli.common.parse_args import Argv, InputModifiers from pcs.cli.common.routing import create_router +from pcs.cli.node import command as node_command + + +def _node_attribute_cmd( + lib: Any, argv: Argv, modifiers: InputModifiers +) -> None: + if len(argv) > 1: + # set command + node.node_attribute_cmd(lib, argv, modifiers) + else: + # config command + node_command.node_attribute_output_cmd(lib, argv, modifiers) + + +def _node_utilization_cmd( + lib: Any, argv: Argv, modifiers: InputModifiers +) -> None: + if len(argv) > 1: + # set command + node.node_utilization_cmd(lib, argv, modifiers) + else: + # config command + node_command.node_utilization_output_cmd(lib, argv, modifiers) + node_cmd = create_router( { @@ -13,8 +39,8 @@ "unmaintenance": partial(node.node_maintenance_cmd, enable=False), "standby": partial(node.node_standby_cmd, enable=True), "unstandby": partial(node.node_standby_cmd, enable=False), - "attribute": node.node_attribute_cmd, - "utilization": node.node_utilization_cmd, + "attribute": _node_attribute_cmd, + "utilization": _node_utilization_cmd, # pcs-to-pcsd use only "pacemaker-status": node.node_pacemaker_status, }, diff --git a/pcs/common/pacemaker/node.py b/pcs/common/pacemaker/node.py new file mode 100644 index 000000000..494e0d135 --- /dev/null +++ b/pcs/common/pacemaker/node.py @@ -0,0 +1,21 @@ +from dataclasses import dataclass +from typing import Optional, Sequence + +from pcs.common.interface.dto import DataTransferObject +from pcs.common.pacemaker.nvset import CibNvsetDto + + +@dataclass(frozen=True) +class CibNodeDto(DataTransferObject): + id: str + uname: str + description: Optional[str] + score: Optional[str] + type: Optional[str] + instance_attributes: Sequence[CibNvsetDto] + utilization: Sequence[CibNvsetDto] + + +@dataclass(frozen=True) +class CibNodeListDto(DataTransferObject): + nodes: Sequence[CibNodeDto] diff --git a/pcs/lib/cib/node.py b/pcs/lib/cib/node.py index c8af236b8..6d4be8e7c 100644 --- a/pcs/lib/cib/node.py +++ b/pcs/lib/cib/node.py @@ -1,11 +1,13 @@ from collections import namedtuple -from typing import Set +from typing import Optional, Set from lxml import etree from lxml.etree import _Element from pcs.common import reports +from pcs.common.pacemaker.node import CibNodeDto from pcs.common.reports.item import ReportItem +from pcs.lib.cib import nvpair_multi, rule from pcs.lib.cib.nvpair import update_nvset from pcs.lib.cib.tools import get_nodes from pcs.lib.errors import LibraryError @@ -14,6 +16,38 @@ get_root, ) +TAG_NODE = "node" + + +def get_all_node_elements(nodes_section: _Element) -> list[_Element]: + return nodes_section.findall(TAG_NODE) + + +def node_el_to_dto( + node_el: _Element, rule_eval: Optional[rule.RuleInEffectEval] = None +) -> CibNodeDto: + if rule_eval is None: + rule_eval = rule.RuleInEffectEvalDummy() + return CibNodeDto( + id=str(node_el.attrib["id"]), + uname=str(node_el.attrib["uname"]), + description=node_el.get("description"), + score=node_el.get("score"), + type=node_el.get("type"), + instance_attributes=[ + nvpair_multi.nvset_element_to_dto(nvset, rule_eval) + for nvset in nvpair_multi.find_nvsets( + node_el, nvpair_multi.NVSET_INSTANCE + ) + ], + utilization=[ + nvpair_multi.nvset_element_to_dto(nvset, rule_eval) + for nvset in nvpair_multi.find_nvsets( + node_el, nvpair_multi.NVSET_UTILIZATION + ) + ], + ) + class PacemakerNode(namedtuple("PacemakerNode", "name addr")): """ diff --git a/pcs/lib/commands/node.py b/pcs/lib/commands/node.py index 9eb5e2cf5..54871160d 100644 --- a/pcs/lib/commands/node.py +++ b/pcs/lib/commands/node.py @@ -1,9 +1,15 @@ from contextlib import contextmanager from pcs.common import reports +from pcs.common.pacemaker.node import CibNodeListDto from pcs.common.reports.item import ReportItem +from pcs.lib.cib import node from pcs.lib.cib.node import update_node_instance_attrs -from pcs.lib.cib.tools import IdProvider +from pcs.lib.cib.rule.in_effect import get_rule_evaluator +from pcs.lib.cib.tools import ( + IdProvider, + get_nodes, +) from pcs.lib.env import ( LibraryEnvironment, WaitType, @@ -182,3 +188,18 @@ def _set_instance_attrs_all_nodes( update_node_instance_attrs( cib, IdProvider(cib), node, attrs, state_nodes=state_nodes ) + + +def get_config_dto( + lib_env: LibraryEnvironment, evaluate_expired: bool = False +) -> CibNodeListDto: + cib = lib_env.get_cib() + rule_in_effect_eval = get_rule_evaluator( + cib, lib_env.cmd_runner(), lib_env.report_processor, evaluate_expired + ) + return CibNodeListDto( + nodes=[ + node.node_el_to_dto(node_el, rule_eval=rule_in_effect_eval) + for node_el in node.get_all_node_elements(get_nodes(cib)) + ] + ) diff --git a/pcs/node.py b/pcs/node.py index cdf0fe98f..d72ad21dc 100644 --- a/pcs/node.py +++ b/pcs/node.py @@ -12,6 +12,7 @@ CmdLineInputError, ) from pcs.cli.common.parse_args import ( + OUTPUT_FORMAT_OPTION, Argv, InputModifiers, KeyValueParser, @@ -23,20 +24,19 @@ def node_attribute_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file (in lib wrapper) - * --force - allows not unique recipient values - * --name - specify attribute name to filter out + * --force - no error if attribute to delete doesn't exist + * --name - specify attribute name for filter + * --output-format - supported formats: text, cmd, json """ del lib - modifiers.ensure_only_supported("-f", "--force", "--name") - if modifiers.get("--name") and len(argv) > 1: + modifiers.ensure_only_supported( + "-f", "--force", "--name", output_format_supported=True + ) + if len(argv) < 2 or modifiers.is_specified_any( + ["--name", OUTPUT_FORMAT_OPTION] + ): raise CmdLineInputError() - if not argv: - attribute_show_cmd(filter_attr=modifiers.get("--name")) - elif len(argv) == 1: - attribute_show_cmd(argv.pop(0), filter_attr=modifiers.get("--name")) - else: - # --force is used only when setting attributes - attribute_set_cmd(argv.pop(0), argv) + attribute_set_cmd(argv.pop(0), argv) def node_utilization_cmd( @@ -45,10 +45,15 @@ def node_utilization_cmd( """ Options: * -f - CIB file (in lib wrapper) - * --name - specify attribute name to filter out + * --name - specify attribute name for filter + * --output-format - supported formats: text, cmd, json """ - modifiers.ensure_only_supported("-f", "--name") - if modifiers.get("--name") and len(argv) > 1: + modifiers.ensure_only_supported( + "-f", "--name", output_format_supported=True + ) + if len(argv) < 2 or modifiers.is_specified_any( + ["--name", OUTPUT_FORMAT_OPTION] + ): raise CmdLineInputError() utils.print_warning_if_utilization_attrs_has_no_effect( PropertyConfigurationFacade.from_properties_dtos( @@ -56,12 +61,7 @@ def node_utilization_cmd( lib.cluster_property.get_properties_metadata(), ) ) - if not argv: - print_node_utilization(filter_name=modifiers.get("--name")) - elif len(argv) == 1: - print_node_utilization(argv.pop(0), filter_name=modifiers.get("--name")) - else: - set_node_utilization(argv.pop(0), argv) + set_node_utilization(argv.pop(0), argv) def node_maintenance_cmd( diff --git a/pcs/utils.py b/pcs/utils.py index f23d9bd8e..8c8918966 100644 --- a/pcs/utils.py +++ b/pcs/utils.py @@ -2779,7 +2779,7 @@ def print_depracation_warning_for_legacy_roles(role: str) -> None: def print_warning_if_utilization_attrs_has_no_effect( properties_facade: PropertyConfigurationFacade, -): +) -> None: PLACEMENT_STRATEGIES_USING_UTILIZATION_ATTRS = [ "balanced", "minimal", From 9879360e1bfa618afe90870422f00050ae043612 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Thu, 17 Apr 2025 11:33:55 +0200 Subject: [PATCH 138/227] fix tier1 tests --- pcs_test/tier1/legacy/test_node.py | 279 ++++++++++++++++++----------- 1 file changed, 171 insertions(+), 108 deletions(-) diff --git a/pcs_test/tier1/legacy/test_node.py b/pcs_test/tier1/legacy/test_node.py index b07c2d9c2..bf1242afb 100644 --- a/pcs_test/tier1/legacy/test_node.py +++ b/pcs_test/tier1/legacy/test_node.py @@ -90,14 +90,7 @@ def test_node_utilization_set(self): stdout, stderr, retval = pcs( self.temp_cib.name, "node utilization rh7-2".split() ) - self.assertEqual( - stdout, - outdent( - """\ - Node Utilization: - """ - ), - ) + self.assertEqual(stdout, "") self.assertEqual(stderr, FIXTURE_UTILIZATION_WARNING) self.assertEqual(retval, 0) @@ -108,8 +101,9 @@ def test_node_utilization_set(self): stdout, outdent( """\ - Node Utilization: - rh7-1: test1=10 + Node: rh7-1 + Utilization: nodes-1-utilization + test1=10 """ ), ) @@ -131,8 +125,10 @@ def test_node_utilization_set(self): stdout, outdent( """\ - Node Utilization: - rh7-1: test1=-10 test4=1234 + Node: rh7-1 + Utilization: nodes-1-utilization + test1=-10 + test4=1234 """ ), ) @@ -155,8 +151,9 @@ def test_node_utilization_set(self): stdout, outdent( """\ - Node Utilization: - rh7-2: test2=321 + Node: rh7-2 + Utilization: nodes-2-utilization + test2=321 """ ), ) @@ -170,9 +167,13 @@ def test_node_utilization_set(self): stdout, outdent( """\ - Node Utilization: - rh7-1: test1=-10 test4=1234 - rh7-2: test2=321 + Node: rh7-1 + Utilization: nodes-1-utilization + test1=-10 + test4=1234 + Node: rh7-2 + Utilization: nodes-2-utilization + test2=321 """ ), ) @@ -193,9 +194,12 @@ def test_node_utilization_set(self): stdout, outdent( """\ - Node Utilization: - rh7-1: test1=-10 - rh7-2: test1=-20 + Node: rh7-1 + Utilization: nodes-1-utilization + test1=-10 + Node: rh7-2 + Utilization: nodes-2-utilization + test1=-20 """ ), ) @@ -210,8 +214,9 @@ def test_node_utilization_set(self): stdout, outdent( """\ - Node Utilization: - rh7-2: test1=-20 + Node: rh7-2 + Utilization: nodes-2-utilization + test1=-20 """ ), ) @@ -295,9 +300,7 @@ def test_no_warning_printed_placement_strategy_is_set(self): self.fixture_xml_no_utilization(), ) self.assert_effect( - "node utilization".split(), - self.fixture_xml_no_utilization(), - stdout_full="Node Utilization:\n", + "node utilization".split(), self.fixture_xml_no_utilization() ) @@ -341,24 +344,30 @@ def setUp(self): self.pcs_runner = PcsRunner(self.temp_cib.name) def tearDown(self): - self.temp_cib.close() + pass + # self.temp_cib.close() def fixture_standby_all(self): self.assert_pcs_success("node standby --all".split()) self.assert_standby_all() def assert_standby_none(self): - self.assert_pcs_success("node attribute".split(), "Node Attributes:\n") + self.assert_pcs_success("node attribute".split()) def assert_standby_all(self): self.assert_pcs_success( "node attribute".split(), outdent( """\ - Node Attributes: - rh7-1: standby=on - rh7-2: standby=on - rh7-3: standby=on + Node: rh7-1 + Attributes: nodes-1 + standby=on + Node: rh7-2 + Attributes: nodes-2 + standby=on + Node: rh7-3 + Attributes: nodes-3 + standby=on """ ), ) @@ -425,8 +434,9 @@ def test_one_node_with_repeat(self): "node attribute".split(), outdent( """\ - Node Attributes: - rh7-1: standby=on + Node: rh7-1 + Attributes: nodes-1 + standby=on """ ), ) @@ -438,9 +448,12 @@ def test_one_node_with_repeat(self): "node attribute".split(), outdent( """\ - Node Attributes: - rh7-2: standby=on - rh7-3: standby=on + Node: rh7-2 + Attributes: nodes-2 + standby=on + Node: rh7-3 + Attributes: nodes-3 + standby=on """ ), ) @@ -453,9 +466,12 @@ def test_more_nodes(self): "node attribute".split(), outdent( """\ - Node Attributes: - rh7-1: standby=on - rh7-2: standby=on + Node: rh7-1 + Attributes: nodes-1 + standby=on + Node: rh7-2 + Attributes: nodes-2 + standby=on """ ), ) @@ -466,8 +482,9 @@ def test_more_nodes(self): "node attribute".split(), outdent( """\ - Node Attributes: - rh7-3: standby=on + Node: rh7-3 + Attributes: nodes-3 + standby=on """ ), ) @@ -497,17 +514,22 @@ def fixture_maintenance_all(self): self.assert_maintenance_all() def assert_maintenance_none(self): - self.assert_pcs_success("node attribute".split(), "Node Attributes:\n") + self.assert_pcs_success("node attribute".split()) def assert_maintenance_all(self): self.assert_pcs_success( "node attribute".split(), outdent( """\ - Node Attributes: - rh7-1: maintenance=on - rh7-2: maintenance=on - rh7-3: maintenance=on + Node: rh7-1 + Attributes: nodes-1 + maintenance=on + Node: rh7-2 + Attributes: nodes-2 + maintenance=on + Node: rh7-3 + Attributes: nodes-3 + maintenance=on """ ), ) @@ -574,8 +596,9 @@ def test_one_node_with_repeat(self): "node attribute".split(), outdent( """\ - Node Attributes: - rh7-1: maintenance=on + Node: rh7-1 + Attributes: nodes-1 + maintenance=on """ ), ) @@ -587,9 +610,12 @@ def test_one_node_with_repeat(self): "node attribute".split(), outdent( """\ - Node Attributes: - rh7-2: maintenance=on - rh7-3: maintenance=on + Node: rh7-2 + Attributes: nodes-2 + maintenance=on + Node: rh7-3 + Attributes: nodes-3 + maintenance=on """ ), ) @@ -602,9 +628,12 @@ def test_more_nodes(self): "node attribute".split(), outdent( """\ - Node Attributes: - rh7-1: maintenance=on - rh7-2: maintenance=on + Node: rh7-1 + Attributes: nodes-1 + maintenance=on + Node: rh7-2 + Attributes: nodes-2 + maintenance=on """ ), ) @@ -615,8 +644,9 @@ def test_more_nodes(self): "node attribute".split(), outdent( """\ - Node Attributes: - rh7-3: maintenance=on + Node: rh7-3 + Attributes: nodes-3 + maintenance=on """ ), ) @@ -714,7 +744,7 @@ def fixture_xml_with_attrs(): def test_show_empty(self): self.fixture_attrs(["rh7-1", "rh7-2"]) - self.assert_pcs_success("node attribute".split(), "Node Attributes:\n") + self.assert_pcs_success("node attribute".split()) def test_show_nonempty(self): self.fixture_attrs( @@ -730,11 +760,16 @@ def test_show_nonempty(self): ) self.assert_pcs_success( "node attribute".split(), - """\ -Node Attributes: - rh7-1: IP=192.168.1.1 - rh7-2: IP=192.168.1.2 -""", + outdent( + """\ + Node: rh7-1 + Attributes: nodes-1 + IP=192.168.1.1 + Node: rh7-2 + Attributes: nodes-2 + IP=192.168.1.2 + """ + ), ) def test_show_multiple_per_node(self): @@ -753,11 +788,18 @@ def test_show_multiple_per_node(self): ) self.assert_pcs_success( "node attribute".split(), - """\ -Node Attributes: - rh7-1: IP=192.168.1.1 alias=node1 - rh7-2: IP=192.168.1.2 alias=node2 -""", + outdent( + """\ + Node: rh7-1 + Attributes: nodes-1 + IP=192.168.1.1 + alias=node1 + Node: rh7-2 + Attributes: nodes-2 + IP=192.168.1.2 + alias=node2 + """ + ), ) def test_show_one_node(self): @@ -776,10 +818,14 @@ def test_show_one_node(self): ) self.assert_pcs_success( "node attribute rh7-1".split(), - """\ -Node Attributes: - rh7-1: IP=192.168.1.1 alias=node1 -""", + outdent( + """\ + Node: rh7-1 + Attributes: nodes-1 + IP=192.168.1.1 + alias=node1 + """ + ), ) def test_show_missing_node(self): @@ -796,11 +842,9 @@ def test_show_missing_node(self): }, }, ) - self.assert_pcs_success( + self.assert_pcs_fail( "node attribute rh7-3".split(), - """\ -Node Attributes: -""", + "Error: Unable to find a node: rh7-3\n", ) def test_show_name(self): @@ -819,11 +863,16 @@ def test_show_name(self): ) self.assert_pcs_success( "node attribute --name alias".split(), - """\ -Node Attributes: - rh7-1: alias=node1 - rh7-2: alias=node2 -""", + outdent( + """\ + Node: rh7-1 + Attributes: nodes-1 + alias=node1 + Node: rh7-2 + Attributes: nodes-2 + alias=node2 + """ + ), ) def test_show_missing_name(self): @@ -840,12 +889,7 @@ def test_show_missing_name(self): }, }, ) - self.assert_pcs_success( - "node attribute --name missing".split(), - """\ -Node Attributes: -""", - ) + self.assert_pcs_success("node attribute --name missing".split()) def test_show_node_and_name(self): self.fixture_attrs( @@ -863,10 +907,13 @@ def test_show_node_and_name(self): ) self.assert_pcs_success( "node attribute --name alias rh7-1".split(), - """\ -Node Attributes: - rh7-1: alias=node1 -""", + outdent( + """\ + Node: rh7-1 + Attributes: nodes-1 + alias=node1 + """ + ), ) def test_set_new(self): @@ -874,19 +921,27 @@ def test_set_new(self): self.assert_pcs_success("node attribute rh7-1 IP=192.168.1.1".split()) self.assert_pcs_success( "node attribute".split(), - """\ -Node Attributes: - rh7-1: IP=192.168.1.1 -""", + outdent( + """\ + Node: rh7-1 + Attributes: nodes-1 + IP=192.168.1.1 + """ + ), ) self.assert_pcs_success("node attribute rh7-2 IP=192.168.1.2".split()) self.assert_pcs_success( "node attribute".split(), - """\ -Node Attributes: - rh7-1: IP=192.168.1.1 - rh7-2: IP=192.168.1.2 -""", + outdent( + """\ + Node: rh7-1 + Attributes: nodes-1 + IP=192.168.1.1 + Node: rh7-2 + Attributes: nodes-2 + IP=192.168.1.2 + """ + ), ) def test_set_existing(self): @@ -904,11 +959,16 @@ def test_set_existing(self): self.assert_pcs_success("node attribute rh7-2 IP=192.168.2.2".split()) self.assert_pcs_success( "node attribute".split(), - """\ -Node Attributes: - rh7-1: IP=192.168.1.1 - rh7-2: IP=192.168.2.2 -""", + outdent( + """\ + Node: rh7-1 + Attributes: nodes-1 + IP=192.168.1.1 + Node: rh7-2 + Attributes: nodes-2 + IP=192.168.2.2 + """ + ), ) def test_unset(self): @@ -926,10 +986,13 @@ def test_unset(self): self.assert_pcs_success("node attribute rh7-2 IP=".split()) self.assert_pcs_success( "node attribute".split(), - """\ -Node Attributes: - rh7-1: IP=192.168.1.1 -""", + outdent( + """\ + Node: rh7-1 + Attributes: nodes-1 + IP=192.168.1.1 + """ + ), ) def test_unset_nonexisting(self): From 9628564d92b69152872aff23f0cab57cfa28ec80 Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Wed, 23 Apr 2025 16:21:04 +0200 Subject: [PATCH 139/227] add test coverage --- pcs_test/Makefile.am | 5 + pcs_test/resources/cib-all.xml | 26 +- pcs_test/resources/node-commands | 4 + pcs_test/tier0/cli/node/__init__.py | 0 pcs_test/tier0/cli/node/test_command.py | 372 +++++++++++++++++++++++ pcs_test/tier0/cli/node/test_output.py | 295 ++++++++++++++++++ pcs_test/tier0/cli/test_nvset.py | 96 ++++++ pcs_test/tier0/lib/cib/test_node.py | 18 ++ pcs_test/tier0/lib/commands/test_node.py | 93 +++++- pcs_test/tier1/test_node.py | 322 ++++++++++++++++++++ pcs_test/tools/nodes_dto.py | 226 ++++++++++++++ 11 files changed, 1455 insertions(+), 2 deletions(-) create mode 100644 pcs_test/resources/node-commands create mode 100644 pcs_test/tier0/cli/node/__init__.py create mode 100644 pcs_test/tier0/cli/node/test_command.py create mode 100644 pcs_test/tier0/cli/node/test_output.py create mode 100644 pcs_test/tier1/test_node.py create mode 100644 pcs_test/tools/nodes_dto.py diff --git a/pcs_test/Makefile.am b/pcs_test/Makefile.am index 535cdb790..53893f655 100644 --- a/pcs_test/Makefile.am +++ b/pcs_test/Makefile.am @@ -97,6 +97,9 @@ EXTRA_DIST = \ tier0/cli/constraint_ticket/test_command.py \ tier0/cli/constraint_ticket/test_parse_args.py \ tier0/cli/__init__.py \ + tier0/cli/node/__init__.py \ + tier0/cli/node/test_command.py \ + tier0/cli/node/test_output.py \ tier0/cli/query/__init__.py \ tier0/cli/query/test_resource.py \ tier0/cli/reports/__init__.py \ @@ -422,6 +425,7 @@ EXTRA_DIST = \ tier1/test_cluster_pcmk_remote.py \ tier1/test_cluster_property.py \ tier1/test_misc.py \ + tier1/test_node.py \ tier1/test_quorum.py \ tier1/test_status.py \ tier1/test_status_query_resource.py \ @@ -508,6 +512,7 @@ EXTRA_DIST = \ tools/fixture.py \ tools/__init__.py \ tools/misc.py \ + tools/nodes_dto.py \ tools/pcs_runner.py \ tools/parallel_test_runner.py \ tools/resources_dto.py \ diff --git a/pcs_test/resources/cib-all.xml b/pcs_test/resources/cib-all.xml index fd51fcfa0..70d070faa 100644 --- a/pcs_test/resources/cib-all.xml +++ b/pcs_test/resources/cib-all.xml @@ -1,7 +1,31 @@ - + + + + + + + + + + + + + + + + + + + + + + +