diff --git a/.gitignore b/.gitignore index fb4514e..9b425cd 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,9 @@ SOURCES/mock-1.0.1.tar.gz SOURCES/multi_json-1.13.1.gem SOURCES/open4-1.3.4.gem SOURCES/orderedhash-0.0.6.gem -SOURCES/pcs-0.9.168.tar.gz +SOURCES/pcs-0.9.169.tar.gz SOURCES/pyagentx-0.4.pcs.1.tar.gz -SOURCES/rack-1.6.11.gem +SOURCES/rack-1.6.12.gem SOURCES/rack-protection-1.5.5.gem SOURCES/rack-test-0.8.3.gem SOURCES/rpam-ruby19-1.2.1.gem diff --git a/.pcs.metadata b/.pcs.metadata index edbbf5a..0662232 100644 --- a/.pcs.metadata +++ b/.pcs.metadata @@ -6,9 +6,9 @@ baa3446eb63557a24c4522dc5a61cfad082fa395 SOURCES/mock-1.0.1.tar.gz ff6e0965061cb6f604ee4d87a2cf96a2917f9f88 SOURCES/multi_json-1.13.1.gem 41a7fe9f8e3e02da5ae76c821b89c5b376a97746 SOURCES/open4-1.3.4.gem 709cc95025009e5d221e37cb0777e98582146809 SOURCES/orderedhash-0.0.6.gem -ff0268d0e67d09897d204046511dca6c70f9c8b6 SOURCES/pcs-0.9.168.tar.gz +44842fc15ada73f8f228e4100a9f14b5dc356d5b SOURCES/pcs-0.9.169.tar.gz 276a92c6d679a71bd0daaf12cb7b3616f1a89b72 SOURCES/pyagentx-0.4.pcs.1.tar.gz -64a0cd32f46c0ff44ffda4055048fe6309903110 SOURCES/rack-1.6.11.gem +d7e31bb0e4edef5ecb5863bd168e5a1ee7394e38 SOURCES/rack-1.6.12.gem f80ea6672253a90fa031db0c1e2e1fe056582118 SOURCES/rack-protection-1.5.5.gem 908e2a877da8eb6745073c51709dc024c4457e44 SOURCES/rack-test-0.8.3.gem a90e5a60d99445404a3c29a66d953a5e9918976d SOURCES/rpam-ruby19-1.2.1.gem diff --git a/SOURCES/bz1458153-01-give-back-orig.-master-behav.-resource-create.patch b/SOURCES/bz1458153-01-give-back-orig.-master-behav.-resource-create.patch index 54fa1f1..b68a989 100644 --- a/SOURCES/bz1458153-01-give-back-orig.-master-behav.-resource-create.patch +++ b/SOURCES/bz1458153-01-give-back-orig.-master-behav.-resource-create.patch @@ -1,7 +1,7 @@ -From 00ef68086938945803d8b2c601e43735d5d5967e Mon Sep 17 00:00:00 2001 +From 9783358accb4002f30b9f7990e9facd68e22331e Mon Sep 17 00:00:00 2001 From: Ivan Devat Date: Mon, 5 Jun 2017 17:13:41 +0200 -Subject: [PATCH 3/6] give back orig. --master behav. (resource create) +Subject: [PATCH 2/6] give back orig. --master behav. (resource create) --- pcs/cli/common/parse_args.py | 8 +- @@ -14,7 +14,7 @@ Subject: [PATCH 3/6] give back orig. --master behav. (resource create) 7 files changed, 228 insertions(+), 59 deletions(-) diff --git a/pcs/cli/common/parse_args.py b/pcs/cli/common/parse_args.py -index 1f3e7b33..b9511d6c 100644 +index 19a658e5..7235835b 100644 --- a/pcs/cli/common/parse_args.py +++ b/pcs/cli/common/parse_args.py @@ -307,7 +307,13 @@ def upgrade_args(arg_list): @@ -87,7 +87,7 @@ index efe38d0e..900094c9 100644 ], upgrade_args([ diff --git a/pcs/resource.py b/pcs/resource.py -index 51233a12..62932387 100644 +index 3274910a..ac24cf4a 100644 --- a/pcs/resource.py +++ b/pcs/resource.py @@ -397,6 +397,25 @@ def resource_create(lib, argv, modifiers): @@ -406,7 +406,7 @@ index ecb16384..57d95350 100644 , "Error: you can specify only one of clone, master, bundle or" diff --git a/pcs/test/test_constraints.py b/pcs/test/test_constraints.py -index 3feaa053..28d68177 100644 +index df8594a5..671f0122 100644 --- a/pcs/test/test_constraints.py +++ b/pcs/test/test_constraints.py @@ -393,43 +393,43 @@ Ticket Constraints: @@ -463,7 +463,7 @@ index 3feaa053..28d68177 100644 output, returnVal = pcs(temp_cib, line) assert returnVal == 0 and output == "" -@@ -1054,7 +1054,7 @@ Ticket Constraints: +@@ -1066,7 +1066,7 @@ Ticket Constraints: assert returnVal == 1 def testLocationBadRules(self): @@ -472,7 +472,7 @@ index 3feaa053..28d68177 100644 ac(o,"") assert r == 0 -@@ -1075,7 +1075,7 @@ Ticket Constraints: +@@ -1087,7 +1087,7 @@ Ticket Constraints: """) assert r == 0 @@ -481,7 +481,7 @@ index 3feaa053..28d68177 100644 ac(o,"") assert r == 0 -@@ -1114,7 +1114,7 @@ Ticket Constraints: +@@ -1126,7 +1126,7 @@ Ticket Constraints: ac(o,"") assert r == 0 @@ -490,7 +490,7 @@ index 3feaa053..28d68177 100644 ac(o, """\ Warning: changing a monitor operation interval from 10 to 11 to make the operation unique """) -@@ -1235,7 +1235,7 @@ Ticket Constraints: +@@ -1247,7 +1247,7 @@ Ticket Constraints: self.assertEqual(0, returnVal) output, returnVal = pcs( @@ -500,7 +500,7 @@ index 3feaa053..28d68177 100644 ac(output, """\ Warning: changing a monitor operation interval from 10 to 11 to make the operation unique diff --git a/pcs/test/test_resource.py b/pcs/test/test_resource.py -index 2a110683..e49b59a1 100644 +index fd1a0731..3a47053f 100644 --- a/pcs/test/test_resource.py +++ b/pcs/test/test_resource.py @@ -2898,7 +2898,7 @@ Ticket Constraints: @@ -530,7 +530,7 @@ index 2a110683..e49b59a1 100644 ) ac(output, "") self.assertEqual(0, returnVal) -@@ -3902,7 +3902,7 @@ Error: Cannot remove more than one resource from cloned group +@@ -3935,7 +3935,7 @@ Error: Cannot remove more than one resource from cloned group # However those test the pcs library. I'm leaving these tests here to # test the cli part for now. self.assert_pcs_success( @@ -539,7 +539,7 @@ index 2a110683..e49b59a1 100644 "Warning: changing a monitor operation interval from 10 to 11 to make the operation unique\n" ) -@@ -5515,7 +5515,7 @@ class CloneMasterUpdate(unittest.TestCase, AssertPcsMixin): +@@ -5548,7 +5548,7 @@ class CloneMasterUpdate(unittest.TestCase, AssertPcsMixin): def test_no_op_allowed_in_master_update(self): self.assert_pcs_success( @@ -549,10 +549,10 @@ index 2a110683..e49b59a1 100644 self.assert_pcs_success( "resource show dummy-master", diff --git a/pcs/utils.py b/pcs/utils.py -index 66f7ebf1..73721a2a 100644 +index e56a1e8b..793f0b5e 100644 --- a/pcs/utils.py +++ b/pcs/utils.py -@@ -2981,6 +2981,13 @@ def get_modifiers(): +@@ -2982,6 +2982,13 @@ def get_modifiers(): "wait": pcs_options.get("--wait", False), "watchdog": pcs_options.get("--watchdog", []), "no_watchdog_validation": "--no-watchdog-validation" in pcs_options, @@ -567,5 +567,5 @@ index 66f7ebf1..73721a2a 100644 def exit_on_cmdline_input_errror(error, main_name, usage_name): -- -2.20.1 +2.21.0 diff --git a/SOURCES/bz1459503-01-OSP-workarounds-not-compatible-wi.patch b/SOURCES/bz1459503-01-OSP-workarounds-not-compatible-wi.patch index adefcb0..f91efcf 100644 --- a/SOURCES/bz1459503-01-OSP-workarounds-not-compatible-wi.patch +++ b/SOURCES/bz1459503-01-OSP-workarounds-not-compatible-wi.patch @@ -1,7 +1,7 @@ -From 0ae2d03488d5959e22e0cf6fd8dd1c10789dc819 Mon Sep 17 00:00:00 2001 +From 8d93e513db9a075a0f1d6f09416701c6068333b5 Mon Sep 17 00:00:00 2001 From: Ivan Devat Date: Wed, 7 Jun 2017 14:36:05 +0200 -Subject: [PATCH 4/6] squash bz1459503 OSP workarounds not compatible wi +Subject: [PATCH 3/6] squash bz1459503 OSP workarounds not compatible wi reuse existing pcmk authkey during setup @@ -14,7 +14,7 @@ show only warn if `resource create` creates remote 4 files changed, 31 insertions(+), 22 deletions(-) diff --git a/pcs/cluster.py b/pcs/cluster.py -index 7727cbef..0bacae16 100644 +index 561a3d20..a32fdc7e 100644 --- a/pcs/cluster.py +++ b/pcs/cluster.py @@ -525,13 +525,21 @@ def cluster_setup(argv): @@ -41,7 +41,7 @@ index 7727cbef..0bacae16 100644 if modifiers["encryption"] == "1": file_definitions.update( diff --git a/pcs/lib/commands/resource.py b/pcs/lib/commands/resource.py -index 1ad03a48..ad7a1a20 100644 +index fff5f40b..749c72a0 100644 --- a/pcs/lib/commands/resource.py +++ b/pcs/lib/commands/resource.py @@ -79,7 +79,8 @@ def _validate_remote_connection( @@ -98,10 +98,10 @@ index 57d95350..3cd49e74 100644 def test_warn_when_on_pacemaker_remote_guest_attempt(self): diff --git a/pcs/test/test_resource.py b/pcs/test/test_resource.py -index e49b59a1..fed8465e 100644 +index 3a47053f..3c09aee5 100644 --- a/pcs/test/test_resource.py +++ b/pcs/test/test_resource.py -@@ -5872,10 +5872,10 @@ class ResourceUpdateRemoteAndGuestChecks(unittest.TestCase, AssertPcsMixin): +@@ -5905,10 +5905,10 @@ class ResourceUpdateRemoteAndGuestChecks(unittest.TestCase, AssertPcsMixin): self.assert_pcs_success( "resource create R ocf:heartbeat:Dummy", ) @@ -115,7 +115,7 @@ index e49b59a1..fed8465e 100644 ) def test_update_warn_on_pacemaker_guest_attempt(self): self.assert_pcs_success( -@@ -5894,10 +5894,10 @@ class ResourceUpdateRemoteAndGuestChecks(unittest.TestCase, AssertPcsMixin): +@@ -5927,10 +5927,10 @@ class ResourceUpdateRemoteAndGuestChecks(unittest.TestCase, AssertPcsMixin): "Warning: this command is not sufficient for creating a guest node," " use 'pcs cluster node add-guest'\n" ) @@ -129,7 +129,7 @@ index e49b59a1..fed8465e 100644 ) def test_update_warn_on_pacemaker_guest_attempt_remove(self): -@@ -5918,10 +5918,10 @@ class ResourceUpdateRemoteAndGuestChecks(unittest.TestCase, AssertPcsMixin): +@@ -5951,10 +5951,10 @@ class ResourceUpdateRemoteAndGuestChecks(unittest.TestCase, AssertPcsMixin): self.assert_pcs_success( "resource create R ocf:heartbeat:Dummy", ) @@ -143,7 +143,7 @@ index e49b59a1..fed8465e 100644 ) def test_meta_warn_on_pacemaker_guest_attempt(self): -@@ -5942,10 +5942,10 @@ class ResourceUpdateRemoteAndGuestChecks(unittest.TestCase, AssertPcsMixin): +@@ -5975,10 +5975,10 @@ class ResourceUpdateRemoteAndGuestChecks(unittest.TestCase, AssertPcsMixin): "Warning: this command is not sufficient for creating a guest node," " use 'pcs cluster node add-guest'\n" ) @@ -158,5 +158,5 @@ index e49b59a1..fed8465e 100644 def test_meta_warn_on_pacemaker_guest_attempt_remove(self): -- -2.20.1 +2.21.0 diff --git a/SOURCES/bz1500012-01-provide-a-hint-about-the-set-opti.patch b/SOURCES/bz1500012-01-provide-a-hint-about-the-set-opti.patch deleted file mode 100644 index 5087fa5..0000000 --- a/SOURCES/bz1500012-01-provide-a-hint-about-the-set-opti.patch +++ /dev/null @@ -1,141 +0,0 @@ -From c85f907253f6379ad726a7f54635ccbe0b64af4c Mon Sep 17 00:00:00 2001 -From: Ivan Devat -Date: Mon, 4 Nov 2019 14:52:03 +0100 -Subject: [PATCH 1/4] squash bz1500012 provide a hint about the set opti - -d2754533 doc: fix `pcs constraint colocation add` command -abb8f0ea don't allow empty value for constraint options ---- - pcs/cli/common/parse_args.py | 9 +++++++-- - pcs/constraint.py | 7 +++++-- - pcs/pcs.8 | 4 ++-- - pcs/test/test_constraints.py | 9 +++++++++ - pcs/usage.py | 6 +++--- - 5 files changed, 26 insertions(+), 9 deletions(-) - -diff --git a/pcs/cli/common/parse_args.py b/pcs/cli/common/parse_args.py -index 6a3210ac..23d8799d 100644 ---- a/pcs/cli/common/parse_args.py -+++ b/pcs/cli/common/parse_args.py -@@ -46,7 +46,7 @@ def split_list(arg_list, separator): - bounds = zip([0]+[i+1 for i in separator_indexes], separator_indexes+[None]) - return [arg_list[i:j] for i, j in bounds] - --def split_option(arg): -+def split_option(arg, allow_empty_value=True): - """ - Get (key, value) from a key=value commandline argument. - -@@ -54,12 +54,17 @@ def split_option(arg): - CmdLineInputError if the argument cannot be splitted. - - string arg -- commandline argument -+ allow_empty_value -- if True, empty value is allowed. Otherwise, -+ CmdLineInputError exception is raised - """ - if "=" not in arg: - raise CmdLineInputError("missing value of '{0}' option".format(arg)) - if arg.startswith("="): - raise CmdLineInputError("missing key in '{0}' option".format(arg)) -- return arg.split("=", 1) -+ key, value = arg.split("=", 1) -+ if not (value or allow_empty_value): -+ raise CmdLineInputError("value of '{0}' option is empty".format(key)) -+ return key, value - - def prepare_options(cmdline_args): - """return dictionary of options from commandline key=value args""" -diff --git a/pcs/constraint.py b/pcs/constraint.py -index c9f6fd5f..ca12aaa7 100644 ---- a/pcs/constraint.py -+++ b/pcs/constraint.py -@@ -208,7 +208,10 @@ def colocation_add(argv): - return SCORE_INFINITY, [] - score = SCORE_INFINITY if "=" in argv[0] else argv.pop(0) - # create a list of 2-tuples (name, value) -- arg_array = [parse_args.split_option(arg) for arg in argv] -+ arg_array = [ -+ parse_args.split_option(arg, allow_empty_value=False) -+ for arg in argv -+ ] - return score, arg_array - - def _validate_and_prepare_role(role): -@@ -444,7 +447,7 @@ def _order_add(resource1, resource2, options_list): - elif arg == "nonsymmetrical": - sym = "false" - else: -- name, value = parse_args.split_option(arg) -+ name, value = parse_args.split_option(arg, allow_empty_value=False) - if name == "id": - id_valid, id_error = utils.validate_xml_id( - value, "constraint id" -diff --git a/pcs/pcs.8 b/pcs/pcs.8 -index 5670f458..f08b5e46 100644 ---- a/pcs/pcs.8 -+++ b/pcs/pcs.8 -@@ -562,8 +562,8 @@ Remove resource from any ordering constraint - colocation [show] [\fB\-\-full\fR] - List all current colocation constraints (if \fB\-\-full\fR is specified show the internal constraint id's as well). - .TP --colocation add [master|slave] with [master|slave] [score] [options] [id=constraint\-id] --Request to run on the same node where pacemaker has determined should run. Positive values of score mean the resources should be run on the same node, negative values mean the resources should not be run on the same node. Specifying 'INFINITY' (or '\-INFINITY') for the score forces to run (or not run) with (score defaults to "INFINITY"). A role can be master or slave (if no role is specified, it defaults to 'started'). -+colocation add [] with [] [score] [options] [id=constraint\-id] -+Request to run on the same node where pacemaker has determined should run. Positive values of score mean the resources should be run on the same node, negative values mean the resources should not be run on the same node. Specifying 'INFINITY' (or '\-INFINITY') for the score forces to run (or not run) with (score defaults to "INFINITY"). A role can be: 'Master', 'Slave', 'Started', 'Stopped' (if no role is specified, it defaults to 'Started'). - .TP - colocation set [resourceN]... [options] [set ... [options]] [setoptions [constraint_options]] - Create a colocation constraint with a resource set. Available options are sequential=true/false and role=Stopped/Started/Master/Slave. Available constraint_options are id and either of: score, score\-attribute, score\-attribute\-mangle. -diff --git a/pcs/test/test_constraints.py b/pcs/test/test_constraints.py -index 36bcb652..3feaa053 100644 ---- a/pcs/test/test_constraints.py -+++ b/pcs/test/test_constraints.py -@@ -262,6 +262,11 @@ Ticket Constraints: - ac(o,"Location Constraints:\nOrdering Constraints:\n stop D1 then stop D2 (kind:Mandatory) (id:order-D1-D2-mandatory)\n start D1 then start D2 (kind:Mandatory) (id:order-D1-D2-mandatory-1)\nColocation Constraints:\nTicket Constraints:\n") - assert r == 0 - -+ def test_order_options_empty_value(self): -+ o, r = pcs("constraint order D1 then D2 option1=") -+ self.assertIn("value of 'option1' option is empty", o) -+ self.assertEqual(r, 1) -+ - def test_order_too_many_resources(self): - msg = ( - "Error: Multiple 'then's cannot be specified.\n" -@@ -525,6 +530,10 @@ Ticket Constraints: - self.assertIn(msg, o) - self.assertEqual(r, 1) - -+ def test_colocation_options_empty_value(self): -+ o, r = pcs("constraint colocation add D1 with D2 option1=") -+ self.assertIn("value of 'option1' option is empty", o) -+ self.assertEqual(r, 1) - - def testColocationSets(self): - # see also BundleColocation -diff --git a/pcs/usage.py b/pcs/usage.py -index 18baaf0e..37c92d26 100644 ---- a/pcs/usage.py -+++ b/pcs/usage.py -@@ -1200,7 +1200,7 @@ Commands: - List all current colocation constraints (if --full is specified show - the internal constraint id's as well). - -- colocation add [master|slave] with [master|slave] -+ colocation add [] with [] - [score] [options] [id=constraint-id] - Request to run on the same node where pacemaker has - determined should run. Positive values of score -@@ -1208,8 +1208,8 @@ Commands: - mean the resources should not be run on the same node. Specifying - 'INFINITY' (or '-INFINITY') for the score forces to - run (or not run) with (score defaults to "INFINITY"). -- A role can be master or slave (if no role is specified, it defaults to -- 'started'). -+ A role can be: 'Master', 'Slave', 'Started', 'Stopped' (if no role is -+ specified, it defaults to 'Started'). - - colocation set [resourceN]... [options] - [set ... [options]] --- -2.21.0 - diff --git a/SOURCES/bz1671174-01-specify-full-path-when-running-external-tools.patch b/SOURCES/bz1671174-01-specify-full-path-when-running-external-tools.patch deleted file mode 100644 index d83e867..0000000 --- a/SOURCES/bz1671174-01-specify-full-path-when-running-external-tools.patch +++ /dev/null @@ -1,39 +0,0 @@ -From e25955d8e2de7378585b4b348f9c10049a878c5e Mon Sep 17 00:00:00 2001 -From: Ivan Devat -Date: Mon, 4 Nov 2019 15:30:42 +0100 -Subject: [PATCH 4/4] specify full path when running external tools - ---- - pcsd/bootstrap.rb | 2 +- - pcsd/pcs.rb | 2 +- - 2 files changed, 2 insertions(+), 2 deletions(-) - -diff --git a/pcsd/bootstrap.rb b/pcsd/bootstrap.rb -index c932a0cc..e79b0d92 100644 ---- a/pcsd/bootstrap.rb -+++ b/pcsd/bootstrap.rb -@@ -63,7 +63,7 @@ end - COROSYNC_QUORUMTOOL = COROSYNC_BINARIES + "corosync-quorumtool" - - if not defined? $cur_node_name -- $cur_node_name = `hostname`.chomp -+ $cur_node_name = `/bin/hostname`.chomp - end - - def configure_logger(log_device) -diff --git a/pcsd/pcs.rb b/pcsd/pcs.rb -index ad153f62..12eb3eb1 100644 ---- a/pcsd/pcs.rb -+++ b/pcsd/pcs.rb -@@ -1849,7 +1849,7 @@ def cluster_status_from_nodes(auth_user, cluster_nodes, cluster_name) - end - - def get_node_uptime() -- uptime = `cat /proc/uptime`.chomp.split(' ')[0].split('.')[0].to_i -+ uptime = `/bin/cat /proc/uptime`.chomp.split(' ')[0].split('.')[0].to_i - mm, ss = uptime.divmod(60) - hh, mm = mm.divmod(60) - dd, hh = hh.divmod(24) --- -2.21.0 - diff --git a/SOURCES/bz1760434-01-do-not-generate-custom-DH-key-unless-requested.patch b/SOURCES/bz1760434-01-do-not-generate-custom-DH-key-unless-requested.patch deleted file mode 100644 index 6105bcf..0000000 --- a/SOURCES/bz1760434-01-do-not-generate-custom-DH-key-unless-requested.patch +++ /dev/null @@ -1,50 +0,0 @@ -From 3254abb3f30a051761eac1c03a236015fcd44cf9 Mon Sep 17 00:00:00 2001 -From: Tomas Jelinek -Date: Thu, 17 Oct 2019 11:52:30 +0200 -Subject: [PATCH 1/5] do not generate custom DH key unless requested - ---- - pcsd/ssl.rb | 17 ++++++++++------- - 1 file changed, 10 insertions(+), 7 deletions(-) - -diff --git a/pcsd/ssl.rb b/pcsd/ssl.rb -index c71aad08..de356e46 100644 ---- a/pcsd/ssl.rb -+++ b/pcsd/ssl.rb -@@ -153,14 +153,15 @@ else - end - end - --dh_key_bits_default = 1024 --dh_key_bits = dh_key_bits_default -+dh_key_bits = 0 - if ENV['PCSD_SSL_DH_KEX_BITS'] -- dh_key_bits = Integer(ENV['PCSD_SSL_DH_KEX_BITS']) rescue dh_key_bits_default -+ dh_key_bits = Integer(ENV['PCSD_SSL_DH_KEX_BITS']) rescue 0 -+end -+if dh_key_bits > 0 -+ $logger.info "Generating #{dh_key_bits}bits long DH key..." -+ dh_key = OpenSSL::PKey::DH.generate(dh_key_bits) -+ $logger.info "DH key created" - end --$logger.info "Generating #{dh_key_bits}bits long DH key..." --dh_key = OpenSSL::PKey::DH.generate(dh_key_bits) --$logger.info "DH key created" - - default_bind = true - # see https://github.com/ClusterLabs/pcs/issues/51 -@@ -185,8 +186,10 @@ webrick_options = { - :SSLPrivateKey => OpenSSL::PKey::RSA.new(key), - :SSLCertName => [[ "CN", server_name ]], - :SSLOptions => get_ssl_options(), -- :SSLTmpDhCallback => lambda {|ctx, is_export, keylen| dh_key}, - } -+if dh_key_bits > 0 -+ webrick_options[:SSLTmpDhCallback] = lambda {|ctx, is_export, keylen| dh_key } -+end - - server = ::Rack::Handler::WEBrick - trap(:INT) do --- -2.21.0 - diff --git a/SOURCES/bz1765606-01-Hiding-Server-Name-HTTP-header-fr.patch b/SOURCES/bz1765606-01-Hiding-Server-Name-HTTP-header-fr.patch deleted file mode 100644 index b7158fa..0000000 --- a/SOURCES/bz1765606-01-Hiding-Server-Name-HTTP-header-fr.patch +++ /dev/null @@ -1,40 +0,0 @@ -From 9b982f8586d3b5c13139e5aca2291dd4c6d7e628 Mon Sep 17 00:00:00 2001 -From: Ivan Devat -Date: Mon, 4 Nov 2019 15:21:35 +0100 -Subject: [PATCH 2/4] squash bz1765606 Hiding Server Name HTTP header fr - -327dba90 do not send Server HTTP header -985067ab do not send Server HTTP header ---- - pcsd/pcsd.rb | 13 +++++++++++++ - 1 file changed, 13 insertions(+) - -diff --git a/pcsd/pcsd.rb b/pcsd/pcsd.rb -index 3d167b8b..b4bf0494 100644 ---- a/pcsd/pcsd.rb -+++ b/pcsd/pcsd.rb -@@ -38,8 +38,21 @@ class HstsMiddleware - end - end - -+class RemoveServerHeaderMiddleware -+ def initialize(app) -+ @app = app -+ end -+ -+ def call(env) -+ status, headers, body = @app.call(env) -+ headers['Server'] = '' -+ [status, headers, body] -+ end -+end -+ - use Rack::CommonLogger - use HstsMiddleware -+use RemoveServerHeaderMiddleware - - set :app_file, __FILE__ - --- -2.21.0 - diff --git a/SOURCES/bz1770973-01-The-cluster-should-not-be-allowed-t.patch b/SOURCES/bz1770973-01-The-cluster-should-not-be-allowed-t.patch deleted file mode 100644 index 3adcb27..0000000 --- a/SOURCES/bz1770973-01-The-cluster-should-not-be-allowed-t.patch +++ /dev/null @@ -1,2616 +0,0 @@ -From c19066d0dade1ae544e8f8d80513310d47e72ebe Mon Sep 17 00:00:00 2001 -From: Tomas Jelinek -Date: Tue, 19 Nov 2019 13:47:02 +0100 -Subject: [PATCH 2/6] squash bz1770973 The cluster should not be allowed to - disable a resource if dependent resources are still online - -add --simulate and --safe to resource disable - -Also add command 'resource safe-disable'. This is an alias for 'resource -disable --safe'. - -fix safe-disabling clones, groups, bundles - -fix simulate_cib_error report - -Putting only one CIB in the report is not enough info. Both original and -changed CIB as well as crm_simulate output would be needed. All that -info can be seen in debug messages. So there is no need to put it in the -report. ---- - pcs/cli/common/console_report.py | 15 + - pcs/cli/common/lib_wrapper.py | 2 + - pcs/cli/common/parse_args.py | 1 + - pcs/cli/common/test/test_console_report.py | 31 + - pcs/common/report_codes.py | 2 + - pcs/lib/cib/resource/common.py | 16 + - pcs/lib/cib/test/test_resource_common.py | 60 +- - pcs/lib/commands/resource.py | 101 +++ - .../resource/test_resource_enable_disable.py | 819 +++++++++++++++++- - pcs/lib/pacemaker/live.py | 66 +- - pcs/lib/pacemaker/simulate.py | 86 ++ - pcs/lib/pacemaker/test/test_live.py | 191 +++- - pcs/lib/pacemaker/test/test_simulate.py | 380 ++++++++ - pcs/lib/reports.py | 35 + - pcs/lib/tools.py | 5 +- - pcs/pcs.8 | 23 +- - pcs/resource.py | 52 +- - pcs/test/test_resource.py | 195 +++++ - .../tools/command_env/config_runner_pcmk.py | 55 +- - pcs/test/tools/command_env/mock_runner.py | 17 + - pcs/usage.py | 36 +- - pcs/utils.py | 3 + - pcsd/capabilities.xml | 14 + - 23 files changed, 2185 insertions(+), 20 deletions(-) - create mode 100644 pcs/lib/pacemaker/simulate.py - create mode 100644 pcs/lib/pacemaker/test/test_simulate.py - -diff --git a/pcs/cli/common/console_report.py b/pcs/cli/common/console_report.py -index 1824bd9a..b5885b6b 100644 ---- a/pcs/cli/common/console_report.py -+++ b/pcs/cli/common/console_report.py -@@ -815,6 +815,14 @@ CODE_TO_MESSAGE_BUILDER_MAP = { - .format(**info) - , - -+ codes.CIB_SIMULATE_ERROR: lambda info: -+ "Unable to simulate changes in CIB{_reason}" -+ .format( -+ _reason=format_optional(info["reason"], ": {0}"), -+ **info -+ ) -+ , -+ - codes.CIB_PUSH_FORCED_FULL_DUE_TO_CRM_FEATURE_SET: lambda info: - ( - "Replacing the whole CIB instead of applying a diff, a race " -@@ -1516,4 +1524,11 @@ CODE_TO_MESSAGE_BUILDER_MAP = { - codes.FENCE_HISTORY_NOT_SUPPORTED: - "Fence history is not supported, please upgrade pacemaker" - , -+ -+ codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES: lambda info: -+ ( -+ "Disabling specified resources would have an effect on other " -+ "resources\n\n{crm_simulate_plaintext_output}" -+ ).format(**info) -+ , - } -diff --git a/pcs/cli/common/lib_wrapper.py b/pcs/cli/common/lib_wrapper.py -index a6cc0ca8..dffbace2 100644 ---- a/pcs/cli/common/lib_wrapper.py -+++ b/pcs/cli/common/lib_wrapper.py -@@ -344,6 +344,8 @@ def load_module(env, middleware_factory, name): - "create_in_group": resource.create_in_group, - "create_into_bundle": resource.create_into_bundle, - "disable": resource.disable, -+ "disable_safe": resource.disable_safe, -+ "disable_simulate": resource.disable_simulate, - "enable": resource.enable, - "get_failcounts": resource.get_failcounts, - "manage": resource.manage, -diff --git a/pcs/cli/common/parse_args.py b/pcs/cli/common/parse_args.py -index 23d8799d..1f3e7b33 100644 ---- a/pcs/cli/common/parse_args.py -+++ b/pcs/cli/common/parse_args.py -@@ -18,6 +18,7 @@ PCS_LONG_OPTIONS = [ - "force", "skip-offline", "autocorrect", "interactive", "autodelete", - "all", "full", "groups", "local", "wait", "config", "async", - "start", "enable", "disabled", "off", "request-timeout=", -+ "safe", "no-strict", "simulate", - "pacemaker", "corosync", - "no-default-ops", "defaults", "nodesc", - "clone", "master", "name=", "group=", "node=", -diff --git a/pcs/cli/common/test/test_console_report.py b/pcs/cli/common/test/test_console_report.py -index 95849615..59192dc2 100644 ---- a/pcs/cli/common/test/test_console_report.py -+++ b/pcs/cli/common/test/test_console_report.py -@@ -1934,6 +1934,21 @@ class CibDiffError(NameBuildTest): - ) - - -+class CibSimulateError(NameBuildTest): -+ code = codes.CIB_SIMULATE_ERROR -+ def test_success(self): -+ self.assert_message_from_report( -+ "Unable to simulate changes in CIB: error message", -+ reports.cib_simulate_error("error message") -+ ) -+ -+ def test_empty_reason(self): -+ self.assert_message_from_report( -+ "Unable to simulate changes in CIB", -+ reports.cib_simulate_error("") -+ ) -+ -+ - class TmpFileWrite(NameBuildTest): - code = codes.TMP_FILE_WRITE - def test_success(self): -@@ -2401,3 +2416,19 @@ class FenceHistoryNotSupported(NameBuildTest): - "Fence history is not supported, please upgrade pacemaker", - reports.fence_history_not_supported() - ) -+ -+ -+class ResourceDisableAffectsOtherResources(NameBuildTest): -+ code = codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES -+ def test_success(self): -+ self.assert_message_from_report( -+ ( -+ "Disabling specified resources would have an effect on other " -+ "resources\n\ncrm_simulate output" -+ ), -+ reports.resource_disable_affects_other_resources( -+ ["D2", "D1"], -+ ["O2", "O1"], -+ "crm_simulate output", -+ ) -+ ) -diff --git a/pcs/common/report_codes.py b/pcs/common/report_codes.py -index 68b48215..8340ab62 100644 ---- a/pcs/common/report_codes.py -+++ b/pcs/common/report_codes.py -@@ -68,6 +68,7 @@ CIB_LOAD_ERROR_SCOPE_MISSING = "CIB_LOAD_ERROR_SCOPE_MISSING" - CIB_PUSH_FORCED_FULL_DUE_TO_CRM_FEATURE_SET = "CIB_PUSH_FORCED_FULL_DUE_TO_CRM_FEATURE_SET" - CIB_PUSH_ERROR = "CIB_PUSH_ERROR" - CIB_SAVE_TMP_ERROR = "CIB_SAVE_TMP_ERROR" -+CIB_SIMULATE_ERROR = "CIB_SIMULATE_ERROR" - CIB_UPGRADE_FAILED = "CIB_UPGRADE_FAILED" - CIB_UPGRADE_FAILED_TO_MINIMAL_REQUIRED_VERSION = "CIB_UPGRADE_FAILED_TO_MINIMAL_REQUIRED_VERSION" - CIB_UPGRADE_SUCCESSFUL = "CIB_UPGRADE_SUCCESSFUL" -@@ -185,6 +186,7 @@ RESOURCE_BUNDLE_ALREADY_CONTAINS_A_RESOURCE = "RESOURCE_BUNDLE_ALREADY_CONTAINS_ - RESOURCE_BUNDLE_UNSUPPORTED_CONTAINER_TYPE = "RESOURCE_BUNDLE_UNSUPPORTED_CONTAINER_TYPE" - RESOURCE_IN_BUNDLE_NOT_ACCESSIBLE = "RESOURCE_IN_BUNDLE_NOT_ACCESSIBLE" - RESOURCE_CLEANUP_ERROR = "RESOURCE_CLEANUP_ERROR" -+RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES = "RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES" - RESOURCE_DOES_NOT_RUN = "RESOURCE_DOES_NOT_RUN" - RESOURCE_FOR_CONSTRAINT_IS_MULTIINSTANCE = 'RESOURCE_FOR_CONSTRAINT_IS_MULTIINSTANCE' - RESOURCE_IS_GUEST_NODE_ALREADY = "RESOURCE_IS_GUEST_NODE_ALREADY" -diff --git a/pcs/lib/cib/resource/common.py b/pcs/lib/cib/resource/common.py -index 87b656b7..f408c2c4 100644 ---- a/pcs/lib/cib/resource/common.py -+++ b/pcs/lib/cib/resource/common.py -@@ -51,6 +51,22 @@ def find_primitives(resource_el): - return [resource_el] - return [] - -+def get_all_inner_resources(resource_el): -+ """ -+ Return all inner resources (both direct and indirect) of a resource -+ Example: for a clone containing a group, this function will return both -+ the group and the resources inside the group -+ -+ resource_el -- resource element to get its inner resources -+ """ -+ all_inner = set() -+ to_process = set([resource_el]) -+ while to_process: -+ new_inner = get_inner_resources(to_process.pop()) -+ to_process.update(set(new_inner) - all_inner) -+ all_inner.update(new_inner) -+ return all_inner -+ - def get_inner_resources(resource_el): - """ - Return list of inner resources (direct descendants) of a resource -diff --git a/pcs/lib/cib/test/test_resource_common.py b/pcs/lib/cib/test/test_resource_common.py -index 596ee57f..06684f95 100644 ---- a/pcs/lib/cib/test/test_resource_common.py -+++ b/pcs/lib/cib/test/test_resource_common.py -@@ -84,10 +84,12 @@ class IsCloneDeactivatedByMeta(TestCase): - - - class FindResourcesMixin(object): -+ _iterable_type = list -+ - def assert_find_resources(self, input_resource_id, output_resource_ids): - self.assertEqual( -- output_resource_ids, -- [ -+ self._iterable_type(output_resource_ids), -+ self._iterable_type([ - element.get("id", "") - for element in - self._tested_fn( -@@ -95,7 +97,7 @@ class FindResourcesMixin(object): - './/*[@id="{0}"]'.format(input_resource_id) - ) - ) -- ] -+ ]) - ) - - def test_group(self): -@@ -119,6 +121,27 @@ class FindResourcesMixin(object): - def test_bundle_with_primitive(self): - self.assert_find_resources("H-bundle", ["H"]) - -+ def test_primitive(self): -+ raise NotImplementedError() -+ -+ def test_primitive_in_clone(self): -+ raise NotImplementedError() -+ -+ def test_primitive_in_master(self): -+ raise NotImplementedError() -+ -+ def test_primitive_in_group(self): -+ raise NotImplementedError() -+ -+ def test_primitive_in_bundle(self): -+ raise NotImplementedError() -+ -+ def test_cloned_group(self): -+ raise NotImplementedError() -+ -+ def test_mastered_group(self): -+ raise NotImplementedError() -+ - - class FindPrimitives(TestCase, FindResourcesMixin): - _tested_fn = staticmethod(common.find_primitives) -@@ -150,6 +173,37 @@ class FindPrimitives(TestCase, FindResourcesMixin): - self.assert_find_resources("F-master", ["F1", "F2"]) - - -+class GetAllInnerResources(TestCase, FindResourcesMixin): -+ _iterable_type = set -+ _tested_fn = staticmethod(common.get_all_inner_resources) -+ -+ def test_primitive(self): -+ self.assert_find_resources("A", set()) -+ -+ def test_primitive_in_clone(self): -+ self.assert_find_resources("B", set()) -+ -+ def test_primitive_in_master(self): -+ self.assert_find_resources("C", set()) -+ -+ def test_primitive_in_group(self): -+ self.assert_find_resources("D1", set()) -+ self.assert_find_resources("D2", set()) -+ self.assert_find_resources("E1", set()) -+ self.assert_find_resources("E2", set()) -+ self.assert_find_resources("F1", set()) -+ self.assert_find_resources("F2", set()) -+ -+ def test_primitive_in_bundle(self): -+ self.assert_find_resources("H", set()) -+ -+ def test_cloned_group(self): -+ self.assert_find_resources("E-clone", {"E", "E1", "E2"}) -+ -+ def test_mastered_group(self): -+ self.assert_find_resources("F-master", {"F", "F1", "F2"}) -+ -+ - class GetInnerResources(TestCase, FindResourcesMixin): - _tested_fn = staticmethod(common.get_inner_resources) - -diff --git a/pcs/lib/commands/resource.py b/pcs/lib/commands/resource.py -index 09a68a68..1ad03a48 100644 ---- a/pcs/lib/commands/resource.py -+++ b/pcs/lib/commands/resource.py -@@ -23,6 +23,8 @@ from pcs.lib.cib.tools import ( - ) - from pcs.lib.env_tools import get_nodes - from pcs.lib.errors import LibraryError -+from pcs.lib.pacemaker import simulate as simulate_tools -+from pcs.lib.pacemaker.live import simulate_cib - from pcs.lib.pacemaker.values import ( - timeout_to_seconds, - validate_id, -@@ -37,6 +39,7 @@ from pcs.lib.resource_agent import( - find_valid_resource_agent_by_name as get_agent - ) - from pcs.lib.validate import value_time_interval -+from pcs.lib.xml_tools import get_root - - @contextmanager - def resource_environment( -@@ -717,13 +720,45 @@ def bundle_update( - meta_attributes - ) - -+def _disable_validate_and_edit_cib(env, resources_section, resource_ids): -+ resource_el_list = _find_resources_or_raise( -+ resources_section, -+ resource_ids -+ ) -+ env.report_processor.process_list( -+ _resource_list_enable_disable( -+ resource_el_list, -+ resource.common.disable, -+ env.get_cluster_state() -+ ) -+ ) -+ - def disable(env, resource_ids, wait): - """ - Disallow specified resource to be started by the cluster -+ - LibraryEnvironment env -- - strings resource_ids -- ids of the resources to be disabled - mixed wait -- False: no wait, None: wait default timeout, int: wait timeout - """ -+ with resource_environment( -+ env, wait, resource_ids, _ensure_disabled_after_wait(True) -+ ) as resources_section: -+ _disable_validate_and_edit_cib(env, resources_section, resource_ids) -+ -+def disable_safe(env, resource_ids, strict, wait): -+ """ -+ Disallow specified resource to be started by the cluster only if there is -+ no effect on other resources -+ -+ LibraryEnvironment env -- -+ strings resource_ids -- ids of the resources to be disabled -+ bool strict -- if False, allow resources to be migrated -+ mixed wait -- False: no wait, None: wait default timeout, int: wait timeout -+ """ -+ if not env.is_cib_live: -+ raise LibraryError(reports.live_environment_required(["CIB"])) -+ - with resource_environment( - env, wait, resource_ids, _ensure_disabled_after_wait(True) - ) as resources_section: -@@ -739,6 +774,72 @@ def disable(env, resource_ids, wait): - ) - ) - -+ inner_resources_names_set = set() -+ for resource_el in resource_el_list: -+ inner_resources_names_set.update({ -+ inner_resource_el.get("id") -+ for inner_resource_el -+ in resource.common.get_all_inner_resources(resource_el) -+ }) -+ -+ plaintext_status, transitions, dummy_cib = simulate_cib( -+ env.cmd_runner(), -+ get_root(resources_section) -+ ) -+ simulated_operations = ( -+ simulate_tools.get_operations_from_transitions(transitions) -+ ) -+ other_affected = set() -+ if strict: -+ other_affected = set( -+ simulate_tools.get_resources_from_operations( -+ simulated_operations, -+ exclude=resource_ids -+ ) -+ ) -+ else: -+ other_affected = set( -+ simulate_tools.get_resources_left_stopped( -+ simulated_operations, -+ exclude=resource_ids -+ ) -+ + -+ simulate_tools.get_resources_left_demoted( -+ simulated_operations, -+ exclude=resource_ids -+ ) -+ ) -+ -+ # Stopping a clone stops all its inner resources. That should not block -+ # stopping the clone. -+ other_affected = other_affected - inner_resources_names_set -+ if other_affected: -+ raise LibraryError( -+ reports.resource_disable_affects_other_resources( -+ resource_ids, -+ other_affected, -+ plaintext_status, -+ ) -+ ) -+ -+def disable_simulate(env, resource_ids): -+ """ -+ Simulate disallowing specified resource to be started by the cluster -+ -+ LibraryEnvironment env -- -+ strings resource_ids -- ids of the resources to be disabled -+ """ -+ if not env.is_cib_live: -+ raise LibraryError(reports.live_environment_required(["CIB"])) -+ -+ resources_section = get_resources(env.get_cib()) -+ _disable_validate_and_edit_cib(env, resources_section, resource_ids) -+ plaintext_status, dummy_transitions, dummy_cib = simulate_cib( -+ env.cmd_runner(), -+ get_root(resources_section) -+ ) -+ return plaintext_status -+ - def enable(env, resource_ids, wait): - """ - Allow specified resource to be started by the cluster -diff --git a/pcs/lib/commands/test/resource/test_resource_enable_disable.py b/pcs/lib/commands/test/resource/test_resource_enable_disable.py -index 194eb5fd..6514d9fc 100644 ---- a/pcs/lib/commands/test/resource/test_resource_enable_disable.py -+++ b/pcs/lib/commands/test/resource/test_resource_enable_disable.py -@@ -14,10 +14,11 @@ from pcs.lib.errors import ( - 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, - outdent, - skip_unless_pacemaker_supports_bundle, - ) --from pcs.test.tools.pcs_unittest import TestCase -+from pcs.test.tools.pcs_unittest import TestCase, mock - - - TIMEOUT=10 -@@ -1646,3 +1647,819 @@ class EnableBundle(TestCase): - fixture_report_unmanaged("A-bundle"), - fixture_report_unmanaged("A"), - ]) -+ -+ -+@mock.patch("pcs.lib.pacemaker.live.write_tmpfile") -+class DisableSimulate(TestCase): -+ def setUp(self): -+ self.env_assist, self.config = get_env_tools(test_case=self) -+ self.tmpfile_new_cib = mock.MagicMock() -+ self.tmpfile_new_cib.name = rc("new_cib.tmp") -+ self.tmpfile_new_cib.read.return_value = "" -+ self.tmpfile_transitions = mock.MagicMock() -+ self.tmpfile_transitions.name = rc("transitions.tmp") -+ self.tmpfile_transitions.read.return_value = "" -+ -+ def test_not_live(self, mock_write_tmpfile): -+ mock_write_tmpfile.side_effect = [ -+ AssertionError("No other write_tmpfile call expected") -+ ] -+ self.config.env.set_cib_data("") -+ self.env_assist.assert_raise_library_error( -+ lambda: resource.disable_simulate(self.env_assist.get_env(), ["A"]), -+ [ -+ fixture.error( -+ report_codes.LIVE_ENVIRONMENT_REQUIRED, -+ forbidden_options=["CIB"] -+ ), -+ ], -+ expected_in_processor=False -+ ) -+ -+ def test_nonexistent_resource(self, mock_write_tmpfile): -+ mock_write_tmpfile.side_effect = [ -+ AssertionError("No other write_tmpfile call expected") -+ ] -+ self.config.runner.cib.load() -+ self.env_assist.assert_raise_library_error( -+ lambda: resource.disable_simulate(self.env_assist.get_env(), ["A"]), -+ [ -+ fixture.report_not_found("A", "resources"), -+ ], -+ expected_in_processor=False -+ ) -+ -+ def test_success(self, mock_write_tmpfile): -+ mock_write_tmpfile.side_effect = [ -+ self.tmpfile_new_cib, self.tmpfile_transitions, -+ AssertionError("No other write_tmpfile call expected") -+ ] -+ (self.config -+ .runner.cib.load(resources=fixture_primitive_cib_enabled) -+ .runner.pcmk.load_state(resources=fixture_primitive_status_managed) -+ .runner.pcmk.simulate_cib( -+ self.tmpfile_new_cib.name, -+ self.tmpfile_transitions.name, -+ stdout="simulate output", -+ resources=fixture_primitive_cib_disabled, -+ ) -+ ) -+ -+ result = resource.disable_simulate(self.env_assist.get_env(), ["A"]) -+ self.assertEqual("simulate output", result) -+ -+ def test_simulate_error(self, mock_write_tmpfile): -+ mock_write_tmpfile.side_effect = [ -+ self.tmpfile_new_cib, self.tmpfile_transitions, -+ AssertionError("No other write_tmpfile call expected") -+ ] -+ (self.config -+ .runner.cib.load(resources=fixture_primitive_cib_enabled) -+ .runner.pcmk.load_state(resources=fixture_primitive_status_managed) -+ .runner.pcmk.simulate_cib( -+ self.tmpfile_new_cib.name, -+ self.tmpfile_transitions.name, -+ stdout="some stdout", -+ stderr="some stderr", -+ returncode=1, -+ resources=fixture_primitive_cib_disabled, -+ ) -+ ) -+ -+ self.env_assist.assert_raise_library_error( -+ lambda: resource.disable_simulate(self.env_assist.get_env(), ["A"]), -+ [ -+ fixture.error( -+ report_codes.CIB_SIMULATE_ERROR, -+ reason="some stderr", -+ ), -+ ], -+ expected_in_processor=False -+ ) -+ -+ -+class DisableSafeMixin(object): -+ fixture_transitions_both_stopped = """ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ """ -+ fixture_transitions_one_migrated = """ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ """ -+ fixture_transitions_master_demoted = """ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ """ -+ fixture_transitions_master_migrated = """ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ """ -+ fixture_cib_with_master = """ -+ -+ -+ -+ -+ -+ -+ -+ -+ """ -+ fixture_cib_with_master_primitive_disabled = """ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ """ -+ fixture_status_with_master_managed = """ -+ -+ -+ -+ -+ -+ -+ -+ """ -+ -+ def setUp(self): -+ # pylint does not know this will be mixed into TestCase classes -+ # pylint: disable=invalid-name -+ self.env_assist, self.config = get_env_tools(test_case=self) -+ self.tmpfile_new_cib = mock.MagicMock() -+ self.tmpfile_new_cib.name = rc("new_cib.tmp") -+ self.tmpfile_new_cib.read.return_value = "" -+ self.tmpfile_transitions = mock.MagicMock() -+ self.tmpfile_transitions.name = rc("transitions.tmp") -+ self.tmpfile_transitions.read.return_value = "" -+ -+ def fixture_disable_both_resources(self, mock_write_tmpfile): -+ self.tmpfile_transitions.read.return_value = ( -+ self.fixture_transitions_both_stopped -+ ) -+ mock_write_tmpfile.side_effect = [ -+ self.tmpfile_new_cib, self.tmpfile_transitions, -+ AssertionError("No other write_tmpfile call expected") -+ ] -+ (self.config -+ .runner.cib.load(resources=fixture_two_primitives_cib_enabled) -+ .runner.pcmk.load_state( -+ resources=fixture_two_primitives_status_managed -+ ) -+ ) -+ -+ def fixture_migrate_one_resource(self, mock_write_tmpfile): -+ self.tmpfile_transitions.read.return_value = ( -+ self.fixture_transitions_one_migrated -+ ) -+ mock_write_tmpfile.side_effect = [ -+ self.tmpfile_new_cib, self.tmpfile_transitions, -+ AssertionError("No other write_tmpfile call expected") -+ ] -+ (self.config -+ .runner.cib.load(resources=fixture_two_primitives_cib_enabled) -+ .runner.pcmk.load_state( -+ resources=fixture_two_primitives_status_managed -+ ) -+ ) -+ -+ def test_not_live(self, mock_write_tmpfile): -+ mock_write_tmpfile.side_effect = [ -+ AssertionError("No other write_tmpfile call expected") -+ ] -+ self.config.env.set_cib_data("") -+ self.env_assist.assert_raise_library_error( -+ lambda: resource.disable_safe( -+ self.env_assist.get_env(), -+ ["A"], -+ self.strict, -+ False -+ ), -+ [ -+ fixture.error( -+ report_codes.LIVE_ENVIRONMENT_REQUIRED, -+ forbidden_options=["CIB"] -+ ), -+ ], -+ expected_in_processor=False -+ ) -+ -+ def test_nonexistent_resource(self, mock_write_tmpfile): -+ mock_write_tmpfile.side_effect = [ -+ AssertionError("No other write_tmpfile call expected") -+ ] -+ self.config.runner.cib.load() -+ self.env_assist.assert_raise_library_error( -+ lambda: resource.disable_safe( -+ self.env_assist.get_env(), -+ ["A"], -+ self.strict, -+ False -+ ), -+ [ -+ fixture.report_not_found("A", "resources"), -+ ], -+ expected_in_processor=False -+ ) -+ -+ def test_simulate_error(self, mock_write_tmpfile): -+ self.fixture_disable_both_resources(mock_write_tmpfile) -+ self.config.runner.pcmk.simulate_cib( -+ self.tmpfile_new_cib.name, -+ self.tmpfile_transitions.name, -+ stdout="some stdout", -+ stderr="some stderr", -+ returncode=1, -+ resources=fixture_two_primitives_cib_disabled_both, -+ ) -+ self.env_assist.assert_raise_library_error( -+ lambda: resource.disable_safe( -+ self.env_assist.get_env(), -+ ["A", "B"], -+ self.strict, -+ False, -+ ), -+ [ -+ fixture.error( -+ report_codes.CIB_SIMULATE_ERROR, -+ reason="some stderr", -+ ), -+ ], -+ expected_in_processor=False -+ ) -+ -+ def test_only_specified_resources_stopped(self, mock_write_tmpfile): -+ self.fixture_disable_both_resources(mock_write_tmpfile) -+ self.config.runner.pcmk.simulate_cib( -+ self.tmpfile_new_cib.name, -+ self.tmpfile_transitions.name, -+ stdout="simulate output", -+ resources=fixture_two_primitives_cib_disabled_both, -+ ) -+ self.config.env.push_cib( -+ resources=fixture_two_primitives_cib_disabled_both -+ ) -+ resource.disable_safe( -+ self.env_assist.get_env(), -+ ["A", "B"], -+ self.strict, -+ False, -+ ) -+ -+ def test_other_resources_stopped(self, mock_write_tmpfile): -+ self.fixture_disable_both_resources(mock_write_tmpfile) -+ self.config.runner.pcmk.simulate_cib( -+ self.tmpfile_new_cib.name, -+ self.tmpfile_transitions.name, -+ stdout="simulate output", -+ resources=fixture_two_primitives_cib_disabled, -+ ) -+ self.env_assist.assert_raise_library_error( -+ lambda: resource.disable_safe( -+ self.env_assist.get_env(), -+ ["A"], -+ self.strict, -+ False, -+ ), -+ [ -+ fixture.error( -+ report_codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES, -+ disabled_resource_list=["A"], -+ affected_resource_list=["B"], -+ crm_simulate_plaintext_output="simulate output", -+ ), -+ ], -+ expected_in_processor=False -+ ) -+ -+ def test_master_demoted(self, mock_write_tmpfile): -+ self.tmpfile_transitions.read.return_value = ( -+ self.fixture_transitions_master_demoted -+ ) -+ mock_write_tmpfile.side_effect = [ -+ self.tmpfile_new_cib, self.tmpfile_transitions, -+ AssertionError("No other write_tmpfile call expected") -+ ] -+ (self.config -+ .runner.cib.load(resources=self.fixture_cib_with_master) -+ .runner.pcmk.load_state( -+ resources=self.fixture_status_with_master_managed -+ ) -+ .runner.pcmk.simulate_cib( -+ self.tmpfile_new_cib.name, -+ self.tmpfile_transitions.name, -+ stdout="simulate output", -+ resources=self.fixture_cib_with_master_primitive_disabled, -+ ) -+ ) -+ self.env_assist.assert_raise_library_error( -+ lambda: resource.disable_safe( -+ self.env_assist.get_env(), -+ ["A"], -+ self.strict, -+ False, -+ ), -+ [ -+ fixture.error( -+ report_codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES, -+ disabled_resource_list=["A"], -+ affected_resource_list=["B"], -+ crm_simulate_plaintext_output="simulate output", -+ ), -+ ], -+ expected_in_processor=False -+ ) -+ -+ def test_wait_success(self, mock_write_tmpfile): -+ self.config.runner.pcmk.can_wait() -+ self.fixture_disable_both_resources(mock_write_tmpfile) -+ (self.config -+ .runner.pcmk.simulate_cib( -+ self.tmpfile_new_cib.name, -+ self.tmpfile_transitions.name, -+ stdout="simulate output", -+ resources=fixture_two_primitives_cib_disabled_both, -+ ) -+ .env.push_cib( -+ resources=fixture_two_primitives_cib_disabled_both, -+ wait=TIMEOUT -+ ) -+ .runner.pcmk.load_state( -+ name="runner.pcmk.load_state_2", -+ resources=""" -+ -+ -+ -+ -+ -+ -+ """ -+ ) -+ ) -+ resource.disable_safe( -+ self.env_assist.get_env(), -+ ["A", "B"], -+ self.strict, -+ TIMEOUT -+ ) -+ self.env_assist.assert_reports([ -+ fixture.report_resource_not_running("A"), -+ fixture.report_resource_not_running("B"), -+ ]) -+ -+ def test_inner_resources(self, mock_write_tmpfile): -+ cib_xml = """ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ """ -+ status_xml = """ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ """ -+ synapses = [] -+ index = 0 -+ for res_name, is_clone in [ -+ ("A", False), -+ ("B", True), -+ ("C", True), -+ ("D1", False), -+ ("D2", False), -+ ("E1", True), -+ ("E2", True), -+ ("F1", True), -+ ("F2", True), -+ ("H", False), -+ ]: -+ if is_clone: -+ synapses.append(""" -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ """.format( -+ index=index, -+ index_1=index + 1, -+ res_name=res_name, -+ )) -+ index += 2 -+ else: -+ synapses.append(""" -+ -+ -+ -+ -+ -+ -+ -+ """.format( -+ index=index, -+ res_name=res_name, -+ )) -+ index += 1 -+ transitions_xml = ( -+ "" + "\n".join(synapses) + "" -+ ) -+ -+ self.tmpfile_transitions.read.return_value = transitions_xml -+ mock_write_tmpfile.side_effect = [ -+ self.tmpfile_new_cib, self.tmpfile_transitions, -+ AssertionError("No other write_tmpfile call expected") -+ ] -+ (self.config -+ .runner.cib.load(resources=cib_xml) -+ .runner.pcmk.load_state(resources=status_xml) -+ ) -+ self.config.runner.pcmk.simulate_cib( -+ self.tmpfile_new_cib.name, -+ self.tmpfile_transitions.name, -+ stdout="simulate output", -+ resources=""" -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ """ -+ ) -+ self.env_assist.assert_raise_library_error( -+ lambda: resource.disable_safe( -+ self.env_assist.get_env(), -+ ["B-clone", "C-master", "D", "E-clone", "F-master", "H-bundle"], -+ self.strict, -+ False, -+ ), -+ [ -+ fixture.error( -+ report_codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES, -+ disabled_resource_list=[ -+ "B-clone", "C-master", "D", "E-clone", "F-master", -+ "H-bundle" -+ ], -+ affected_resource_list=["A"], -+ crm_simulate_plaintext_output="simulate output", -+ ), -+ ], -+ expected_in_processor=False -+ ) -+ -+@mock.patch("pcs.lib.pacemaker.live.write_tmpfile") -+class DisableSafe(DisableSafeMixin, TestCase): -+ strict = False -+ -+ def test_resources_migrated(self, mock_write_tmpfile): -+ self.fixture_migrate_one_resource(mock_write_tmpfile) -+ self.config.runner.pcmk.simulate_cib( -+ self.tmpfile_new_cib.name, -+ self.tmpfile_transitions.name, -+ stdout="simulate output", -+ resources=fixture_two_primitives_cib_disabled, -+ ) -+ self.config.env.push_cib(resources=fixture_two_primitives_cib_disabled) -+ resource.disable_safe( -+ self.env_assist.get_env(), -+ ["A"], -+ self.strict, -+ False, -+ ) -+ -+ def test_master_migrated(self, mock_write_tmpfile): -+ self.tmpfile_transitions.read.return_value = ( -+ self.fixture_transitions_master_migrated -+ ) -+ mock_write_tmpfile.side_effect = [ -+ self.tmpfile_new_cib, self.tmpfile_transitions, -+ AssertionError("No other write_tmpfile call expected") -+ ] -+ (self.config -+ .runner.cib.load(resources=self.fixture_cib_with_master) -+ .runner.pcmk.load_state( -+ resources=self.fixture_status_with_master_managed -+ ) -+ .runner.pcmk.simulate_cib( -+ self.tmpfile_new_cib.name, -+ self.tmpfile_transitions.name, -+ stdout="simulate output", -+ resources=self.fixture_cib_with_master_primitive_disabled, -+ ) -+ .env.push_cib( -+ resources=self.fixture_cib_with_master_primitive_disabled -+ ) -+ ) -+ resource.disable_safe( -+ self.env_assist.get_env(), -+ ["A"], -+ self.strict, -+ False, -+ ) -+ -+@mock.patch("pcs.lib.pacemaker.live.write_tmpfile") -+class DisableSafeStrict(DisableSafeMixin, TestCase): -+ strict = True -+ -+ def test_resources_migrated(self, mock_write_tmpfile): -+ self.fixture_migrate_one_resource(mock_write_tmpfile) -+ self.config.runner.pcmk.simulate_cib( -+ self.tmpfile_new_cib.name, -+ self.tmpfile_transitions.name, -+ stdout="simulate output", -+ resources=fixture_two_primitives_cib_disabled, -+ ) -+ self.env_assist.assert_raise_library_error( -+ lambda: resource.disable_safe( -+ self.env_assist.get_env(), -+ ["A"], -+ self.strict, -+ False, -+ ), -+ [ -+ fixture.error( -+ report_codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES, -+ disabled_resource_list=["A"], -+ affected_resource_list=["B"], -+ crm_simulate_plaintext_output="simulate output", -+ ), -+ ], -+ expected_in_processor=False -+ ) -+ -+ def test_master_migrated(self, mock_write_tmpfile): -+ self.tmpfile_transitions.read.return_value = ( -+ self.fixture_transitions_master_migrated -+ ) -+ mock_write_tmpfile.side_effect = [ -+ self.tmpfile_new_cib, self.tmpfile_transitions, -+ AssertionError("No other write_tmpfile call expected") -+ ] -+ (self.config -+ .runner.cib.load(resources=self.fixture_cib_with_master) -+ .runner.pcmk.load_state( -+ resources=self.fixture_status_with_master_managed -+ ) -+ .runner.pcmk.simulate_cib( -+ self.tmpfile_new_cib.name, -+ self.tmpfile_transitions.name, -+ stdout="simulate output", -+ resources=self.fixture_cib_with_master_primitive_disabled, -+ ) -+ ) -+ self.env_assist.assert_raise_library_error( -+ lambda: resource.disable_safe( -+ self.env_assist.get_env(), -+ ["A"], -+ self.strict, -+ False, -+ ), -+ [ -+ fixture.error( -+ report_codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES, -+ disabled_resource_list=["A"], -+ affected_resource_list=["B"], -+ crm_simulate_plaintext_output="simulate output", -+ ), -+ ], -+ expected_in_processor=False -+ ) -diff --git a/pcs/lib/pacemaker/live.py b/pcs/lib/pacemaker/live.py -index 992cc639..bc444229 100644 ---- a/pcs/lib/pacemaker/live.py -+++ b/pcs/lib/pacemaker/live.py -@@ -9,8 +9,9 @@ import os.path - - from pcs import settings - from pcs.common.tools import ( -+ format_environment_error, - join_multilines, -- xml_fromstring -+ xml_fromstring, - ) - from pcs.lib import reports - from pcs.lib.cib.tools import get_pacemaker_version_by_which_cib_was_validated -@@ -205,6 +206,69 @@ def _upgrade_cib(runner): - reports.cib_upgrade_failed(join_multilines([stderr, stdout])) - ) - -+def simulate_cib_xml(runner, cib_xml): -+ """ -+ Run crm_simulate to get effects the cib would have on the live cluster -+ -+ CommandRunner runner -- runner -+ string cib_xml -- CIB XML to simulate -+ """ -+ try: -+ new_cib_file = write_tmpfile(None) -+ transitions_file = write_tmpfile(None) -+ except EnvironmentError as e: -+ raise LibraryError( -+ reports.cib_simulate_error(format_environment_error(e)) -+ ) -+ -+ cmd = [ -+ __exec("crm_simulate"), -+ "--simulate", -+ "--save-output", new_cib_file.name, -+ "--save-graph", transitions_file.name, -+ "--xml-pipe", -+ ] -+ stdout, stderr, retval = runner.run(cmd, stdin_string=cib_xml) -+ if retval != 0: -+ raise LibraryError( -+ reports.cib_simulate_error(stderr.strip()) -+ ) -+ -+ try: -+ new_cib_file.seek(0) -+ transitions_file.seek(0) -+ new_cib_xml = new_cib_file.read() -+ transitions_xml = transitions_file.read() -+ new_cib_file.close() -+ transitions_file.close() -+ return stdout, transitions_xml, new_cib_xml -+ except EnvironmentError as e: -+ raise LibraryError( -+ reports.cib_simulate_error(format_environment_error(e)) -+ ) -+ -+def simulate_cib(runner, cib): -+ """ -+ Run crm_simulate to get effects the cib would have on the live cluster -+ -+ CommandRunner runner -- runner -+ etree cib -- cib tree to simulate -+ """ -+ cib_xml = etree_to_str(cib) -+ try: -+ plaintext_result, transitions_xml, new_cib_xml = simulate_cib_xml( -+ runner, cib_xml -+ ) -+ return ( -+ plaintext_result.strip(), -+ xml_fromstring(transitions_xml), -+ xml_fromstring(new_cib_xml) -+ ) -+ except (etree.XMLSyntaxError, etree.DocumentInvalid) as e: -+ raise LibraryError( -+ reports.cib_simulate_error(str(e)) -+ ) -+ - ### wait for idle - - def has_wait_for_idle_support(runner): -diff --git a/pcs/lib/pacemaker/simulate.py b/pcs/lib/pacemaker/simulate.py -new file mode 100644 -index 00000000..aff38058 ---- /dev/null -+++ b/pcs/lib/pacemaker/simulate.py -@@ -0,0 +1,86 @@ -+from collections import defaultdict -+ -+def get_operations_from_transitions(transitions): -+ """ -+ Extract resource operations from simulated transitions -+ -+ etree transitions -- simulated transitions from crm_simulate -+ """ -+ operation_list = [] -+ watched_operations = { -+ "start", "stop", "promote", "demote", "migrate_from", "migrate_to" -+ } -+ for rsc_op in transitions.iterfind("synapse/action_set/rsc_op"): -+ operation = rsc_op.get("operation", "").lower() -+ if operation not in watched_operations: -+ continue -+ for primitive in rsc_op.iterfind("primitive"): -+ primitive_id = primitive.get("id") -+ operation_list.append(( -+ int(rsc_op.get("id")), -+ { -+ "primitive_id": primitive_id, -+ "primitive_long_id": ( -+ primitive.get("long-id") or primitive_id -+ ), -+ "operation": operation, -+ "on_node": rsc_op.get("on_node"), -+ } -+ )) -+ operation_list.sort(key=lambda x: x[0]) -+ op_list = [op[1] for op in operation_list] -+ return op_list -+ -+def get_resources_from_operations(operation_list, exclude=None): -+ """ -+ Get names of all resources from the provided operation list -+ -+ list operation_list -- result of get_operations_from_transitions -+ iterable exclude -- resources to exclude from the result -+ """ -+ exclude = exclude or set() -+ return sorted({ -+ op["primitive_id"] -+ for op in operation_list -+ if op["primitive_id"] not in exclude -+ }) -+ -+def get_resources_left_stopped(operation_list, exclude=None): -+ """ -+ Get names of resources which are left stopped by the provided operation list -+ -+ list operation_list -- result of get_operations_from_transitions -+ iterable exclude -- resources to exclude from the result -+ """ -+ return _resources_with_imbalanced_operations( -+ operation_list, "stop", "start", exclude -+ ) -+ -+def get_resources_left_demoted(operation_list, exclude=None): -+ """ -+ Get names of resources which are left demoted by the provided operation list -+ -+ list operation_list -- result of get_operations_from_transitions -+ iterable exclude -- resources to exclude from the result -+ """ -+ return _resources_with_imbalanced_operations( -+ operation_list, "demote", "promote", exclude -+ ) -+ -+def _resources_with_imbalanced_operations( -+ operation_list, increment_op, decrement_op, exclude -+): -+ exclude = exclude or set() -+ counter = defaultdict(int) -+ for res_op in operation_list: -+ resource = res_op["primitive_id"] -+ operation = res_op["operation"] -+ if operation == increment_op: -+ counter[resource] += 1 -+ elif operation == decrement_op: -+ counter[resource] -= 1 -+ return sorted([ -+ resource -+ for resource, count in counter.items() -+ if count > 0 and resource not in exclude -+ ]) -diff --git a/pcs/lib/pacemaker/test/test_live.py b/pcs/lib/pacemaker/test/test_live.py -index 904e3498..fad95037 100644 ---- a/pcs/lib/pacemaker/test/test_live.py -+++ b/pcs/lib/pacemaker/test/test_live.py -@@ -16,7 +16,7 @@ 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 - from pcs.test.tools.pcs_unittest import TestCase, mock --from pcs.test.tools.xml import XmlManipulation -+from pcs.test.tools.xml import etree_to_str, XmlManipulation - - from pcs import settings - from pcs.common import report_codes -@@ -435,6 +435,195 @@ class EnsureCibVersionTest(TestCase): - mock_upgrade.assert_called_once_with(self.mock_runner) - mock_get_cib.assert_called_once_with(self.mock_runner) - -+ -+@mock.patch("pcs.lib.pacemaker.live.write_tmpfile") -+class SimulateCibXml(LibraryPacemakerTest): -+ def test_success(self, mock_write_tmpfile): -+ tmpfile_new_cib = mock.MagicMock() -+ tmpfile_new_cib.name = rc("new_cib.tmp") -+ tmpfile_new_cib.read.return_value = "new cib data" -+ tmpfile_transitions = mock.MagicMock() -+ tmpfile_transitions.name = rc("transitions.tmp") -+ tmpfile_transitions.read.return_value = "transitions data" -+ mock_write_tmpfile.side_effect = [tmpfile_new_cib, tmpfile_transitions] -+ -+ expected_stdout = "simulate output" -+ expected_stderr = "" -+ expected_retval = 0 -+ mock_runner = get_runner( -+ expected_stdout, -+ expected_stderr, -+ expected_retval -+ ) -+ -+ result = lib.simulate_cib_xml(mock_runner, "") -+ self.assertEqual(result[0], expected_stdout) -+ self.assertEqual(result[1], "transitions data") -+ self.assertEqual(result[2], "new cib data") -+ -+ mock_runner.run.assert_called_once_with( -+ [ -+ self.path("crm_simulate"), -+ "--simulate", -+ "--save-output", tmpfile_new_cib.name, -+ "--save-graph", tmpfile_transitions.name, -+ "--xml-pipe", -+ ], -+ stdin_string="" -+ ) -+ -+ def test_error_creating_cib(self, mock_write_tmpfile): -+ mock_write_tmpfile.side_effect = OSError(1, "some error") -+ mock_runner = get_runner() -+ assert_raise_library_error( -+ lambda: lib.simulate_cib_xml(mock_runner, ""), -+ fixture.error( -+ report_codes.CIB_SIMULATE_ERROR, -+ reason="some error", -+ ), -+ ) -+ mock_runner.run.assert_not_called() -+ -+ def test_error_creating_transitions(self, mock_write_tmpfile): -+ tmpfile_new_cib = mock.MagicMock() -+ mock_write_tmpfile.side_effect = [ -+ tmpfile_new_cib, -+ OSError(1, "some error") -+ ] -+ mock_runner = get_runner() -+ assert_raise_library_error( -+ lambda: lib.simulate_cib_xml(mock_runner, ""), -+ fixture.error( -+ report_codes.CIB_SIMULATE_ERROR, -+ reason="some error", -+ ), -+ ) -+ mock_runner.run.assert_not_called() -+ -+ def test_error_running_simulate(self, mock_write_tmpfile): -+ tmpfile_new_cib = mock.MagicMock() -+ tmpfile_new_cib.name = rc("new_cib.tmp") -+ tmpfile_transitions = mock.MagicMock() -+ tmpfile_transitions.name = rc("transitions.tmp") -+ mock_write_tmpfile.side_effect = [tmpfile_new_cib, tmpfile_transitions] -+ -+ expected_stdout = "some stdout" -+ expected_stderr = "some error" -+ expected_retval = 1 -+ mock_runner = get_runner( -+ expected_stdout, -+ expected_stderr, -+ expected_retval -+ ) -+ -+ assert_raise_library_error( -+ lambda: lib.simulate_cib_xml(mock_runner, ""), -+ fixture.error( -+ report_codes.CIB_SIMULATE_ERROR, -+ reason="some error", -+ ), -+ ) -+ -+ def test_error_reading_cib(self, mock_write_tmpfile): -+ tmpfile_new_cib = mock.MagicMock() -+ tmpfile_new_cib.name = rc("new_cib.tmp") -+ tmpfile_new_cib.read.side_effect = OSError(1, "some error") -+ tmpfile_transitions = mock.MagicMock() -+ tmpfile_transitions.name = rc("transitions.tmp") -+ mock_write_tmpfile.side_effect = [tmpfile_new_cib, tmpfile_transitions] -+ -+ expected_stdout = "simulate output" -+ expected_stderr = "" -+ expected_retval = 0 -+ mock_runner = get_runner( -+ expected_stdout, -+ expected_stderr, -+ expected_retval -+ ) -+ -+ assert_raise_library_error( -+ lambda: lib.simulate_cib_xml(mock_runner, ""), -+ fixture.error( -+ report_codes.CIB_SIMULATE_ERROR, -+ reason="some error", -+ ), -+ ) -+ -+ def test_error_reading_transitions(self, mock_write_tmpfile): -+ tmpfile_new_cib = mock.MagicMock() -+ tmpfile_new_cib.name = rc("new_cib.tmp") -+ tmpfile_new_cib.read.return_value = "new cib data" -+ tmpfile_transitions = mock.MagicMock() -+ tmpfile_transitions.name = rc("transitions.tmp") -+ tmpfile_transitions.read.side_effect = OSError(1, "some error") -+ mock_write_tmpfile.side_effect = [tmpfile_new_cib, tmpfile_transitions] -+ -+ expected_stdout = "simulate output" -+ expected_stderr = "" -+ expected_retval = 0 -+ mock_runner = get_runner( -+ expected_stdout, -+ expected_stderr, -+ expected_retval -+ ) -+ -+ assert_raise_library_error( -+ lambda: lib.simulate_cib_xml(mock_runner, ""), -+ fixture.error( -+ report_codes.CIB_SIMULATE_ERROR, -+ reason="some error", -+ ), -+ ) -+ -+ -+@mock.patch("pcs.lib.pacemaker.live.simulate_cib_xml") -+class SimulateCib(TestCase): -+ def setUp(self): -+ self.runner = "mock runner" -+ self.cib_xml = "" -+ self.cib = etree.fromstring(self.cib_xml) -+ self.simulate_output = " some output " -+ self.transitions = "" -+ self.new_cib = "" -+ -+ def test_success(self, mock_simulate): -+ mock_simulate.return_value = ( -+ self.simulate_output, self.transitions, self.new_cib -+ ) -+ result = lib.simulate_cib(self.runner, self.cib) -+ self.assertEqual(result[0], "some output") -+ self.assertEqual(etree_to_str(result[1]), self.transitions) -+ self.assertEqual(etree_to_str(result[2]), self.new_cib) -+ mock_simulate.assert_called_once_with(self.runner, self.cib_xml) -+ -+ def test_invalid_cib(self, mock_simulate): -+ mock_simulate.return_value = ( -+ self.simulate_output, "bad transitions", self.new_cib -+ ) -+ assert_raise_library_error( -+ lambda: lib.simulate_cib(self.runner, self.cib), -+ fixture.error( -+ report_codes.CIB_SIMULATE_ERROR, -+ reason=( -+ "Start tag expected, '<' not found, line 1, column 1" -+ ), -+ ), -+ ) -+ -+ def test_invalid_transitions(self, mock_simulate): -+ mock_simulate.return_value = ( -+ self.simulate_output, self.transitions, "bad new cib" -+ ) -+ assert_raise_library_error( -+ lambda: lib.simulate_cib(self.runner, self.cib), -+ fixture.error( -+ report_codes.CIB_SIMULATE_ERROR, -+ reason=( -+ "Start tag expected, '<' not found, line 1, column 1" -+ ), -+ ), -+ ) -+ - class GetLocalNodeStatusTest(LibraryPacemakerNodeStatusTest): - def test_offline(self): - expected_stdout = "some info" -diff --git a/pcs/lib/pacemaker/test/test_simulate.py b/pcs/lib/pacemaker/test/test_simulate.py -new file mode 100644 -index 00000000..2fe0789a ---- /dev/null -+++ b/pcs/lib/pacemaker/test/test_simulate.py -@@ -0,0 +1,380 @@ -+from __future__ import ( -+ absolute_import, -+ division, -+ print_function, -+) -+ -+from lxml import etree -+ -+from pcs.lib.pacemaker import simulate -+ -+from pcs.test.tools.pcs_unittest import TestCase -+from pcs.test.tools.misc import get_test_resource as rc -+ -+ -+class GetOperationsFromTransitions(TestCase): -+ def test_transitions1(self): -+ transitions = etree.parse(rc("transitions01.xml")) -+ self.assertEqual( -+ [ -+ { -+ "primitive_id": "dummy", -+ "primitive_long_id": "dummy", -+ "operation": "stop", -+ "on_node": "rh7-3", -+ }, -+ { -+ "primitive_id": "dummy", -+ "primitive_long_id": "dummy", -+ "operation": "start", -+ "on_node": "rh7-2", -+ }, -+ { -+ "primitive_id": "d0", -+ "primitive_long_id": "d0:1", -+ "operation": "stop", -+ "on_node": "rh7-1", -+ }, -+ { -+ "primitive_id": "d0", -+ "primitive_long_id": "d0:1", -+ "operation": "start", -+ "on_node": "rh7-2", -+ }, -+ { -+ "primitive_id": "state", -+ "primitive_long_id": "state:0", -+ "operation": "stop", -+ "on_node": "rh7-3", -+ }, -+ { -+ "primitive_id": "state", -+ "primitive_long_id": "state:0", -+ "operation": "start", -+ "on_node": "rh7-2", -+ }, -+ ], -+ simulate.get_operations_from_transitions(transitions) -+ ) -+ -+ def test_transitions2(self): -+ transitions = etree.parse(rc("transitions02.xml")) -+ self.assertEqual( -+ [ -+ { -+ "primitive_id": "RemoteNode", -+ "primitive_long_id": "RemoteNode", -+ "operation": "stop", -+ "on_node": "virt-143", -+ }, -+ { -+ "primitive_id": "RemoteNode", -+ "primitive_long_id": "RemoteNode", -+ "operation": "migrate_to", -+ "on_node": "virt-143", -+ }, -+ { -+ "primitive_id": "RemoteNode", -+ "primitive_long_id": "RemoteNode", -+ "operation": "migrate_from", -+ "on_node": "virt-142", -+ }, -+ { -+ "primitive_id": "dummy8", -+ "primitive_long_id": "dummy8", -+ "operation": "stop", -+ "on_node": "virt-143", -+ }, -+ { -+ "primitive_id": "dummy8", -+ "primitive_long_id": "dummy8", -+ "operation": "start", -+ "on_node": "virt-142", -+ } -+ ], -+ simulate.get_operations_from_transitions(transitions) -+ ) -+ -+class GetResourcesFromOperations(TestCase): -+ operations = [ -+ { -+ "primitive_id": "dummy2", -+ "primitive_long_id": "dummy2:1", -+ "operation": "stop", -+ "on_node": "node1", -+ }, -+ { -+ "primitive_id": "dummy1", -+ "primitive_long_id": "dummy1", -+ "operation": "stop", -+ "on_node": "node3", -+ }, -+ { -+ "primitive_id": "dummy1", -+ "primitive_long_id": "dummy1", -+ "operation": "start", -+ "on_node": "node2", -+ }, -+ ] -+ def test_no_operations(self): -+ self.assertEqual( -+ [], -+ simulate.get_resources_from_operations( -+ [] -+ ) -+ ) -+ -+ def test_no_operations_exclude(self): -+ self.assertEqual( -+ [], -+ simulate.get_resources_from_operations( -+ [], exclude={"dummy1"} -+ ) -+ ) -+ -+ def test_some_operations(self): -+ self.assertEqual( -+ ["dummy1", "dummy2"], -+ simulate.get_resources_from_operations( -+ self.operations -+ ) -+ ) -+ -+ def test_some_operations_exclude(self): -+ self.assertEqual( -+ ["dummy2"], -+ simulate.get_resources_from_operations( -+ self.operations, exclude={"dummy1", "dummy2:1", "dummyX"} -+ ) -+ ) -+ -+class GetResourcesLeftStoppedDemotedMixin(object): -+ def test_no_operations(self): -+ self.assertEqual([], self.call([])) -+ -+ def test_down(self): -+ self.assertEqual( -+ ["dummy"], -+ self.call([ -+ { -+ "primitive_id": "dummy", -+ "primitive_long_id": "dummy", -+ "operation": self.action_down, -+ "on_node": "node3", -+ }, -+ ]) -+ ) -+ -+ def test_up(self): -+ self.assertEqual( -+ [], -+ self.call([ -+ { -+ "primitive_id": "dummy", -+ "primitive_long_id": "dummy", -+ "operation": self.action_up, -+ "on_node": "node3", -+ }, -+ ]) -+ ) -+ -+ def test_down_up(self): -+ self.assertEqual( -+ [], -+ self.call([ -+ { -+ "primitive_id": "dummy", -+ "primitive_long_id": "dummy", -+ "operation": self.action_down, -+ "on_node": "node2", -+ }, -+ { -+ "primitive_id": "dummy", -+ "primitive_long_id": "dummy", -+ "operation": self.action_up, -+ "on_node": "node3", -+ }, -+ ]) -+ ) -+ -+ def test_up_down(self): -+ self.assertEqual( -+ [], -+ self.call([ -+ { -+ "primitive_id": "dummy", -+ "primitive_long_id": "dummy", -+ "operation": self.action_up, -+ "on_node": "node2", -+ }, -+ { -+ "primitive_id": "dummy", -+ "primitive_long_id": "dummy", -+ "operation": self.action_down, -+ "on_node": "node3", -+ }, -+ ]) -+ ) -+ -+ def test_mixed(self): -+ self.assertEqual( -+ ["dummy1", "dummy2"], -+ self.call([ -+ { -+ "primitive_id": "dummy1", -+ "primitive_long_id": "dummy1", -+ "operation": self.action_down, -+ "on_node": "node3", -+ }, -+ { -+ "primitive_id": "dummy2", -+ "primitive_long_id": "dummy2", -+ "operation": self.action_down, -+ "on_node": "node3", -+ }, -+ { -+ "primitive_id": "dummy3", -+ "primitive_long_id": "dummy3", -+ "operation": self.action_down, -+ "on_node": "node3", -+ }, -+ { -+ "primitive_id": "dummy3", -+ "primitive_long_id": "dummy3", -+ "operation": self.action_up, -+ "on_node": "node2", -+ }, -+ ]) -+ ) -+ -+ def test_exclude(self): -+ self.assertEqual( -+ ["dummy2"], -+ self.call( -+ [ -+ { -+ "primitive_id": "dummy1", -+ "primitive_long_id": "dummy1", -+ "operation": self.action_down, -+ "on_node": "node3", -+ }, -+ { -+ "primitive_id": "dummy2", -+ "primitive_long_id": "dummy2", -+ "operation": self.action_down, -+ "on_node": "node3", -+ }, -+ ], -+ exclude={"dummy1", "dummyX"} -+ ) -+ ) -+ -+class GetResourcesLeftStopped(GetResourcesLeftStoppedDemotedMixin, TestCase): -+ action_up = "start" -+ action_down = "stop" -+ call = staticmethod(simulate.get_resources_left_stopped) -+ -+ def test_clone_move(self): -+ self.assertEqual( -+ [], -+ self.call([ -+ { -+ "primitive_id": "dummy1", -+ "primitive_long_id": "dummy1:0", -+ "operation": self.action_down, -+ "on_node": "node3", -+ }, -+ { -+ "primitive_id": "dummy1", -+ "primitive_long_id": "dummy1:1", -+ "operation": self.action_down, -+ "on_node": "node1", -+ }, -+ { -+ "primitive_id": "dummy1", -+ "primitive_long_id": "dummy1:0", -+ "operation": self.action_up, -+ "on_node": "node2", -+ }, -+ { -+ "primitive_id": "dummy1", -+ "primitive_long_id": "dummy1:1", -+ "operation": self.action_up, -+ "on_node": "node4", -+ }, -+ ]) -+ ) -+ -+ def test_clone_stop(self): -+ self.assertEqual( -+ ["dummy1"], -+ self.call([ -+ { -+ "primitive_id": "dummy1", -+ "primitive_long_id": "dummy1:0", -+ "operation": self.action_down, -+ "on_node": "node3", -+ }, -+ { -+ "primitive_id": "dummy1", -+ "primitive_long_id": "dummy1:1", -+ "operation": self.action_down, -+ "on_node": "node1", -+ }, -+ { -+ "primitive_id": "dummy1", -+ "primitive_long_id": "dummy1:1", -+ "operation": self.action_up, -+ "on_node": "node4", -+ }, -+ ]) -+ ) -+ -+class GetResourcesLeftDemoted(GetResourcesLeftStoppedDemotedMixin, TestCase): -+ action_up = "promote" -+ action_down = "demote" -+ call = staticmethod(simulate.get_resources_left_demoted) -+ -+ def test_master_move(self): -+ self.assertEqual( -+ [], -+ self.call([ -+ { -+ "primitive_id": "dummy1", -+ "primitive_long_id": "dummy1:0", -+ "operation": self.action_down, -+ "on_node": "node3", -+ }, -+ { -+ "primitive_id": "dummy1", -+ "primitive_long_id": "dummy1:1", -+ "operation": self.action_up, -+ "on_node": "node4", -+ }, -+ ]) -+ ) -+ -+ def test_master_stop(self): -+ self.assertEqual( -+ ["dummy1"], -+ self.call([ -+ { -+ "primitive_id": "dummy1", -+ "primitive_long_id": "dummy1:0", -+ "operation": self.action_down, -+ "on_node": "node3", -+ }, -+ { -+ "primitive_id": "dummy1", -+ "primitive_long_id": "dummy1:1", -+ "operation": self.action_up, -+ "on_node": "node4", -+ }, -+ { -+ "primitive_id": "dummy1", -+ "primitive_long_id": "dummy1:2", -+ "operation": self.action_down, -+ "on_node": "node1", -+ }, -+ ]) -+ ) -diff --git a/pcs/lib/reports.py b/pcs/lib/reports.py -index 1408f620..3c5380a7 100644 ---- a/pcs/lib/reports.py -+++ b/pcs/lib/reports.py -@@ -1406,6 +1406,19 @@ def cib_diff_error(reason, cib_old, cib_new): - } - ) - -+def cib_simulate_error(reason): -+ """ -+ cannot simulate effects a CIB would have on a live cluster -+ -+ string reason -- error description -+ """ -+ return ReportItem.error( -+ report_codes.CIB_SIMULATE_ERROR, -+ info={ -+ "reason": reason, -+ } -+ ) -+ - def cib_push_forced_full_due_to_crm_feature_set(required_set, current_set): - """ - Pcs uses the "push full CIB" approach so race conditions may occur. -@@ -3021,3 +3034,25 @@ def fence_history_not_supported(): - return ReportItem.error( - report_codes.FENCE_HISTORY_NOT_SUPPORTED - ) -+ -+def resource_disable_affects_other_resources( -+ disabled_resource_list, -+ affected_resource_list, -+ crm_simulate_plaintext_output -+): -+ """ -+ User requested disabling resources without affecting other resources but -+ some resources would be affected -+ -+ iterable disabled_resource_list -- list of resources to disable -+ iterable affected_resource_list -- other affected resources -+ string crm_simulate_plaintext_output -- plaintext output from pacemaker -+ """ -+ return ReportItem.error( -+ report_codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES, -+ info={ -+ "disabled_resource_list": sorted(disabled_resource_list), -+ "affected_resource_list": sorted(affected_resource_list), -+ "crm_simulate_plaintext_output": crm_simulate_plaintext_output, -+ } -+ ) -diff --git a/pcs/lib/tools.py b/pcs/lib/tools.py -index 0d0dc461..55983af9 100644 ---- a/pcs/lib/tools.py -+++ b/pcs/lib/tools.py -@@ -64,6 +64,7 @@ def write_tmpfile(data, binary=False): - """ - mode = "w+b" if binary else "w+" - tmpfile = tempfile.NamedTemporaryFile(mode=mode, suffix=".pcs") -- tmpfile.write(data) -- tmpfile.flush() -+ if data is not None: -+ tmpfile.write(data) -+ tmpfile.flush() - return tmpfile -diff --git a/pcs/pcs.8 b/pcs/pcs.8 -index 0e8b15f7..596ceb7a 100644 ---- a/pcs/pcs.8 -+++ b/pcs/pcs.8 -@@ -90,8 +90,27 @@ Deletes the resource, group, master or clone (and all resources within the group - 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. - .TP --disable ... [\fB\-\-wait\fR[=n]] --Attempt to stop the resources if they are running and forbid the cluster from starting them again. Depending on the rest of the configuration (constraints, options, failures, etc), the resources may remain started. If \fB\-\-wait\fR is specified, pcs will wait up to 'n' seconds for the resources to stop and then return 0 if the resources are stopped or 1 if the resources have not stopped. If 'n' is not specified it defaults to 60 minutes. -+disable ... [\fB\-\-safe\fR [\fB\-\-no\-strict\fR]] [\fB\-\-simulate\fR] [\fB\-\-wait\fR[=n]] -+Attempt to stop the resources if they are running and forbid the cluster from starting them again. Depending on the rest of the configuration (constraints, options, failures, etc), the resources may remain started. -+.br -+If \fB\-\-safe\fR is specified, no changes to the cluster configuration will be made if other than specified resources would be affected in any way. -+.br -+If \fB\-\-no\-strict\fR is specified, no changes to the cluster configuration will be made if other than specified resources would get stopped or demoted. Moving resources between nodes is allowed. -+.br -+If \fB\-\-simulate\fR is specified, no changes to the cluster configuration will be made and the effect of the changes will be printed instead. -+.br -+If \fB\-\-wait\fR is specified, pcs will wait up to 'n' seconds for the resources to stop and then return 0 if the resources are stopped or 1 if the resources have not stopped. If 'n' is not specified it defaults to 60 minutes. -+.TP -+safe\-disable ... [\fB\-\-no\-strict\fR] [\fB\-\-simulate\fR] [\fB\-\-wait\fR[=n]] [\fB\-\-force\fR] -+Attempt to stop the resources if they are running and forbid the cluster from starting them again. Depending on the rest of the configuration (constraints, options, failures, etc), the resources may remain started. No changes to the cluster configuration will be made if other than specified resources would be affected in any way. -+.br -+If \fB\-\-no\-strict\fR is specified, no changes to the cluster configuration will be made if other than specified resources would get stopped or demoted. Moving resources between nodes is allowed. -+.br -+If \fB\-\-simulate\fR is specified, no changes to the cluster configuration will be made and the effect of the changes will be printed instead. -+.br -+If \fB\-\-wait\fR is specified, pcs will wait up to 'n' seconds for the resources to stop and then return 0 if the resources are stopped or 1 if the resources have not stopped. If 'n' is not specified it defaults to 60 minutes. -+.br -+If \fB\-\-force\fR is specified, checks for safe disable will be skipped. - .TP - restart [node] [\fB\-\-wait\fR=n] - Restart the resource specified. If a node is specified and if the resource is a clone or master/slave it will be restarted only on the node specified. If \fB\-\-wait\fR is specified, then we will wait up to 'n' seconds for the resource to be restarted and return 0 if the restart was successful or 1 if it was not. -diff --git a/pcs/resource.py b/pcs/resource.py -index 27af2405..51233a12 100644 ---- a/pcs/resource.py -+++ b/pcs/resource.py -@@ -137,6 +137,8 @@ def resource_cmd(argv): - resource_enable_cmd(lib, argv_next, modifiers) - elif sub_cmd == "disable": - resource_disable_cmd(lib, argv_next, modifiers) -+ elif sub_cmd == "safe-disable": -+ resource_safe_disable_cmd(lib, argv_next, modifiers) - elif sub_cmd == "restart": - resource_restart(argv_next) - elif sub_cmd == "debug-start": -@@ -2034,11 +2036,53 @@ def resource_show(argv, stonith=False): - utils.err("unable to find resource '"+arg+"'") - resource_found = False - -+ - def resource_disable_cmd(lib, argv, modifiers): -- if len(argv) < 1: -- utils.err("You must specify resource(s) to disable") -- resources = argv -- lib.resource.disable(resources, modifiers["wait"]) -+ """ -+ Options: -+ * -f - CIB file -+ * --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 -+ * --wait -+ """ -+ if not argv: -+ raise CmdLineInputError("You must specify resource(s) to disable") -+ -+ if modifiers["simulate"]: -+ print(lib.resource.disable_simulate(argv)) -+ return -+ if modifiers["safe"] or modifiers["no-strict"]: -+ lib.resource.disable_safe( -+ argv, -+ not modifiers["no-strict"], -+ modifiers["wait"], -+ ) -+ return -+ lib.resource.disable(argv, modifiers["wait"]) -+ -+ -+def resource_safe_disable_cmd(lib, argv, modifiers): -+ """ -+ Options: -+ * --force - skip checks for safe resource disable -+ * --no-strict - allow disable if other resource is affected -+ * --simulate - do not push the CIB, print its effects -+ * --wait -+ """ -+ if modifiers["safe"]: -+ raise CmdLineInputError( -+ "Option '--safe' is not supported in this command" -+ ) -+ if modifiers["force"]: -+ warn( -+ "option '--force' is specified therefore checks for disabling " -+ "resource safely will be skipped" -+ ) -+ elif not modifiers["simulate"]: -+ modifiers["safe"] = True -+ resource_disable_cmd(lib, argv, modifiers) -+ - - def resource_enable_cmd(lib, argv, modifiers): - if len(argv) < 1: -diff --git a/pcs/test/test_resource.py b/pcs/test/test_resource.py -index 20f6af73..2a110683 100644 ---- a/pcs/test/test_resource.py -+++ b/pcs/test/test_resource.py -@@ -32,6 +32,7 @@ from pcs.test.bin_mock import get_mock_settings - - from pcs import utils - from pcs import resource -+from pcs.cli.common.errors import CmdLineInputError - - empty_cib = rc("cib-empty.xml") - temp_cib = rc("temp-cib.xml") -@@ -6450,3 +6451,197 @@ class FailcountShow(TestCase): - ), - full=True - ) -+ -+ -+class ResourceDisable(TestCase): -+ def setUp(self): -+ self.lib = mock.Mock(spec_set=["resource"]) -+ self.resource = mock.Mock( -+ spec_set=["disable", "disable_safe", "disable_simulate"] -+ ) -+ self.lib.resource = self.resource -+ -+ def run_cmd(self, argv, modifiers=None): -+ default_modifiers = { -+ "safe": False, -+ "simulate": False, -+ "no-strict": False, -+ "wait": False, -+ } -+ if modifiers: -+ default_modifiers.update(modifiers) -+ resource.resource_disable_cmd(self.lib, argv, default_modifiers) -+ -+ def test_no_args(self): -+ with self.assertRaises(CmdLineInputError) as cm: -+ self.run_cmd([]) -+ self.assertEqual( -+ cm.exception.message, -+ "You must specify resource(s) to disable" -+ ) -+ self.resource.disable.assert_not_called() -+ self.resource.disable_safe.assert_not_called() -+ self.resource.disable_simulate.assert_not_called() -+ -+ def test_one_resource(self): -+ self.run_cmd(["R1"]) -+ self.resource.disable.assert_called_once_with(["R1"], False) -+ self.resource.disable_safe.assert_not_called() -+ self.resource.disable_simulate.assert_not_called() -+ -+ def test_more_resources(self): -+ self.run_cmd(["R1", "R2"]) -+ self.resource.disable.assert_called_once_with(["R1", "R2"], False) -+ self.resource.disable_safe.assert_not_called() -+ self.resource.disable_simulate.assert_not_called() -+ -+ def test_safe(self): -+ self.run_cmd(["R1", "R2"], dict(safe=True)) -+ self.resource.disable_safe.assert_called_once_with( -+ ["R1", "R2"], True, False -+ ) -+ self.resource.disable.assert_not_called() -+ self.resource.disable_simulate.assert_not_called() -+ -+ def test_safe_wait(self): -+ self.run_cmd(["R1", "R2"], dict(safe=True, wait="10")) -+ self.resource.disable_safe.assert_called_once_with( -+ ["R1", "R2"], True, "10" -+ ) -+ self.resource.disable.assert_not_called() -+ self.resource.disable_simulate.assert_not_called() -+ -+ def test_safe_no_strict(self): -+ self.run_cmd(["R1", "R2"], {"no-strict": True}) -+ self.resource.disable_safe.assert_called_once_with( -+ ["R1", "R2"], False, False -+ ) -+ self.resource.disable.assert_not_called() -+ self.resource.disable_simulate.assert_not_called() -+ -+ def test_safe_no_strict_wait(self): -+ self.run_cmd(["R1", "R2"], {"no-strict": True, "wait": "10"}) -+ self.resource.disable_safe.assert_called_once_with( -+ ["R1", "R2"], False, "10" -+ ) -+ self.resource.disable.assert_not_called() -+ self.resource.disable_simulate.assert_not_called() -+ -+ @mock.patch("pcs.resource.print") -+ def test_simulate(self, mock_print): -+ self.resource.disable_simulate.return_value = "simulate output" -+ self.run_cmd(["R1", "R2"], dict(simulate=True)) -+ self.resource.disable_simulate.assert_called_once_with(["R1", "R2"]) -+ self.resource.disable.assert_not_called() -+ self.resource.disable_safe.assert_not_called() -+ mock_print.assert_called_once_with("simulate output") -+ -+ def test_wait(self): -+ self.run_cmd(["R1", "R2"], dict(wait="10")) -+ self.resource.disable.assert_called_once_with(["R1", "R2"], "10") -+ self.resource.disable_safe.assert_not_called() -+ self.resource.disable_simulate.assert_not_called() -+ -+ -+class ResourceSafeDisable(TestCase): -+ def setUp(self): -+ self.lib = mock.Mock(spec_set=["resource"]) -+ self.resource = mock.Mock( -+ spec_set=["disable", "disable_safe", "disable_simulate"] -+ ) -+ self.lib.resource = self.resource -+ self.force_warning = ( -+ "option '--force' is specified therefore checks for disabling " -+ "resource safely will be skipped" -+ ) -+ -+ def run_cmd(self, argv, modifiers=None): -+ default_modifiers = { -+ "safe": False, -+ "simulate": False, -+ "no-strict": False, -+ "wait": False, -+ "force": False, -+ } -+ if modifiers: -+ default_modifiers.update(modifiers) -+ resource.resource_safe_disable_cmd(self.lib, argv, default_modifiers) -+ -+ def test_no_args(self): -+ with self.assertRaises(CmdLineInputError) as cm: -+ self.run_cmd([]) -+ self.assertEqual( -+ cm.exception.message, -+ "You must specify resource(s) to disable" -+ ) -+ self.resource.disable.assert_not_called() -+ self.resource.disable_safe.assert_not_called() -+ self.resource.disable_simulate.assert_not_called() -+ -+ def test_one_resource(self): -+ self.run_cmd(["R1"]) -+ self.resource.disable_safe.assert_called_once_with(["R1"], True, False) -+ self.resource.disable.assert_not_called() -+ self.resource.disable_simulate.assert_not_called() -+ -+ def test_more_resources(self): -+ self.run_cmd(["R1", "R2"]) -+ self.resource.disable_safe.assert_called_once_with( -+ ["R1", "R2"], True, False -+ ) -+ self.resource.disable.assert_not_called() -+ self.resource.disable_simulate.assert_not_called() -+ -+ def test_wait(self): -+ self.run_cmd(["R1", "R2"], dict(wait="10")) -+ self.resource.disable_safe.assert_called_once_with( -+ ["R1", "R2"], True, "10" -+ ) -+ self.resource.disable.assert_not_called() -+ self.resource.disable_simulate.assert_not_called() -+ -+ def test_no_strict(self): -+ self.run_cmd(["R1", "R2"], {"no-strict": True}) -+ self.resource.disable_safe.assert_called_once_with( -+ ["R1", "R2"], False, False -+ ) -+ self.resource.disable.assert_not_called() -+ self.resource.disable_simulate.assert_not_called() -+ -+ def test_no_strict_wait(self): -+ self.run_cmd(["R1", "R2"], {"no-strict": True, "wait": "10"}) -+ self.resource.disable_safe.assert_called_once_with( -+ ["R1", "R2"], False, "10" -+ ) -+ self.resource.disable.assert_not_called() -+ self.resource.disable_simulate.assert_not_called() -+ -+ @mock.patch("pcs.resource.warn") -+ def test_force(self, mock_warn): -+ self.run_cmd(["R1", "R2"], {"force": True}) -+ self.resource.disable.assert_called_once_with( -+ ["R1", "R2"], False -+ ) -+ self.resource.disable_safe.assert_not_called() -+ self.resource.disable_simulate.assert_not_called() -+ mock_warn.assert_called_once_with(self.force_warning) -+ -+ @mock.patch("pcs.resource.warn") -+ def test_force_wait(self, mock_warn): -+ self.run_cmd(["R1", "R2"], {"force": True, "wait": "10"}) -+ self.resource.disable.assert_called_once_with( -+ ["R1", "R2"], "10" -+ ) -+ self.resource.disable_safe.assert_not_called() -+ self.resource.disable_simulate.assert_not_called() -+ mock_warn.assert_called_once_with(self.force_warning) -+ -+ -+ @mock.patch("pcs.resource.print") -+ def test_simulate(self, mock_print): -+ self.resource.disable_simulate.return_value = "simulate output" -+ self.run_cmd(["R1", "R2"], dict(simulate=True)) -+ self.resource.disable_simulate.assert_called_once_with(["R1", "R2"]) -+ self.resource.disable.assert_not_called() -+ self.resource.disable_safe.assert_not_called() -+ mock_print.assert_called_once_with("simulate output") -diff --git a/pcs/test/tools/command_env/config_runner_pcmk.py b/pcs/test/tools/command_env/config_runner_pcmk.py -index e9cc6397..e561dd5b 100644 ---- a/pcs/test/tools/command_env/config_runner_pcmk.py -+++ b/pcs/test/tools/command_env/config_runner_pcmk.py -@@ -7,8 +7,12 @@ import os - - from lxml import etree - --from pcs.test.tools.command_env.mock_runner import Call as RunnerCall -+from pcs.test.tools.command_env.mock_runner import ( -+ Call as RunnerCall, -+ CheckStdinEqualXml, -+) - from pcs.test.tools.fixture import complete_state_resources -+from pcs.test.tools.fixture_cib import modify_cib - from pcs.test.tools.misc import get_test_resource as rc - from pcs.test.tools.xml import etree_to_str - -@@ -340,3 +344,52 @@ class PcmkShortcuts(object): - name, - RunnerCall("crm_node --force --remove {0}".format(node_name)), - ) -+ -+ def simulate_cib( -+ self, new_cib_filepath, transitions_filepath, -+ cib_modifiers=None, cib_load_name="runner.cib.load", -+ stdout="", stderr="", returncode=0, -+ name="runner.pcmk.simulate_cib", -+ **modifier_shortcuts -+ ): -+ """ -+ Create a call for simulating effects of cib changes -+ -+ string new_cib_filepath -- a temp file for storing a new cib -+ string transitions_filepath -- a temp file for storing transitions -+ list of callable modifiers -- every callable takes etree.Element and -+ returns new etree.Element with desired modification -+ string cib_load_name -- key of a call from whose stdout the cib is taken -+ string stdout -- pacemaker's stdout -+ string stderr -- pacemaker's stderr -+ int returncode -- pacemaker's returncode -+ string name -- key of the call -+ dict modifier_shortcuts -- a new modifier is generated from each -+ modifier shortcut. -+ As key there can be keys of MODIFIER_GENERATORS. -+ Value is passed into appropriate generator from MODIFIER_GENERATORS. -+ For details see pcs_test.tools.fixture_cib (mainly the variable -+ MODIFIER_GENERATORS - please refer it when you are adding params -+ here) -+ """ -+ cib_xml = modify_cib( -+ self.__calls.get(cib_load_name).stdout, -+ cib_modifiers, -+ **modifier_shortcuts -+ ) -+ cmd = [ -+ "crm_simulate", "--simulate", -+ "--save-output", new_cib_filepath, -+ "--save-graph", transitions_filepath, -+ "--xml-pipe", -+ ] -+ self.__calls.place( -+ name, -+ RunnerCall( -+ " ".join(cmd), -+ stdout=stdout, -+ stderr=stderr, -+ returncode=returncode, -+ check_stdin=CheckStdinEqualXml(cib_xml), -+ ), -+ ) -diff --git a/pcs/test/tools/command_env/mock_runner.py b/pcs/test/tools/command_env/mock_runner.py -index 393e4995..41a09ea4 100644 ---- a/pcs/test/tools/command_env/mock_runner.py -+++ b/pcs/test/tools/command_env/mock_runner.py -@@ -12,6 +12,22 @@ from pcs.test.tools.assertions import assert_xml_equal - - CALL_TYPE_RUNNER = "CALL_TYPE_RUNNER" - -+ -+class CheckStdinEqualXml(object): -+ def __init__(self, expected_stdin): -+ self.expected_stdin = expected_stdin -+ -+ def __call__(self, stdin, command, order_num): -+ assert_xml_equal( -+ self.expected_stdin, -+ stdin, -+ ( -+ "Trying to run command no. {0}" -+ "\n\n '{1}'\n\nwith expected xml stdin.\n" -+ ).format(order_num, command) -+ ) -+ -+ - def create_check_stdin_xml(expected_stdin): - def stdin_xml_check(stdin, command, order_num): - assert_xml_equal( -@@ -62,6 +78,7 @@ COMMAND_COMPLETIONS = { - "crm_mon": path.join(settings.pacemaker_binaries, "crm_mon"), - "crm_node": path.join(settings.pacemaker_binaries, "crm_node"), - "crm_resource": path.join(settings.pacemaker_binaries, "crm_resource"), -+ "crm_simulate": path.join(settings.pacemaker_binaries, "crm_simulate"), - "crm_verify": path.join(settings.pacemaker_binaries, "crm_verify"), - "sbd": settings.sbd_binary, - } -diff --git a/pcs/usage.py b/pcs/usage.py -index 582bc53f..acad3730 100644 ---- a/pcs/usage.py -+++ b/pcs/usage.py -@@ -252,14 +252,40 @@ Commands: - started, or 1 if the resources have not yet started. If 'n' is not - specified it defaults to 60 minutes. - -- disable ... [--wait[=n]] -+ disable ... [--safe [--no-strict]] [--simulate] [--wait[=n]] - Attempt to stop the resources if they are running and forbid the - cluster from starting them again. Depending on the rest of the - configuration (constraints, options, failures, etc), the resources may -- remain started. If --wait is specified, pcs will wait up to 'n' seconds -- for the resources to stop and then return 0 if the resources are -- stopped or 1 if the resources have not stopped. If 'n' is not specified -- it defaults to 60 minutes. -+ remain started. -+ If --safe is specified, no changes to the cluster configuration will be -+ made if other than specified resources would be affected in any way. -+ If --no-strict is specified, no changes to the cluster configuration -+ will be made if other than specified resources would get stopped or -+ demoted. Moving resources between nodes is allowed. -+ If --simulate is specified, no changes to the cluster configuration -+ will be made and the effect of the changes will be printed instead. -+ If --wait is specified, pcs will wait up to 'n' seconds for the -+ resources to stop and then return 0 if the resources are stopped or 1 -+ if the resources have not stopped. If 'n' is not specified it defaults -+ to 60 minutes. -+ -+ safe-disable ... [--no-strict] [--simulate] [--wait[=n]] -+ [--force] -+ Attempt to stop the resources if they are running and forbid the -+ cluster from starting them again. Depending on the rest of the -+ configuration (constraints, options, failures, etc), the resources may -+ remain started. No changes to the cluster configuration will be -+ made if other than specified resources would be affected in any way. -+ If --no-strict is specified, no changes to the cluster configuration -+ will be made if other than specified resources would get stopped or -+ demoted. Moving resources between nodes is allowed. -+ If --simulate is specified, no changes to the cluster configuration -+ will be made and the effect of the changes will be printed instead. -+ If --wait is specified, pcs will wait up to 'n' seconds for the -+ resources to stop and then return 0 if the resources are stopped or 1 -+ if the resources have not stopped. If 'n' is not specified it defaults -+ to 60 minutes. -+ If --force is specified, checks for safe disable will be skipped. - - restart [node] [--wait=n] - Restart the resource specified. If a node is specified and if the -diff --git a/pcs/utils.py b/pcs/utils.py -index 11e3b361..66f7ebf1 100644 ---- a/pcs/utils.py -+++ b/pcs/utils.py -@@ -2973,6 +2973,9 @@ def get_modifiers(): - "monitor": "--monitor" in pcs_options, - "name": pcs_options.get("--name", None), - "no-default-ops": "--no-default-ops" in pcs_options, -+ "no-strict": "--no-strict" in pcs_options, -+ "safe": "--safe" in pcs_options, -+ "simulate": "--simulate" in pcs_options, - "skip_offline_nodes": "--skip-offline" in pcs_options, - "start": "--start" in pcs_options, - "wait": pcs_options.get("--wait", False), -diff --git a/pcsd/capabilities.xml b/pcsd/capabilities.xml -index bc1d69c8..322ae300 100644 ---- a/pcsd/capabilities.xml -+++ b/pcsd/capabilities.xml -@@ -1134,6 +1134,20 @@ - pcs commands: resource disable --wait, resource enable --wait - - -+ -+ -+ Do not disable resources if other resources would be affected. -+ -+ pcs commands: resource disable --safe [--no-strict] -+ -+ -+ -+ -+ Show effects caused by disabling resources. -+ -+ pcs commands: resource disable --simulate -+ -+ - - - Put a resource into unmanaged and managed mode. --- -2.20.1 - diff --git a/SOURCES/bz1770975-01-add-resource-relations-command.patch b/SOURCES/bz1770975-01-add-resource-relations-command.patch deleted file mode 100644 index 1ac28a7..0000000 --- a/SOURCES/bz1770975-01-add-resource-relations-command.patch +++ /dev/null @@ -1,3035 +0,0 @@ -From 5fa38022bbfe6134f71622174a0961d0e0bc3138 Mon Sep 17 00:00:00 2001 -From: Ondrej Mular -Date: Thu, 14 Nov 2019 13:40:57 +0100 -Subject: [PATCH 1/6] add `resource relations` command - -This new commad displays relations of a specified resource ---- - pcs/cli/common/lib_wrapper.py | 2 + - pcs/cli/common/printable_tree.py | 50 ++ - pcs/cli/common/test/test_printable_tree.py | 216 ++++++ - pcs/cli/resource/relations.py | 202 +++++ - pcs/cli/resource/test/test_relations.py | 492 +++++++++++++ - pcs/common/interface/__init__.py | 0 - pcs/common/interface/dto.py | 18 + - pcs/common/pacemaker/__init__.py | 0 - pcs/common/pacemaker/resource/__init__.py | 0 - pcs/common/pacemaker/resource/relations.py | 66 ++ - .../pacemaker/resource/test/__init__.py | 0 - .../pacemaker/resource/test/test_relations.py | 169 +++++ - pcs/lib/cib/resource/__init__.py | 1 + - pcs/lib/cib/resource/common.py | 48 ++ - pcs/lib/cib/resource/relations.py | 265 +++++++ - pcs/lib/cib/test/test_resource_common.py | 174 ++++- - pcs/lib/cib/test/test_resource_relations.py | 690 ++++++++++++++++++ - pcs/lib/commands/resource.py | 22 + - .../test/resource/test_resource_relations.py | 300 ++++++++ - pcs/pcs.8 | 3 + - pcs/resource.py | 3 + - pcs/test/tools/fixture_cib.py | 3 + - pcs/usage.py | 7 + - pcsd/capabilities.xml | 8 + - test/centos7/Dockerfile | 4 +- - 25 files changed, 2729 insertions(+), 14 deletions(-) - create mode 100644 pcs/cli/common/printable_tree.py - create mode 100644 pcs/cli/common/test/test_printable_tree.py - create mode 100644 pcs/cli/resource/relations.py - create mode 100644 pcs/cli/resource/test/test_relations.py - create mode 100644 pcs/common/interface/__init__.py - create mode 100644 pcs/common/interface/dto.py - create mode 100644 pcs/common/pacemaker/__init__.py - create mode 100644 pcs/common/pacemaker/resource/__init__.py - create mode 100644 pcs/common/pacemaker/resource/relations.py - create mode 100644 pcs/common/pacemaker/resource/test/__init__.py - create mode 100644 pcs/common/pacemaker/resource/test/test_relations.py - create mode 100644 pcs/lib/cib/resource/relations.py - create mode 100644 pcs/lib/cib/test/test_resource_relations.py - create mode 100644 pcs/lib/commands/test/resource/test_resource_relations.py - -diff --git a/pcs/cli/common/lib_wrapper.py b/pcs/cli/common/lib_wrapper.py -index bb7ca29d..a6cc0ca8 100644 ---- a/pcs/cli/common/lib_wrapper.py -+++ b/pcs/cli/common/lib_wrapper.py -@@ -348,6 +348,8 @@ def load_module(env, middleware_factory, name): - "get_failcounts": resource.get_failcounts, - "manage": resource.manage, - "unmanage": resource.unmanage, -+ "get_resource_relations_tree": -+ resource.get_resource_relations_tree, - } - ) - -diff --git a/pcs/cli/common/printable_tree.py b/pcs/cli/common/printable_tree.py -new file mode 100644 -index 00000000..f16efc99 ---- /dev/null -+++ b/pcs/cli/common/printable_tree.py -@@ -0,0 +1,50 @@ -+class PrintableTreeNode(object): -+ @property -+ def members(self): -+ raise NotImplementedError() -+ -+ @property -+ def detail(self): -+ raise NotImplementedError() -+ -+ @property -+ def is_leaf(self): -+ raise NotImplementedError() -+ -+ def get_title(self, verbose): -+ raise NotImplementedError() -+ -+ -+def tree_to_lines(node, verbose=False, title_prefix="", indent=""): -+ """ -+ Return sequence of strings representing lines to print out tree structure on -+ command line. -+ """ -+ result = [] -+ note = "" -+ if node.is_leaf: -+ note = " [displayed elsewhere]" -+ title = node.get_title(verbose) -+ result.append("{}{}{}".format(title_prefix, title, note)) -+ if node.is_leaf: -+ return result -+ _indent = "| " -+ if not node.members: -+ _indent = " " -+ for line in node.detail: -+ result.append("{}{}{}".format(indent, _indent, line)) -+ _indent = "| " -+ _title_prefix = "|- " -+ for member in node.members: -+ if member == node.members[-1]: -+ _indent = " " -+ _title_prefix = "`- " -+ result.extend( -+ tree_to_lines( -+ member, -+ verbose, -+ indent="{}{}".format(indent, _indent), -+ title_prefix="{}{}".format(indent, _title_prefix), -+ ) -+ ) -+ return result -diff --git a/pcs/cli/common/test/test_printable_tree.py b/pcs/cli/common/test/test_printable_tree.py -new file mode 100644 -index 00000000..6f4757ee ---- /dev/null -+++ b/pcs/cli/common/test/test_printable_tree.py -@@ -0,0 +1,216 @@ -+from pcs.test.tools.pcs_unittest import TestCase -+# from unittest import TestCase -+ -+from pcs.cli.common import printable_tree as lib -+ -+ -+class Node(object): -+ def __init__(self, title, detail, is_leaf, members): -+ self.title = title -+ self.detail = detail -+ self.is_leaf = is_leaf -+ self.members = members -+ -+ def get_title(self, verbose): -+ detail = " (verbose)" if verbose else "" -+ return "{}{}".format(self.title, detail) -+ -+def node(an_id, detail=0, leaf=False, members=None): -+ return Node( -+ "{}-title".format(an_id), -+ ["{}-detail{}".format(an_id, i) for i in range(detail)], -+ leaf, -+ members=members or [], -+ ) -+ -+class TreeToLines(TestCase): -+ def test_verbose(self): -+ self.assertEqual( -+ ["l0-title (verbose)"], lib.tree_to_lines(node("l0"), verbose=True) -+ ) -+ -+ def test_empty(self): -+ self.assertEqual(["l0-title"], lib.tree_to_lines(node("l0"))) -+ -+ def test_empty_leaf(self): -+ self.assertEqual( -+ ["l0-title [displayed elsewhere]"], -+ lib.tree_to_lines(node("l0", leaf=True)), -+ ) -+ -+ def test_detail_simple(self): -+ self.assertEqual( -+ [ -+ "l0-title", -+ " l0-detail0", -+ ], -+ lib.tree_to_lines(node("l0", 1)), -+ ) -+ -+ def test_detail(self): -+ self.assertEqual( -+ [ -+ "l0-title", -+ " l0-detail0", -+ " l0-detail1", -+ " l0-detail2", -+ ], -+ lib.tree_to_lines(node("l0", 3)), -+ ) -+ -+ def test_detail_leaf(self): -+ self.assertEqual( -+ ["l0-title [displayed elsewhere]"], -+ lib.tree_to_lines(node("l0", 3, True)), -+ ) -+ -+ def test_one_member(self): -+ self.assertEqual( -+ [ -+ "l0-title", -+ "`- l1-title", -+ ], -+ lib.tree_to_lines(node("l0", members=[node("l1")])), -+ ) -+ -+ def test_one_member_leaf(self): -+ self.assertEqual( -+ ["l0-title [displayed elsewhere]"], -+ lib.tree_to_lines(node("l0", leaf=True, members=[node("l1")])), -+ ) -+ -+ def test_multiple_members(self): -+ self.assertEqual( -+ [ -+ "l0-title", -+ "|- l1-title", -+ "|- l2-title", -+ "`- l3-title", -+ ], -+ lib.tree_to_lines( -+ node("l0", members=[node("l1"), node("l2"), node("l3")]) -+ ), -+ ) -+ -+ def test_multiple_members_detail(self): -+ self.assertEqual( -+ [ -+ "l0-title", -+ "| l0-detail0", -+ "| l0-detail1", -+ "|- l1-title", -+ "|- l2-title", -+ "`- l3-title", -+ ], -+ lib.tree_to_lines( -+ node( -+ "l0", detail=2, members=[node("l1"), node("l2"), node("l3")] -+ ) -+ ), -+ ) -+ -+ def test_multiple_members_detail_leaf(self): -+ self.assertEqual( -+ ["l0-title [displayed elsewhere]"], -+ lib.tree_to_lines( -+ node("l0", 2, True, [node("l1"), node("l2"), node("l3")]) -+ ), -+ ) -+ -+ def test_complex_tree_wide(self): -+ self.assertEqual( -+ [ -+ "0-title", -+ "| 0-detail0", -+ "| 0-detail1", -+ "|- 00-title", -+ "| 00-detail0", -+ "| 00-detail1", -+ "|- 01-title", -+ "| |- 010-title", -+ "| | `- 0100-title [displayed elsewhere]", -+ "| `- 011-title", -+ "|- 02-title", -+ "| | 02-detail0", -+ "| | 02-detail1", -+ "| | 02-detail2", -+ "| |- 020-title", -+ "| `- 021-title", -+ "| | 021-detail0", -+ "| |- 0210-title [displayed elsewhere]", -+ "| |- 0211-title", -+ "| | 0211-detail0", -+ "| | 0211-detail1", -+ "| `- 0212-title", -+ "| `- 02120-title", -+ "|- 03-title", -+ "| `- 030-title", -+ "| `- 0300-title", -+ "|- 04-title [displayed elsewhere]", -+ "`- 05-title", -+ " 05-detail0", -+ ], -+ lib.tree_to_lines( -+ node( -+ "0", -+ 2, -+ members=[ -+ node("00", 2), -+ node( -+ "01", members=[ -+ node("010", members=[node("0100", leaf=True)]), -+ node("011"), -+ ], -+ ), -+ node( -+ "02", 3, members=[ -+ node("020"), -+ node( -+ "021", 1, members=[ -+ node("0210", leaf=True), -+ node("0211", 2), -+ node("0212", members=[node("02120")]), -+ ], -+ ), -+ ], -+ ), -+ node( -+ "03", members=[node("030", members=[node("0300")])], -+ ), -+ node("04", leaf=True), -+ node("05", 1), -+ ], -+ ) -+ ), -+ ) -+ -+ def test_complex_tree_deep(self): -+ self.assertEqual( -+ [ -+ "0-title", -+ "| 0-detail0", -+ "`- 00-title", -+ " |- 000-title", -+ " | 000-detail0", -+ " | 000-detail1", -+ " `- 001-title", -+ " |- 0010-title [displayed elsewhere]", -+ " |- 0011-title", -+ " | 0011-detail0", -+ " | 0011-detail1", -+ " `- 0012-title", -+ " `- 00120-title", -+ ], -+ lib.tree_to_lines( -+ node("0", 1, members=[ -+ node("00", members=[ -+ node("000", 2), -+ node("001", members=[ -+ node("0010", leaf=True), -+ node("0011", 2), -+ node("0012", members=[node("00120")]), -+ ]), -+ ]), -+ ]) -+ ), -+ ) -diff --git a/pcs/cli/resource/relations.py b/pcs/cli/resource/relations.py -new file mode 100644 -index 00000000..07d34d4d ---- /dev/null -+++ b/pcs/cli/resource/relations.py -@@ -0,0 +1,202 @@ -+from __future__ import print_function -+ -+from pcs.common.pacemaker.resource.relations import ( -+ ResourceRelationDto, -+ ResourceRelationType, -+) -+from pcs.cli.common.console_report import format_optional -+from pcs.cli.common.errors import CmdLineInputError -+from pcs.cli.common.printable_tree import ( -+ tree_to_lines, -+ PrintableTreeNode, -+) -+ -+ -+def show_resource_relations_cmd(lib, argv, modifiers): -+ """ -+ Options: -+ * -f - CIB file -+ * --full - show constraint ids and resource types -+ """ -+ if len(argv) != 1: -+ raise CmdLineInputError() -+ tree = ResourcePrintableNode.from_dto( -+ ResourceRelationDto.from_dict( -+ lib.resource.get_resource_relations_tree(argv[0]) -+ ) -+ ) -+ for line in tree_to_lines(tree, verbose=modifiers["full"]): -+ print(line) -+ -+ -+class ResourceRelationBase(PrintableTreeNode): -+ def __init__( self, relation_entity, members, is_leaf): -+ self._relation_entity = relation_entity -+ self._members = members -+ self._is_leaf = is_leaf -+ -+ @property -+ def is_leaf(self): -+ return self._is_leaf -+ -+ @property -+ def relation_entity(self): -+ return self._relation_entity -+ -+ @property -+ def members(self): -+ return self._members -+ -+ @property -+ def detail(self): -+ raise NotImplementedError() -+ -+ def get_title(self, verbose): -+ raise NotImplementedError() -+ -+ -+class ResourcePrintableNode(ResourceRelationBase): -+ @classmethod -+ def from_dto(cls, resource_dto): -+ def _relation_comparator(item): -+ type_priorities = ( -+ ResourceRelationType.INNER_RESOURCES, -+ ResourceRelationType.OUTER_RESOURCE, -+ ResourceRelationType.ORDER, -+ ResourceRelationType.ORDER_SET, -+ ) -+ priority_map = { -+ _type: value for value, _type in enumerate(type_priorities) -+ } -+ return "{_type}_{_id}".format( -+ _type=priority_map.get( -+ # Hardcoded number 9 is intentional. If there is more than -+ # 10 items, it would be required to also prepend zeros for -+ # lower numbers. E.g: if there is 100 options, it should -+ # starts as 000, 001, ... -+ item.relation_entity.type, 9 # type: ignore -+ ), -+ _id=item.relation_entity.id -+ ) -+ -+ return cls( -+ resource_dto.relation_entity, -+ sorted( -+ [ -+ RelationPrintableNode.from_dto(member_dto) -+ for member_dto in resource_dto.members -+ ], -+ key=_relation_comparator, -+ ), -+ resource_dto.is_leaf -+ ) -+ -+ def get_title(self, verbose): -+ rsc_type = self._relation_entity.type -+ metadata = self._relation_entity.metadata -+ if rsc_type == "primitive": -+ rsc_type = "{_class}{_provider}{_type}".format( -+ _class=format_optional(metadata.get("class"), "{}:"), -+ _provider=format_optional(metadata.get("provider"), "{}:"), -+ _type=metadata.get("type"), -+ ) -+ detail = " (resource: {})".format(rsc_type) if verbose else "" -+ return "{}{}".format(self._relation_entity.id, detail) -+ -+ @property -+ def detail(self): -+ return [] -+ -+ -+class RelationPrintableNode(ResourceRelationBase): -+ @classmethod -+ def from_dto(cls, relation_dto): -+ return cls( -+ relation_dto.relation_entity, -+ sorted( -+ [ -+ ResourcePrintableNode.from_dto(member_dto) -+ for member_dto in relation_dto.members -+ ], -+ key=lambda item: item.relation_entity.id, -+ ), -+ relation_dto.is_leaf -+ ) -+ -+ def get_title(self, verbose): -+ rel_type_map = { -+ ResourceRelationType.ORDER: "order", -+ ResourceRelationType.ORDER_SET: "order set", -+ ResourceRelationType.INNER_RESOURCES: "inner resource(s)", -+ ResourceRelationType.OUTER_RESOURCE: "outer resource", -+ } -+ detail = ( -+ " ({})".format(self._relation_entity.metadata.get("id")) -+ if verbose -+ else "" -+ ) -+ return "{type}{detail}".format( -+ type=rel_type_map.get(self._relation_entity.type, ""), -+ detail=detail, -+ ) -+ -+ @property -+ def detail(self): -+ ent = self._relation_entity -+ if ent.type == ResourceRelationType.ORDER: -+ return _order_metadata_to_str(ent.metadata) -+ if ent.type == ResourceRelationType.ORDER_SET: -+ return _order_set_metadata_to_str(ent.metadata) -+ if ( -+ ent.type == ResourceRelationType.INNER_RESOURCES -+ and -+ len(ent.members) > 1 -+ ): -+ return ["members: {}".format(" ".join(ent.members))] -+ return [] -+ -+ -+def _order_metadata_to_str(metadata): -+ return [ -+ "{action1} {resource1} then {action2} {resource2}".format( -+ action1=metadata["first-action"], -+ resource1=metadata["first"], -+ action2=metadata["then-action"], -+ resource2=metadata["then"], -+ ) -+ ] + _order_common_metadata_to_str(metadata) -+ -+ -+def _order_set_metadata_to_str(metadata): -+ 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"]), -+ )) -+ return _order_common_metadata_to_str(metadata) + result -+ -+ -+def _resource_set_options_to_str(metadata): -+ supported_keys = ( -+ "sequential", "require-all", "ordering", "action", "role", "kind", -+ "score", -+ ) -+ result = _filter_supported_keys(metadata, supported_keys) -+ return " ({})".format(result) if result else "" -+ -+ -+def _filter_supported_keys(data, supported_keys): -+ return " ".join([ -+ "{}={}".format(key, value) -+ for key, value in sorted(data.items()) -+ if key in supported_keys -+ ]) -+ -+ -+def _order_common_metadata_to_str(metadata): -+ result = _filter_supported_keys( -+ metadata, ("symmetrical", "kind", "require-all", "score") -+ ) -+ return [result] if result else [] -+ -diff --git a/pcs/cli/resource/test/test_relations.py b/pcs/cli/resource/test/test_relations.py -new file mode 100644 -index 00000000..77c1c782 ---- /dev/null -+++ b/pcs/cli/resource/test/test_relations.py -@@ -0,0 +1,492 @@ -+from pcs.test.tools.pcs_unittest import mock, TestCase -+ -+from pcs.common.pacemaker.resource.relations import ( -+ RelationEntityDto, -+ ResourceRelationDto, -+ ResourceRelationType, -+) -+from pcs.cli.common.errors import CmdLineInputError -+from pcs.cli.resource import relations -+ -+ -+DEFAULT_MODIFIERS = {"full": False} -+ -+ -+class ShowResourceRelationsCmd(TestCase): -+ def setUp(self): -+ self.maxDiff = None -+ self.lib_call = mock.Mock() -+ self.lib = mock.Mock(spec_set=["resource"]) -+ self.lib.resource = mock.Mock(spec_set=["get_resource_relations_tree"]) -+ self.lib.resource.get_resource_relations_tree = self.lib_call -+ self.lib_call.return_value = ResourceRelationDto( -+ RelationEntityDto( -+ "d1", "primitive", [], { -+ "class": "ocf", -+ "provider": "pacemaker", -+ "type": "Dummy", -+ } -+ ), -+ [ -+ ResourceRelationDto( -+ RelationEntityDto( -+ "order1", ResourceRelationType.ORDER, [], { -+ "first-action": "start", -+ "first": "d1", -+ "then-action": "start", -+ "then": "d2", -+ "kind": "Mandatory", -+ "symmetrical": "true", -+ } -+ ), -+ [ -+ ResourceRelationDto( -+ RelationEntityDto( -+ "d2", "primitive", [], { -+ "class": "ocf", -+ "provider": "heartbeat", -+ "type": "Dummy", -+ } -+ ), -+ [], -+ False -+ ), -+ ], -+ False -+ ), -+ ResourceRelationDto( -+ RelationEntityDto( -+ "inner:g1", ResourceRelationType.INNER_RESOURCES, [], {} -+ ), -+ [ -+ ResourceRelationDto( -+ RelationEntityDto("g1", "group", [], {}), -+ [], -+ True, -+ ), -+ ], -+ False -+ ) -+ ], -+ False, -+ ).to_dict() -+ -+ def test_no_args(self): -+ with self.assertRaises(CmdLineInputError) as cm: -+ relations.show_resource_relations_cmd( -+ self.lib, [], DEFAULT_MODIFIERS -+ ) -+ self.assertIsNone(cm.exception.message) -+ -+ def test_more_args(self): -+ with self.assertRaises(CmdLineInputError) as cm: -+ relations.show_resource_relations_cmd( -+ self.lib, ["a1", "a2"], DEFAULT_MODIFIERS -+ ) -+ self.assertIsNone(cm.exception.message) -+ -+ @mock.patch("pcs.cli.resource.relations.print") -+ def test_success(self, mock_print): -+ relations.show_resource_relations_cmd( -+ self.lib, ["d1"], DEFAULT_MODIFIERS -+ ) -+ self.lib_call.assert_called_once_with("d1") -+ self.assertEqual( -+ [ -+ mock.call("d1"), -+ mock.call("|- inner resource(s)"), -+ mock.call("| `- g1 [displayed elsewhere]"), -+ mock.call("`- order"), -+ mock.call(" | start d1 then start d2"), -+ mock.call(" | kind=Mandatory symmetrical=true"), -+ mock.call(" `- d2"), -+ ], -+ mock_print.call_args_list -+ ) -+ -+ @mock.patch("pcs.cli.resource.relations.print") -+ def test_verbose(self, mock_print): -+ relations.show_resource_relations_cmd(self.lib, ["d1"], {"full": True}) -+ self.lib_call.assert_called_once_with("d1") -+ self.assertEqual( -+ [ -+ mock.call("d1 (resource: ocf:pacemaker:Dummy)"), -+ mock.call("|- inner resource(s) (None)"), -+ mock.call("| `- g1 (resource: group) [displayed elsewhere]"), -+ mock.call("`- order (None)"), -+ mock.call(" | start d1 then start d2"), -+ mock.call(" | kind=Mandatory symmetrical=true"), -+ mock.call(" `- d2 (resource: ocf:heartbeat:Dummy)"), -+ ], -+ mock_print.call_args_list -+ ) -+ -+ -+def _fixture_dummy(_id): -+ return RelationEntityDto( -+ _id, "primitive", [], { -+ "class": "ocf", -+ "provider": "pacemaker", -+ "type": "Dummy", -+ } -+) -+ -+ -+D1_PRIMITIVE = _fixture_dummy("d1") -+D2_PRIMITIVE = _fixture_dummy("d2") -+ -+ -+def _fixture_res_rel_dto(ent): -+ return ResourceRelationDto(ent, [], True) -+ -+ -+class ResourcePrintableNode(TestCase): -+ def assert_member(self, member, ent): -+ self.assertTrue(isinstance(member, relations.RelationPrintableNode)) -+ self.assertEqual(ent, member.relation_entity) -+ self.assertEqual(True, member.is_leaf) -+ self.assertEqual(0, len(member.members)) -+ -+ def test_from_dto(self): -+ inner_ent = RelationEntityDto( -+ "inner:g1", ResourceRelationType.INNER_RESOURCES, [], {} -+ ) -+ outer_ent = RelationEntityDto( -+ "outer:g1", ResourceRelationType.OUTER_RESOURCE, [], {} -+ ) -+ order_ent1 = RelationEntityDto( -+ "order1", ResourceRelationType.ORDER, [], {} -+ ) -+ order_ent2 = RelationEntityDto( -+ "order2", ResourceRelationType.ORDER, [], {} -+ ) -+ order_set_ent = RelationEntityDto( -+ "order_set", ResourceRelationType.ORDER_SET, [], {} -+ ) -+ -+ dto = ResourceRelationDto( -+ D1_PRIMITIVE, -+ [ -+ _fixture_res_rel_dto(order_set_ent), -+ _fixture_res_rel_dto(order_ent2), -+ _fixture_res_rel_dto(outer_ent), -+ _fixture_res_rel_dto(inner_ent), -+ _fixture_res_rel_dto(order_ent1), -+ ], -+ False, -+ ) -+ obj = relations.ResourcePrintableNode.from_dto(dto) -+ self.assertEqual(D1_PRIMITIVE, obj.relation_entity) -+ self.assertEqual(False, obj.is_leaf) -+ expected_members = ( -+ inner_ent, outer_ent, order_ent1, order_ent2, order_set_ent -+ ) -+ self.assertEqual(len(expected_members), len(obj.members)) -+ for i, member in enumerate(obj.members): -+ self.assert_member(member, expected_members[i]) -+ -+ def test_primitive(self): -+ obj = relations.ResourcePrintableNode(D1_PRIMITIVE, [], False) -+ self.assertEqual( -+ "d1 (resource: ocf:pacemaker:Dummy)", obj.get_title(verbose=True) -+ ) -+ self.assertEqual([], obj.detail) -+ -+ def test_primitive_not_verbose(self): -+ obj = relations.ResourcePrintableNode(D1_PRIMITIVE, [], False) -+ self.assertEqual("d1", obj.get_title(verbose=False)) -+ self.assertEqual([], obj.detail) -+ -+ def test_primitive_without_provider_class(self): -+ obj = relations.ResourcePrintableNode( -+ RelationEntityDto( -+ "d1", "primitive", [], { -+ "type": "Dummy", -+ } -+ ), -+ [], -+ False, -+ ) -+ self.assertEqual("d1 (resource: Dummy)", obj.get_title(verbose=True)) -+ self.assertEqual([], obj.detail) -+ -+ def test_primitive_without_provider(self): -+ obj = relations.ResourcePrintableNode( -+ RelationEntityDto( -+ "d1", "primitive", [], { -+ "class": "ocf", -+ "type": "Dummy", -+ } -+ ), -+ [], -+ False, -+ ) -+ self.assertEqual( -+ "d1 (resource: ocf:Dummy)", obj.get_title(verbose=True) -+ ) -+ self.assertEqual([], obj.detail) -+ -+ def test_primitive_without_class(self): -+ obj = relations.ResourcePrintableNode( -+ RelationEntityDto( -+ "d1", "primitive", [], { -+ "provider": "pacemaker", -+ "type": "Dummy", -+ } -+ ), -+ [], -+ False, -+ ) -+ self.assertEqual( -+ "d1 (resource: pacemaker:Dummy)", obj.get_title(verbose=True) -+ ) -+ self.assertEqual([], obj.detail) -+ -+ def test_other(self): -+ obj = relations.ResourcePrintableNode( -+ RelationEntityDto("an_id", "a_type", [], {}), [], False, -+ ) -+ self.assertEqual( -+ "an_id (resource: a_type)", obj.get_title(verbose=True) -+ ) -+ self.assertEqual([], obj.detail) -+ -+ def test_other_not_verbose(self): -+ obj = relations.ResourcePrintableNode( -+ RelationEntityDto("an_id", "a_type", [], {}), [], False, -+ ) -+ self.assertEqual("an_id", obj.get_title(verbose=False)) -+ self.assertEqual([], obj.detail) -+ -+ -+class RelationPrintableNode(TestCase): -+ def setUp(self): -+ self.order_entity = RelationEntityDto( -+ "order1", ResourceRelationType.ORDER, [], { -+ "id": "order1", -+ "first-action": "start", -+ "first": "d1", -+ "then-action": "start", -+ "then": "d2", -+ } -+ ) -+ self.order_set_entity = RelationEntityDto( -+ "order_set_id", ResourceRelationType.ORDER_SET, [], { -+ "id": "order_set_id", -+ "sets": [ -+ { -+ "members": ["d1", "d2", "d3"], -+ "metadata": {}, -+ }, -+ { -+ "members": ["d4", "d5", "d0"], -+ "metadata": { -+ "sequential": "true", -+ "require-all": "false", -+ "score": "10", -+ }, -+ }, -+ ], -+ } -+ ) -+ -+ def assert_member(self, member, ent): -+ self.assertTrue(isinstance(member, relations.ResourcePrintableNode)) -+ self.assertEqual(ent, member.relation_entity) -+ self.assertEqual(True, member.is_leaf) -+ self.assertEqual(0, len(member.members)) -+ -+ def test_from_dto(self): -+ dto = ResourceRelationDto( -+ self.order_entity, -+ [ -+ ResourceRelationDto(D2_PRIMITIVE, [], True), -+ ResourceRelationDto(D1_PRIMITIVE, [], True), -+ ], -+ False -+ ) -+ obj = relations.RelationPrintableNode.from_dto(dto) -+ self.assertEqual(self.order_entity, obj.relation_entity) -+ self.assertEqual(False, obj.is_leaf) -+ self.assertEqual(2, len(obj.members)) -+ self.assert_member(obj.members[0], D1_PRIMITIVE) -+ self.assert_member(obj.members[1], D2_PRIMITIVE) -+ -+ def test_order_not_verbose(self): -+ obj = relations.RelationPrintableNode(self.order_entity, [], False) -+ self.assertEqual("order", obj.get_title(verbose=False)) -+ self.assertEqual(["start d1 then start d2"], obj.detail) -+ -+ def test_order(self): -+ obj = relations.RelationPrintableNode(self.order_entity, [], False) -+ self.assertEqual("order (order1)", obj.get_title(verbose=True)) -+ self.assertEqual(["start d1 then start d2"], obj.detail) -+ -+ def test_order_full(self): -+ self.order_entity.metadata.update({ -+ "kind": "Optional", -+ "symmetrical": "true", -+ "unsupported": "value", -+ "score": "1000", -+ }) -+ obj = relations.RelationPrintableNode(self.order_entity, [], False) -+ self.assertEqual("order (order1)", obj.get_title(verbose=True)) -+ self.assertEqual( -+ [ -+ "start d1 then start d2", -+ "kind=Optional score=1000 symmetrical=true" -+ ], -+ obj.detail, -+ ) -+ -+ def test_order_set_not_verbose(self): -+ obj = relations.RelationPrintableNode(self.order_set_entity, [], False) -+ self.assertEqual("order set", obj.get_title(verbose=False)) -+ self.assertEqual( -+ [ -+ " set d1 d2 d3", -+ " set d4 d5 d0 (require-all=false score=10 sequential=true)", -+ ], -+ obj.detail, -+ ) -+ -+ def test_order_set(self): -+ obj = relations.RelationPrintableNode(self.order_set_entity, [], False) -+ self.assertEqual( -+ "order set (order_set_id)", obj.get_title(verbose=True) -+ ) -+ self.assertEqual( -+ [ -+ " set d1 d2 d3", -+ " set d4 d5 d0 (require-all=false score=10 sequential=true)", -+ ], -+ obj.detail, -+ ) -+ -+ def test_order_set_full(self): -+ self.order_set_entity.metadata.update({ -+ "symmetrical": "true", -+ "kind": "Optional", -+ "require-all": "true", -+ "score": "100", -+ "unsupported": "value", -+ }) -+ self.order_set_entity.metadata["sets"].append({ -+ "members": ["d9", "d8", "d6", "d7"], -+ "metadata": { -+ "sequential": "true", -+ "require-all": "false", -+ "score": "10", -+ "ordering": "value", -+ "action": "start", -+ "role": "promoted", -+ "kind": "Optional", -+ "unsupported": "value", -+ }, -+ }) -+ obj = relations.RelationPrintableNode(self.order_set_entity, [], False) -+ self.assertEqual( -+ "order set (order_set_id)", obj.get_title(verbose=True) -+ ) -+ self.assertEqual( -+ [ -+ "kind=Optional require-all=true score=100 symmetrical=true", -+ " set d1 d2 d3", -+ " set d4 d5 d0 (require-all=false score=10 sequential=true)", -+ " set d9 d8 d6 d7 (action=start kind=Optional ordering=value " -+ "require-all=false role=promoted score=10 sequential=true)", -+ ], -+ obj.detail, -+ ) -+ -+ def test_multiple_inner_resources(self): -+ obj = relations.RelationPrintableNode( -+ RelationEntityDto( -+ "inner:g1", -+ ResourceRelationType.INNER_RESOURCES, -+ ["m1", "m2", "m0"], -+ {"id": "g1"} -+ ), -+ [], -+ False, -+ ) -+ self.assertEqual("inner resource(s) (g1)", obj.get_title(verbose=True)) -+ self.assertEqual(["members: m1 m2 m0"], obj.detail) -+ -+ def test_inner_resources_not_verbose(self): -+ obj = relations.RelationPrintableNode( -+ RelationEntityDto( -+ "inner:g1", ResourceRelationType.INNER_RESOURCES, ["m0"], { -+ "id": "g1", -+ } -+ ), -+ [], -+ False, -+ ) -+ self.assertEqual("inner resource(s)", obj.get_title(verbose=False)) -+ self.assertEqual([], obj.detail) -+ -+ def test_inner_resources(self): -+ obj = relations.RelationPrintableNode( -+ RelationEntityDto( -+ "inner:g1", ResourceRelationType.INNER_RESOURCES, ["m0"], { -+ "id": "g1", -+ } -+ ), -+ [], -+ False, -+ ) -+ self.assertEqual("inner resource(s) (g1)", obj.get_title(verbose=True)) -+ self.assertEqual([], obj.detail) -+ -+ def test_outer_resourcenot_verbose(self): -+ obj = relations.RelationPrintableNode( -+ RelationEntityDto( -+ "outer:g1", ResourceRelationType.OUTER_RESOURCE, [], { -+ "id": "g1", -+ } -+ ), -+ [], -+ False, -+ ) -+ self.assertEqual("outer resource", obj.get_title(verbose=False)) -+ self.assertEqual([], obj.detail) -+ -+ def test_outer_resource(self): -+ obj = relations.RelationPrintableNode( -+ RelationEntityDto( -+ "outer:g1", ResourceRelationType.OUTER_RESOURCE, [], { -+ "id": "g1", -+ } -+ ), -+ [], -+ False, -+ ) -+ self.assertEqual("outer resource (g1)", obj.get_title(verbose=True)) -+ self.assertEqual([], obj.detail) -+ -+ def test_unknown_not_verbose(self): -+ obj = relations.RelationPrintableNode( -+ RelationEntityDto( -+ "random", "undifined type", [], { -+ "id": "random_id", -+ } -+ ), -+ [], -+ False, -+ ) -+ self.assertEqual("", obj.get_title(verbose=False)) -+ self.assertEqual([], obj.detail) -+ -+ def test_unknown(self): -+ obj = relations.RelationPrintableNode( -+ RelationEntityDto( -+ "random", "undifined type", [], { -+ "id": "random_id", -+ } -+ ), -+ [], -+ False, -+ ) -+ self.assertEqual(" (random_id)", obj.get_title(verbose=True)) -+ self.assertEqual([], obj.detail) -diff --git a/pcs/common/interface/__init__.py b/pcs/common/interface/__init__.py -new file mode 100644 -index 00000000..e69de29b -diff --git a/pcs/common/interface/dto.py b/pcs/common/interface/dto.py -new file mode 100644 -index 00000000..b95367fd ---- /dev/null -+++ b/pcs/common/interface/dto.py -@@ -0,0 +1,18 @@ -+class DataTransferObject(object): -+ def to_dict(self): -+ raise NotImplementedError() -+ -+ @classmethod -+ def from_dict(cls, payload): -+ raise NotImplementedError() -+ -+ -+class ImplementsToDto(object): -+ def to_dto(self): -+ raise NotImplementedError() -+ -+ -+class ImplementsFromDto(object): -+ @classmethod -+ def from_dto(cls, dto_obj): -+ raise NotImplementedError() -diff --git a/pcs/common/pacemaker/__init__.py b/pcs/common/pacemaker/__init__.py -new file mode 100644 -index 00000000..e69de29b -diff --git a/pcs/common/pacemaker/resource/__init__.py b/pcs/common/pacemaker/resource/__init__.py -new file mode 100644 -index 00000000..e69de29b -diff --git a/pcs/common/pacemaker/resource/relations.py b/pcs/common/pacemaker/resource/relations.py -new file mode 100644 -index 00000000..c074a212 ---- /dev/null -+++ b/pcs/common/pacemaker/resource/relations.py -@@ -0,0 +1,66 @@ -+from pcs.common.interface.dto import DataTransferObject -+ -+ -+class ResourceRelationType(object): -+ ORDER = "ORDER" -+ ORDER_SET = "ORDER_SET" -+ INNER_RESOURCES = "INNER_RESOURCES" -+ OUTER_RESOURCE = "OUTER_RESOURCE" -+ -+ -+class RelationEntityDto(DataTransferObject): -+ def __init__(self, id_, type_, members, metadata): -+ # pylint: disable=invalid-name -+ self.id = id_ -+ self.type = type_ -+ self.members = members -+ self.metadata = metadata -+ -+ def __eq__(self, other): -+ return ( -+ isinstance(other, self.__class__) -+ and -+ self.to_dict() == other.to_dict() -+ ) -+ -+ def to_dict(self): -+ return dict( -+ id=self.id, -+ type=self.type, -+ members=self.members, -+ metadata=self.metadata, -+ ) -+ -+ @classmethod -+ def from_dict(cls, payload): -+ return cls( -+ payload["id"], -+ payload["type"], -+ payload["members"], -+ payload["metadata"], -+ ) -+ -+ -+class ResourceRelationDto(DataTransferObject): -+ def __init__(self, relation_entity, members, is_leaf): -+ self.relation_entity = relation_entity -+ self.members = members -+ self.is_leaf = is_leaf -+ -+ def to_dict(self): -+ return dict( -+ relation_entity=self.relation_entity.to_dict(), -+ members=[member.to_dict() for member in self.members], -+ is_leaf=self.is_leaf, -+ ) -+ -+ @classmethod -+ def from_dict(cls, payload): -+ return cls( -+ RelationEntityDto.from_dict(payload["relation_entity"]), -+ [ -+ ResourceRelationDto.from_dict(member_data) -+ for member_data in payload["members"] -+ ], -+ payload["is_leaf"], -+ ) -diff --git a/pcs/common/pacemaker/resource/test/__init__.py b/pcs/common/pacemaker/resource/test/__init__.py -new file mode 100644 -index 00000000..e69de29b -diff --git a/pcs/common/pacemaker/resource/test/test_relations.py b/pcs/common/pacemaker/resource/test/test_relations.py -new file mode 100644 -index 00000000..19127ddd ---- /dev/null -+++ b/pcs/common/pacemaker/resource/test/test_relations.py -@@ -0,0 +1,169 @@ -+from pcs.test.tools.pcs_unittest import TestCase -+ -+from pcs.common.pacemaker.resource import relations -+ -+ -+class RelationEntityDto(TestCase): -+ def setUp(self): -+ self.an_id = "an_id" -+ self.members = ["m2", "m1", "m3", "m0"] -+ self.metadata = dict( -+ key1="val1", -+ key0="vel0", -+ keyx="valx", -+ ) -+ self.str_type = "a_type" -+ self.enum_type = relations.ResourceRelationType.ORDER_SET -+ -+ def dto_fixture(self, a_type): -+ return relations.RelationEntityDto( -+ self.an_id, a_type, self.members, self.metadata -+ ) -+ -+ def dict_fixture(self, a_type): -+ return dict( -+ id=self.an_id, -+ type=a_type, -+ members=self.members, -+ metadata=self.metadata, -+ ) -+ -+ def test_to_dict_type_str(self): -+ dto = self.dto_fixture(self.str_type) -+ self.assertEqual(self.dict_fixture(self.str_type), dto.to_dict()) -+ -+ def test_to_dict_type_enum(self): -+ dto = self.dto_fixture(self.enum_type) -+ self.assertEqual(self.dict_fixture(self.enum_type), dto.to_dict()) -+ -+ def test_from_dict_type_str(self): -+ dto = relations.RelationEntityDto.from_dict( -+ self.dict_fixture(self.str_type) -+ ) -+ self.assertEqual(dto.id, self.an_id) -+ self.assertEqual(dto.type, self.str_type) -+ self.assertEqual(dto.members, self.members) -+ self.assertEqual(dto.metadata, self.metadata) -+ -+ def test_from_dict_type_enum(self): -+ dto = relations.RelationEntityDto.from_dict( -+ self.dict_fixture(self.enum_type) -+ ) -+ self.assertEqual(dto.id, self.an_id) -+ self.assertEqual(dto.type, self.enum_type) -+ self.assertEqual(dto.members, self.members) -+ self.assertEqual(dto.metadata, self.metadata) -+ -+ -+class ResourceRelationDto(TestCase): -+ @staticmethod -+ def ent_dto_fixture(an_id): -+ return relations.RelationEntityDto( -+ an_id, "a_type", ["m1", "m0", "m2"], dict(k1="v1", kx="vx") -+ ) -+ -+ def test_to_dict_no_members(self): -+ ent_dto = self.ent_dto_fixture("an_id") -+ dto = relations.ResourceRelationDto(ent_dto, [], True) -+ self.assertEqual( -+ dict( -+ relation_entity=ent_dto.to_dict(), -+ members=[], -+ is_leaf=True, -+ ), -+ dto.to_dict() -+ ) -+ -+ def test_to_dict_with_members(self): -+ ent_dto = self.ent_dto_fixture("an_id") -+ m1_ent = self.ent_dto_fixture("m1_ent") -+ m2_ent = self.ent_dto_fixture("m2_ent") -+ m3_ent = self.ent_dto_fixture("m3_ent") -+ members = [ -+ relations.ResourceRelationDto( -+ m1_ent, [], False -+ ), -+ relations.ResourceRelationDto( -+ m2_ent, -+ [relations.ResourceRelationDto(m3_ent, [], True)], -+ False -+ ), -+ ] -+ dto = relations.ResourceRelationDto(ent_dto, members, True) -+ self.assertEqual( -+ dict( -+ relation_entity=ent_dto.to_dict(), -+ members=[ -+ dict( -+ relation_entity=m1_ent.to_dict(), -+ members=[], -+ is_leaf=False, -+ ), -+ dict( -+ relation_entity=m2_ent.to_dict(), -+ members=[ -+ dict( -+ relation_entity=m3_ent.to_dict(), -+ members=[], -+ is_leaf=True, -+ ) -+ ], -+ is_leaf=False, -+ ), -+ ], -+ is_leaf=True, -+ ), -+ dto.to_dict() -+ ) -+ -+ def test_from_dict(self): -+ ent_dto = self.ent_dto_fixture("an_id") -+ m1_ent = self.ent_dto_fixture("m1_ent") -+ m2_ent = self.ent_dto_fixture("m2_ent") -+ m3_ent = self.ent_dto_fixture("m3_ent") -+ dto = relations.ResourceRelationDto.from_dict( -+ dict( -+ relation_entity=ent_dto.to_dict(), -+ members=[ -+ dict( -+ relation_entity=m1_ent.to_dict(), -+ members=[], -+ is_leaf=False, -+ ), -+ dict( -+ relation_entity=m2_ent.to_dict(), -+ members=[ -+ dict( -+ relation_entity=m3_ent.to_dict(), -+ members=[], -+ is_leaf=True, -+ ) -+ ], -+ is_leaf=False, -+ ), -+ ], -+ is_leaf=True, -+ ) -+ ) -+ self.assertEqual(ent_dto.to_dict(), dto.relation_entity.to_dict()) -+ self.assertEqual(True, dto.is_leaf) -+ self.assertEqual(2, len(dto.members)) -+ -+ self.assertEqual( -+ m1_ent.to_dict(), dto.members[0].relation_entity.to_dict() -+ ) -+ self.assertEqual(False, dto.members[0].is_leaf) -+ self.assertEqual(0, len(dto.members[0].members)) -+ -+ self.assertEqual( -+ m2_ent.to_dict(), dto.members[1].relation_entity.to_dict() -+ ) -+ self.assertEqual(False, dto.members[1].is_leaf) -+ self.assertEqual(1, len(dto.members[1].members)) -+ -+ self.assertEqual( -+ m3_ent.to_dict(), -+ dto.members[1].members[0].relation_entity.to_dict(), -+ ) -+ self.assertEqual(True, dto.members[1].members[0].is_leaf) -+ self.assertEqual(0, len(dto.members[1].members[0].members)) -diff --git a/pcs/lib/cib/resource/__init__.py b/pcs/lib/cib/resource/__init__.py -index 620af424..dedc03af 100644 ---- a/pcs/lib/cib/resource/__init__.py -+++ b/pcs/lib/cib/resource/__init__.py -@@ -12,5 +12,6 @@ from pcs.lib.cib.resource import ( - guest_node, - operations, - primitive, -+ relations, - remote_node, - ) -diff --git a/pcs/lib/cib/resource/common.py b/pcs/lib/cib/resource/common.py -index cc4bb1d0..87b656b7 100644 ---- a/pcs/lib/cib/resource/common.py -+++ b/pcs/lib/cib/resource/common.py -@@ -51,6 +51,54 @@ def find_primitives(resource_el): - return [resource_el] - return [] - -+def get_inner_resources(resource_el): -+ """ -+ Return list of inner resources (direct descendants) of a resource -+ specified as resource_el. -+ Example: for clone containing a group, this function will return only -+ group and not resource inside the group -+ -+ resource_el -- resource element to get its inner resources -+ """ -+ if is_bundle(resource_el): -+ in_bundle = get_bundle_inner_resource(resource_el) -+ return [in_bundle] if in_bundle is not None else [] -+ if is_any_clone(resource_el): -+ return [get_clone_inner_resource(resource_el)] -+ if is_group(resource_el): -+ return get_group_inner_resources(resource_el) -+ return [] -+ -+def is_wrapper_resource(resource_el): -+ """ -+ Return True for resource_el of types that can contain other resource(s) -+ (these are: group, bundle, clone) and False otherwise. -+ -+ resource_el -- resource element to check -+ """ -+ return ( -+ is_group(resource_el) -+ or -+ is_bundle(resource_el) -+ or -+ is_any_clone(resource_el) -+ ) -+ -+def get_parent_resource(resource_el): -+ """ -+ Return a direct ancestor of a specified resource or None if the resource -+ has no ancestor. -+ Example: for a resource in group which is in clone, this function will -+ return group element. -+ -+ resource_el -- resource element of which parent resource should be returned -+ """ -+ parent_el = resource_el.getparent() -+ if parent_el is not None and is_wrapper_resource(parent_el): -+ return parent_el -+ return None -+ -+ - def find_resources_to_enable(resource_el): - """ - Get resources to enable in order to enable specified resource succesfully -diff --git a/pcs/lib/cib/resource/relations.py b/pcs/lib/cib/resource/relations.py -new file mode 100644 -index 00000000..7935336a ---- /dev/null -+++ b/pcs/lib/cib/resource/relations.py -@@ -0,0 +1,265 @@ -+from pcs.common.pacemaker.resource.relations import ( -+ RelationEntityDto, -+ ResourceRelationDto, -+ ResourceRelationType, -+) -+from pcs.lib.cib import tools -+from pcs.lib.cib.resource import common -+ -+from pcs.lib.cib.resource.bundle import TAG as TAG_BUNDLE -+from pcs.lib.cib.resource.clone import ALL_TAGS as TAG_CLONE_ALL -+from pcs.lib.cib.resource.group import TAG as TAG_GROUP -+from pcs.lib.cib.resource.primitive import TAG as TAG_PRIMITIVE -+ -+ -+# character ':' ensures that there is no conflict with any id in CIB, as it -+# would be an invalid id -+INNER_RESOURCE_ID_TEMPLATE = "inner:{}" -+OUTER_RESOURCE_ID_TEMPLATE = "outer:{}" -+ -+ -+def _get_opposite_relation_id_template(relation_type): -+ return { -+ ResourceRelationType.INNER_RESOURCES: OUTER_RESOURCE_ID_TEMPLATE, -+ ResourceRelationType.OUTER_RESOURCE: INNER_RESOURCE_ID_TEMPLATE, -+ }.get(relation_type, "") -+ -+ -+class ResourceRelationNode(object): -+ def __init__(self, entity): -+ self._obj = entity -+ self._members = [] -+ self._is_leaf = False -+ self._parent = None -+ self._opposite_id = _get_opposite_relation_id_template( -+ self._obj.type -+ ).format(self._obj.metadata["id"]) -+ -+ @property -+ def obj(self): -+ return self._obj -+ -+ @property -+ def members(self): -+ return self._members -+ -+ def to_dto(self): -+ return ResourceRelationDto( -+ self._obj, -+ [member.to_dto() for member in self._members], -+ self._is_leaf, -+ ) -+ -+ def stop(self): -+ self._is_leaf = True -+ -+ def add_member(self, member): -+ # pylint: disable=protected-access -+ if member._parent is not None: -+ raise AssertionError( -+ "object {} already has a parent set: {}".format( -+ repr(member), repr(member._parent) -+ ) -+ ) -+ # we don't want opposite relations (inner resource vs outer resource) -+ # in a branch, so we are filtering them out -+ parents = set(self._get_all_parents()) -+ if ( -+ self != member -+ and -+ member.obj.id not in parents -+ and -+ ( -+ member._opposite_id not in parents -+ or -+ len(member.obj.members) > 1 -+ ) -+ ): -+ member._parent = self -+ self._members.append(member) -+ -+ def _get_all_parents(self): -+ # pylint: disable=protected-access -+ if self._parent is None: -+ return [] -+ return self._parent._get_all_parents() + [self._parent.obj.id] -+ -+ -+class ResourceRelationTreeBuilder(object): -+ def __init__(self, resource_entities, relation_entities): -+ self._resources = resource_entities -+ self._all = dict(resource_entities) -+ self._all.update(relation_entities) -+ self._init_structures() -+ -+ def _init_structures(self): -+ self._processed_nodes = set() -+ # queue -+ self._nodes_to_process = [] -+ -+ def get_tree(self, resource_id): -+ self._init_structures() -+ if resource_id not in self._resources: -+ raise AssertionError( -+ "Resource with id '{}' not found in resource " -+ "relation structures".format(resource_id) -+ ) -+ -+ # self._all is a superset of self._resources, see __init__ -+ root = ResourceRelationNode(self._all[resource_id]) -+ self._nodes_to_process.append(root) -+ -+ while self._nodes_to_process: -+ node = self._nodes_to_process.pop(0) -+ if node.obj.id in self._processed_nodes: -+ node.stop() -+ continue -+ self._processed_nodes.add(node.obj.id) -+ for node_id in node.obj.members: -+ node.add_member( -+ ResourceRelationNode(self._all[node_id]) -+ ) -+ self._nodes_to_process.extend(node.members) -+ return root -+ -+ -+class ResourceRelationsFetcher(object): -+ def __init__(self, cib): -+ self._cib = cib -+ self._resources_section = tools.get_resources(self._cib) -+ self._constraints_section = tools.get_constraints(self._cib) -+ -+ def get_relations(self, resource_id): -+ resources_to_process = {resource_id} -+ relations = {} -+ resources = {} -+ while resources_to_process: -+ res_id = resources_to_process.pop() -+ if res_id in resources: -+ # already processed -+ continue -+ res_el = self._get_resource_el(res_id) -+ res_relations = { -+ rel.id: rel for rel in self._get_resource_relations(res_el) -+ } -+ resources[res_id] = RelationEntityDto( -+ res_id, -+ res_el.tag, -+ list(res_relations.keys()), -+ dict(res_el.attrib), -+ ) -+ relations.update(res_relations) -+ resources_to_process.update( -+ self._get_all_members(res_relations.values()) -+ ) -+ return resources, relations -+ -+ def _get_resource_el(self, res_id): -+ # client of this class should ensure that res_id really exists in CIB, -+ # so here we don't need to handle possible reports -+ for tag in TAG_CLONE_ALL + [TAG_GROUP, TAG_PRIMITIVE, TAG_BUNDLE]: -+ element_list = self._resources_section.xpath( -+ './/{}[@id="{}"]'.format(tag, res_id) -+ ) -+ if element_list: -+ return element_list[0] -+ -+ @staticmethod -+ def _get_all_members(relation_list): -+ result = set() -+ for relation in relation_list: -+ result.update(relation.members) -+ return result -+ -+ def _get_resource_relations(self, resource_el): -+ resource_id = resource_el.attrib["id"] -+ relations = [ -+ _get_ordering_constraint_relation(item) -+ for item in self._get_ordering_coinstraints(resource_id) -+ ] + [ -+ _get_ordering_set_constraint_relation(item) -+ for item in self._get_ordering_set_constraints(resource_id) -+ ] -+ -+ # special type of relation, group (note that a group can be a resource -+ # and a relation) -+ if common.is_wrapper_resource(resource_el): -+ relations.append(_get_inner_resources_relation(resource_el)) -+ -+ # handle resources in a wrapper resource (group/bundle/clone relation) -+ parent_el = common.get_parent_resource(resource_el) -+ if parent_el is not None: -+ relations.append(_get_outer_resource_relation(parent_el)) -+ return relations -+ -+ def _get_ordering_coinstraints(self, resource_id): -+ return self._constraints_section.xpath(""" -+ .//rsc_order[ -+ not (descendant::resource_set) -+ and -+ (@first='{0}' or @then='{0}') -+ ] -+ """.format(resource_id)) -+ -+ def _get_ordering_set_constraints(self, resource_id): -+ return self._constraints_section.xpath( -+ ".//rsc_order[./resource_set/resource_ref[@id='{}']]".format( -+ resource_id -+ ) -+ ) -+ -+ -+# relation obj to RelationEntityDto obj -+def _get_inner_resources_relation(parent_resource_el): -+ attrs = parent_resource_el.attrib -+ return RelationEntityDto( -+ INNER_RESOURCE_ID_TEMPLATE.format(attrs["id"]), -+ ResourceRelationType.INNER_RESOURCES, -+ [ -+ res.attrib["id"] -+ for res in common.get_inner_resources(parent_resource_el) -+ ], -+ dict(attrs), -+ ) -+ -+ -+def _get_outer_resource_relation(parent_resource_el): -+ attrs = parent_resource_el.attrib -+ return RelationEntityDto( -+ OUTER_RESOURCE_ID_TEMPLATE.format(attrs["id"]), -+ ResourceRelationType.OUTER_RESOURCE, -+ [attrs["id"]], -+ dict(attrs), -+ ) -+ -+ -+def _get_ordering_constraint_relation(ord_const_el): -+ attrs = ord_const_el.attrib -+ return RelationEntityDto( -+ attrs["id"], -+ ResourceRelationType.ORDER, -+ [attrs["first"], attrs["then"]], -+ dict(attrs), -+ ) -+ -+ -+def _get_ordering_set_constraint_relation(ord_set_const_el): -+ attrs = ord_set_const_el.attrib -+ members = set() -+ metadata = dict(attrs) -+ metadata["sets"] = [] -+ for rsc_set_el in ord_set_const_el.findall("resource_set"): -+ rsc_set = dict( -+ id=rsc_set_el.get("id"), -+ metadata=dict(rsc_set_el.attrib), -+ members=[], -+ ) -+ metadata["sets"].append(rsc_set) -+ for rsc_ref in rsc_set_el.findall("resource_ref"): -+ rsc_id = rsc_ref.attrib["id"] -+ members.add(rsc_id) -+ rsc_set["members"].append(rsc_id) -+ -+ return RelationEntityDto( -+ attrs["id"], ResourceRelationType.ORDER_SET, sorted(members), metadata -+ ) -diff --git a/pcs/lib/cib/test/test_resource_common.py b/pcs/lib/cib/test/test_resource_common.py -index 21380060..596ee57f 100644 ---- a/pcs/lib/cib/test/test_resource_common.py -+++ b/pcs/lib/cib/test/test_resource_common.py -@@ -83,14 +83,14 @@ class IsCloneDeactivatedByMeta(TestCase): - self.assert_is_not_disabled({"clone-node-max": "1.1"}) - - --class FindPrimitives(TestCase): -+class FindResourcesMixin(object): - def assert_find_resources(self, input_resource_id, output_resource_ids): - self.assertEqual( - output_resource_ids, - [ - element.get("id", "") - for element in -- common.find_primitives( -+ self._tested_fn( - fixture_cib.find( - './/*[@id="{0}"]'.format(input_resource_id) - ) -@@ -98,6 +98,31 @@ class FindPrimitives(TestCase): - ] - ) - -+ def test_group(self): -+ self.assert_find_resources("D", ["D1", "D2"]) -+ -+ def test_group_in_clone(self): -+ self.assert_find_resources("E", ["E1", "E2"]) -+ -+ def test_group_in_master(self): -+ self.assert_find_resources("F", ["F1", "F2"]) -+ -+ def test_cloned_primitive(self): -+ self.assert_find_resources("B-clone", ["B"]) -+ -+ def test_mastered_primitive(self): -+ self.assert_find_resources("C-master", ["C"]) -+ -+ def test_bundle_empty(self): -+ self.assert_find_resources("G-bundle", []) -+ -+ def test_bundle_with_primitive(self): -+ self.assert_find_resources("H-bundle", ["H"]) -+ -+ -+class FindPrimitives(TestCase, FindResourcesMixin): -+ _tested_fn = staticmethod(common.find_primitives) -+ - def test_primitive(self): - self.assert_find_resources("A", ["A"]) - -@@ -118,32 +143,155 @@ class FindPrimitives(TestCase): - def test_primitive_in_bundle(self): - self.assert_find_resources("H", ["H"]) - -+ def test_cloned_group(self): -+ self.assert_find_resources("E-clone", ["E1", "E2"]) -+ -+ def test_mastered_group(self): -+ self.assert_find_resources("F-master", ["F1", "F2"]) -+ -+ -+class GetInnerResources(TestCase, FindResourcesMixin): -+ _tested_fn = staticmethod(common.get_inner_resources) -+ -+ def test_primitive(self): -+ self.assert_find_resources("A", []) -+ -+ def test_primitive_in_clone(self): -+ self.assert_find_resources("B", []) -+ -+ def test_primitive_in_master(self): -+ self.assert_find_resources("C", []) -+ -+ def test_primitive_in_group(self): -+ self.assert_find_resources("D1", []) -+ self.assert_find_resources("D2", []) -+ self.assert_find_resources("E1", []) -+ self.assert_find_resources("E2", []) -+ self.assert_find_resources("F1", []) -+ self.assert_find_resources("F2", []) -+ -+ def test_primitive_in_bundle(self): -+ self.assert_find_resources("H", []) -+ -+ def test_mastered_group(self): -+ self.assert_find_resources("F-master", ["F"]) -+ -+ def test_cloned_group(self): -+ self.assert_find_resources("E-clone", ["E"]) -+ -+ -+class IsWrapperResource(TestCase): -+ def assert_is_wrapper(self, res_id, is_wrapper): -+ self.assertEqual( -+ is_wrapper, -+ common.is_wrapper_resource( -+ fixture_cib.find('.//*[@id="{0}"]'.format(res_id)) -+ ) -+ ) -+ -+ def test_primitive(self): -+ self.assert_is_wrapper("A", False) -+ -+ def test_primitive_in_clone(self): -+ self.assert_is_wrapper("B", False) -+ -+ def test_primitive_in_master(self): -+ self.assert_is_wrapper("C", False) -+ -+ def test_primitive_in_group(self): -+ self.assert_is_wrapper("D1", False) -+ self.assert_is_wrapper("D2", False) -+ self.assert_is_wrapper("E1", False) -+ self.assert_is_wrapper("E2", False) -+ self.assert_is_wrapper("F1", False) -+ self.assert_is_wrapper("F2", False) -+ -+ def test_primitive_in_bundle(self): -+ self.assert_is_wrapper("H", False) -+ -+ def test_cloned_group(self): -+ self.assert_is_wrapper("E-clone", True) -+ -+ def test_mastered_group(self): -+ self.assert_is_wrapper("F-master", True) -+ - def test_group(self): -- self.assert_find_resources("D", ["D1", "D2"]) -+ self.assert_is_wrapper("D", True) - - def test_group_in_clone(self): -- self.assert_find_resources("E", ["E1", "E2"]) -+ self.assert_is_wrapper("E", True) - - def test_group_in_master(self): -- self.assert_find_resources("F", ["F1", "F2"]) -+ self.assert_is_wrapper("F", True) - - def test_cloned_primitive(self): -- self.assert_find_resources("B-clone", ["B"]) -- -- def test_cloned_group(self): -- self.assert_find_resources("E-clone", ["E1", "E2"]) -+ self.assert_is_wrapper("B-clone", True) - - def test_mastered_primitive(self): -- self.assert_find_resources("C-master", ["C"]) -+ self.assert_is_wrapper("C-master", True) -+ -+ def test_bundle_empty(self): -+ self.assert_is_wrapper("G-bundle", True) -+ -+ def test_bundle_with_primitive(self): -+ self.assert_is_wrapper("H-bundle", True) -+ -+ -+class GetParentResource(TestCase): -+ def assert_parent_resource(self, input_resource_id, output_resource_id): -+ res_el = common.get_parent_resource(fixture_cib.find( -+ './/*[@id="{0}"]'.format(input_resource_id) -+ )) -+ self.assertEqual( -+ output_resource_id, res_el.get("id") if res_el is not None else None -+ ) -+ -+ def test_primitive(self): -+ self.assert_parent_resource("A", None) -+ -+ def test_primitive_in_clone(self): -+ self.assert_parent_resource("B", "B-clone") -+ -+ def test_primitive_in_master(self): -+ self.assert_parent_resource("C", "C-master") -+ -+ def test_primitive_in_group(self): -+ self.assert_parent_resource("D1", "D") -+ self.assert_parent_resource("D2", "D") -+ self.assert_parent_resource("E1", "E") -+ self.assert_parent_resource("E2", "E") -+ self.assert_parent_resource("F1", "F") -+ self.assert_parent_resource("F2", "F") -+ -+ def test_primitive_in_bundle(self): -+ self.assert_parent_resource("H", "H-bundle") -+ -+ def test_cloned_group(self): -+ self.assert_parent_resource("E-clone", None) - - def test_mastered_group(self): -- self.assert_find_resources("F-master", ["F1", "F2"]) -+ self.assert_parent_resource("F-master", None) -+ -+ def test_group(self): -+ self.assert_parent_resource("D", None) -+ -+ def test_group_in_clone(self): -+ self.assert_parent_resource("E", "E-clone") -+ -+ def test_group_in_master(self): -+ self.assert_parent_resource("F", "F-master") -+ -+ def test_cloned_primitive(self): -+ self.assert_parent_resource("B-clone", None) -+ -+ def test_mastered_primitive(self): -+ self.assert_parent_resource("C-master", None) - - def test_bundle_empty(self): -- self.assert_find_resources("G-bundle", []) -+ self.assert_parent_resource("G-bundle", None) - - def test_bundle_with_primitive(self): -- self.assert_find_resources("H-bundle", ["H"]) -+ self.assert_parent_resource("H-bundle", None) - - - class FindResourcesToEnable(TestCase): -diff --git a/pcs/lib/cib/test/test_resource_relations.py b/pcs/lib/cib/test/test_resource_relations.py -new file mode 100644 -index 00000000..63998962 ---- /dev/null -+++ b/pcs/lib/cib/test/test_resource_relations.py -@@ -0,0 +1,690 @@ -+from lxml import etree -+ -+from pcs.test.tools.pcs_unittest import TestCase -+from pcs.common.pacemaker.resource.relations import ( -+ RelationEntityDto, -+ ResourceRelationDto, -+ ResourceRelationType, -+) -+from pcs.lib.cib.resource import relations as lib -+ -+ -+def fixture_cib(resources, constraints): -+ return etree.fromstring(""" -+ -+ -+ -+ {} -+ -+ -+ {} -+ -+ -+ -+ """.format(resources, constraints)) -+ -+ -+def fixture_dummy_metadata(_id): -+ return { -+ "id": _id, -+ "class": "c", -+ "provider": "pcmk", -+ "type": "Dummy", -+ } -+ -+ -+class ResourceRelationNode(TestCase): -+ @staticmethod -+ def entity_fixture(index): -+ return RelationEntityDto.from_dict(dict( -+ id="ent_id{}".format(index), -+ type="ent_type", -+ members=[ -+ "{}{}".format(index, member) for member in ("m1", "m2", "m0") -+ ], -+ metadata=dict( -+ id="ent_id{}".format(index), -+ k0="val0", -+ k1="val1", -+ ) -+ )) -+ -+ def test_no_members(self): -+ ent = self.entity_fixture("0") -+ obj = lib.ResourceRelationNode(ent) -+ self.assertEqual( -+ ResourceRelationDto(ent, [], False).to_dict(), -+ obj.to_dto().to_dict(), -+ ) -+ -+ def test_with_members(self): -+ ent0 = self.entity_fixture("0") -+ ent1 = self.entity_fixture("1") -+ ent2 = self.entity_fixture("2") -+ ent3 = self.entity_fixture("3") -+ obj = lib.ResourceRelationNode(ent0) -+ obj.add_member(lib.ResourceRelationNode(ent1)) -+ member = lib.ResourceRelationNode(ent2) -+ member.add_member(lib.ResourceRelationNode(ent3)) -+ obj.add_member(member) -+ self.assertEqual( -+ ResourceRelationDto( -+ ent0, -+ [ -+ ResourceRelationDto(ent1, [], False), -+ ResourceRelationDto( -+ ent2, -+ [ -+ ResourceRelationDto(ent3, [], False), -+ ], -+ False, -+ ), -+ ], -+ False, -+ ).to_dict(), -+ obj.to_dto().to_dict(), -+ ) -+ -+ def test_stop(self): -+ ent = self.entity_fixture("0") -+ obj = lib.ResourceRelationNode(ent) -+ obj.stop() -+ self.assertEqual( -+ ResourceRelationDto(ent, [], True).to_dict(), -+ obj.to_dto().to_dict(), -+ ) -+ -+ def test_add_member(self): -+ ent0 = self.entity_fixture("0") -+ ent1 = self.entity_fixture("1") -+ obj = lib.ResourceRelationNode(ent0) -+ obj.add_member(lib.ResourceRelationNode(ent1)) -+ self.assertEqual( -+ ResourceRelationDto( -+ ent0, [ResourceRelationDto(ent1, [], False)], False, -+ ).to_dict(), -+ obj.to_dto().to_dict(), -+ ) -+ -+ def test_add_member_itself(self): -+ ent = self.entity_fixture("0") -+ obj = lib.ResourceRelationNode(ent) -+ obj.add_member(obj) -+ self.assertEqual( -+ ResourceRelationDto(ent, [], False).to_dict(), -+ obj.to_dto().to_dict(), -+ ) -+ -+ def test_add_member_already_have_parent(self): -+ obj0 = lib.ResourceRelationNode(self.entity_fixture("0")) -+ obj1 = lib.ResourceRelationNode(self.entity_fixture("1")) -+ obj2 = lib.ResourceRelationNode(self.entity_fixture("2")) -+ obj0.add_member(obj1) -+ with self.assertRaises(AssertionError): -+ obj2.add_member(obj1) -+ -+ def test_add_member_already_in_branch(self): -+ ent0 = self.entity_fixture("0") -+ ent1 = self.entity_fixture("1") -+ obj0 = lib.ResourceRelationNode(ent0) -+ obj1 = lib.ResourceRelationNode(ent1) -+ obj0.add_member(obj1) -+ obj1.add_member(obj0) -+ self.assertEqual( -+ ResourceRelationDto( -+ ent0, [ResourceRelationDto(ent1, [], False)], False -+ ).to_dict(), -+ obj0.to_dto().to_dict(), -+ ) -+ -+ def test_add_member_already_in_different_branch(self): -+ ent0 = self.entity_fixture("0") -+ ent1 = self.entity_fixture("1") -+ obj0 = lib.ResourceRelationNode(ent0) -+ obj0.add_member(lib.ResourceRelationNode(ent1)) -+ obj0.add_member(lib.ResourceRelationNode(ent1)) -+ self.assertEqual( -+ ResourceRelationDto( -+ ent0, [ -+ ResourceRelationDto(ent1, [], False), -+ ResourceRelationDto(ent1, [], False), -+ ], -+ False, -+ ).to_dict(), -+ obj0.to_dto().to_dict(), -+ ) -+ -+ -+class ResourceRelationsFetcher(TestCase): -+ def test_ordering_constraint(self): -+ obj = lib.ResourceRelationsFetcher(fixture_cib( -+ """ -+ -+ -+ """, -+ """ -+ -+ """ -+ )) -+ expected = ( -+ { -+ "d1": RelationEntityDto( -+ "d1", -+ "primitive", -+ ["order-d1-d2-mandatory"], -+ fixture_dummy_metadata("d1"), -+ ), -+ "d2": RelationEntityDto( -+ "d2", -+ "primitive", -+ ["order-d1-d2-mandatory"], -+ fixture_dummy_metadata("d2"), -+ ), -+ }, -+ { -+ "order-d1-d2-mandatory": RelationEntityDto( -+ "order-d1-d2-mandatory", -+ ResourceRelationType.ORDER, -+ members=["d1", "d2"], -+ metadata={ -+ "id": "order-d1-d2-mandatory", -+ "first": "d1", -+ "first-action": "start", -+ "then": "d2", -+ "then-action": "start", -+ "kind": "Mandatory", -+ }, -+ ), -+ }, -+ ) -+ for res in ("d1", "d2"): -+ self.assertEqual(expected, obj.get_relations(res)) -+ -+ def test_ordering_set_constraint(self): -+ obj = lib.ResourceRelationsFetcher(fixture_cib( -+ """ -+ -+ -+ -+ -+ -+ -+ """, -+ """ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ """ -+ )) -+ rsc_entity = lambda _id: RelationEntityDto( -+ _id, -+ "primitive", -+ ["pcs_rsc_order_set_1"], -+ fixture_dummy_metadata(_id), -+ ) -+ res_list = ("d1", "d2", "d3", "d4", "d5", "d6") -+ expected = ( -+ {_id: rsc_entity(_id) for _id in res_list}, -+ { -+ "pcs_rsc_order_set_1": RelationEntityDto( -+ "pcs_rsc_order_set_1", -+ ResourceRelationType.ORDER_SET, -+ members=["d1", "d2", "d3", "d4", "d5", "d6"], -+ metadata={ -+ "id": "pcs_rsc_order_set_1", -+ "sets": [ -+ { -+ "id": "pcs_rsc_set_1", -+ "metadata": { -+ "id": "pcs_rsc_set_1", -+ "sequential": "true", -+ "require-all": "true", -+ "action": "start", -+ }, -+ "members": ["d1", "d3", "d2"], -+ }, -+ { -+ "id": "pcs_rsc_set_2", -+ "metadata": { -+ "id": "pcs_rsc_set_2", -+ "sequential": "false", -+ "require-all": "false", -+ "action": "stop", -+ }, -+ "members": ["d6", "d5", "d4"], -+ }, -+ ], -+ "kind": "Serialize", -+ "symmetrical": "true", -+ }, -+ ), -+ }, -+ ) -+ for res in res_list: -+ self.assertEqual(expected, obj.get_relations(res)) -+ -+ def test_group(self): -+ obj = lib.ResourceRelationsFetcher(fixture_cib( -+ """ -+ -+ -+ -+ -+ """, -+ "" -+ )) -+ expected = ( -+ { -+ "d1": RelationEntityDto( -+ "d1", -+ "primitive", -+ ["outer:g1"], -+ fixture_dummy_metadata("d1"), -+ ), -+ "d2": RelationEntityDto( -+ "d2", -+ "primitive", -+ ["outer:g1"], -+ fixture_dummy_metadata("d2"), -+ ), -+ "g1": RelationEntityDto( -+ "g1", "group", ["inner:g1"], {"id": "g1"} -+ ), -+ }, -+ { -+ "inner:g1": RelationEntityDto( -+ "inner:g1", -+ ResourceRelationType.INNER_RESOURCES, -+ ["d1", "d2"], -+ {"id": "g1"}, -+ ), -+ "outer:g1": RelationEntityDto( -+ "outer:g1", -+ ResourceRelationType.OUTER_RESOURCE, -+ ["g1"], -+ {"id": "g1"}, -+ ), -+ }, -+ ) -+ for res in ("d1", "d2", "g1"): -+ self.assertEqual(expected, obj.get_relations(res)) -+ -+ def _test_wrapper(self, wrapper_tag): -+ obj = lib.ResourceRelationsFetcher(fixture_cib( -+ """ -+ <{0} id="w1"> -+ -+ -+ """.format(wrapper_tag), -+ "" -+ )) -+ expected = ( -+ { -+ "d1": RelationEntityDto( -+ "d1", -+ "primitive", -+ ["outer:w1"], -+ fixture_dummy_metadata("d1"), -+ ), -+ "w1": RelationEntityDto( -+ "w1", wrapper_tag, ["inner:w1"], {"id": "w1"} -+ ), -+ }, -+ { -+ "inner:w1": RelationEntityDto( -+ "inner:w1", -+ ResourceRelationType.INNER_RESOURCES, -+ ["d1"], -+ {"id": "w1"}, -+ ), -+ "outer:w1": RelationEntityDto( -+ "outer:w1", -+ ResourceRelationType.OUTER_RESOURCE, -+ ["w1"], -+ {"id": "w1"}, -+ ), -+ }, -+ ) -+ for res in ("d1", "w1"): -+ self.assertEqual(expected, obj.get_relations(res)) -+ -+ def test_clone(self): -+ self._test_wrapper("clone") -+ -+ def test_bundle(self): -+ self._test_wrapper("bundle") -+ -+ def test_cloned_group(self): -+ obj = lib.ResourceRelationsFetcher(fixture_cib( -+ """ -+ -+ -+ -+ -+ -+ -+ """, -+ "" -+ )) -+ expected = ( -+ { -+ "d1": RelationEntityDto( -+ "d1", -+ "primitive", -+ ["outer:g1"], -+ fixture_dummy_metadata("d1"), -+ ), -+ "d2": RelationEntityDto( -+ "d2", -+ "primitive", -+ ["outer:g1"], -+ fixture_dummy_metadata("d2"), -+ ), -+ "g1": RelationEntityDto( -+ "g1", "group", ["outer:c1", "inner:g1"], {"id": "g1"} -+ ), -+ "c1": RelationEntityDto( -+ "c1", "clone", ["inner:c1"], {"id": "c1"} -+ ) -+ }, -+ { -+ "inner:g1": RelationEntityDto( -+ "inner:g1", -+ ResourceRelationType.INNER_RESOURCES, -+ ["d1", "d2"], -+ {"id": "g1"}, -+ ), -+ "outer:g1": RelationEntityDto( -+ "outer:g1", -+ ResourceRelationType.OUTER_RESOURCE, -+ ["g1"], -+ {"id": "g1"}, -+ ), -+ "inner:c1": RelationEntityDto( -+ "inner:c1", -+ ResourceRelationType.INNER_RESOURCES, -+ ["g1"], -+ {"id": "c1"} -+ ), -+ "outer:c1": RelationEntityDto( -+ "outer:c1", -+ ResourceRelationType.OUTER_RESOURCE, -+ ["c1"], -+ {"id": "c1"} -+ ), -+ }, -+ ) -+ for res in ("d1", "d2", "g1", "c1"): -+ self.assertEqual(expected, obj.get_relations(res)) -+ -+ -+class ResourceRelationTreeBuilder(TestCase): -+ @staticmethod -+ def primitive_fixture(_id, members): -+ return RelationEntityDto( -+ _id, "primitive", members, fixture_dummy_metadata(_id) -+ ) -+ -+ def test_resource_not_present(self): -+ with self.assertRaises(AssertionError): -+ lib.ResourceRelationTreeBuilder({}, {}).get_tree("not_existing") -+ -+ def test_simple_order(self): -+ resources_members = ["order-d1-d2-mandatory"] -+ resources = { -+ "d1": self.primitive_fixture("d1", resources_members), -+ "d2": self.primitive_fixture("d2", resources_members), -+ } -+ relations = { -+ "order-d1-d2-mandatory": RelationEntityDto( -+ "order-d1-d2-mandatory", -+ ResourceRelationType.ORDER, -+ members=["d1", "d2"], -+ metadata={ -+ "id": "order-d1-d2-mandatory", -+ "first": "d1", -+ "first-action": "start", -+ "then": "d2", -+ "then-action": "start", -+ "kind": "Mandatory", -+ }, -+ ), -+ } -+ expected = dict( -+ relation_entity=resources["d2"].to_dict(), -+ is_leaf=False, -+ members=[ -+ dict( -+ relation_entity=( -+ relations["order-d1-d2-mandatory"].to_dict() -+ ), -+ is_leaf=False, -+ members=[ -+ dict( -+ relation_entity=resources["d1"].to_dict(), -+ is_leaf=False, -+ members=[], -+ ) -+ ] -+ ) -+ ], -+ ) -+ self.assertEqual( -+ expected, -+ lib.ResourceRelationTreeBuilder( -+ resources, relations -+ ).get_tree("d2").to_dto().to_dict() -+ ) -+ -+ def test_simple_order_set(self): -+ res_list = ("d1", "d2", "d3", "d4", "d5", "d6") -+ resources_members = ["pcs_rsc_order_set_1"] -+ resources = { -+ _id: self.primitive_fixture(_id, resources_members) -+ for _id in res_list -+ } -+ relations = { -+ "pcs_rsc_order_set_1": RelationEntityDto( -+ "pcs_rsc_order_set_1", -+ ResourceRelationType.ORDER_SET, -+ members=["d1", "d2", "d3", "d4", "d5", "d6"], -+ metadata={ -+ "id": "pcs_rsc_order_set_1", -+ "sets": [ -+ { -+ "id": "pcs_rsc_set_1", -+ "metadata": { -+ "id": "pcs_rsc_set_1", -+ "sequential": "true", -+ "require-all": "true", -+ "action": "start", -+ }, -+ "members": ["d1", "d3", "d2"], -+ }, -+ { -+ "id": "pcs_rsc_set_2", -+ "metadata": { -+ "id": "pcs_rsc_set_2", -+ "sequential": "false", -+ "require-all": "false", -+ "action": "stop", -+ }, -+ "members": ["d6", "d5", "d4"], -+ }, -+ ], -+ "kind": "Serialize", -+ "symmetrical": "true", -+ }, -+ ), -+ } -+ get_res = lambda _id: dict( -+ relation_entity=resources[_id].to_dict(), -+ is_leaf=False, -+ members=[], -+ ) -+ expected = dict( -+ relation_entity=resources["d5"].to_dict(), -+ is_leaf=False, -+ members=[ -+ dict( -+ relation_entity=relations["pcs_rsc_order_set_1"].to_dict(), -+ is_leaf=False, -+ members=[ -+ get_res(_id) for _id in ("d1", "d2", "d3", "d4", "d6") -+ ] -+ ) -+ ], -+ ) -+ self.assertEqual( -+ expected, -+ lib.ResourceRelationTreeBuilder( -+ resources, relations -+ ).get_tree("d5").to_dto().to_dict() -+ ) -+ -+ def test_simple_in_group(self): -+ resources_members = ["outer:g1"] -+ resources = { -+ "d1": self.primitive_fixture("d1", resources_members), -+ "d2": self.primitive_fixture("d2", resources_members), -+ "g1": RelationEntityDto( -+ "g1", "group", ["inner:g1"], {"id": "g1"} -+ ), -+ } -+ relations = { -+ "inner:g1": RelationEntityDto( -+ "inner:g1", -+ ResourceRelationType.INNER_RESOURCES, -+ ["d1", "d2"], -+ {"id": "g1"}, -+ ), -+ "outer:g1": RelationEntityDto( -+ "outer:g1", -+ ResourceRelationType.OUTER_RESOURCE, -+ ["g1"], -+ {"id": "g1"}, -+ ), -+ } -+ expected = dict( -+ relation_entity=resources["d1"].to_dict(), -+ is_leaf=False, -+ members=[ -+ dict( -+ relation_entity=relations["outer:g1"].to_dict(), -+ is_leaf=False, -+ members=[ -+ dict( -+ relation_entity=resources["g1"].to_dict(), -+ is_leaf=False, -+ members=[ -+ dict( -+ relation_entity=( -+ relations["inner:g1"].to_dict() -+ ), -+ is_leaf=False, -+ members=[ -+ dict( -+ relation_entity=( -+ resources["d2"].to_dict() -+ ), -+ is_leaf=False, -+ members=[], -+ ), -+ ], -+ ), -+ ], -+ ), -+ ], -+ ), -+ ], -+ ) -+ self.assertEqual( -+ expected, -+ lib.ResourceRelationTreeBuilder( -+ resources, relations -+ ).get_tree("d1").to_dto().to_dict() -+ ) -+ -+ def test_order_loop(self): -+ resources_members = ["order-d1-d2-mandatory", "order-d2-d1-mandatory"] -+ resources = { -+ "d1": self.primitive_fixture("d1", resources_members), -+ "d2": self.primitive_fixture("d2", resources_members), -+ } -+ order_fixture = lambda r1, r2: RelationEntityDto( -+ "order-{}-{}-mandatory".format(r1, r2), -+ ResourceRelationType.ORDER, -+ members=[r1, r2], -+ metadata={ -+ "id": "order-{}-{}-mandatory".format(r1, r2), -+ "first": r1, -+ "first-action": "start", -+ "then": r2, -+ "then-action": "start", -+ "kind": "Mandatory", -+ }, -+ ) -+ relations = { -+ "order-d1-d2-mandatory": order_fixture("d1", "d2"), -+ "order-d2-d1-mandatory": order_fixture("d2", "d1"), -+ } -+ expected = dict( -+ relation_entity=resources["d1"].to_dict(), -+ is_leaf=False, -+ members=[ -+ dict( -+ relation_entity=( -+ relations["order-d1-d2-mandatory"].to_dict() -+ ), -+ is_leaf=False, -+ members=[ -+ dict( -+ relation_entity=resources["d2"].to_dict(), -+ is_leaf=False, -+ members=[ -+ dict( -+ relation_entity=relations[ -+ "order-d2-d1-mandatory" -+ ].to_dict(), -+ is_leaf=True, -+ members=[], -+ ), -+ ], -+ ), -+ ], -+ ), -+ dict( -+ relation_entity=( -+ relations["order-d2-d1-mandatory"].to_dict() -+ ), -+ is_leaf=False, -+ members=[ -+ dict( -+ relation_entity=resources["d2"].to_dict(), -+ is_leaf=True, -+ members=[], -+ ), -+ ], -+ ), -+ ], -+ ) -+ self.assertEqual( -+ expected, -+ lib.ResourceRelationTreeBuilder( -+ resources, relations -+ ).get_tree("d1").to_dto().to_dict() -+ ) -diff --git a/pcs/lib/commands/resource.py b/pcs/lib/commands/resource.py -index 0073964d..09a68a68 100644 ---- a/pcs/lib/commands/resource.py -+++ b/pcs/lib/commands/resource.py -@@ -897,6 +897,28 @@ def get_failcounts( - interval=interval_ms - ) - -+ -+def get_resource_relations_tree(env, resource_id): -+ """ -+ 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() -+ _find_resources_or_raise(get_resources(cib), [resource_id]) -+ resources_dict, relations_dict = ( -+ resource.relations.ResourceRelationsFetcher( -+ cib -+ ).get_relations(resource_id) -+ ) -+ return resource.relations.ResourceRelationTreeBuilder( -+ resources_dict, relations_dict -+ ).get_tree(resource_id).to_dto().to_dict() -+ -+ - def _find_resources_or_raise( - resources_section, resource_ids, additional_search=None - ): -diff --git a/pcs/lib/commands/test/resource/test_resource_relations.py b/pcs/lib/commands/test/resource/test_resource_relations.py -new file mode 100644 -index 00000000..404780f3 ---- /dev/null -+++ b/pcs/lib/commands/test/resource/test_resource_relations.py -@@ -0,0 +1,300 @@ -+from pcs.test.tools.pcs_unittest import TestCase -+from pcs.test.tools import fixture -+from pcs.test.tools.command_env import get_env_tools -+ -+from pcs.common.pacemaker.resource.relations import ResourceRelationType -+from pcs.lib.commands import resource -+ -+ -+def fixture_primitive(_id, members): -+ return dict( -+ id=_id, -+ type="primitive", -+ metadata={ -+ "id": _id, -+ "class": "ocf", -+ "provider": "pacemaker", -+ "type": "Dummy", -+ }, -+ members=members, -+ ) -+ -+ -+def fixture_primitive_xml(_id): -+ return """ -+ -+ """.format(_id) -+ -+def fixture_node(entity, members=None, leaf=False): -+ return dict( -+ relation_entity=entity, -+ is_leaf=leaf, -+ members=members or [], -+ ) -+ -+def fixture_order(res1, res2, kind="Mandatory", score=None): -+ _id = "order-{}-{}".format(res1, res2) -+ out = dict( -+ id=_id, -+ type=ResourceRelationType.ORDER, -+ members=[res1, res2], -+ metadata={ -+ "id": _id, -+ "first": res1, -+ "first-action": "start", -+ "then": res2, -+ "then-action": "start", -+ "kind": kind, -+ }, -+ ) -+ if score: -+ out["metadata"]["score"] = score -+ return out -+ -+ -+class GetResourceRelationsTree(TestCase): -+ def setUp(self): -+ self.env_assist, self.config = get_env_tools(test_case=self) -+ -+ def test_not_existing_resource(self): -+ self.config.runner.cib.load() -+ resource_id = "not_existing" -+ self.env_assist.assert_raise_library_error( -+ lambda: resource.get_resource_relations_tree( -+ self.env_assist.get_env(), -+ resource_id, -+ ), -+ [ -+ fixture.report_not_found(resource_id, context_type="resources"), -+ ], -+ expected_in_processor=False, -+ ) -+ -+ def test_simple(self): -+ self.config.runner.cib.load( -+ resources="{}".format( -+ fixture_primitive_xml("d1") + fixture_primitive_xml("d2") -+ ), -+ constraints=""" -+ -+ -+ -+ """, -+ ) -+ prim_members = ["order-d1-d2"] -+ expected = fixture_node( -+ fixture_primitive("d1", prim_members), -+ [ -+ fixture_node( -+ fixture_order("d1", "d2"), -+ [fixture_node(fixture_primitive("d2", prim_members))], -+ ) -+ ], -+ ) -+ self.assertEqual( -+ expected, -+ resource.get_resource_relations_tree( -+ self.env_assist.get_env(), "d1" -+ ) -+ ) -+ -+class GetResourceRelationsTreeComplex(TestCase): -+ def setUp(self): -+ self.env_assist, self.config = get_env_tools(test_case=self) -+ self.config.runner.cib.load( -+ resources=""" -+ -+ {primitives} -+ -+ -+ {in_group} -+ -+ -+ -+ """.format( -+ primitives=( -+ fixture_primitive_xml("d1") + -+ fixture_primitive_xml("d2") + -+ fixture_primitive_xml("d3") -+ ), -+ in_group=( -+ fixture_primitive_xml("cgd1") + -+ fixture_primitive_xml("cgd2") + -+ fixture_primitive_xml("cgd0") -+ ) -+ ), -+ constraints=""" -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ """, -+ ) -+ self.d1_members = ["order-d1-d2", "pcs_rsc_order_set_1"] -+ self.d2_members = [ -+ "order-d1-d2", "pcs_rsc_order_set_1", "order-cgd1-d2", -+ ] -+ self.order_set = dict( -+ id="pcs_rsc_order_set_1", -+ type=ResourceRelationType.ORDER_SET, -+ members=["cg", "d1", "d2", "d3"], -+ metadata={ -+ "id": "pcs_rsc_order_set_1", -+ "sets": [ -+ { -+ "id": "pcs_rsc_set_1", -+ "metadata": { -+ "id": "pcs_rsc_set_1", -+ "sequential": "true", -+ "require-all": "true", -+ "action": "start", -+ }, -+ "members": ["d1", "d3"], -+ }, -+ { -+ "id": "pcs_rsc_set_2", -+ "metadata": { -+ "id": "pcs_rsc_set_2", -+ "sequential": "false", -+ "require-all": "false", -+ "action": "stop", -+ }, -+ "members": ["cg", "d2"], -+ }, -+ ], -+ "kind": "Serialize", -+ "symmetrical": "true", -+ }, -+ ) -+ self.cg_ent = dict( -+ id="cg", -+ type="group", -+ members=["inner:cg", "pcs_rsc_order_set_1", "outer:c"], -+ metadata=dict(id="cg"), -+ ) -+ -+ def test_d1(self): -+ outer_cg = dict( -+ id="outer:cg", -+ type=ResourceRelationType.OUTER_RESOURCE, -+ members=["cg"], -+ metadata=dict(id="cg"), -+ ) -+ order_opt = fixture_order("cgd1", "d2", kind="Optional", score="10") -+ expected = fixture_node( -+ fixture_primitive("d1", self.d1_members), [ -+ fixture_node( -+ fixture_order("d1", "d2"), [ -+ fixture_node( -+ fixture_primitive("d2", self.d2_members), [ -+ fixture_node(self.order_set, leaf=True), -+ fixture_node( -+ order_opt, [ -+ fixture_node( -+ fixture_primitive( -+ "cgd1", -+ ["order-cgd1-d2", "outer:cg"], -+ ), [ -+ fixture_node( -+ outer_cg, [ -+ fixture_node( -+ self.cg_ent, -+ leaf=True, -+ ), -+ ] -+ ) -+ ] -+ ), -+ ], -+ ), -+ ], -+ ) -+ ] -+ ), -+ fixture_node( -+ self.order_set, [ -+ fixture_node( -+ self.cg_ent, [ -+ fixture_node( -+ dict( -+ id="inner:cg", -+ type=( -+ ResourceRelationType.INNER_RESOURCES -+ ), -+ members=["cgd1", "cgd2", "cgd0"], -+ metadata=dict(id="cg"), -+ ), [ -+ fixture_node( -+ fixture_primitive( -+ "cgd1", -+ ["order-cgd1-d2", "outer:cg"], -+ ), -+ leaf=True, -+ ), -+ fixture_node( -+ fixture_primitive( -+ "cgd2", ["outer:cg"] -+ ), -+ ), -+ fixture_node( -+ fixture_primitive( -+ "cgd0", ["outer:cg"] -+ ), -+ ), -+ ] -+ ), -+ fixture_node( -+ dict( -+ id="outer:c", -+ type=( -+ ResourceRelationType.OUTER_RESOURCE -+ ), -+ members=["c"], -+ metadata=dict(id="c") -+ ), [ -+ fixture_node( -+ dict( -+ id="c", -+ type="clone", -+ members=["inner:c"], -+ metadata=dict(id="c"), -+ ), -+ [] -+ ) -+ ] -+ ) -+ ], -+ ), -+ fixture_node( -+ fixture_primitive("d2", self.d2_members), leaf=True -+ ), -+ fixture_node( -+ fixture_primitive("d3", ["pcs_rsc_order_set_1"]), -+ ) -+ ], -+ ) -+ ], -+ ) -+ self.assertEqual( -+ expected, -+ resource.get_resource_relations_tree( -+ self.env_assist.get_env(), "d1" -+ ) -+ ) -diff --git a/pcs/pcs.8 b/pcs/pcs.8 -index f08b5e46..0e8b15f7 100644 ---- a/pcs/pcs.8 -+++ b/pcs/pcs.8 -@@ -209,6 +209,9 @@ Remove all constraints created by the 'relocate run' command. - .TP - utilization [ [= ...]] - Add specified utilization options to specified resource. If resource is not specified, shows utilization of all resources. If utilization options are not specified, shows utilization of specified resource. 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 resource utilization TestResource cpu= ram=20 -+.TP -+relations [\fB\-\-full\fR] -+Display relations of a resource specified by its id with other resources in a tree structure. Supported types of resource relations are: ordering constraints, ordering set constraints, relations defined by resource hierarchy (clones, groups, bundles). If \fB\-\-full\fR is used, more verbose output will be printed. - .SS "cluster" - .TP - auth [[:]] [...] [\fB\-u\fR ] [\fB\-p\fR ] [\fB\-\-force\fR] [\fB\-\-local\fR] -diff --git a/pcs/resource.py b/pcs/resource.py -index d3761327..27af2405 100644 ---- a/pcs/resource.py -+++ b/pcs/resource.py -@@ -28,6 +28,7 @@ from pcs.cli.resource.parse_args import ( - parse_bundle_update_options, - parse_create as parse_create_args, - ) -+from pcs.cli.resource.relations import show_resource_relations_cmd - import pcs.lib.cib.acl as lib_acl - from pcs.lib.cib.resource import ( - guest_node, -@@ -200,6 +201,8 @@ def resource_cmd(argv): - get_resource_agent_info(argv_next) - elif sub_cmd == "bundle": - resource_bundle_cmd(lib, argv_next, modifiers) -+ elif sub_cmd == "relations": -+ show_resource_relations_cmd(lib, argv_next, modifiers) - else: - usage.resource() - sys.exit(1) -diff --git a/pcs/test/tools/fixture_cib.py b/pcs/test/tools/fixture_cib.py -index 907650a7..04be4d09 100644 ---- a/pcs/test/tools/fixture_cib.py -+++ b/pcs/test/tools/fixture_cib.py -@@ -132,6 +132,9 @@ MODIFIER_GENERATORS = { - "append": append_all, - "resources": lambda xml: replace_all({"./configuration/resources": xml}), - "optional_in_conf": lambda xml: put_or_replace("./configuration", xml), -+ "constraints": lambda xml: replace_all( -+ {"./configuration/constraints": xml} -+ ), - #common modifier `put_or_replace` makes not sense - see explanation inside - #this function - all occurences should be satisfied by `optional_in_conf` - } -diff --git a/pcs/usage.py b/pcs/usage.py -index 37c92d26..582bc53f 100644 ---- a/pcs/usage.py -+++ b/pcs/usage.py -@@ -552,6 +552,13 @@ Commands: - integer. Options may be removed by setting an option without a value. - Example: pcs resource utilization TestResource cpu= ram=20 - -+ relations [--full] -+ Display relations of a resource specified by its id with other resources -+ in a tree structure. Supported types of resource relations are: -+ ordering constraints, ordering set constraints, relations defined by -+ resource hierarchy (clones, groups, bundles). If --full is used, more -+ verbose output will be printed. -+ - Examples: - - pcs resource show -diff --git a/pcsd/capabilities.xml b/pcsd/capabilities.xml -index 80fcd33b..bc1d69c8 100644 ---- a/pcsd/capabilities.xml -+++ b/pcsd/capabilities.xml -@@ -1261,6 +1261,14 @@ - pcs commands: resource restart - - -+ -+ -+ Display relations of a resource specified by its id with other resources -+ in a tree structure. -+ -+ pcs commands: resource relations -+ -+ - - - -diff --git a/test/centos7/Dockerfile b/test/centos7/Dockerfile -index 2969f8d5..9313dd1f 100644 ---- a/test/centos7/Dockerfile -+++ b/test/centos7/Dockerfile -@@ -10,7 +10,7 @@ RUN yum install -y \ - python \ - python-devel \ - python-lxml \ -- python-mock \ -+ python-pip \ - python-pycurl \ - # ruby - ruby \ -@@ -40,6 +40,8 @@ RUN yum install -y \ - fence-virt \ - booth-site - -+RUN pip install mock -+ - COPY . $src_path - - # build ruby gems --- -2.20.1 - diff --git a/SOURCES/bz1820813-01-use-subprocess32-instead-of-subprocess.patch b/SOURCES/bz1820813-01-use-subprocess32-instead-of-subprocess.patch new file mode 100644 index 0000000..e90e9ab --- /dev/null +++ b/SOURCES/bz1820813-01-use-subprocess32-instead-of-subprocess.patch @@ -0,0 +1,68 @@ +From 3b0cf1b45a1cad222eb089944df5c531d6da018d Mon Sep 17 00:00:00 2001 +From: Ivan Devat +Date: Mon, 20 Apr 2020 12:22:05 +0200 +Subject: [PATCH 6/6] use subprocess32 instead of subprocess + +See rhbz#1820813 for details. +--- + pcs/cluster.py | 2 +- + pcs/lib/external.py | 2 +- + pcs/test/test_lib_external.py | 2 +- + pcs/utils.py | 2 +- + 4 files changed, 4 insertions(+), 4 deletions(-) + +diff --git a/pcs/cluster.py b/pcs/cluster.py +index e8fa0ce4..97414f83 100644 +--- a/pcs/cluster.py ++++ b/pcs/cluster.py +@@ -6,7 +6,7 @@ from __future__ import ( + + import math + import os +-import subprocess ++import subprocess32 as subprocess + import re + import sys + import socket +diff --git a/pcs/lib/external.py b/pcs/lib/external.py +index 852929b2..c399256b 100644 +--- a/pcs/lib/external.py ++++ b/pcs/lib/external.py +@@ -17,7 +17,7 @@ except ImportError: + from shlex import quote as shell_quote + import re + import signal +-import subprocess ++import subprocess32 as subprocess + import sys + try: + # python2 +diff --git a/pcs/test/test_lib_external.py b/pcs/test/test_lib_external.py +index 827cb801..58655cc8 100644 +--- a/pcs/test/test_lib_external.py ++++ b/pcs/test/test_lib_external.py +@@ -35,7 +35,7 @@ _service = settings.service_binary + _systemctl = settings.systemctl_binary + + +-@mock.patch("subprocess.Popen", autospec=True) ++@mock.patch("subprocess32.Popen", autospec=True) + class CommandRunnerTest(TestCase): + def setUp(self): + self.mock_logger = mock.MagicMock(logging.Logger) +diff --git a/pcs/utils.py b/pcs/utils.py +index 793f0b5e..9ffc39ed 100644 +--- a/pcs/utils.py ++++ b/pcs/utils.py +@@ -6,7 +6,7 @@ from __future__ import ( + + import os + import sys +-import subprocess ++import subprocess32 as subprocess + import xml.dom.minidom + from xml.dom.minidom import parseString, parse + import xml.etree.ElementTree as ET +-- +2.21.0 + diff --git a/SOURCES/bz1824206-01-Keep-autogenerated-IDs-of-set-constraints-short.patch b/SOURCES/bz1824206-01-Keep-autogenerated-IDs-of-set-constraints-short.patch new file mode 100644 index 0000000..ccbf2ce --- /dev/null +++ b/SOURCES/bz1824206-01-Keep-autogenerated-IDs-of-set-constraints-short.patch @@ -0,0 +1,815 @@ +From e2420c1912b126e2be1818cf6fa52af595ad553b Mon Sep 17 00:00:00 2001 +From: Tomas Jelinek +Date: Fri, 17 Apr 2020 13:27:46 +0200 +Subject: [PATCH 1/6] Keep autogenerated IDs of set constraints short + +--- + pcs/lib/cib/constraint/colocation.py | 2 +- + pcs/lib/cib/constraint/constraint.py | 18 +- + pcs/lib/cib/constraint/order.py | 2 +- + pcs/lib/cib/constraint/resource_set.py | 5 +- + pcs/lib/cib/constraint/ticket.py | 2 +- + pcs/lib/cib/test/test_constraint.py | 14 +- + .../cib/test/test_constraint_colocation.py | 2 +- + pcs/lib/cib/test/test_constraint_order.py | 2 +- + pcs/lib/cib/test/test_constraint_ticket.py | 2 +- + pcs/lib/cib/test/test_resource_set.py | 12 +- + .../commands/test/test_constraint_common.py | 20 +- + pcs/test/test_constraints.py | 202 +++++++++--------- + 12 files changed, 141 insertions(+), 142 deletions(-) + +diff --git a/pcs/lib/cib/constraint/colocation.py b/pcs/lib/cib/constraint/colocation.py +index 8006a36f..d19522ab 100644 +--- a/pcs/lib/cib/constraint/colocation.py ++++ b/pcs/lib/cib/constraint/colocation.py +@@ -20,7 +20,7 @@ def prepare_options_with_set(cib, options, resource_set_list): + options = constraint.prepare_options( + tuple(SCORE_NAMES), + options, +- partial(constraint.create_id, cib, TAG_NAME, resource_set_list), ++ partial(constraint.create_id, cib, "colocation", resource_set_list), + partial(check_new_id_applicable, cib, DESCRIPTION), + ) + +diff --git a/pcs/lib/cib/constraint/constraint.py b/pcs/lib/cib/constraint/constraint.py +index 02af9d02..936918c2 100644 +--- a/pcs/lib/cib/constraint/constraint.py ++++ b/pcs/lib/cib/constraint/constraint.py +@@ -98,11 +98,19 @@ def export_plain(element): + return {"options": export_attributes(element)} + + def create_id(cib, type_prefix, resource_set_list): +- id = "pcs_" +type_prefix +"".join([ +- "_set_"+"_".join(id_set) +- for id_set in resource_set.extract_id_set_list(resource_set_list) +- ]) +- return find_unique_id(cib, id) ++ # Create a semi-random id. We need it to be predictable (for testing), short ++ # and somehow different than other ids so that we don't spend much time in ++ # find_unique_id. ++ # Avoid using actual resource names. It makes the id very long (consider 10 ++ # or more resources in a set constraint). Also, if a resource is deleted ++ # and therefore removed from the constraint, the id no longer matches the ++ # constraint. ++ resource_ids = [] ++ for _set in resource_set_list: ++ resource_ids.extend(_set["ids"]) ++ id_part = "".join([_id[0] + _id[-1] for _id in resource_ids][:3]) ++ return find_unique_id(cib, "{0}_set_{1}".format(type_prefix, id_part) ++ ) + + def have_duplicate_resource_sets(element, other_element): + get_id_set_list = lambda element: [ +diff --git a/pcs/lib/cib/constraint/order.py b/pcs/lib/cib/constraint/order.py +index 4c520ae7..6085f0db 100644 +--- a/pcs/lib/cib/constraint/order.py ++++ b/pcs/lib/cib/constraint/order.py +@@ -24,7 +24,7 @@ def prepare_options_with_set(cib, options, resource_set_list): + tuple(ATTRIB.keys()), + options, + create_id=partial( +- constraint.create_id, cib, TAG_NAME, resource_set_list ++ constraint.create_id, cib, "order", resource_set_list + ), + validate_id=partial(check_new_id_applicable, cib, DESCRIPTION), + ) +diff --git a/pcs/lib/cib/constraint/resource_set.py b/pcs/lib/cib/constraint/resource_set.py +index 6ec18205..e9833ff7 100644 +--- a/pcs/lib/cib/constraint/resource_set.py ++++ b/pcs/lib/cib/constraint/resource_set.py +@@ -39,9 +39,6 @@ def validate_options(options): + reports.invalid_option_value(name, value, ATTRIB[name]) + ) + +-def extract_id_set_list(resource_set_list): +- return [resource_set["ids"] for resource_set in resource_set_list] +- + def create(parent, resource_set): + """ + parent - lxml element for append new resource_set +@@ -50,7 +47,7 @@ def create(parent, resource_set): + element.attrib.update(resource_set["options"]) + element.attrib["id"] = find_unique_id( + parent.getroottree(), +- "pcs_rsc_set_{0}".format("_".join(resource_set["ids"])) ++ "{0}_set".format(parent.attrib.get("id", "constraint_set")) + ) + + for id in resource_set["ids"]: +diff --git a/pcs/lib/cib/constraint/ticket.py b/pcs/lib/cib/constraint/ticket.py +index a2dbd5fe..cc1413ea 100644 +--- a/pcs/lib/cib/constraint/ticket.py ++++ b/pcs/lib/cib/constraint/ticket.py +@@ -48,7 +48,7 @@ def prepare_options_with_set(cib, options, resource_set_list): + tuple(ATTRIB.keys()), + options, + create_id=partial( +- constraint.create_id, cib, TAG_NAME, resource_set_list ++ constraint.create_id, cib, "ticket", resource_set_list + ), + validate_id=partial(tools.check_new_id_applicable, cib, DESCRIPTION), + ) +diff --git a/pcs/lib/cib/test/test_constraint.py b/pcs/lib/cib/test/test_constraint.py +index aa0f21cd..2712a81f 100644 +--- a/pcs/lib/cib/test/test_constraint.py ++++ b/pcs/lib/cib/test/test_constraint.py +@@ -263,19 +263,15 @@ class PrepareOptionsTest(TestCase): + mock_validate_id.assert_called_once_with("invalid") + + class CreateIdTest(TestCase): +- @mock.patch( +- "pcs.lib.cib.constraint.constraint.resource_set.extract_id_set_list" +- ) + @mock.patch("pcs.lib.cib.constraint.constraint.find_unique_id") +- def test_create_id_from_resource_set_list(self, mock_find_id, mock_extract): +- mock_extract.return_value = [["A", "B"], ["C"]] ++ def test_create_id_from_resource_set_list(self, mock_find_id): ++ resource_set_list = [{"ids": ["A", "B"]}, {"ids": ["C"]}] + mock_find_id.return_value = "some_id" + self.assertEqual( + "some_id", +- constraint.create_id("cib", "PREFIX", "resource_set_list") ++ constraint.create_id("cib", "PREFIX", resource_set_list) + ) +- mock_extract.assert_called_once_with("resource_set_list") +- mock_find_id.assert_called_once_with("cib", "pcs_PREFIX_set_A_B_set_C") ++ mock_find_id.assert_called_once_with("cib", "PREFIX_set_AABBCC") + + def fixture_constraint_section(return_value): + constraint_section = mock.MagicMock() +@@ -359,7 +355,7 @@ class CreateWithSetTest(TestCase): + assert_xml_equal(etree.tostring(constraint_section).decode(), """ + + +- ++ + + + +diff --git a/pcs/lib/cib/test/test_constraint_colocation.py b/pcs/lib/cib/test/test_constraint_colocation.py +index 25923bb3..75f7924c 100644 +--- a/pcs/lib/cib/test/test_constraint_colocation.py ++++ b/pcs/lib/cib/test/test_constraint_colocation.py +@@ -36,7 +36,7 @@ class PrepareOptionsWithSetTest(TestCase): + self.assertEqual(expected_options, self.prepare(options)) + mock_create_id.assert_called_once_with( + self.cib, +- colocation.TAG_NAME, ++ "colocation", + self.resource_set_list + ) + +diff --git a/pcs/lib/cib/test/test_constraint_order.py b/pcs/lib/cib/test/test_constraint_order.py +index c43fa2c4..4f5719d3 100644 +--- a/pcs/lib/cib/test/test_constraint_order.py ++++ b/pcs/lib/cib/test/test_constraint_order.py +@@ -36,7 +36,7 @@ class PrepareOptionsWithSetTest(TestCase): + self.assertEqual(expected_options, self.prepare(options)) + mock_create_id.assert_called_once_with( + self.cib, +- order.TAG_NAME, ++ "order", + self.resource_set_list + ) + +diff --git a/pcs/lib/cib/test/test_constraint_ticket.py b/pcs/lib/cib/test/test_constraint_ticket.py +index ff91f255..111c88e8 100644 +--- a/pcs/lib/cib/test/test_constraint_ticket.py ++++ b/pcs/lib/cib/test/test_constraint_ticket.py +@@ -183,7 +183,7 @@ class PrepareOptionsWithSetTest(TestCase): + self.assertEqual(expected_options, self.prepare(options)) + mock_create_id.assert_called_once_with( + self.cib, +- ticket.TAG_NAME, ++ "ticket", + self.resource_set_list + ) + +diff --git a/pcs/lib/cib/test/test_resource_set.py b/pcs/lib/cib/test/test_resource_set.py +index 5e0d75c2..fda2723e 100644 +--- a/pcs/lib/cib/test/test_resource_set.py ++++ b/pcs/lib/cib/test/test_resource_set.py +@@ -60,16 +60,6 @@ class PrepareSetTest(TestCase): + }), + ) + +-class ExtractIdListTest(TestCase): +- def test_return_id_list_from_resource_set_list(self): +- self.assertEqual( +- [["A", "B"], ["C", "D"]], +- resource_set.extract_id_set_list([ +- {"ids": ["A", "B"], "options": {}}, +- {"ids": ["C", "D"], "options": {}}, +- ]) +- ) +- + class CreateTest(TestCase): + def test_resource_set_to_parent(self): + constraint_element = etree.Element("constraint") +@@ -79,7 +69,7 @@ class CreateTest(TestCase): + ) + assert_xml_equal(etree.tostring(constraint_element).decode(), """ + +- ++ + + + +diff --git a/pcs/lib/commands/test/test_constraint_common.py b/pcs/lib/commands/test/test_constraint_common.py +index bf359380..ddbcbe8e 100644 +--- a/pcs/lib/commands/test/test_constraint_common.py ++++ b/pcs/lib/commands/test/test_constraint_common.py +@@ -62,11 +62,11 @@ class CreateWithSetTest(TestCase): + self.env.push_cib.assert_called_once_with() + self.independent_cib.find(".//constraints").append(etree.XML(""" + +- ++ + + + +- ++ + + + +@@ -90,11 +90,11 @@ class CreateWithSetTest(TestCase): + 'resource_sets': [ + { + 'ids': ['A', 'B'], +- 'options':{'role':'Master', 'id':'pcs_rsc_set_A_B'} ++ 'options':{'role':'Master', 'id':'some_id_set'} + }, + { + 'ids': ['E', 'F'], +- 'options':{'action':'start', 'id':'pcs_rsc_set_E_F'} ++ 'options':{'action':'start', 'id':'some_id_set-1'} + } + ], + }] +@@ -115,11 +115,11 @@ class CreateWithSetTest(TestCase): + constraint_section = self.independent_cib.find(".//constraints") + constraint_section.append(etree.XML(""" + +- ++ + + + +- ++ + + + +@@ -127,11 +127,11 @@ class CreateWithSetTest(TestCase): + """)) + constraint_section.append(etree.XML(""" + +- ++ + + + +- ++ + + + +@@ -177,7 +177,7 @@ class ShowTest(TestCase): + { + 'resource_sets': [{ + 'ids': ['A', 'B'], +- 'options': {'role': 'Master', 'id': 'pcs_rsc_set_A_B'}, ++ 'options': {'role': 'Master', 'id': 'some_id_set'}, + }], + 'options': {'symmetrical': 'true', 'id': 'some_id'} + }, +@@ -185,7 +185,7 @@ class ShowTest(TestCase): + 'options': {'symmetrical': 'true', 'id': 'some_id'}, + 'resource_sets': [{ + 'ids': ['E', 'F'], +- 'options': {'action': 'start', 'id': 'pcs_rsc_set_E_F'} ++ 'options': {'action': 'start', 'id': 'some_id_set-1'} + }] + } + ] +diff --git a/pcs/test/test_constraints.py b/pcs/test/test_constraints.py +index 3feaa053..df8594a5 100644 +--- a/pcs/test/test_constraints.py ++++ b/pcs/test/test_constraints.py +@@ -581,13 +581,13 @@ Ticket Constraints: + ac(o, """\ + Colocation Constraints: + Resource Sets: +- set D5 D6 D7 require-all=true sequential=false (id:pcs_rsc_set_D5_D6_D7) set D8 D9 action=start require-all=false role=Stopped sequential=true (id:pcs_rsc_set_D8_D9) setoptions score=INFINITY (id:pcs_rsc_colocation_set_D5_D6_D7_set_D8_D9) +- set D5 D6 (id:pcs_rsc_set_D5_D6) setoptions score=INFINITY (id:pcs_rsc_colocation_set_D5_D6) +- set D5 D6 action=stop role=Started (id:pcs_rsc_set_D5_D6-1) set D7 D8 action=promote role=Slave (id:pcs_rsc_set_D7_D8) set D8 D9 action=demote role=Master (id:pcs_rsc_set_D8_D9-1) setoptions score=INFINITY (id:pcs_rsc_colocation_set_D5_D6_set_D7_D8_set_D8_D9) ++ set D5 D6 D7 require-all=true sequential=false (id:colocation_set_D5D6D7_set) set D8 D9 action=start require-all=false role=Stopped sequential=true (id:colocation_set_D5D6D7_set-1) setoptions score=INFINITY (id:colocation_set_D5D6D7) ++ set D5 D6 (id:colocation_set_D5D6_set) setoptions score=INFINITY (id:colocation_set_D5D6) ++ set D5 D6 action=stop role=Started (id:colocation_set_D5D6D7-1_set) set D7 D8 action=promote role=Slave (id:colocation_set_D5D6D7-1_set-1) set D8 D9 action=demote role=Master (id:colocation_set_D5D6D7-1_set-2) setoptions score=INFINITY (id:colocation_set_D5D6D7-1) + """) + assert r == 0 + +- o, r = pcs(temp_cib, "constraint remove pcs_rsc_colocation_set_D5_D6") ++ o, r = pcs(temp_cib, "constraint remove colocation_set_D5D6") + ac(o,"") + assert r == 0 + +@@ -595,16 +595,16 @@ Colocation Constraints: + ac(o, """\ + Colocation Constraints: + Resource Sets: +- set D5 D6 D7 require-all=true sequential=false (id:pcs_rsc_set_D5_D6_D7) set D8 D9 action=start require-all=false role=Stopped sequential=true (id:pcs_rsc_set_D8_D9) setoptions score=INFINITY (id:pcs_rsc_colocation_set_D5_D6_D7_set_D8_D9) +- set D5 D6 action=stop role=Started (id:pcs_rsc_set_D5_D6-1) set D7 D8 action=promote role=Slave (id:pcs_rsc_set_D7_D8) set D8 D9 action=demote role=Master (id:pcs_rsc_set_D8_D9-1) setoptions score=INFINITY (id:pcs_rsc_colocation_set_D5_D6_set_D7_D8_set_D8_D9) ++ set D5 D6 D7 require-all=true sequential=false (id:colocation_set_D5D6D7_set) set D8 D9 action=start require-all=false role=Stopped sequential=true (id:colocation_set_D5D6D7_set-1) setoptions score=INFINITY (id:colocation_set_D5D6D7) ++ set D5 D6 action=stop role=Started (id:colocation_set_D5D6D7-1_set) set D7 D8 action=promote role=Slave (id:colocation_set_D5D6D7-1_set-1) set D8 D9 action=demote role=Master (id:colocation_set_D5D6D7-1_set-2) setoptions score=INFINITY (id:colocation_set_D5D6D7-1) + """) + assert r == 0 + + o, r = pcs(temp_cib, "resource delete D5") + ac(o, outdent( + """\ +- Removing D5 from set pcs_rsc_set_D5_D6_D7 +- Removing D5 from set pcs_rsc_set_D5_D6-1 ++ Removing D5 from set colocation_set_D5D6D7_set ++ Removing D5 from set colocation_set_D5D6D7-1_set + Deleting Resource - D5 + """ + )) +@@ -613,20 +613,32 @@ Colocation Constraints: + o, r = pcs(temp_cib, "resource delete D6") + ac(o, outdent( + """\ +- Removing D6 from set pcs_rsc_set_D5_D6_D7 +- Removing D6 from set pcs_rsc_set_D5_D6-1 +- Removing set pcs_rsc_set_D5_D6-1 ++ 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 + """ + )) + assert r == 0 + + o, r = pcs(temp_cib, "constraint ref D7") +- ac(o,"Resource: D7\n pcs_rsc_colocation_set_D5_D6_D7_set_D8_D9\n pcs_rsc_colocation_set_D5_D6_set_D7_D8_set_D8_D9\n") ++ ac(o, outdent( ++ """\ ++ Resource: D7 ++ colocation_set_D5D6D7 ++ colocation_set_D5D6D7-1 ++ """ ++ )) + assert r == 0 + + o, r = pcs(temp_cib, "constraint ref D8") +- ac(o,"Resource: D8\n pcs_rsc_colocation_set_D5_D6_D7_set_D8_D9\n pcs_rsc_colocation_set_D5_D6_set_D7_D8_set_D8_D9\n") ++ ac(o, outdent( ++ """\ ++ Resource: D8 ++ colocation_set_D5D6D7 ++ colocation_set_D5D6D7-1 ++ """ ++ )) + assert r == 0 + + output, retValue = pcs(temp_cib, "constraint colocation set D1 D2 sequential=foo") +@@ -852,12 +864,12 @@ Colocation Constraints: + ac(o,"""\ + Ordering Constraints: + Resource Sets: +- set D5 D6 D7 require-all=true sequential=false (id:pcs_rsc_set_D5_D6_D7) set D8 D9 action=start require-all=false role=Stopped sequential=true (id:pcs_rsc_set_D8_D9) (id:pcs_rsc_order_set_D5_D6_D7_set_D8_D9) +- set D5 D6 (id:pcs_rsc_set_D5_D6) (id:pcs_rsc_order_set_D5_D6) +- set D5 D6 action=stop role=Started (id:pcs_rsc_set_D5_D6-1) set D7 D8 action=promote role=Slave (id:pcs_rsc_set_D7_D8) set D8 D9 action=demote role=Master (id:pcs_rsc_set_D8_D9-1) (id:pcs_rsc_order_set_D5_D6_set_D7_D8_set_D8_D9) ++ set D5 D6 D7 require-all=true sequential=false (id:order_set_D5D6D7_set) set D8 D9 action=start require-all=false role=Stopped sequential=true (id:order_set_D5D6D7_set-1) (id:order_set_D5D6D7) ++ set D5 D6 (id:order_set_D5D6_set) (id:order_set_D5D6) ++ set D5 D6 action=stop role=Started (id:order_set_D5D6D7-1_set) set D7 D8 action=promote role=Slave (id:order_set_D5D6D7-1_set-1) set D8 D9 action=demote role=Master (id:order_set_D5D6D7-1_set-2) (id:order_set_D5D6D7-1) + """) + +- o, r = pcs(temp_cib, "constraint remove pcs_rsc_order_set_D5_D6") ++ o, r = pcs(temp_cib, "constraint remove order_set_D5D6") + assert r == 0 + ac(o,"") + +@@ -866,15 +878,15 @@ Ordering Constraints: + ac(o,"""\ + Ordering Constraints: + Resource Sets: +- set D5 D6 D7 require-all=true sequential=false (id:pcs_rsc_set_D5_D6_D7) set D8 D9 action=start require-all=false role=Stopped sequential=true (id:pcs_rsc_set_D8_D9) (id:pcs_rsc_order_set_D5_D6_D7_set_D8_D9) +- set D5 D6 action=stop role=Started (id:pcs_rsc_set_D5_D6-1) set D7 D8 action=promote role=Slave (id:pcs_rsc_set_D7_D8) set D8 D9 action=demote role=Master (id:pcs_rsc_set_D8_D9-1) (id:pcs_rsc_order_set_D5_D6_set_D7_D8_set_D8_D9) ++ set D5 D6 D7 require-all=true sequential=false (id:order_set_D5D6D7_set) set D8 D9 action=start require-all=false role=Stopped sequential=true (id:order_set_D5D6D7_set-1) (id:order_set_D5D6D7) ++ set D5 D6 action=stop role=Started (id:order_set_D5D6D7-1_set) set D7 D8 action=promote role=Slave (id:order_set_D5D6D7-1_set-1) set D8 D9 action=demote role=Master (id:order_set_D5D6D7-1_set-2) (id:order_set_D5D6D7-1) + """) + + o, r = pcs(temp_cib, "resource delete D5") + ac(o, outdent( + """\ +- Removing D5 from set pcs_rsc_set_D5_D6_D7 +- Removing D5 from set pcs_rsc_set_D5_D6-1 ++ Removing D5 from set order_set_D5D6D7_set ++ Removing D5 from set order_set_D5D6D7-1_set + Deleting Resource - D5 + """ + )) +@@ -883,9 +895,9 @@ Ordering Constraints: + o, r = pcs(temp_cib, "resource delete D6") + ac(o, outdent( + """\ +- Removing D6 from set pcs_rsc_set_D5_D6_D7 +- Removing D6 from set pcs_rsc_set_D5_D6-1 +- Removing set pcs_rsc_set_D5_D6-1 ++ 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 + """ + )) +@@ -946,9 +958,9 @@ Error: invalid option 'foo', allowed options are: id, kind, symmetrical + Location Constraints: + Ordering Constraints: + Resource Sets: +- set D7 require-all=true sequential=false (id:pcs_rsc_set_D5_D6_D7) set D8 D9 action=start require-all=false role=Stopped sequential=true (id:pcs_rsc_set_D8_D9) (id:pcs_rsc_order_set_D5_D6_D7_set_D8_D9) +- set D7 D8 action=promote role=Slave (id:pcs_rsc_set_D7_D8) set D8 D9 action=demote role=Master (id:pcs_rsc_set_D8_D9-1) (id:pcs_rsc_order_set_D5_D6_set_D7_D8_set_D8_D9) +- set D1 D2 (id:pcs_rsc_set_D1_D2) setoptions kind=Mandatory symmetrical=false (id:pcs_rsc_order_set_D1_D2) ++ set D7 require-all=true sequential=false (id:order_set_D5D6D7_set) set D8 D9 action=start require-all=false role=Stopped sequential=true (id:order_set_D5D6D7_set-1) (id:order_set_D5D6D7) ++ set D7 D8 action=promote role=Slave (id:order_set_D5D6D7-1_set-1) set D8 D9 action=demote role=Master (id:order_set_D5D6D7-1_set-2) (id:order_set_D5D6D7-1) ++ set D1 D2 (id:order_set_D1D2_set) setoptions kind=Mandatory symmetrical=false (id:order_set_D1D2) + Colocation Constraints: + Ticket Constraints: + """) +@@ -1220,11 +1232,11 @@ Location Constraints: + Ordering Constraints: + start stateful1 then start dummy1 (kind:Mandatory) (id:order-stateful1-dummy1-mandatory) + Resource Sets: +- set stateful1 dummy1 (id:pcs_rsc_set_stateful1_dummy1) (id:pcs_rsc_order_set_stateful1_dummy1) ++ set stateful1 dummy1 (id:order_set_s1d1_set) (id:order_set_s1d1) + Colocation Constraints: + stateful1 with dummy1 (score:INFINITY) (id:colocation-stateful1-dummy1-INFINITY) + Resource Sets: +- set stateful1 dummy1 (id:pcs_rsc_set_stateful1_dummy1-1) setoptions score=INFINITY (id:pcs_rsc_colocation_set_stateful1_dummy1) ++ set stateful1 dummy1 (id:colocation_set_s1d1_set) setoptions score=INFINITY (id:colocation_set_s1d1) + Ticket Constraints: + """) + assert r == 0 +@@ -1347,14 +1359,14 @@ Ordering Constraints: + start stateful1-master then start dummy1 (kind:Mandatory) (id:order-stateful1-master-dummy1-mandatory) + start dummy1 then start statefulG-master (kind:Mandatory) (id:order-dummy1-statefulG-master-mandatory) + Resource Sets: +- set stateful1-master dummy1 (id:pcs_rsc_set_stateful1-master_dummy1) (id:pcs_rsc_order_set_stateful1_dummy1) +- set dummy1 statefulG-master (id:pcs_rsc_set_dummy1_statefulG-master) (id:pcs_rsc_order_set_dummy1_statefulG) ++ set stateful1-master dummy1 (id:order_set_s1d1_set) (id:order_set_s1d1) ++ set dummy1 statefulG-master (id:order_set_d1sG_set) (id:order_set_d1sG) + Colocation Constraints: + stateful1-master with dummy1 (score:INFINITY) (id:colocation-stateful1-master-dummy1-INFINITY) + dummy1 with statefulG-master (score:INFINITY) (id:colocation-dummy1-statefulG-master-INFINITY) + Resource Sets: +- set dummy1 stateful1-master (id:pcs_rsc_set_dummy1_stateful1-master) setoptions score=INFINITY (id:pcs_rsc_colocation_set_dummy1_stateful1) +- set statefulG-master dummy1 (id:pcs_rsc_set_statefulG-master_dummy1) setoptions score=INFINITY (id:pcs_rsc_colocation_set_statefulG_dummy1) ++ set dummy1 stateful1-master (id:colocation_set_d1s1_set) setoptions score=INFINITY (id:colocation_set_d1s1) ++ set statefulG-master dummy1 (id:colocation_set_sGd1_set) setoptions score=INFINITY (id:colocation_set_sGd1) + Ticket Constraints: + """) + self.assertEqual(0, returnVal) +@@ -1398,7 +1410,7 @@ Adding stateful1-master dummy1 (kind: Mandatory) (Options: first-action=start th + ) + ac(output, """\ + Error: duplicate constraint already exists, use --force to override +- set stateful1-master dummy1 (id:pcs_rsc_set_stateful1-master_dummy1) (id:pcs_rsc_order_set_stateful1_dummy1) ++ set stateful1-master dummy1 (id:order_set_s1d1_set) (id:order_set_s1d1) + """) + self.assertEqual(1, returnVal) + +@@ -1407,7 +1419,7 @@ Error: duplicate constraint already exists, use --force to override + ) + ac(output, console_report( + "Warning: duplicate constraint already exists", +- " set stateful1-master dummy1 (id:pcs_rsc_set_stateful1-master_dummy1) (id:pcs_rsc_order_set_stateful1_dummy1)", ++ " set stateful1-master dummy1 (id:order_set_s1d1_set) (id:order_set_s1d1)", + )) + self.assertEqual(0, returnVal) + +@@ -1431,7 +1443,7 @@ Error: duplicate constraint already exists, use --force to override + ) + ac(output, """\ + Error: duplicate constraint already exists, use --force to override +- set dummy1 stateful1-master (id:pcs_rsc_set_dummy1_stateful1-master) setoptions score=INFINITY (id:pcs_rsc_colocation_set_dummy1_stateful1) ++ set dummy1 stateful1-master (id:colocation_set_d1s1_set) setoptions score=INFINITY (id:colocation_set_d1s1) + """) + self.assertEqual(1, returnVal) + +@@ -1440,7 +1452,7 @@ Error: duplicate constraint already exists, use --force to override + ) + ac(output, console_report( + "Warning: duplicate constraint already exists", +- " set dummy1 stateful1-master (id:pcs_rsc_set_dummy1_stateful1-master) setoptions score=INFINITY (id:pcs_rsc_colocation_set_dummy1_stateful1)", ++ " set dummy1 stateful1-master (id:colocation_set_d1s1_set) setoptions score=INFINITY (id:colocation_set_d1s1)", + )) + self.assertEqual(0, returnVal) + +@@ -1465,17 +1477,17 @@ Ordering Constraints: + start dummy1 then start statefulG-master (kind:Mandatory) (id:order-dummy1-statefulG-master-mandatory) + start stateful1-master then start dummy1 (kind:Mandatory) (id:order-stateful1-master-dummy1-mandatory-1) + Resource Sets: +- set stateful1-master dummy1 (id:pcs_rsc_set_stateful1-master_dummy1) (id:pcs_rsc_order_set_stateful1_dummy1) +- set dummy1 statefulG-master (id:pcs_rsc_set_dummy1_statefulG-master) (id:pcs_rsc_order_set_dummy1_statefulG) +- set stateful1-master dummy1 (id:pcs_rsc_set_stateful1-master_dummy1-1) (id:pcs_rsc_order_set_stateful1_dummy1-1) ++ set stateful1-master dummy1 (id:order_set_s1d1_set) (id:order_set_s1d1) ++ set dummy1 statefulG-master (id:order_set_d1sG_set) (id:order_set_d1sG) ++ set stateful1-master dummy1 (id:order_set_s1d1-1_set) (id:order_set_s1d1-1) + Colocation Constraints: + stateful1-master with dummy1 (score:INFINITY) (id:colocation-stateful1-master-dummy1-INFINITY) + dummy1 with statefulG-master (score:INFINITY) (id:colocation-dummy1-statefulG-master-INFINITY) + stateful1-master with dummy1 (score:INFINITY) (id:colocation-stateful1-master-dummy1-INFINITY-1) + Resource Sets: +- set dummy1 stateful1-master (id:pcs_rsc_set_dummy1_stateful1-master) setoptions score=INFINITY (id:pcs_rsc_colocation_set_dummy1_stateful1) +- set statefulG-master dummy1 (id:pcs_rsc_set_statefulG-master_dummy1) setoptions score=INFINITY (id:pcs_rsc_colocation_set_statefulG_dummy1) +- set dummy1 stateful1-master (id:pcs_rsc_set_dummy1_stateful1-master-1) setoptions score=INFINITY (id:pcs_rsc_colocation_set_dummy1_stateful1-1) ++ set dummy1 stateful1-master (id:colocation_set_d1s1_set) setoptions score=INFINITY (id:colocation_set_d1s1) ++ set statefulG-master dummy1 (id:colocation_set_sGd1_set) setoptions score=INFINITY (id:colocation_set_sGd1) ++ set dummy1 stateful1-master (id:colocation_set_d1s1-1_set) setoptions score=INFINITY (id:colocation_set_d1s1-1) + Ticket Constraints: + """) + self.assertEqual(0, returnVal) +@@ -1587,11 +1599,11 @@ Location Constraints: + Ordering Constraints: + start dummy then start dummy1 (kind:Mandatory) (id:order-dummy-dummy1-mandatory) + Resource Sets: +- set dummy1 dummy (id:pcs_rsc_set_dummy1_dummy) (id:pcs_rsc_order_set_dummy1_dummy) ++ set dummy1 dummy (id:order_set_d1dy_set) (id:order_set_d1dy) + Colocation Constraints: + dummy with dummy1 (score:INFINITY) (id:colocation-dummy-dummy1-INFINITY) + Resource Sets: +- set dummy1 dummy (id:pcs_rsc_set_dummy1_dummy-1) setoptions score=INFINITY (id:pcs_rsc_colocation_set_dummy1_dummy) ++ set dummy1 dummy (id:colocation_set_d1dy_set) setoptions score=INFINITY (id:colocation_set_d1dy) + Ticket Constraints: + """) + assert r == 0 +@@ -1710,14 +1722,14 @@ Ordering Constraints: + start dummy-clone then start dummy1 (kind:Mandatory) (id:order-dummy-clone-dummy1-mandatory) + start dummy1 then start dummyG-clone (kind:Mandatory) (id:order-dummy1-dummyG-clone-mandatory) + Resource Sets: +- set dummy1 dummy-clone (id:pcs_rsc_set_dummy1_dummy-clone) (id:pcs_rsc_order_set_dummy1_dummy) +- set dummyG-clone dummy1 (id:pcs_rsc_set_dummyG-clone_dummy1) (id:pcs_rsc_order_set_dummyG_dummy1) ++ set dummy1 dummy-clone (id:order_set_d1dy_set) (id:order_set_d1dy) ++ set dummyG-clone dummy1 (id:order_set_dGd1_set) (id:order_set_dGd1) + Colocation Constraints: + dummy-clone with dummy1 (score:INFINITY) (id:colocation-dummy-clone-dummy1-INFINITY) + dummy1 with dummyG-clone (score:INFINITY) (id:colocation-dummy1-dummyG-clone-INFINITY) + Resource Sets: +- set dummy1 dummy-clone (id:pcs_rsc_set_dummy1_dummy-clone-1) setoptions score=INFINITY (id:pcs_rsc_colocation_set_dummy1_dummy) +- set dummy1 dummyG-clone (id:pcs_rsc_set_dummy1_dummyG-clone) setoptions score=INFINITY (id:pcs_rsc_colocation_set_dummy1_dummyG) ++ set dummy1 dummy-clone (id:colocation_set_d1dy_set) setoptions score=INFINITY (id:colocation_set_d1dy) ++ set dummy1 dummyG-clone (id:colocation_set_d1dG_set) setoptions score=INFINITY (id:colocation_set_d1dG) + Ticket Constraints: + """) + self.assertEqual(0, returnVal) +@@ -1761,7 +1773,7 @@ Adding dummy-clone dummy1 (kind: Mandatory) (Options: first-action=start then-ac + ) + ac(output, """\ + Error: duplicate constraint already exists, use --force to override +- set dummy1 dummy-clone (id:pcs_rsc_set_dummy1_dummy-clone) (id:pcs_rsc_order_set_dummy1_dummy) ++ set dummy1 dummy-clone (id:order_set_d1dy_set) (id:order_set_d1dy) + """) + self.assertEqual(1, returnVal) + +@@ -1770,7 +1782,7 @@ Error: duplicate constraint already exists, use --force to override + ) + ac(output, console_report( + "Warning: duplicate constraint already exists", +- " set dummy1 dummy-clone (id:pcs_rsc_set_dummy1_dummy-clone) (id:pcs_rsc_order_set_dummy1_dummy)", ++ " set dummy1 dummy-clone (id:order_set_d1dy_set) (id:order_set_d1dy)", + )) + self.assertEqual(0, returnVal) + +@@ -1794,7 +1806,7 @@ Error: duplicate constraint already exists, use --force to override + ) + ac(output, """\ + Error: duplicate constraint already exists, use --force to override +- set dummy1 dummy-clone (id:pcs_rsc_set_dummy1_dummy-clone-1) setoptions score=INFINITY (id:pcs_rsc_colocation_set_dummy1_dummy) ++ set dummy1 dummy-clone (id:colocation_set_d1dy_set) setoptions score=INFINITY (id:colocation_set_d1dy) + """) + self.assertEqual(1, returnVal) + +@@ -1803,7 +1815,7 @@ Error: duplicate constraint already exists, use --force to override + ) + ac(output, console_report( + "Warning: duplicate constraint already exists", +- " set dummy1 dummy-clone (id:pcs_rsc_set_dummy1_dummy-clone-1) setoptions score=INFINITY (id:pcs_rsc_colocation_set_dummy1_dummy)", ++ " set dummy1 dummy-clone (id:colocation_set_d1dy_set) setoptions score=INFINITY (id:colocation_set_d1dy)", + )) + self.assertEqual(0, returnVal) + +@@ -1828,17 +1840,17 @@ Ordering Constraints: + start dummy1 then start dummyG-clone (kind:Mandatory) (id:order-dummy1-dummyG-clone-mandatory) + start dummy-clone then start dummy1 (kind:Mandatory) (id:order-dummy-clone-dummy1-mandatory-1) + Resource Sets: +- set dummy1 dummy-clone (id:pcs_rsc_set_dummy1_dummy-clone) (id:pcs_rsc_order_set_dummy1_dummy) +- set dummyG-clone dummy1 (id:pcs_rsc_set_dummyG-clone_dummy1) (id:pcs_rsc_order_set_dummyG_dummy1) +- set dummy1 dummy-clone (id:pcs_rsc_set_dummy1_dummy-clone-2) (id:pcs_rsc_order_set_dummy1_dummy-1) ++ set dummy1 dummy-clone (id:order_set_d1dy_set) (id:order_set_d1dy) ++ set dummyG-clone dummy1 (id:order_set_dGd1_set) (id:order_set_dGd1) ++ set dummy1 dummy-clone (id:order_set_d1dy-1_set) (id:order_set_d1dy-1) + Colocation Constraints: + dummy-clone with dummy1 (score:INFINITY) (id:colocation-dummy-clone-dummy1-INFINITY) + dummy1 with dummyG-clone (score:INFINITY) (id:colocation-dummy1-dummyG-clone-INFINITY) + dummy-clone with dummy1 (score:INFINITY) (id:colocation-dummy-clone-dummy1-INFINITY-1) + Resource Sets: +- set dummy1 dummy-clone (id:pcs_rsc_set_dummy1_dummy-clone-1) setoptions score=INFINITY (id:pcs_rsc_colocation_set_dummy1_dummy) +- set dummy1 dummyG-clone (id:pcs_rsc_set_dummy1_dummyG-clone) setoptions score=INFINITY (id:pcs_rsc_colocation_set_dummy1_dummyG) +- set dummy1 dummy-clone (id:pcs_rsc_set_dummy1_dummy-clone-3) setoptions score=INFINITY (id:pcs_rsc_colocation_set_dummy1_dummy-1) ++ set dummy1 dummy-clone (id:colocation_set_d1dy_set) setoptions score=INFINITY (id:colocation_set_d1dy) ++ set dummy1 dummyG-clone (id:colocation_set_d1dG_set) setoptions score=INFINITY (id:colocation_set_d1dG) ++ set dummy1 dummy-clone (id:colocation_set_d1dy-1_set) setoptions score=INFINITY (id:colocation_set_d1dy-1) + Ticket Constraints: + """) + self.assertEqual(0, returnVal) +@@ -2420,14 +2432,14 @@ Ticket Constraints: + output, returnVal = pcs("constraint order set D1 D2") + ac(output, """\ + Error: duplicate constraint already exists, use --force to override +- set D1 D2 (id:pcs_rsc_set_D1_D2) (id:pcs_rsc_order_set_D1_D2) ++ set D1 D2 (id:order_set_D1D2_set) (id:order_set_D1D2) + """) + self.assertEqual(1, returnVal) + + output, returnVal = pcs("constraint order set D1 D2 --force") + ac(output, console_report( + "Warning: duplicate constraint already exists", +- " set D1 D2 (id:pcs_rsc_set_D1_D2) (id:pcs_rsc_order_set_D1_D2)", ++ " set D1 D2 (id:order_set_D1D2_set) (id:order_set_D1D2)", + )) + self.assertEqual(0, returnVal) + +@@ -2438,14 +2450,14 @@ Error: duplicate constraint already exists, use --force to override + output, returnVal = pcs("constraint order set D1 D2 set D5 D6") + ac(output, """\ + Error: duplicate constraint already exists, use --force to override +- set D1 D2 (id:pcs_rsc_set_D1_D2-2) set D5 D6 (id:pcs_rsc_set_D5_D6) (id:pcs_rsc_order_set_D1_D2_set_D5_D6) ++ set D1 D2 (id:order_set_D1D2D5_set) set D5 D6 (id:order_set_D1D2D5_set-1) (id:order_set_D1D2D5) + """) + self.assertEqual(1, returnVal) + + output, returnVal = pcs("constraint order set D1 D2 set D5 D6 --force") + ac(output, console_report( + "Warning: duplicate constraint already exists", +- " set D1 D2 (id:pcs_rsc_set_D1_D2-2) set D5 D6 (id:pcs_rsc_set_D5_D6) (id:pcs_rsc_order_set_D1_D2_set_D5_D6)", ++ " set D1 D2 (id:order_set_D1D2D5_set) set D5 D6 (id:order_set_D1D2D5_set-1) (id:order_set_D1D2D5)", + )) + self.assertEqual(0, returnVal) + +@@ -2457,14 +2469,14 @@ Error: duplicate constraint already exists, use --force to override + output, returnVal = pcs("constraint colocation set D1 D2") + ac(output, """\ + Error: duplicate constraint already exists, use --force to override +- set D1 D2 (id:pcs_rsc_set_D1_D2-4) setoptions score=INFINITY (id:pcs_rsc_colocation_set_D1_D2) ++ set D1 D2 (id:colocation_set_D1D2_set) setoptions score=INFINITY (id:colocation_set_D1D2) + """) + self.assertEqual(1, returnVal) + + output, returnVal = pcs("constraint colocation set D1 D2 --force") + ac(output, console_report( + "Warning: duplicate constraint already exists", +- " set D1 D2 (id:pcs_rsc_set_D1_D2-4) setoptions score=INFINITY (id:pcs_rsc_colocation_set_D1_D2)" ++ " set D1 D2 (id:colocation_set_D1D2_set) setoptions score=INFINITY (id:colocation_set_D1D2)" + )) + self.assertEqual(0, returnVal) + +@@ -2475,7 +2487,7 @@ Error: duplicate constraint already exists, use --force to override + output, returnVal = pcs("constraint colocation set D1 D2 set D5 D6") + ac(output, """\ + Error: duplicate constraint already exists, use --force to override +- set D1 D2 (id:pcs_rsc_set_D1_D2-6) set D5 D6 (id:pcs_rsc_set_D5_D6-2) setoptions score=INFINITY (id:pcs_rsc_colocation_set_D1_D2_set_D5_D6) ++ set D1 D2 (id:colocation_set_D1D2D5_set) set D5 D6 (id:colocation_set_D1D2D5_set-1) setoptions score=INFINITY (id:colocation_set_D1D2D5) + """) + self.assertEqual(1, returnVal) + +@@ -2484,7 +2496,7 @@ Error: duplicate constraint already exists, use --force to override + ) + ac(output, console_report( + "Warning: duplicate constraint already exists", +- " set D1 D2 (id:pcs_rsc_set_D1_D2-6) set D5 D6 (id:pcs_rsc_set_D5_D6-2) setoptions score=INFINITY (id:pcs_rsc_colocation_set_D1_D2_set_D5_D6)" ++ " set D1 D2 (id:colocation_set_D1D2D5_set) set D5 D6 (id:colocation_set_D1D2D5_set-1) setoptions score=INFINITY (id:colocation_set_D1D2D5)" + )) + self.assertEqual(0, returnVal) + +@@ -2503,18 +2515,18 @@ Error: duplicate constraint already exists, use --force to override + Location Constraints: + Ordering Constraints: + Resource Sets: +- set D1 D2 (id:pcs_rsc_set_D1_D2) (id:pcs_rsc_order_set_D1_D2) +- set D1 D2 (id:pcs_rsc_set_D1_D2-1) (id:pcs_rsc_order_set_D1_D2-1) +- set D1 D2 (id:pcs_rsc_set_D1_D2-2) set D5 D6 (id:pcs_rsc_set_D5_D6) (id:pcs_rsc_order_set_D1_D2_set_D5_D6) +- set D1 D2 (id:pcs_rsc_set_D1_D2-3) set D5 D6 (id:pcs_rsc_set_D5_D6-1) (id:pcs_rsc_order_set_D1_D2_set_D5_D6-1) +- set D6 D1 (id:pcs_rsc_set_D6_D1-1) (id:pcs_rsc_order_set_D6_D1) ++ set D1 D2 (id:order_set_D1D2_set) (id:order_set_D1D2) ++ set D1 D2 (id:order_set_D1D2-1_set) (id:order_set_D1D2-1) ++ set D1 D2 (id:order_set_D1D2D5_set) set D5 D6 (id:order_set_D1D2D5_set-1) (id:order_set_D1D2D5) ++ set D1 D2 (id:order_set_D1D2D5-1_set) set D5 D6 (id:order_set_D1D2D5-1_set-1) (id:order_set_D1D2D5-1) ++ set D6 D1 (id:order_set_D6D1_set) (id:order_set_D6D1) + Colocation Constraints: + Resource Sets: +- set D1 D2 (id:pcs_rsc_set_D1_D2-4) setoptions score=INFINITY (id:pcs_rsc_colocation_set_D1_D2) +- set D1 D2 (id:pcs_rsc_set_D1_D2-5) setoptions score=INFINITY (id:pcs_rsc_colocation_set_D1_D2-1) +- set D1 D2 (id:pcs_rsc_set_D1_D2-6) set D5 D6 (id:pcs_rsc_set_D5_D6-2) setoptions score=INFINITY (id:pcs_rsc_colocation_set_D1_D2_set_D5_D6) +- set D1 D2 (id:pcs_rsc_set_D1_D2-7) set D5 D6 (id:pcs_rsc_set_D5_D6-3) setoptions score=INFINITY (id:pcs_rsc_colocation_set_D1_D2_set_D5_D6-1) +- set D6 D1 (id:pcs_rsc_set_D6_D1) setoptions score=INFINITY (id:pcs_rsc_colocation_set_D6_D1) ++ set D1 D2 (id:colocation_set_D1D2_set) setoptions score=INFINITY (id:colocation_set_D1D2) ++ set D1 D2 (id:colocation_set_D1D2-1_set) setoptions score=INFINITY (id:colocation_set_D1D2-1) ++ set D1 D2 (id:colocation_set_D1D2D5_set) set D5 D6 (id:colocation_set_D1D2D5_set-1) setoptions score=INFINITY (id:colocation_set_D1D2D5) ++ set D1 D2 (id:colocation_set_D1D2D5-1_set) set D5 D6 (id:colocation_set_D1D2D5-1_set-1) setoptions score=INFINITY (id:colocation_set_D1D2D5-1) ++ set D6 D1 (id:colocation_set_D6D1_set) setoptions score=INFINITY (id:colocation_set_D6D1) + Ticket Constraints: + """) + +@@ -2782,14 +2794,14 @@ Ordering Constraints: + start D1 then start D2 (kind:Mandatory) (id:id7) + start D2 then start D1 (kind:Optional) (id:id8) + Resource Sets: +- set D1 D2 (id:pcs_rsc_set_D1_D2-1) (id:id5) +- set D2 D1 (id:pcs_rsc_set_D2_D1-1) setoptions kind=Mandatory (id:id6) ++ set D1 D2 (id:id5_set) (id:id5) ++ set D2 D1 (id:id6_set) setoptions kind=Mandatory (id:id6) + Colocation Constraints: + D1 with D2 (score:INFINITY) (id:id1) + D2 with D1 (score:100) (id:id2) + Resource Sets: +- set D1 D2 (id:pcs_rsc_set_D1_D2) setoptions score=INFINITY (id:id3) +- set D2 D1 (id:pcs_rsc_set_D2_D1) setoptions score=100 (id:id4) ++ set D1 D2 (id:id3_set) setoptions score=INFINITY (id:id3) ++ set D2 D1 (id:id4_set) setoptions score=100 (id:id4) + Ticket Constraints: + """) + self.assertEqual(0, returnVal) +@@ -3527,10 +3539,8 @@ class BundleColocation(Bundle): + "constraint colocation set B X", + """ + +- +- ++ ++ + + + +@@ -3553,10 +3563,8 @@ class BundleColocation(Bundle): + "constraint colocation set R X --force", + """ + +- +- ++ ++ + + + +@@ -3615,8 +3623,8 @@ class BundleOrder(Bundle): + "constraint order set B X", + """ + +- +- ++ ++ + + + +@@ -3639,8 +3647,8 @@ class BundleOrder(Bundle): + "constraint order set R X --force", + """ + +- +- ++ ++ + + + +@@ -3693,8 +3701,8 @@ class BundleTicket(Bundle): + "constraint ticket set B setoptions ticket=T", + """ + +- +- ++ ++ + + + +@@ -3716,8 +3724,8 @@ class BundleTicket(Bundle): + "constraint ticket set R setoptions ticket=T --force", + """ + +- +- ++ ++ + + + +-- +2.21.0 + diff --git a/SOURCES/bz1833115-01-add-brief-to-resource-disable-simulate.patch b/SOURCES/bz1833115-01-add-brief-to-resource-disable-simulate.patch new file mode 100644 index 0000000..1728e25 --- /dev/null +++ b/SOURCES/bz1833115-01-add-brief-to-resource-disable-simulate.patch @@ -0,0 +1,771 @@ +From c73cbac945259429e18fcd457341f83f8c8f6e59 Mon Sep 17 00:00:00 2001 +From: Tomas Jelinek +Date: Tue, 9 Jun 2020 13:39:35 +0200 +Subject: [PATCH 1/6] add --brief to 'resource disable --simulate' + +--- + pcs/cli/common/parse_args.py | 2 +- + pcs/lib/commands/resource.py | 116 ++++---- + .../resource/test_resource_enable_disable.py | 252 +++++++++++------- + pcs/pcs.8 | 8 +- + pcs/resource.py | 14 +- + pcs/test/test_resource.py | 110 +++++++- + pcs/usage.py | 15 +- + pcs/utils.py | 1 + + pcsd/capabilities.xml | 12 +- + 9 files changed, 370 insertions(+), 160 deletions(-) + +diff --git a/pcs/cli/common/parse_args.py b/pcs/cli/common/parse_args.py +index 715e7643..19a658e5 100644 +--- a/pcs/cli/common/parse_args.py ++++ b/pcs/cli/common/parse_args.py +@@ -18,7 +18,7 @@ PCS_LONG_OPTIONS = [ + "force", "skip-offline", "autocorrect", "interactive", "autodelete", + "all", "full", "groups", "local", "wait", "config", "async", + "start", "enable", "disabled", "off", "request-timeout=", +- "safe", "no-strict", "strict", "simulate", ++ "safe", "no-strict", "strict", "simulate", "brief", + "pacemaker", "corosync", + "no-default-ops", "defaults", "nodesc", + "clone", "master", "name=", "group=", "node=", +diff --git a/pcs/lib/commands/resource.py b/pcs/lib/commands/resource.py +index 1ad03a48..fff5f40b 100644 +--- a/pcs/lib/commands/resource.py ++++ b/pcs/lib/commands/resource.py +@@ -732,6 +732,48 @@ def _disable_validate_and_edit_cib(env, resources_section, resource_ids): + env.get_cluster_state() + ) + ) ++ return resource_el_list ++ ++def _disable_run_simulate(cmd_runner, cib, disabled_resource_el_list, strict): ++ inner_resources_names_set = set() ++ disabled_resource_ids = set() ++ for resource_el in disabled_resource_el_list: ++ disabled_resource_ids.add(resource_el.get("id")) ++ inner_resources_names_set.update({ ++ inner_resource_el.get("id") ++ for inner_resource_el ++ in resource.common.get_all_inner_resources(resource_el) ++ }) ++ ++ plaintext_status, transitions, dummy_cib = simulate_cib(cmd_runner, cib) ++ simulated_operations = ( ++ simulate_tools.get_operations_from_transitions(transitions) ++ ) ++ other_affected = set() ++ if strict: ++ other_affected = set( ++ simulate_tools.get_resources_from_operations( ++ simulated_operations, ++ exclude=disabled_resource_ids, ++ ) ++ ) ++ else: ++ other_affected = set( ++ simulate_tools.get_resources_left_stopped( ++ simulated_operations, ++ exclude=disabled_resource_ids, ++ ) ++ + ++ simulate_tools.get_resources_left_demoted( ++ simulated_operations, ++ exclude=disabled_resource_ids, ++ ) ++ ) ++ ++ # Stopping a clone stops all its inner resources. That should not block ++ # stopping the clone. ++ other_affected = other_affected - inner_resources_names_set ++ return plaintext_status, other_affected + + def disable(env, resource_ids, wait): + """ +@@ -762,57 +804,15 @@ def disable_safe(env, resource_ids, strict, wait): + with resource_environment( + env, wait, resource_ids, _ensure_disabled_after_wait(True) + ) as resources_section: +- resource_el_list = _find_resources_or_raise( +- resources_section, +- resource_ids +- ) +- env.report_processor.process_list( +- _resource_list_enable_disable( +- resource_el_list, +- resource.common.disable, +- env.get_cluster_state() +- ) ++ resource_el_list =_disable_validate_and_edit_cib( ++ env, resources_section, resource_ids + ) +- +- inner_resources_names_set = set() +- for resource_el in resource_el_list: +- inner_resources_names_set.update({ +- inner_resource_el.get("id") +- for inner_resource_el +- in resource.common.get_all_inner_resources(resource_el) +- }) +- +- plaintext_status, transitions, dummy_cib = simulate_cib( ++ plaintext_status, other_affected = _disable_run_simulate( + env.cmd_runner(), +- get_root(resources_section) +- ) +- simulated_operations = ( +- simulate_tools.get_operations_from_transitions(transitions) ++ get_root(resources_section), ++ resource_el_list, ++ strict, + ) +- other_affected = set() +- if strict: +- other_affected = set( +- simulate_tools.get_resources_from_operations( +- simulated_operations, +- exclude=resource_ids +- ) +- ) +- else: +- other_affected = set( +- simulate_tools.get_resources_left_stopped( +- simulated_operations, +- exclude=resource_ids +- ) +- + +- simulate_tools.get_resources_left_demoted( +- simulated_operations, +- exclude=resource_ids +- ) +- ) +- +- # Stopping a clone stops all its inner resources. That should not block +- # stopping the clone. +- other_affected = other_affected - inner_resources_names_set + if other_affected: + raise LibraryError( + reports.resource_disable_affects_other_resources( +@@ -822,23 +822,31 @@ def disable_safe(env, resource_ids, strict, wait): + ) + ) + +-def disable_simulate(env, resource_ids): ++def disable_simulate(env, resource_ids, strict): + """ + Simulate disallowing specified resource to be started by the cluster + + LibraryEnvironment env -- + strings resource_ids -- ids of the resources to be disabled ++ bool strict -- if False, allow resources to be migrated + """ + if not env.is_cib_live: + raise LibraryError(reports.live_environment_required(["CIB"])) + +- resources_section = get_resources(env.get_cib()) +- _disable_validate_and_edit_cib(env, resources_section, resource_ids) +- plaintext_status, dummy_transitions, dummy_cib = simulate_cib( ++ cib = env.get_cib() ++ resource_el_list = _disable_validate_and_edit_cib( ++ env, get_resources(cib), resource_ids ++ ) ++ plaintext_status, other_affected = _disable_run_simulate( + env.cmd_runner(), +- get_root(resources_section) ++ cib, ++ resource_el_list, ++ strict, ++ ) ++ return dict( ++ plaintext_simulated_status=plaintext_status, ++ other_affected_resource_list=sorted(other_affected), + ) +- return plaintext_status + + def enable(env, resource_ids, wait): + """ +diff --git a/pcs/lib/commands/test/resource/test_resource_enable_disable.py b/pcs/lib/commands/test/resource/test_resource_enable_disable.py +index 6514d9fc..07d86850 100644 +--- a/pcs/lib/commands/test/resource/test_resource_enable_disable.py ++++ b/pcs/lib/commands/test/resource/test_resource_enable_disable.py +@@ -1649,96 +1649,7 @@ class EnableBundle(TestCase): + ]) + + +-@mock.patch("pcs.lib.pacemaker.live.write_tmpfile") +-class DisableSimulate(TestCase): +- def setUp(self): +- self.env_assist, self.config = get_env_tools(test_case=self) +- self.tmpfile_new_cib = mock.MagicMock() +- self.tmpfile_new_cib.name = rc("new_cib.tmp") +- self.tmpfile_new_cib.read.return_value = "" +- self.tmpfile_transitions = mock.MagicMock() +- self.tmpfile_transitions.name = rc("transitions.tmp") +- self.tmpfile_transitions.read.return_value = "" +- +- def test_not_live(self, mock_write_tmpfile): +- mock_write_tmpfile.side_effect = [ +- AssertionError("No other write_tmpfile call expected") +- ] +- self.config.env.set_cib_data("") +- self.env_assist.assert_raise_library_error( +- lambda: resource.disable_simulate(self.env_assist.get_env(), ["A"]), +- [ +- fixture.error( +- report_codes.LIVE_ENVIRONMENT_REQUIRED, +- forbidden_options=["CIB"] +- ), +- ], +- expected_in_processor=False +- ) +- +- def test_nonexistent_resource(self, mock_write_tmpfile): +- mock_write_tmpfile.side_effect = [ +- AssertionError("No other write_tmpfile call expected") +- ] +- self.config.runner.cib.load() +- self.env_assist.assert_raise_library_error( +- lambda: resource.disable_simulate(self.env_assist.get_env(), ["A"]), +- [ +- fixture.report_not_found("A", "resources"), +- ], +- expected_in_processor=False +- ) +- +- def test_success(self, mock_write_tmpfile): +- mock_write_tmpfile.side_effect = [ +- self.tmpfile_new_cib, self.tmpfile_transitions, +- AssertionError("No other write_tmpfile call expected") +- ] +- (self.config +- .runner.cib.load(resources=fixture_primitive_cib_enabled) +- .runner.pcmk.load_state(resources=fixture_primitive_status_managed) +- .runner.pcmk.simulate_cib( +- self.tmpfile_new_cib.name, +- self.tmpfile_transitions.name, +- stdout="simulate output", +- resources=fixture_primitive_cib_disabled, +- ) +- ) +- +- result = resource.disable_simulate(self.env_assist.get_env(), ["A"]) +- self.assertEqual("simulate output", result) +- +- def test_simulate_error(self, mock_write_tmpfile): +- mock_write_tmpfile.side_effect = [ +- self.tmpfile_new_cib, self.tmpfile_transitions, +- AssertionError("No other write_tmpfile call expected") +- ] +- (self.config +- .runner.cib.load(resources=fixture_primitive_cib_enabled) +- .runner.pcmk.load_state(resources=fixture_primitive_status_managed) +- .runner.pcmk.simulate_cib( +- self.tmpfile_new_cib.name, +- self.tmpfile_transitions.name, +- stdout="some stdout", +- stderr="some stderr", +- returncode=1, +- resources=fixture_primitive_cib_disabled, +- ) +- ) +- +- self.env_assist.assert_raise_library_error( +- lambda: resource.disable_simulate(self.env_assist.get_env(), ["A"]), +- [ +- fixture.error( +- report_codes.CIB_SIMULATE_ERROR, +- reason="some stderr", +- ), +- ], +- expected_in_processor=False +- ) +- +- +-class DisableSafeMixin(object): ++class DisableSafeFixturesMixin(object): + fixture_transitions_both_stopped = """ + + +@@ -1925,6 +1836,167 @@ class DisableSafeMixin(object): + ) + ) + ++ ++@mock.patch("pcs.lib.pacemaker.live.write_tmpfile") ++class DisableSimulate(DisableSafeFixturesMixin, TestCase): ++ def test_not_live(self, mock_write_tmpfile): ++ mock_write_tmpfile.side_effect = [ ++ AssertionError("No other write_tmpfile call expected") ++ ] ++ self.config.env.set_cib_data("") ++ self.env_assist.assert_raise_library_error( ++ lambda: resource.disable_simulate( ++ self.env_assist.get_env(), ++ ["A"], ++ True, ++ ), ++ [ ++ fixture.error( ++ report_codes.LIVE_ENVIRONMENT_REQUIRED, ++ forbidden_options=["CIB"] ++ ), ++ ], ++ expected_in_processor=False ++ ) ++ ++ def test_nonexistent_resource(self, mock_write_tmpfile): ++ mock_write_tmpfile.side_effect = [ ++ AssertionError("No other write_tmpfile call expected") ++ ] ++ self.config.runner.cib.load() ++ self.env_assist.assert_raise_library_error( ++ lambda: resource.disable_simulate( ++ self.env_assist.get_env(), ++ ["A"], ++ True, ++ ), ++ [ ++ fixture.report_not_found("A", "resources"), ++ ], ++ expected_in_processor=False ++ ) ++ ++ def test_success_no_others_stopped(self, mock_write_tmpfile): ++ self.fixture_disable_both_resources(mock_write_tmpfile) ++ self.config.runner.pcmk.simulate_cib( ++ self.tmpfile_new_cib.name, ++ self.tmpfile_transitions.name, ++ stdout="simulate output", ++ resources=fixture_two_primitives_cib_disabled_both, ++ ) ++ ++ result = resource.disable_simulate( ++ self.env_assist.get_env(), ++ ["A", "B"], ++ True, ++ ) ++ self.assertEqual( ++ result, ++ dict( ++ plaintext_simulated_status="simulate output", ++ other_affected_resource_list=[], ++ ), ++ ) ++ ++ def test_success_others_stopped(self, mock_write_tmpfile): ++ self.fixture_disable_both_resources(mock_write_tmpfile) ++ self.config.runner.pcmk.simulate_cib( ++ self.tmpfile_new_cib.name, ++ self.tmpfile_transitions.name, ++ stdout="simulate output", ++ resources=fixture_two_primitives_cib_disabled, ++ ) ++ ++ result = resource.disable_simulate( ++ self.env_assist.get_env(), ++ ["A"], ++ True, ++ ) ++ self.assertEqual( ++ result, ++ dict( ++ plaintext_simulated_status="simulate output", ++ other_affected_resource_list=["B"], ++ ), ++ ) ++ ++ def test_success_others_migrated_strict(self, mock_write_tmpfile): ++ self.fixture_migrate_one_resource(mock_write_tmpfile) ++ self.config.runner.pcmk.simulate_cib( ++ self.tmpfile_new_cib.name, ++ self.tmpfile_transitions.name, ++ stdout="simulate output", ++ resources=fixture_two_primitives_cib_disabled, ++ ) ++ result = resource.disable_simulate( ++ self.env_assist.get_env(), ++ ["A"], ++ True, ++ ) ++ self.assertEqual( ++ result, ++ dict( ++ plaintext_simulated_status="simulate output", ++ other_affected_resource_list=["B"], ++ ), ++ ) ++ ++ def test_success_others_migrated_no_strict(self, mock_write_tmpfile): ++ self.fixture_migrate_one_resource(mock_write_tmpfile) ++ self.config.runner.pcmk.simulate_cib( ++ self.tmpfile_new_cib.name, ++ self.tmpfile_transitions.name, ++ stdout="simulate output", ++ resources=fixture_two_primitives_cib_disabled, ++ ) ++ result = resource.disable_simulate( ++ self.env_assist.get_env(), ++ ["A"], ++ False, ++ ) ++ self.assertEqual( ++ result, ++ dict( ++ plaintext_simulated_status="simulate output", ++ other_affected_resource_list=[], ++ ), ++ ) ++ ++ def test_simulate_error(self, mock_write_tmpfile): ++ mock_write_tmpfile.side_effect = [ ++ self.tmpfile_new_cib, self.tmpfile_transitions, ++ AssertionError("No other write_tmpfile call expected") ++ ] ++ (self.config ++ .runner.cib.load(resources=fixture_primitive_cib_enabled) ++ .runner.pcmk.load_state(resources=fixture_primitive_status_managed) ++ .runner.pcmk.simulate_cib( ++ self.tmpfile_new_cib.name, ++ self.tmpfile_transitions.name, ++ stdout="some stdout", ++ stderr="some stderr", ++ returncode=1, ++ resources=fixture_primitive_cib_disabled, ++ ) ++ ) ++ ++ self.env_assist.assert_raise_library_error( ++ lambda: resource.disable_simulate( ++ self.env_assist.get_env(), ++ ["A"], ++ True, ++ ), ++ [ ++ fixture.error( ++ report_codes.CIB_SIMULATE_ERROR, ++ reason="some stderr", ++ ), ++ ], ++ expected_in_processor=False ++ ) ++ ++ ++class DisableSafeMixin(DisableSafeFixturesMixin): + def test_not_live(self, mock_write_tmpfile): + mock_write_tmpfile.side_effect = [ + AssertionError("No other write_tmpfile call expected") +diff --git a/pcs/pcs.8 b/pcs/pcs.8 +index 80b80ef7..3367f979 100644 +--- a/pcs/pcs.8 ++++ b/pcs/pcs.8 +@@ -90,23 +90,23 @@ Deletes the resource, group, master or clone (and all resources within the group + 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. + .TP +-disable ... [\fB\-\-safe\fR [\fB\-\-no\-strict\fR]] [\fB\-\-simulate\fR] [\fB\-\-wait\fR[=n]] ++disable ... [\fB\-\-safe\fR [\fB\-\-no\-strict\fR]] [\fB\-\-simulate\fR [\fB\-\-brief\fR]] [\fB\-\-wait\fR[=n]] + Attempt to stop the resources if they are running and forbid the cluster from starting them again. Depending on the rest of the configuration (constraints, options, failures, etc), the resources may remain started. + .br + If \fB\-\-safe\fR is specified, no changes to the cluster configuration will be made if other than specified resources would be affected in any way. + .br + If \fB\-\-no\-strict\fR is specified, no changes to the cluster configuration will be made if other than specified resources would get stopped or demoted. Moving resources between nodes is allowed. + .br +-If \fB\-\-simulate\fR is specified, no changes to the cluster configuration will be made and the effect of the changes will be printed instead. ++If \fB\-\-simulate\fR is specified, no changes to the cluster configuration will be made and the effect of the changes will be printed instead. If \fB\-\-brief\fR is also specified, only a list of affected resources will be printed. + .br + If \fB\-\-wait\fR is specified, pcs will wait up to 'n' seconds for the resources to stop and then return 0 if the resources are stopped or 1 if the resources have not stopped. If 'n' is not specified it defaults to 60 minutes. + .TP +-safe\-disable ... [\fB\-\-no\-strict\fR] [\fB\-\-simulate\fR] [\fB\-\-wait\fR[=n]] [\fB\-\-force\fR] ++safe\-disable ... [\fB\-\-no\-strict\fR] [\fB\-\-simulate\fR [\fB\-\-brief\fR]] [\fB\-\-wait\fR[=n]] [\fB\-\-force\fR] + Attempt to stop the resources if they are running and forbid the cluster from starting them again. Depending on the rest of the configuration (constraints, options, failures, etc), the resources may remain started. No changes to the cluster configuration will be made if other than specified resources would be affected in any way. + .br + If \fB\-\-no\-strict\fR is specified, no changes to the cluster configuration will be made if other than specified resources would get stopped or demoted. Moving resources between nodes is allowed. + .br +-If \fB\-\-simulate\fR is specified, no changes to the cluster configuration will be made and the effect of the changes will be printed instead. ++If \fB\-\-simulate\fR is specified, no changes to the cluster configuration will be made and the effect of the changes will be printed instead. If \fB\-\-brief\fR is also specified, only a list of affected resources will be printed. + .br + If \fB\-\-wait\fR is specified, pcs will wait up to 'n' seconds for the resources to stop and then return 0 if the resources are stopped or 1 if the resources have not stopped. If 'n' is not specified it defaults to 60 minutes. + .br +diff --git a/pcs/resource.py b/pcs/resource.py +index 33f76656..3274910a 100644 +--- a/pcs/resource.py ++++ b/pcs/resource.py +@@ -2041,6 +2041,7 @@ def resource_disable_cmd(lib, argv, modifiers): + """ + Options: + * -f - CIB file ++ * --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 + * --no-strict - allow disable if other resource is affected +@@ -2050,7 +2051,17 @@ def resource_disable_cmd(lib, argv, modifiers): + raise CmdLineInputError("You must specify resource(s) to disable") + + if modifiers["simulate"]: +- print(lib.resource.disable_simulate(argv)) ++ result = lib.resource.disable_simulate( ++ argv, ++ not modifiers["no-strict"], ++ ) ++ if modifiers["brief"]: ++ # if the result is empty, printing it would produce a new line, ++ # which is not wanted ++ if result["other_affected_resource_list"]: ++ print("\n".join(result["other_affected_resource_list"])) ++ return ++ print(result["plaintext_simulated_status"]) + return + if modifiers["safe"] or modifiers["no-strict"]: + lib.resource.disable_safe( +@@ -2065,6 +2076,7 @@ def resource_disable_cmd(lib, argv, modifiers): + def resource_safe_disable_cmd(lib, argv, modifiers): + """ + Options: ++ * --brief - show brief output of --simulate + * --force - skip checks for safe resource disable + * --no-strict - allow disable if other resource is affected + * --simulate - do not push the CIB, print its effects +diff --git a/pcs/test/test_resource.py b/pcs/test/test_resource.py +index 2a110683..972323f9 100644 +--- a/pcs/test/test_resource.py ++++ b/pcs/test/test_resource.py +@@ -6463,6 +6463,7 @@ class ResourceDisable(TestCase): + + def run_cmd(self, argv, modifiers=None): + default_modifiers = { ++ "brief": False, + "safe": False, + "simulate": False, + "no-strict": False, +@@ -6472,6 +6473,16 @@ class ResourceDisable(TestCase): + default_modifiers.update(modifiers) + resource.resource_disable_cmd(self.lib, argv, default_modifiers) + ++ @staticmethod ++ def _fixture_output(plaintext=None, resources=None): ++ plaintext = plaintext if plaintext is not None else "simulate output" ++ resources = resources if resources is not None else ["Rx", "Ry"] ++ return dict( ++ plaintext_simulated_status=plaintext, ++ other_affected_resource_list=resources, ++ ) ++ ++ + def test_no_args(self): + with self.assertRaises(CmdLineInputError) as cm: + self.run_cmd([]) +@@ -6529,13 +6540,52 @@ class ResourceDisable(TestCase): + + @mock.patch("pcs.resource.print") + def test_simulate(self, mock_print): +- self.resource.disable_simulate.return_value = "simulate output" ++ self.resource.disable_simulate.return_value = self._fixture_output() + self.run_cmd(["R1", "R2"], dict(simulate=True)) +- self.resource.disable_simulate.assert_called_once_with(["R1", "R2"]) ++ self.resource.disable_simulate.assert_called_once_with( ++ ["R1", "R2"], True ++ ) + self.resource.disable.assert_not_called() + self.resource.disable_safe.assert_not_called() + mock_print.assert_called_once_with("simulate output") + ++ @mock.patch("pcs.resource.print") ++ def test_simulate_brief(self, mock_print): ++ self.resource.disable_simulate.return_value = self._fixture_output() ++ self.run_cmd(["R1", "R2"], dict(simulate=True, brief=True)) ++ self.resource.disable_simulate.assert_called_once_with( ++ ["R1", "R2"], True ++ ) ++ self.resource.disable.assert_not_called() ++ self.resource.disable_safe.assert_not_called() ++ mock_print.assert_called_once_with("Rx\nRy") ++ ++ @mock.patch("pcs.resource.print") ++ def test_simulate_brief_nostrict(self, mock_print): ++ self.resource.disable_simulate.return_value = self._fixture_output() ++ self.run_cmd( ++ ["R1", "R2"], {"simulate": True, "brief": True, "no-strict": True} ++ ) ++ self.resource.disable_simulate.assert_called_once_with( ++ ["R1", "R2"], False ++ ) ++ self.resource.disable.assert_not_called() ++ self.resource.disable_safe.assert_not_called() ++ mock_print.assert_called_once_with("Rx\nRy") ++ ++ @mock.patch("pcs.resource.print") ++ def test_simulate_brief_nothing_affected(self, mock_print): ++ self.resource.disable_simulate.return_value = self._fixture_output( ++ resources=[] ++ ) ++ self.run_cmd(["R1", "R2"], dict(simulate=True, brief=True)) ++ self.resource.disable_simulate.assert_called_once_with( ++ ["R1", "R2"], True ++ ) ++ self.resource.disable.assert_not_called() ++ self.resource.disable_safe.assert_not_called() ++ mock_print.assert_not_called() ++ + def test_wait(self): + self.run_cmd(["R1", "R2"], dict(wait="10")) + self.resource.disable.assert_called_once_with(["R1", "R2"], "10") +@@ -6557,6 +6607,7 @@ class ResourceSafeDisable(TestCase): + + def run_cmd(self, argv, modifiers=None): + default_modifiers = { ++ "brief": False, + "safe": False, + "simulate": False, + "no-strict": False, +@@ -6567,6 +6618,15 @@ class ResourceSafeDisable(TestCase): + default_modifiers.update(modifiers) + resource.resource_safe_disable_cmd(self.lib, argv, default_modifiers) + ++ @staticmethod ++ def _fixture_output(plaintext=None, resources=None): ++ plaintext = plaintext if plaintext is not None else "simulate output" ++ resources = resources if resources is not None else ["Rx", "Ry"] ++ return dict( ++ plaintext_simulated_status=plaintext, ++ other_affected_resource_list=resources, ++ ) ++ + def test_no_args(self): + with self.assertRaises(CmdLineInputError) as cm: + self.run_cmd([]) +@@ -6639,9 +6699,51 @@ class ResourceSafeDisable(TestCase): + + @mock.patch("pcs.resource.print") + def test_simulate(self, mock_print): +- self.resource.disable_simulate.return_value = "simulate output" ++ self.resource.disable_simulate.return_value = self._fixture_output() + self.run_cmd(["R1", "R2"], dict(simulate=True)) +- self.resource.disable_simulate.assert_called_once_with(["R1", "R2"]) ++ self.resource.disable_simulate.assert_called_once_with( ++ ["R1", "R2"], True ++ ) + self.resource.disable.assert_not_called() + self.resource.disable_safe.assert_not_called() + mock_print.assert_called_once_with("simulate output") ++ ++ @mock.patch("pcs.resource.print") ++ def test_simulate_brief(self, mock_print): ++ self.resource.disable_simulate.return_value = self._fixture_output() ++ self.run_cmd(["R1", "R2"], dict(simulate=True, brief=True)) ++ self.resource.disable_simulate.assert_called_once_with( ++ ["R1", "R2"], True ++ ) ++ self.resource.disable.assert_not_called() ++ self.resource.disable_safe.assert_not_called() ++ mock_print.assert_called_once_with("Rx\nRy") ++ ++ @mock.patch("pcs.resource.print") ++ def test_simulate_brief_nostrict(self, mock_print): ++ self.resource.disable_simulate.return_value = self._fixture_output() ++ self.run_cmd( ++ ["R1", "R2"], {"simulate": True, "brief": True, "no-strict": True} ++ ) ++ self.resource.disable_simulate.assert_called_once_with( ++ ["R1", "R2"], False ++ ) ++ self.resource.disable.assert_not_called() ++ self.resource.disable_safe.assert_not_called() ++ mock_print.assert_called_once_with("Rx\nRy") ++ ++ @mock.patch("pcs.resource.print") ++ def test_simulate_brief_nothing_affected(self, mock_print): ++ self.resource.disable_simulate.return_value = self._fixture_output( ++ resources=[] ++ ) ++ self.run_cmd( ++ ["R1", "R2"], ++ {"simulate": True, "brief": True, "no-strict": True} ++ ) ++ self.resource.disable_simulate.assert_called_once_with( ++ ["R1", "R2"], False ++ ) ++ self.resource.disable.assert_not_called() ++ self.resource.disable_safe.assert_not_called() ++ mock_print.assert_not_called() +diff --git a/pcs/usage.py b/pcs/usage.py +index 61e6826e..408f9514 100644 +--- a/pcs/usage.py ++++ b/pcs/usage.py +@@ -252,7 +252,8 @@ Commands: + started, or 1 if the resources have not yet started. If 'n' is not + specified it defaults to 60 minutes. + +- disable ... [--safe [--no-strict]] [--simulate] [--wait[=n]] ++ disable ... [--safe [--no-strict]] [--simulate [--brief]] ++ [--wait[=n]] + Attempt to stop the resources if they are running and forbid the + cluster from starting them again. Depending on the rest of the + configuration (constraints, options, failures, etc), the resources may +@@ -263,14 +264,16 @@ Commands: + will be made if other than specified resources would get stopped or + demoted. Moving resources between nodes is allowed. + If --simulate is specified, no changes to the cluster configuration +- will be made and the effect of the changes will be printed instead. ++ will be made and the effect of the changes will be printed instead. If ++ --brief is also specified, only a list of affected resources will be ++ printed. + If --wait is specified, pcs will wait up to 'n' seconds for the + resources to stop and then return 0 if the resources are stopped or 1 + if the resources have not stopped. If 'n' is not specified it defaults + to 60 minutes. + +- safe-disable ... [--no-strict] [--simulate] [--wait[=n]] +- [--force] ++ safe-disable ... [--no-strict] [--simulate [--brief]] ++ [--wait[=n]] [--force] + Attempt to stop the resources if they are running and forbid the + cluster from starting them again. Depending on the rest of the + configuration (constraints, options, failures, etc), the resources may +@@ -280,7 +283,9 @@ Commands: + will be made if other than specified resources would get stopped or + demoted. Moving resources between nodes is allowed. + If --simulate is specified, no changes to the cluster configuration +- will be made and the effect of the changes will be printed instead. ++ will be made and the effect of the changes will be printed instead. If ++ --brief is also specified, only a list of affected resources will be ++ printed. + If --wait is specified, pcs will wait up to 'n' seconds for the + resources to stop and then return 0 if the resources are stopped or 1 + if the resources have not stopped. If 'n' is not specified it defaults +diff --git a/pcs/utils.py b/pcs/utils.py +index 66f7ebf1..e56a1e8b 100644 +--- a/pcs/utils.py ++++ b/pcs/utils.py +@@ -2960,6 +2960,7 @@ def get_modifiers(): + "autocorrect": "--autocorrect" in pcs_options, + "autodelete": "--autodelete" in pcs_options, + "before": pcs_options.get("--before", None), ++ "brief": "--brief" in pcs_options, + "corosync_conf": pcs_options.get("--corosync_conf", None), + "describe": "--nodesc" not in pcs_options, + "device": pcs_options.get("--device", []), +diff --git a/pcsd/capabilities.xml b/pcsd/capabilities.xml +index dafc771a..df1da355 100644 +--- a/pcsd/capabilities.xml ++++ b/pcsd/capabilities.xml +@@ -1145,7 +1145,17 @@ + + Show effects caused by disabling resources. + +- pcs commands: resource disable --simulate ++ pcs commands: resource disable --simulate, ++ resource safe-disable --simulate ++ ++ ++ ++ ++ Show only a list of affected resources instead of the whole listing of ++ effects caused by disabling resources. ++ ++ pcs commands: resource disable --simulate --brief, ++ resource safe-disable --simulate --brief + + + +-- +2.21.0 + diff --git a/SOURCES/bz1843593-01-GUI-fix-race-condition-when-removing-resource.patch b/SOURCES/bz1843593-01-GUI-fix-race-condition-when-removing-resource.patch new file mode 100644 index 0000000..be77daf --- /dev/null +++ b/SOURCES/bz1843593-01-GUI-fix-race-condition-when-removing-resource.patch @@ -0,0 +1,158 @@ +From b56bf42759b18ed7b98dc98d3897b3cdf7b3ce5a Mon Sep 17 00:00:00 2001 +From: Ondrej Mular +Date: Thu, 11 Jun 2020 16:53:09 +0200 +Subject: [PATCH 1/6] GUI: fix race-condition when removing resource + +--- + pcs/lib/cib/tools.py | 30 ++++++++++++++++++++---------- + pcs/test/test_acl.py | 4 ++-- + pcs/test/test_resource.py | 33 +++++++++++++++++++++++++++++++++ + pcsd/public/js/pcsd.js | 2 +- + 4 files changed, 56 insertions(+), 13 deletions(-) + +diff --git a/pcs/lib/cib/tools.py b/pcs/lib/cib/tools.py +index ab2a9df5..e9599c43 100644 +--- a/pcs/lib/cib/tools.py ++++ b/pcs/lib/cib/tools.py +@@ -56,11 +56,14 @@ class IdProvider(object): + return report_list + + +-def does_id_exist(tree, check_id): ++def get_configuration_elements_by_id(tree, check_id): + """ +- Checks to see if id exists in the xml dom passed +- tree cib etree node +- check_id id to check ++ Return any configuration elements (not in status section of cib) with value ++ of attribte id specified as 'check_id' ++ ++ tree -- any element in xml tree, whole tree (not only its subtree) will be ++ searched ++ check_id -- id to find + """ + + # do not search in /cib/status, it may contain references to previously +@@ -70,7 +73,7 @@ def does_id_exist(tree, check_id): + #which will be named the same as the value of the remote-node attribute of + #the explicit resource. So the value of nvpair named "remote-node" is + #considered to be id +- existing = get_root(tree).xpath(""" ++ return get_root(tree).xpath(""" + ( + /cib/*[name()!="status"] + | +@@ -96,7 +99,15 @@ def does_id_exist(tree, check_id): + ) + ] + """.format(check_id)) +- return len(existing) > 0 ++ ++ ++def does_id_exist(tree, check_id): ++ """ ++ Checks to see if id exists in the xml dom passed ++ tree cib etree node ++ check_id id to check ++ """ ++ return len(get_configuration_elements_by_id(tree, check_id)) > 0 + + def validate_id_does_not_exist(tree, id): + """ +@@ -155,11 +166,10 @@ def find_element_by_tag_and_id( + if element_list: + return element_list[0] + +- element = get_root(context_element).find( +- './/*[@id="{0}"]'.format(element_id) +- ) ++ elements = get_configuration_elements_by_id(context_element, element_id) + +- if element is not None: ++ if elements: ++ element = elements[0] + raise LibraryError( + reports.id_belongs_to_unexpected_type( + element_id, +diff --git a/pcs/test/test_acl.py b/pcs/test/test_acl.py +index a59beb0a..2a3b6d66 100644 +--- a/pcs/test/test_acl.py ++++ b/pcs/test/test_acl.py +@@ -395,7 +395,7 @@ Group: group2 + + o,r = pcs("acl group delete user1") + assert r == 1 +- ac(o,"Error: 'user1' is not an ACL group\n") ++ ac(o,"Error: ACL group 'user1' does not exist\n") + + o,r = pcs("acl") + ac(o, """\ +@@ -861,7 +861,7 @@ Role: role4 + self.assert_pcs_success("acl user create user1") + self.assert_pcs_fail( + "acl role assign role1 to group user1", +- "Error: 'user1' is not an ACL group\n" ++ "Error: ACL group 'user1' does not exist\n" + ) + + def test_assign_unassign_role_to_group_with_to(self): +diff --git a/pcs/test/test_resource.py b/pcs/test/test_resource.py +index 972323f9..fd1a0731 100644 +--- a/pcs/test/test_resource.py ++++ b/pcs/test/test_resource.py +@@ -3542,6 +3542,39 @@ Error: Cannot remove more than one resource from cloned group + ) + ) + ++ def testResourceDisableNonExistingWithFailedAction(self): ++ with open(temp_cib, "w") as f: ++ f.truncate() ++ f.write( ++ """ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ """ ++ ) ++ self.assert_pcs_fail( ++ "resource disable A", ++ "Error: bundle/clone/group/master/resource 'A' does not exist\n" ++ ) ++ ++ + def testResourceEnable(self): + # These tests were moved to + # pcs/lib/commands/test/resource/test_resource_enable_disable.py . +diff --git a/pcsd/public/js/pcsd.js b/pcsd/public/js/pcsd.js +index 20c19434..60c3da59 100644 +--- a/pcsd/public/js/pcsd.js ++++ b/pcsd/public/js/pcsd.js +@@ -1441,7 +1441,7 @@ function remove_resource(ids, force) { + error == "timeout" || + xhr.responseText == '{"noresponse":true}' + ) { +- message = "Operation takes longer to complete than expected."; ++ message = "Operation takes longer to complete than expected but it will continue in the background."; + } else { + message = "Unable to remove resources (" + error + ")"; + if ( +-- +2.21.0 + diff --git a/SOURCES/change-cman-to-rhel6-in-messages.patch b/SOURCES/change-cman-to-rhel6-in-messages.patch index 0589e6c..c64bb5b 100644 --- a/SOURCES/change-cman-to-rhel6-in-messages.patch +++ b/SOURCES/change-cman-to-rhel6-in-messages.patch @@ -1,7 +1,7 @@ -From 33e52f98527cc17a7e5ddfa51d6e21a022d8ce13 Mon Sep 17 00:00:00 2001 +From 44f4f498e323a54fc83068975eaec19667801ee8 Mon Sep 17 00:00:00 2001 From: Ivan Devat Date: Mon, 23 May 2016 17:00:13 +0200 -Subject: [PATCH 5/6] change cman to rhel6 in messages +Subject: [PATCH 4/6] change cman to rhel6 in messages --- pcs/cli/common/console_report.py | 8 ++++---- @@ -54,10 +54,10 @@ index b5885b6b..9108257e 100644 ), diff --git a/pcs/cluster.py b/pcs/cluster.py -index 0bacae16..ca47072d 100644 +index a32fdc7e..e8fa0ce4 100644 --- a/pcs/cluster.py +++ b/pcs/cluster.py -@@ -2122,7 +2122,7 @@ def node_add(lib_env, node0, node1, modifiers): +@@ -2125,7 +2125,7 @@ def node_add(lib_env, node0, node1, modifiers): else: utils.err("Unable to update any nodes") if utils.is_cman_with_udpu_transport(): @@ -66,7 +66,7 @@ index 0bacae16..ca47072d 100644 + "cluster restart is required to apply node addition") if wait: print() -@@ -2198,7 +2198,7 @@ def node_remove(lib_env, node0, modifiers): +@@ -2201,7 +2201,7 @@ def node_remove(lib_env, node0, modifiers): output, retval = utils.reloadCorosync() output, retval = utils.run(["crm_node", "--force", "-R", node0]) if utils.is_cman_with_udpu_transport(): @@ -75,7 +75,7 @@ index 0bacae16..ca47072d 100644 + "cluster restart is required to apply node removal") def cluster_localnode(argv): -@@ -2366,7 +2366,7 @@ def cluster_uidgid(argv, silent_list = False): +@@ -2369,7 +2369,7 @@ def cluster_uidgid(argv, silent_list = False): def cluster_get_corosync_conf(argv): if utils.is_rhel6(): @@ -98,10 +98,10 @@ index 6c0f8194..f2cdbaa3 100644 cluster_conf = settings.cluster_conf_file dry_run_output = None diff --git a/pcs/pcs.8 b/pcs/pcs.8 -index 596ceb7a..b100c82d 100644 +index 3367f979..bb085339 100644 --- a/pcs/pcs.8 +++ b/pcs/pcs.8 -@@ -237,13 +237,13 @@ auth [[:]] [...] [\fB\-u\fR ] [\fB\-p\fR ] [\fB\ +@@ -249,13 +249,13 @@ auth [[:]] [...] [\fB\-u\fR ] [\fB\-p\fR ] [\fB\ Authenticate pcs to pcsd on nodes specified, or on all nodes configured in the local cluster if no nodes are specified (authorization tokens are stored in ~/.pcs/tokens or /var/lib/pcsd/tokens for root). By default all nodes are also authenticated to each other, using \fB\-\-local\fR only authenticates the local node (and does not authenticate the remote nodes with each other). Using \fB\-\-force\fR forces re\-authentication to occur. .TP setup [\fB\-\-start\fR [\fB\-\-wait\fR[=]]] [\fB\-\-local\fR] [\fB\-\-enable\fR] \fB\-\-name\fR [] [...] [\fB\-\-transport\fR udpu|udp] [\fB\-\-rrpmode\fR active|passive] [\fB\-\-addr0\fR [[[\fB\-\-mcast0\fR
] [\fB\-\-mcastport0\fR ] [\fB\-\-ttl0\fR ]] | [\fB\-\-broadcast0\fR]] [\fB\-\-addr1\fR [[[\fB\-\-mcast1\fR
] [\fB\-\-mcastport1\fR ] [\fB\-\-ttl1\fR ]] | [\fB\-\-broadcast1\fR]]]] [\fB\-\-wait_for_all\fR=<0|1>] [\fB\-\-auto_tie_breaker\fR=<0|1>] [\fB\-\-last_man_standing\fR=<0|1> [\fB\-\-last_man_standing_window\fR=