Blob Blame History Raw
From c19066d0dade1ae544e8f8d80513310d47e72ebe Mon Sep 17 00:00:00 2001
From: Tomas Jelinek <tojeline@redhat.com>
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 = "<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):
+    fixture_transitions_both_stopped = """
+        <transition_graph>
+          <synapse>
+            <action_set>
+              <rsc_op id="0" operation="stop" on_node="node1">
+                <primitive
+                    id="A" class="ocf" provider="pacemaker" type="Dummy"
+                />
+              </rsc_op>
+            </action_set>
+          </synapse>
+          <synapse>
+            <action_set>
+              <rsc_op id="1" operation="stop" on_node="node2">
+                <primitive
+                    id="B" class="ocf" provider="pacemaker" type="Dummy"
+                />
+              </rsc_op>
+            </action_set>
+          </synapse>
+        </transition_graph>
+    """
+    fixture_transitions_one_migrated = """
+        <transition_graph>
+          <synapse>
+            <action_set>
+              <rsc_op id="0" operation="stop" on_node="node1">
+                <primitive
+                    id="A" class="ocf" provider="pacemaker" type="Dummy"
+                />
+              </rsc_op>
+            </action_set>
+          </synapse>
+          <synapse>
+            <action_set>
+              <rsc_op id="1" operation="stop" on_node="node2">
+                <primitive
+                    id="B" class="ocf" provider="pacemaker" type="Dummy"
+                />
+              </rsc_op>
+            </action_set>
+          </synapse>
+          <synapse>
+            <action_set>
+              <rsc_op id="2" operation="start" on_node="node1">
+                <primitive
+                    id="B" class="ocf" provider="pacemaker" type="Dummy"
+                />
+              </rsc_op>
+            </action_set>
+          </synapse>
+        </transition_graph>
+    """
+    fixture_transitions_master_demoted = """
+        <transition_graph>
+          <synapse>
+            <action_set>
+              <rsc_op id="0" operation="stop" on_node="node1">
+                <primitive
+                    id="A" class="ocf" provider="pacemaker" type="Dummy"
+                />
+              </rsc_op>
+            </action_set>
+          </synapse>
+          <synapse>
+            <action_set>
+              <rsc_op id="1" operation="demote" on_node="node2">
+                <primitive
+                    id="B" class="ocf" provider="pacemaker" type="Dummy"
+                    long_id="B:0"
+                />
+              </rsc_op>
+            </action_set>
+          </synapse>
+        </transition_graph>
+    """
+    fixture_transitions_master_migrated = """
+        <transition_graph>
+          <synapse>
+            <action_set>
+              <rsc_op id="0" operation="stop" on_node="node1">
+                <primitive
+                    id="A" class="ocf" provider="pacemaker" type="Dummy"
+                />
+              </rsc_op>
+            </action_set>
+          </synapse>
+          <synapse>
+            <action_set>
+              <rsc_op id="1" operation="demote" on_node="node2">
+                <primitive
+                    id="B" class="ocf" provider="pacemaker" type="Dummy"
+                    long_id="B:0"
+                />
+              </rsc_op>
+            </action_set>
+          </synapse>
+          <synapse>
+            <action_set>
+              <rsc_op id="2" operation="promote" on_node="node1">
+                <primitive
+                    id="B" class="ocf" provider="pacemaker" type="Dummy"
+                    long_id="B:1"
+                />
+              </rsc_op>
+            </action_set>
+          </synapse>
+        </transition_graph>
+    """
+    fixture_cib_with_master = """
+        <resources>
+            <primitive class="ocf" id="A" provider="heartbeat" type="Dummy">
+            </primitive>
+            <master id="B-master">
+                <primitive class="ocf" id="B" provider="heartbeat" type="Dummy">
+                </primitive>
+            </master>
+        </resources>
+    """
+    fixture_cib_with_master_primitive_disabled = """
+        <resources>
+            <primitive class="ocf" id="A" provider="heartbeat" type="Dummy">
+                <meta_attributes id="A-meta_attributes">
+                    <nvpair id="A-meta_attributes-target-role"
+                        name="target-role" value="Stopped" />
+                </meta_attributes>
+            </primitive>
+            <master id="B-master">
+                <primitive class="ocf" id="B" provider="heartbeat" type="Dummy">
+                </primitive>
+            </master>
+        </resources>
+    """
+    fixture_status_with_master_managed = """
+        <resources>
+            <resource id="A" managed="true" />
+            <clone id="B-master" managed="true" multi_state="true"
+                unique="false"
+            >
+                <resource id="B" managed="true" />
+                <resource id="B" managed="true" />
+            </clone>
+        </resources>
+    """
+
+    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 = "<new-cib/>"
+        self.tmpfile_transitions = mock.MagicMock()
+        self.tmpfile_transitions.name = rc("transitions.tmp")
+        self.tmpfile_transitions.read.return_value = "<transitions/>"
+
+    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("<cib />")
+        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="""
+                    <resources>
+                        <resource id="A" managed="true" role="Stopped">
+                        </resource>
+                        <resource id="B" managed="true" role="Stopped">
+                        </resource>
+                    </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 = """
+            <resources>
+                <primitive id="A" />
+                <clone id="B-clone">
+                    <primitive id="B" />
+                </clone>
+                <master id="C-master">
+                    <primitive id="C" />
+                </master>
+                <group id="D">
+                    <primitive id="D1" />
+                    <primitive id="D2" />
+                </group>
+                <clone id="E-clone">
+                    <group id="E">
+                        <primitive id="E1" />
+                        <primitive id="E2" />
+                    </group>
+                </clone>
+                <master id="F-master">
+                    <group id="F">
+                        <primitive id="F1" />
+                        <primitive id="F2" />
+                    </group>
+                </master>
+                <bundle id="G-bundle" />
+                <bundle id="H-bundle">
+                    <primitive id="H" />
+                </bundle>
+            </resources>
+        """
+        status_xml = """
+            <resources>
+                <resource id="A" managed="true" />
+                <clone id="B-clone" managed="true" multi_state="false"
+                    unique="false"
+                >
+                    <resource id="B" managed="true" />
+                    <resource id="B" managed="true" />
+                </clone>
+                <clone id="C-master" managed="true" multi_state="true"
+                    unique="false"
+                >
+                    <resource id="C" managed="true" />
+                    <resource id="C" managed="true" />
+                </clone>
+                <group id="D" number_resources="2">
+                    <resource id="D1" managed="true" />
+                    <resource id="D2" managed="true" />
+                </group>
+                <clone id="E-clone" managed="true" multi_state="false"
+                    unique="false"
+                >
+                    <group id="E:0" number_resources="2">
+                        <resource id="E1" managed="true" />
+                        <resource id="E2" managed="true" />
+                    </group>
+                    <group id="E:1" number_resources="2">
+                        <resource id="E1" managed="true" />
+                        <resource id="E2" managed="true" />
+                    </group>
+                </clone>
+                <clone id="F-master" managed="true" multi_state="true"
+                    unique="false"
+                >
+                    <group id="F:0" number_resources="2">
+                        <resource id="F1" managed="true" />
+                        <resource id="F2" managed="true" />
+                    </group>
+                    <group id="F:1" number_resources="2">
+                        <resource id="F1" managed="true" />
+                        <resource id="F2" managed="true" />
+                    </group>
+                </clone>
+                <bundle id="H-bundle" type="docker" image="pcmktest:http"
+                    unique="false" managed="true" failed="false"
+                >
+                    <replica id="0">
+                        <resource id="H" />
+                    </replica>
+                    <replica id="1">
+                        <resource id="H" />
+                    </replica>
+                </bundle>
+            </resources>
+        """
+        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("""
+                  <synapse>
+                    <action_set>
+                      <rsc_op id="{index}" operation="stop" on_node="node1">
+                        <primitive id="{res_name}" long_id="{res_name}:0" />
+                      </rsc_op>
+                    </action_set>
+                  </synapse>
+                  <synapse>
+                    <action_set>
+                      <rsc_op id="{index_1}" operation="stop" on_node="node2">
+                        <primitive id="{res_name}" long_id="{res_name}:1" />
+                      </rsc_op>
+                    </action_set>
+                  </synapse>
+                """.format(
+                    index=index,
+                    index_1=index + 1,
+                    res_name=res_name,
+                ))
+                index += 2
+            else:
+                synapses.append("""
+                  <synapse>
+                    <action_set>
+                      <rsc_op id="{index}" operation="stop" on_node="node1">
+                        <primitive id="{res_name}" />
+                      </rsc_op>
+                    </action_set>
+                  </synapse>
+                """.format(
+                    index=index,
+                    res_name=res_name,
+                ))
+                index += 1
+        transitions_xml = (
+            "<transition_graph>" + "\n".join(synapses) + "</transition_graph>"
+        )
+
+        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="""
+                <resources>
+                    <primitive id="A" />
+                    <clone id="B-clone">
+                        <meta_attributes id="B-clone-meta_attributes">
+                            <nvpair name="target-role" value="Stopped"
+                                id="B-clone-meta_attributes-target-role"
+                            />
+                        </meta_attributes>
+                        <primitive id="B" />
+                    </clone>
+                    <master id="C-master">
+                        <meta_attributes id="C-master-meta_attributes">
+                            <nvpair name="target-role" value="Stopped"
+                                id="C-master-meta_attributes-target-role"
+                            />
+                        </meta_attributes>
+                        <primitive id="C" />
+                    </master>
+                    <group id="D">
+                        <meta_attributes id="D-meta_attributes">
+                            <nvpair name="target-role" value="Stopped"
+                                id="D-meta_attributes-target-role"
+                            />
+                        </meta_attributes>
+                        <primitive id="D1" />
+                        <primitive id="D2" />
+                    </group>
+                    <clone id="E-clone">
+                        <meta_attributes id="E-clone-meta_attributes">
+                            <nvpair name="target-role" value="Stopped"
+                                id="E-clone-meta_attributes-target-role"
+                            />
+                        </meta_attributes>
+                        <group id="E">
+                            <primitive id="E1" />
+                            <primitive id="E2" />
+                        </group>
+                    </clone>
+                    <master id="F-master">
+                        <meta_attributes id="F-master-meta_attributes">
+                            <nvpair name="target-role" value="Stopped"
+                                id="F-master-meta_attributes-target-role"
+                            />
+                        </meta_attributes>
+                        <group id="F">
+                            <primitive id="F1" />
+                            <primitive id="F2" />
+                        </group>
+                    </master>
+                    <bundle id="G-bundle" />
+                    <bundle id="H-bundle">
+                        <meta_attributes id="H-bundle-meta_attributes">
+                            <nvpair name="target-role" value="Stopped"
+                                id="H-bundle-meta_attributes-target-role"
+                            />
+                        </meta_attributes>
+                        <primitive id="H" />
+                    </bundle>
+                </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, "<cib />")
+        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="<cib />"
+        )
+
+    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, "<cib />"),
+            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, "<cib />"),
+            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, "<cib />"),
+            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, "<cib />"),
+            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, "<cib />"),
+            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 = "<cib/>"
+        self.cib = etree.fromstring(self.cib_xml)
+        self.simulate_output = "  some output  "
+        self.transitions = "<transitions/>"
+        self.new_cib = "<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 <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\-\-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 <resource id>... [\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 <resource id>... [\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 <resource id> [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 <resource id>... [--wait[=n]]
+    disable <resource id>... [--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 <resource id>... [--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 <resource id> [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
       </description>
     </capability>
+    <capability id="pcmk.resource.disable.safe" in-pcs="1" in-pcsd="0">
+      <description>
+        Do not disable resources if other resources would be affected.
+
+        pcs commands: resource disable --safe [--no-strict]
+      </description>
+    </capability>
+    <capability id="pcmk.resource.disable.simulate" in-pcs="1" in-pcsd="0">
+      <description>
+        Show effects caused by disabling resources.
+
+        pcs commands: resource disable --simulate
+      </description>
+    </capability>
     <capability id="pcmk.resource.manage-unmanage" in-pcs="1" in-pcsd="1">
       <description>
         Put a resource into unmanaged and managed mode.
-- 
2.20.1