Blob Blame History Raw
From c73cbac945259429e18fcd457341f83f8c8f6e59 Mon Sep 17 00:00:00 2001
From: Tomas Jelinek <tojeline@redhat.com>
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 = "<new-cib/>"
-        self.tmpfile_transitions = mock.MagicMock()
-        self.tmpfile_transitions.name = rc("transitions.tmp")
-        self.tmpfile_transitions.read.return_value = "<transitions/>"
-
-    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("<cib />")
-        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 = """
         <transition_graph>
           <synapse>
@@ -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("<cib />")
+        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 <resource id>... [\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 <resource id>... [\fB\-\-safe\fR [\fB\-\-no\-strict\fR]] [\fB\-\-simulate\fR] [\fB\-\-wait\fR[=n]]
+disable <resource id>... [\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 <resource id>... [\fB\-\-no\-strict\fR] [\fB\-\-simulate\fR] [\fB\-\-wait\fR[=n]] [\fB\-\-force\fR]
+safe\-disable <resource id>... [\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 <resource id>... [--safe [--no-strict]] [--simulate] [--wait[=n]]
+    disable <resource id>... [--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 <resource id>... [--no-strict] [--simulate] [--wait[=n]]
-            [--force]
+    safe-disable <resource id>... [--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 @@
       <description>
         Show effects caused by disabling resources.
 
-        pcs commands: resource disable --simulate
+        pcs commands: resource disable --simulate,
+          resource safe-disable --simulate
+      </description>
+    </capability>
+    <capability id="pcmk.resource.disable.simulate.brief" in-pcs="1" in-pcsd="0">
+      <description>
+        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
       </description>
     </capability>
     <capability id="pcmk.resource.manage-unmanage" in-pcs="1" in-pcsd="1">
-- 
2.21.0