Blame SOURCES/bz1770973-01-The-cluster-should-not-be-allowed-t.patch

d01bb5
From c19066d0dade1ae544e8f8d80513310d47e72ebe Mon Sep 17 00:00:00 2001
d01bb5
From: Tomas Jelinek <tojeline@redhat.com>
d01bb5
Date: Tue, 19 Nov 2019 13:47:02 +0100
d01bb5
Subject: [PATCH 2/6] squash bz1770973 The cluster should not be allowed to
d01bb5
 disable a resource if dependent resources are still online
d01bb5
d01bb5
add --simulate and --safe to resource disable
d01bb5
d01bb5
Also add command 'resource safe-disable'. This is an alias for 'resource
d01bb5
disable --safe'.
d01bb5
d01bb5
fix safe-disabling clones, groups, bundles
d01bb5
d01bb5
fix simulate_cib_error report
d01bb5
d01bb5
Putting only one CIB in the report is not enough info. Both original and
d01bb5
changed CIB as well as crm_simulate output would be needed. All that
d01bb5
info can be seen in debug messages. So there is no need to put it in the
d01bb5
report.
d01bb5
---
d01bb5
 pcs/cli/common/console_report.py              |  15 +
d01bb5
 pcs/cli/common/lib_wrapper.py                 |   2 +
d01bb5
 pcs/cli/common/parse_args.py                  |   1 +
d01bb5
 pcs/cli/common/test/test_console_report.py    |  31 +
d01bb5
 pcs/common/report_codes.py                    |   2 +
d01bb5
 pcs/lib/cib/resource/common.py                |  16 +
d01bb5
 pcs/lib/cib/test/test_resource_common.py      |  60 +-
d01bb5
 pcs/lib/commands/resource.py                  | 101 +++
d01bb5
 .../resource/test_resource_enable_disable.py  | 819 +++++++++++++++++-
d01bb5
 pcs/lib/pacemaker/live.py                     |  66 +-
d01bb5
 pcs/lib/pacemaker/simulate.py                 |  86 ++
d01bb5
 pcs/lib/pacemaker/test/test_live.py           | 191 +++-
d01bb5
 pcs/lib/pacemaker/test/test_simulate.py       | 380 ++++++++
d01bb5
 pcs/lib/reports.py                            |  35 +
d01bb5
 pcs/lib/tools.py                              |   5 +-
d01bb5
 pcs/pcs.8                                     |  23 +-
d01bb5
 pcs/resource.py                               |  52 +-
d01bb5
 pcs/test/test_resource.py                     | 195 +++++
d01bb5
 .../tools/command_env/config_runner_pcmk.py   |  55 +-
d01bb5
 pcs/test/tools/command_env/mock_runner.py     |  17 +
d01bb5
 pcs/usage.py                                  |  36 +-
d01bb5
 pcs/utils.py                                  |   3 +
d01bb5
 pcsd/capabilities.xml                         |  14 +
d01bb5
 23 files changed, 2185 insertions(+), 20 deletions(-)
d01bb5
 create mode 100644 pcs/lib/pacemaker/simulate.py
d01bb5
 create mode 100644 pcs/lib/pacemaker/test/test_simulate.py
d01bb5
d01bb5
diff --git a/pcs/cli/common/console_report.py b/pcs/cli/common/console_report.py
d01bb5
index 1824bd9a..b5885b6b 100644
d01bb5
--- a/pcs/cli/common/console_report.py
d01bb5
+++ b/pcs/cli/common/console_report.py
d01bb5
@@ -815,6 +815,14 @@ CODE_TO_MESSAGE_BUILDER_MAP = {
d01bb5
         .format(**info)
d01bb5
     ,
d01bb5
 
d01bb5
+    codes.CIB_SIMULATE_ERROR: lambda info:
d01bb5
+        "Unable to simulate changes in CIB{_reason}"
d01bb5
+        .format(
d01bb5
+            _reason=format_optional(info["reason"], ": {0}"),
d01bb5
+            **info
d01bb5
+        )
d01bb5
+    ,
d01bb5
+
d01bb5
     codes.CIB_PUSH_FORCED_FULL_DUE_TO_CRM_FEATURE_SET: lambda info:
d01bb5
         (
d01bb5
             "Replacing the whole CIB instead of applying a diff, a race "
d01bb5
@@ -1516,4 +1524,11 @@ CODE_TO_MESSAGE_BUILDER_MAP = {
d01bb5
     codes.FENCE_HISTORY_NOT_SUPPORTED:
d01bb5
         "Fence history is not supported, please upgrade pacemaker"
d01bb5
     ,
d01bb5
+
d01bb5
+    codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES: lambda info:
d01bb5
+        (
d01bb5
+            "Disabling specified resources would have an effect on other "
d01bb5
+            "resources\n\n{crm_simulate_plaintext_output}"
d01bb5
+        ).format(**info)
d01bb5
+    ,
d01bb5
 }
d01bb5
diff --git a/pcs/cli/common/lib_wrapper.py b/pcs/cli/common/lib_wrapper.py
d01bb5
index a6cc0ca8..dffbace2 100644
d01bb5
--- a/pcs/cli/common/lib_wrapper.py
d01bb5
+++ b/pcs/cli/common/lib_wrapper.py
d01bb5
@@ -344,6 +344,8 @@ def load_module(env, middleware_factory, name):
d01bb5
                 "create_in_group": resource.create_in_group,
d01bb5
                 "create_into_bundle": resource.create_into_bundle,
d01bb5
                 "disable": resource.disable,
d01bb5
+                "disable_safe": resource.disable_safe,
d01bb5
+                "disable_simulate": resource.disable_simulate,
d01bb5
                 "enable": resource.enable,
d01bb5
                 "get_failcounts": resource.get_failcounts,
d01bb5
                 "manage": resource.manage,
d01bb5
diff --git a/pcs/cli/common/parse_args.py b/pcs/cli/common/parse_args.py
d01bb5
index 23d8799d..1f3e7b33 100644
d01bb5
--- a/pcs/cli/common/parse_args.py
d01bb5
+++ b/pcs/cli/common/parse_args.py
d01bb5
@@ -18,6 +18,7 @@ PCS_LONG_OPTIONS = [
d01bb5
     "force", "skip-offline", "autocorrect", "interactive", "autodelete",
d01bb5
     "all", "full", "groups", "local", "wait", "config", "async",
d01bb5
     "start", "enable", "disabled", "off", "request-timeout=",
d01bb5
+    "safe", "no-strict", "simulate",
d01bb5
     "pacemaker", "corosync",
d01bb5
     "no-default-ops", "defaults", "nodesc",
d01bb5
     "clone", "master", "name=", "group=", "node=",
d01bb5
diff --git a/pcs/cli/common/test/test_console_report.py b/pcs/cli/common/test/test_console_report.py
d01bb5
index 95849615..59192dc2 100644
d01bb5
--- a/pcs/cli/common/test/test_console_report.py
d01bb5
+++ b/pcs/cli/common/test/test_console_report.py
d01bb5
@@ -1934,6 +1934,21 @@ class CibDiffError(NameBuildTest):
d01bb5
         )
d01bb5
 
d01bb5
 
d01bb5
+class CibSimulateError(NameBuildTest):
d01bb5
+    code = codes.CIB_SIMULATE_ERROR
d01bb5
+    def test_success(self):
d01bb5
+        self.assert_message_from_report(
d01bb5
+            "Unable to simulate changes in CIB: error message",
d01bb5
+            reports.cib_simulate_error("error message")
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_empty_reason(self):
d01bb5
+        self.assert_message_from_report(
d01bb5
+            "Unable to simulate changes in CIB",
d01bb5
+            reports.cib_simulate_error("")
d01bb5
+        )
d01bb5
+
d01bb5
+
d01bb5
 class TmpFileWrite(NameBuildTest):
d01bb5
     code = codes.TMP_FILE_WRITE
d01bb5
     def test_success(self):
d01bb5
@@ -2401,3 +2416,19 @@ class FenceHistoryNotSupported(NameBuildTest):
d01bb5
             "Fence history is not supported, please upgrade pacemaker",
d01bb5
             reports.fence_history_not_supported()
d01bb5
         )
d01bb5
+
d01bb5
+
d01bb5
+class ResourceDisableAffectsOtherResources(NameBuildTest):
d01bb5
+    code = codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES
d01bb5
+    def test_success(self):
d01bb5
+        self.assert_message_from_report(
d01bb5
+            (
d01bb5
+                "Disabling specified resources would have an effect on other "
d01bb5
+                "resources\n\ncrm_simulate output"
d01bb5
+            ),
d01bb5
+            reports.resource_disable_affects_other_resources(
d01bb5
+                ["D2", "D1"],
d01bb5
+                ["O2", "O1"],
d01bb5
+                "crm_simulate output",
d01bb5
+            )
d01bb5
+        )
d01bb5
diff --git a/pcs/common/report_codes.py b/pcs/common/report_codes.py
d01bb5
index 68b48215..8340ab62 100644
d01bb5
--- a/pcs/common/report_codes.py
d01bb5
+++ b/pcs/common/report_codes.py
d01bb5
@@ -68,6 +68,7 @@ CIB_LOAD_ERROR_SCOPE_MISSING = "CIB_LOAD_ERROR_SCOPE_MISSING"
d01bb5
 CIB_PUSH_FORCED_FULL_DUE_TO_CRM_FEATURE_SET = "CIB_PUSH_FORCED_FULL_DUE_TO_CRM_FEATURE_SET"
d01bb5
 CIB_PUSH_ERROR = "CIB_PUSH_ERROR"
d01bb5
 CIB_SAVE_TMP_ERROR = "CIB_SAVE_TMP_ERROR"
d01bb5
+CIB_SIMULATE_ERROR = "CIB_SIMULATE_ERROR"
d01bb5
 CIB_UPGRADE_FAILED = "CIB_UPGRADE_FAILED"
d01bb5
 CIB_UPGRADE_FAILED_TO_MINIMAL_REQUIRED_VERSION = "CIB_UPGRADE_FAILED_TO_MINIMAL_REQUIRED_VERSION"
d01bb5
 CIB_UPGRADE_SUCCESSFUL = "CIB_UPGRADE_SUCCESSFUL"
d01bb5
@@ -185,6 +186,7 @@ RESOURCE_BUNDLE_ALREADY_CONTAINS_A_RESOURCE = "RESOURCE_BUNDLE_ALREADY_CONTAINS_
d01bb5
 RESOURCE_BUNDLE_UNSUPPORTED_CONTAINER_TYPE = "RESOURCE_BUNDLE_UNSUPPORTED_CONTAINER_TYPE"
d01bb5
 RESOURCE_IN_BUNDLE_NOT_ACCESSIBLE = "RESOURCE_IN_BUNDLE_NOT_ACCESSIBLE"
d01bb5
 RESOURCE_CLEANUP_ERROR = "RESOURCE_CLEANUP_ERROR"
d01bb5
+RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES = "RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES"
d01bb5
 RESOURCE_DOES_NOT_RUN = "RESOURCE_DOES_NOT_RUN"
d01bb5
 RESOURCE_FOR_CONSTRAINT_IS_MULTIINSTANCE = 'RESOURCE_FOR_CONSTRAINT_IS_MULTIINSTANCE'
d01bb5
 RESOURCE_IS_GUEST_NODE_ALREADY = "RESOURCE_IS_GUEST_NODE_ALREADY"
d01bb5
diff --git a/pcs/lib/cib/resource/common.py b/pcs/lib/cib/resource/common.py
d01bb5
index 87b656b7..f408c2c4 100644
d01bb5
--- a/pcs/lib/cib/resource/common.py
d01bb5
+++ b/pcs/lib/cib/resource/common.py
d01bb5
@@ -51,6 +51,22 @@ def find_primitives(resource_el):
d01bb5
         return [resource_el]
d01bb5
     return []
d01bb5
 
d01bb5
+def get_all_inner_resources(resource_el):
d01bb5
+    """
d01bb5
+    Return all inner resources (both direct and indirect) of a resource
d01bb5
+    Example: for a clone containing a group, this function will return both
d01bb5
+    the group and the resources inside the group
d01bb5
+
d01bb5
+    resource_el -- resource element to get its inner resources
d01bb5
+    """
d01bb5
+    all_inner = set()
d01bb5
+    to_process = set([resource_el])
d01bb5
+    while to_process:
d01bb5
+        new_inner = get_inner_resources(to_process.pop())
d01bb5
+        to_process.update(set(new_inner) - all_inner)
d01bb5
+        all_inner.update(new_inner)
d01bb5
+    return all_inner
d01bb5
+
d01bb5
 def get_inner_resources(resource_el):
d01bb5
     """
d01bb5
     Return list of inner resources (direct descendants) of a resource
d01bb5
diff --git a/pcs/lib/cib/test/test_resource_common.py b/pcs/lib/cib/test/test_resource_common.py
d01bb5
index 596ee57f..06684f95 100644
d01bb5
--- a/pcs/lib/cib/test/test_resource_common.py
d01bb5
+++ b/pcs/lib/cib/test/test_resource_common.py
d01bb5
@@ -84,10 +84,12 @@ class IsCloneDeactivatedByMeta(TestCase):
d01bb5
 
d01bb5
 
d01bb5
 class FindResourcesMixin(object):
d01bb5
+    _iterable_type = list
d01bb5
+
d01bb5
     def assert_find_resources(self, input_resource_id, output_resource_ids):
d01bb5
         self.assertEqual(
d01bb5
-            output_resource_ids,
d01bb5
-            [
d01bb5
+            self._iterable_type(output_resource_ids),
d01bb5
+            self._iterable_type([
d01bb5
                 element.get("id", "")
d01bb5
                 for element in
d01bb5
                 self._tested_fn(
d01bb5
@@ -95,7 +97,7 @@ class FindResourcesMixin(object):
d01bb5
                         './/*[@id="{0}"]'.format(input_resource_id)
d01bb5
                     )
d01bb5
                 )
d01bb5
-            ]
d01bb5
+            ])
d01bb5
         )
d01bb5
 
d01bb5
     def test_group(self):
d01bb5
@@ -119,6 +121,27 @@ class FindResourcesMixin(object):
d01bb5
     def test_bundle_with_primitive(self):
d01bb5
         self.assert_find_resources("H-bundle", ["H"])
d01bb5
 
d01bb5
+    def test_primitive(self):
d01bb5
+        raise NotImplementedError()
d01bb5
+
d01bb5
+    def test_primitive_in_clone(self):
d01bb5
+        raise NotImplementedError()
d01bb5
+
d01bb5
+    def test_primitive_in_master(self):
d01bb5
+        raise NotImplementedError()
d01bb5
+
d01bb5
+    def test_primitive_in_group(self):
d01bb5
+        raise NotImplementedError()
d01bb5
+
d01bb5
+    def test_primitive_in_bundle(self):
d01bb5
+        raise NotImplementedError()
d01bb5
+
d01bb5
+    def test_cloned_group(self):
d01bb5
+        raise NotImplementedError()
d01bb5
+
d01bb5
+    def test_mastered_group(self):
d01bb5
+        raise NotImplementedError()
d01bb5
+
d01bb5
 
d01bb5
 class FindPrimitives(TestCase, FindResourcesMixin):
d01bb5
     _tested_fn = staticmethod(common.find_primitives)
d01bb5
@@ -150,6 +173,37 @@ class FindPrimitives(TestCase, FindResourcesMixin):
d01bb5
         self.assert_find_resources("F-master", ["F1", "F2"])
d01bb5
 
d01bb5
 
d01bb5
+class GetAllInnerResources(TestCase, FindResourcesMixin):
d01bb5
+    _iterable_type = set
d01bb5
+    _tested_fn = staticmethod(common.get_all_inner_resources)
d01bb5
+
d01bb5
+    def test_primitive(self):
d01bb5
+        self.assert_find_resources("A", set())
d01bb5
+
d01bb5
+    def test_primitive_in_clone(self):
d01bb5
+        self.assert_find_resources("B", set())
d01bb5
+
d01bb5
+    def test_primitive_in_master(self):
d01bb5
+        self.assert_find_resources("C", set())
d01bb5
+
d01bb5
+    def test_primitive_in_group(self):
d01bb5
+        self.assert_find_resources("D1", set())
d01bb5
+        self.assert_find_resources("D2", set())
d01bb5
+        self.assert_find_resources("E1", set())
d01bb5
+        self.assert_find_resources("E2", set())
d01bb5
+        self.assert_find_resources("F1", set())
d01bb5
+        self.assert_find_resources("F2", set())
d01bb5
+
d01bb5
+    def test_primitive_in_bundle(self):
d01bb5
+        self.assert_find_resources("H", set())
d01bb5
+
d01bb5
+    def test_cloned_group(self):
d01bb5
+        self.assert_find_resources("E-clone", {"E", "E1", "E2"})
d01bb5
+
d01bb5
+    def test_mastered_group(self):
d01bb5
+        self.assert_find_resources("F-master", {"F", "F1", "F2"})
d01bb5
+
d01bb5
+
d01bb5
 class GetInnerResources(TestCase, FindResourcesMixin):
d01bb5
     _tested_fn = staticmethod(common.get_inner_resources)
d01bb5
 
d01bb5
diff --git a/pcs/lib/commands/resource.py b/pcs/lib/commands/resource.py
d01bb5
index 09a68a68..1ad03a48 100644
d01bb5
--- a/pcs/lib/commands/resource.py
d01bb5
+++ b/pcs/lib/commands/resource.py
d01bb5
@@ -23,6 +23,8 @@ from pcs.lib.cib.tools import (
d01bb5
 )
d01bb5
 from pcs.lib.env_tools import get_nodes
d01bb5
 from pcs.lib.errors import LibraryError
d01bb5
+from pcs.lib.pacemaker import simulate as simulate_tools
d01bb5
+from pcs.lib.pacemaker.live import simulate_cib
d01bb5
 from pcs.lib.pacemaker.values import (
d01bb5
     timeout_to_seconds,
d01bb5
     validate_id,
d01bb5
@@ -37,6 +39,7 @@ from pcs.lib.resource_agent import(
d01bb5
     find_valid_resource_agent_by_name as get_agent
d01bb5
 )
d01bb5
 from pcs.lib.validate import value_time_interval
d01bb5
+from pcs.lib.xml_tools import get_root
d01bb5
 
d01bb5
 @contextmanager
d01bb5
 def resource_environment(
d01bb5
@@ -717,13 +720,45 @@ def bundle_update(
d01bb5
             meta_attributes
d01bb5
         )
d01bb5
 
d01bb5
+def _disable_validate_and_edit_cib(env, resources_section, resource_ids):
d01bb5
+    resource_el_list = _find_resources_or_raise(
d01bb5
+        resources_section,
d01bb5
+        resource_ids
d01bb5
+    )
d01bb5
+    env.report_processor.process_list(
d01bb5
+        _resource_list_enable_disable(
d01bb5
+            resource_el_list,
d01bb5
+            resource.common.disable,
d01bb5
+            env.get_cluster_state()
d01bb5
+        )
d01bb5
+    )
d01bb5
+
d01bb5
 def disable(env, resource_ids, wait):
d01bb5
     """
d01bb5
     Disallow specified resource to be started by the cluster
d01bb5
+
d01bb5
     LibraryEnvironment env --
d01bb5
     strings resource_ids -- ids of the resources to be disabled
d01bb5
     mixed wait -- False: no wait, None: wait default timeout, int: wait timeout
d01bb5
     """
d01bb5
+    with resource_environment(
d01bb5
+        env, wait, resource_ids, _ensure_disabled_after_wait(True)
d01bb5
+    ) as resources_section:
d01bb5
+        _disable_validate_and_edit_cib(env, resources_section, resource_ids)
d01bb5
+
d01bb5
+def disable_safe(env, resource_ids, strict, wait):
d01bb5
+    """
d01bb5
+    Disallow specified resource to be started by the cluster only if there is
d01bb5
+    no effect on other resources
d01bb5
+
d01bb5
+    LibraryEnvironment env --
d01bb5
+    strings resource_ids -- ids of the resources to be disabled
d01bb5
+    bool strict -- if False, allow resources to be migrated
d01bb5
+    mixed wait -- False: no wait, None: wait default timeout, int: wait timeout
d01bb5
+    """
d01bb5
+    if not env.is_cib_live:
d01bb5
+        raise LibraryError(reports.live_environment_required(["CIB"]))
d01bb5
+
d01bb5
     with resource_environment(
d01bb5
         env, wait, resource_ids, _ensure_disabled_after_wait(True)
d01bb5
     ) as resources_section:
d01bb5
@@ -739,6 +774,72 @@ def disable(env, resource_ids, wait):
d01bb5
             )
d01bb5
         )
d01bb5
 
d01bb5
+        inner_resources_names_set = set()
d01bb5
+        for resource_el in resource_el_list:
d01bb5
+            inner_resources_names_set.update({
d01bb5
+                inner_resource_el.get("id")
d01bb5
+                for inner_resource_el
d01bb5
+                    in resource.common.get_all_inner_resources(resource_el)
d01bb5
+            })
d01bb5
+
d01bb5
+        plaintext_status, transitions, dummy_cib = simulate_cib(
d01bb5
+            env.cmd_runner(),
d01bb5
+            get_root(resources_section)
d01bb5
+        )
d01bb5
+        simulated_operations = (
d01bb5
+            simulate_tools.get_operations_from_transitions(transitions)
d01bb5
+        )
d01bb5
+        other_affected = set()
d01bb5
+        if strict:
d01bb5
+            other_affected = set(
d01bb5
+                simulate_tools.get_resources_from_operations(
d01bb5
+                    simulated_operations,
d01bb5
+                    exclude=resource_ids
d01bb5
+                )
d01bb5
+            )
d01bb5
+        else:
d01bb5
+            other_affected = set(
d01bb5
+                simulate_tools.get_resources_left_stopped(
d01bb5
+                    simulated_operations,
d01bb5
+                    exclude=resource_ids
d01bb5
+                )
d01bb5
+                +
d01bb5
+                simulate_tools.get_resources_left_demoted(
d01bb5
+                    simulated_operations,
d01bb5
+                    exclude=resource_ids
d01bb5
+                )
d01bb5
+            )
d01bb5
+
d01bb5
+        # Stopping a clone stops all its inner resources. That should not block
d01bb5
+        # stopping the clone.
d01bb5
+        other_affected = other_affected - inner_resources_names_set
d01bb5
+        if other_affected:
d01bb5
+            raise LibraryError(
d01bb5
+                reports.resource_disable_affects_other_resources(
d01bb5
+                    resource_ids,
d01bb5
+                    other_affected,
d01bb5
+                    plaintext_status,
d01bb5
+                )
d01bb5
+            )
d01bb5
+
d01bb5
+def disable_simulate(env, resource_ids):
d01bb5
+    """
d01bb5
+    Simulate disallowing specified resource to be started by the cluster
d01bb5
+
d01bb5
+    LibraryEnvironment env --
d01bb5
+    strings resource_ids -- ids of the resources to be disabled
d01bb5
+    """
d01bb5
+    if not env.is_cib_live:
d01bb5
+        raise LibraryError(reports.live_environment_required(["CIB"]))
d01bb5
+
d01bb5
+    resources_section = get_resources(env.get_cib())
d01bb5
+    _disable_validate_and_edit_cib(env, resources_section, resource_ids)
d01bb5
+    plaintext_status, dummy_transitions, dummy_cib = simulate_cib(
d01bb5
+        env.cmd_runner(),
d01bb5
+        get_root(resources_section)
d01bb5
+    )
d01bb5
+    return plaintext_status
d01bb5
+
d01bb5
 def enable(env, resource_ids, wait):
d01bb5
     """
d01bb5
     Allow specified resource to be started by the cluster
d01bb5
diff --git a/pcs/lib/commands/test/resource/test_resource_enable_disable.py b/pcs/lib/commands/test/resource/test_resource_enable_disable.py
d01bb5
index 194eb5fd..6514d9fc 100644
d01bb5
--- a/pcs/lib/commands/test/resource/test_resource_enable_disable.py
d01bb5
+++ b/pcs/lib/commands/test/resource/test_resource_enable_disable.py
d01bb5
@@ -14,10 +14,11 @@ from pcs.lib.errors import (
d01bb5
 from pcs.test.tools import fixture
d01bb5
 from pcs.test.tools.command_env import get_env_tools
d01bb5
 from pcs.test.tools.misc import (
d01bb5
+    get_test_resource as rc,
d01bb5
     outdent,
d01bb5
     skip_unless_pacemaker_supports_bundle,
d01bb5
 )
d01bb5
-from pcs.test.tools.pcs_unittest import TestCase
d01bb5
+from pcs.test.tools.pcs_unittest import TestCase, mock
d01bb5
 
d01bb5
 
d01bb5
 TIMEOUT=10
d01bb5
@@ -1646,3 +1647,819 @@ class EnableBundle(TestCase):
d01bb5
             fixture_report_unmanaged("A-bundle"),
d01bb5
             fixture_report_unmanaged("A"),
d01bb5
         ])
d01bb5
+
d01bb5
+
d01bb5
+@mock.patch("pcs.lib.pacemaker.live.write_tmpfile")
d01bb5
+class DisableSimulate(TestCase):
d01bb5
+    def setUp(self):
d01bb5
+        self.env_assist, self.config = get_env_tools(test_case=self)
d01bb5
+        self.tmpfile_new_cib = mock.MagicMock()
d01bb5
+        self.tmpfile_new_cib.name = rc("new_cib.tmp")
d01bb5
+        self.tmpfile_new_cib.read.return_value = "<new-cib/>"
d01bb5
+        self.tmpfile_transitions = mock.MagicMock()
d01bb5
+        self.tmpfile_transitions.name = rc("transitions.tmp")
d01bb5
+        self.tmpfile_transitions.read.return_value = "<transitions/>"
d01bb5
+
d01bb5
+    def test_not_live(self, mock_write_tmpfile):
d01bb5
+        mock_write_tmpfile.side_effect = [
d01bb5
+            AssertionError("No other write_tmpfile call expected")
d01bb5
+        ]
d01bb5
+        self.config.env.set_cib_data("<cib />")
d01bb5
+        self.env_assist.assert_raise_library_error(
d01bb5
+            lambda: resource.disable_simulate(self.env_assist.get_env(), ["A"]),
d01bb5
+            [
d01bb5
+                fixture.error(
d01bb5
+                    report_codes.LIVE_ENVIRONMENT_REQUIRED,
d01bb5
+                    forbidden_options=["CIB"]
d01bb5
+                ),
d01bb5
+            ],
d01bb5
+            expected_in_processor=False
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_nonexistent_resource(self, mock_write_tmpfile):
d01bb5
+        mock_write_tmpfile.side_effect = [
d01bb5
+            AssertionError("No other write_tmpfile call expected")
d01bb5
+        ]
d01bb5
+        self.config.runner.cib.load()
d01bb5
+        self.env_assist.assert_raise_library_error(
d01bb5
+            lambda: resource.disable_simulate(self.env_assist.get_env(), ["A"]),
d01bb5
+            [
d01bb5
+                fixture.report_not_found("A", "resources"),
d01bb5
+            ],
d01bb5
+            expected_in_processor=False
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_success(self, mock_write_tmpfile):
d01bb5
+        mock_write_tmpfile.side_effect = [
d01bb5
+            self.tmpfile_new_cib, self.tmpfile_transitions,
d01bb5
+            AssertionError("No other write_tmpfile call expected")
d01bb5
+        ]
d01bb5
+        (self.config
d01bb5
+            .runner.cib.load(resources=fixture_primitive_cib_enabled)
d01bb5
+            .runner.pcmk.load_state(resources=fixture_primitive_status_managed)
d01bb5
+            .runner.pcmk.simulate_cib(
d01bb5
+                self.tmpfile_new_cib.name,
d01bb5
+                self.tmpfile_transitions.name,
d01bb5
+                stdout="simulate output",
d01bb5
+                resources=fixture_primitive_cib_disabled,
d01bb5
+            )
d01bb5
+        )
d01bb5
+
d01bb5
+        result = resource.disable_simulate(self.env_assist.get_env(), ["A"])
d01bb5
+        self.assertEqual("simulate output", result)
d01bb5
+
d01bb5
+    def test_simulate_error(self, mock_write_tmpfile):
d01bb5
+        mock_write_tmpfile.side_effect = [
d01bb5
+            self.tmpfile_new_cib, self.tmpfile_transitions,
d01bb5
+            AssertionError("No other write_tmpfile call expected")
d01bb5
+        ]
d01bb5
+        (self.config
d01bb5
+            .runner.cib.load(resources=fixture_primitive_cib_enabled)
d01bb5
+            .runner.pcmk.load_state(resources=fixture_primitive_status_managed)
d01bb5
+            .runner.pcmk.simulate_cib(
d01bb5
+                self.tmpfile_new_cib.name,
d01bb5
+                self.tmpfile_transitions.name,
d01bb5
+                stdout="some stdout",
d01bb5
+                stderr="some stderr",
d01bb5
+                returncode=1,
d01bb5
+                resources=fixture_primitive_cib_disabled,
d01bb5
+            )
d01bb5
+        )
d01bb5
+
d01bb5
+        self.env_assist.assert_raise_library_error(
d01bb5
+            lambda: resource.disable_simulate(self.env_assist.get_env(), ["A"]),
d01bb5
+            [
d01bb5
+                fixture.error(
d01bb5
+                    report_codes.CIB_SIMULATE_ERROR,
d01bb5
+                    reason="some stderr",
d01bb5
+                ),
d01bb5
+            ],
d01bb5
+            expected_in_processor=False
d01bb5
+        )
d01bb5
+
d01bb5
+
d01bb5
+class DisableSafeMixin(object):
d01bb5
+    fixture_transitions_both_stopped = """
d01bb5
+        <transition_graph>
d01bb5
+          <synapse>
d01bb5
+            <action_set>
d01bb5
+              <rsc_op id="0" operation="stop" on_node="node1">
d01bb5
+                
d01bb5
+                    id="A" class="ocf" provider="pacemaker" type="Dummy"
d01bb5
+                />
d01bb5
+              </rsc_op>
d01bb5
+            </action_set>
d01bb5
+          </synapse>
d01bb5
+          <synapse>
d01bb5
+            <action_set>
d01bb5
+              <rsc_op id="1" operation="stop" on_node="node2">
d01bb5
+                
d01bb5
+                    id="B" class="ocf" provider="pacemaker" type="Dummy"
d01bb5
+                />
d01bb5
+              </rsc_op>
d01bb5
+            </action_set>
d01bb5
+          </synapse>
d01bb5
+        </transition_graph>
d01bb5
+    """
d01bb5
+    fixture_transitions_one_migrated = """
d01bb5
+        <transition_graph>
d01bb5
+          <synapse>
d01bb5
+            <action_set>
d01bb5
+              <rsc_op id="0" operation="stop" on_node="node1">
d01bb5
+                
d01bb5
+                    id="A" class="ocf" provider="pacemaker" type="Dummy"
d01bb5
+                />
d01bb5
+              </rsc_op>
d01bb5
+            </action_set>
d01bb5
+          </synapse>
d01bb5
+          <synapse>
d01bb5
+            <action_set>
d01bb5
+              <rsc_op id="1" operation="stop" on_node="node2">
d01bb5
+                
d01bb5
+                    id="B" class="ocf" provider="pacemaker" type="Dummy"
d01bb5
+                />
d01bb5
+              </rsc_op>
d01bb5
+            </action_set>
d01bb5
+          </synapse>
d01bb5
+          <synapse>
d01bb5
+            <action_set>
d01bb5
+              <rsc_op id="2" operation="start" on_node="node1">
d01bb5
+                
d01bb5
+                    id="B" class="ocf" provider="pacemaker" type="Dummy"
d01bb5
+                />
d01bb5
+              </rsc_op>
d01bb5
+            </action_set>
d01bb5
+          </synapse>
d01bb5
+        </transition_graph>
d01bb5
+    """
d01bb5
+    fixture_transitions_master_demoted = """
d01bb5
+        <transition_graph>
d01bb5
+          <synapse>
d01bb5
+            <action_set>
d01bb5
+              <rsc_op id="0" operation="stop" on_node="node1">
d01bb5
+                
d01bb5
+                    id="A" class="ocf" provider="pacemaker" type="Dummy"
d01bb5
+                />
d01bb5
+              </rsc_op>
d01bb5
+            </action_set>
d01bb5
+          </synapse>
d01bb5
+          <synapse>
d01bb5
+            <action_set>
d01bb5
+              <rsc_op id="1" operation="demote" on_node="node2">
d01bb5
+                
d01bb5
+                    id="B" class="ocf" provider="pacemaker" type="Dummy"
d01bb5
+                    long_id="B:0"
d01bb5
+                />
d01bb5
+              </rsc_op>
d01bb5
+            </action_set>
d01bb5
+          </synapse>
d01bb5
+        </transition_graph>
d01bb5
+    """
d01bb5
+    fixture_transitions_master_migrated = """
d01bb5
+        <transition_graph>
d01bb5
+          <synapse>
d01bb5
+            <action_set>
d01bb5
+              <rsc_op id="0" operation="stop" on_node="node1">
d01bb5
+                
d01bb5
+                    id="A" class="ocf" provider="pacemaker" type="Dummy"
d01bb5
+                />
d01bb5
+              </rsc_op>
d01bb5
+            </action_set>
d01bb5
+          </synapse>
d01bb5
+          <synapse>
d01bb5
+            <action_set>
d01bb5
+              <rsc_op id="1" operation="demote" on_node="node2">
d01bb5
+                
d01bb5
+                    id="B" class="ocf" provider="pacemaker" type="Dummy"
d01bb5
+                    long_id="B:0"
d01bb5
+                />
d01bb5
+              </rsc_op>
d01bb5
+            </action_set>
d01bb5
+          </synapse>
d01bb5
+          <synapse>
d01bb5
+            <action_set>
d01bb5
+              <rsc_op id="2" operation="promote" on_node="node1">
d01bb5
+                
d01bb5
+                    id="B" class="ocf" provider="pacemaker" type="Dummy"
d01bb5
+                    long_id="B:1"
d01bb5
+                />
d01bb5
+              </rsc_op>
d01bb5
+            </action_set>
d01bb5
+          </synapse>
d01bb5
+        </transition_graph>
d01bb5
+    """
d01bb5
+    fixture_cib_with_master = """
d01bb5
+        <resources>
d01bb5
+            <primitive class="ocf" id="A" provider="heartbeat" type="Dummy">
d01bb5
+            </primitive>
d01bb5
+            <master id="B-master">
d01bb5
+                <primitive class="ocf" id="B" provider="heartbeat" type="Dummy">
d01bb5
+                </primitive>
d01bb5
+            </master>
d01bb5
+        </resources>
d01bb5
+    """
d01bb5
+    fixture_cib_with_master_primitive_disabled = """
d01bb5
+        <resources>
d01bb5
+            <primitive class="ocf" id="A" provider="heartbeat" type="Dummy">
d01bb5
+                <meta_attributes id="A-meta_attributes">
d01bb5
+                    
d01bb5
+                        name="target-role" value="Stopped" />
d01bb5
+                </meta_attributes>
d01bb5
+            </primitive>
d01bb5
+            <master id="B-master">
d01bb5
+                <primitive class="ocf" id="B" provider="heartbeat" type="Dummy">
d01bb5
+                </primitive>
d01bb5
+            </master>
d01bb5
+        </resources>
d01bb5
+    """
d01bb5
+    fixture_status_with_master_managed = """
d01bb5
+        <resources>
d01bb5
+            <resource id="A" managed="true" />
d01bb5
+            
d01bb5
+                unique="false"
d01bb5
+            >
d01bb5
+                <resource id="B" managed="true" />
d01bb5
+                <resource id="B" managed="true" />
d01bb5
+            </clone>
d01bb5
+        </resources>
d01bb5
+    """
d01bb5
+
d01bb5
+    def setUp(self):
d01bb5
+        # pylint does not know this will be mixed into TestCase classes
d01bb5
+        # pylint: disable=invalid-name
d01bb5
+        self.env_assist, self.config = get_env_tools(test_case=self)
d01bb5
+        self.tmpfile_new_cib = mock.MagicMock()
d01bb5
+        self.tmpfile_new_cib.name = rc("new_cib.tmp")
d01bb5
+        self.tmpfile_new_cib.read.return_value = "<new-cib/>"
d01bb5
+        self.tmpfile_transitions = mock.MagicMock()
d01bb5
+        self.tmpfile_transitions.name = rc("transitions.tmp")
d01bb5
+        self.tmpfile_transitions.read.return_value = "<transitions/>"
d01bb5
+
d01bb5
+    def fixture_disable_both_resources(self, mock_write_tmpfile):
d01bb5
+        self.tmpfile_transitions.read.return_value = (
d01bb5
+            self.fixture_transitions_both_stopped
d01bb5
+        )
d01bb5
+        mock_write_tmpfile.side_effect = [
d01bb5
+            self.tmpfile_new_cib, self.tmpfile_transitions,
d01bb5
+            AssertionError("No other write_tmpfile call expected")
d01bb5
+        ]
d01bb5
+        (self.config
d01bb5
+            .runner.cib.load(resources=fixture_two_primitives_cib_enabled)
d01bb5
+            .runner.pcmk.load_state(
d01bb5
+                resources=fixture_two_primitives_status_managed
d01bb5
+            )
d01bb5
+        )
d01bb5
+
d01bb5
+    def fixture_migrate_one_resource(self, mock_write_tmpfile):
d01bb5
+        self.tmpfile_transitions.read.return_value = (
d01bb5
+            self.fixture_transitions_one_migrated
d01bb5
+        )
d01bb5
+        mock_write_tmpfile.side_effect = [
d01bb5
+            self.tmpfile_new_cib, self.tmpfile_transitions,
d01bb5
+            AssertionError("No other write_tmpfile call expected")
d01bb5
+        ]
d01bb5
+        (self.config
d01bb5
+            .runner.cib.load(resources=fixture_two_primitives_cib_enabled)
d01bb5
+            .runner.pcmk.load_state(
d01bb5
+                resources=fixture_two_primitives_status_managed
d01bb5
+            )
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_not_live(self, mock_write_tmpfile):
d01bb5
+        mock_write_tmpfile.side_effect = [
d01bb5
+            AssertionError("No other write_tmpfile call expected")
d01bb5
+        ]
d01bb5
+        self.config.env.set_cib_data("<cib />")
d01bb5
+        self.env_assist.assert_raise_library_error(
d01bb5
+            lambda: resource.disable_safe(
d01bb5
+                self.env_assist.get_env(),
d01bb5
+                ["A"],
d01bb5
+                self.strict,
d01bb5
+                False
d01bb5
+            ),
d01bb5
+            [
d01bb5
+                fixture.error(
d01bb5
+                    report_codes.LIVE_ENVIRONMENT_REQUIRED,
d01bb5
+                    forbidden_options=["CIB"]
d01bb5
+                ),
d01bb5
+            ],
d01bb5
+            expected_in_processor=False
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_nonexistent_resource(self, mock_write_tmpfile):
d01bb5
+        mock_write_tmpfile.side_effect = [
d01bb5
+            AssertionError("No other write_tmpfile call expected")
d01bb5
+        ]
d01bb5
+        self.config.runner.cib.load()
d01bb5
+        self.env_assist.assert_raise_library_error(
d01bb5
+            lambda: resource.disable_safe(
d01bb5
+                self.env_assist.get_env(),
d01bb5
+                ["A"],
d01bb5
+                self.strict,
d01bb5
+                False
d01bb5
+            ),
d01bb5
+            [
d01bb5
+                fixture.report_not_found("A", "resources"),
d01bb5
+            ],
d01bb5
+            expected_in_processor=False
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_simulate_error(self, mock_write_tmpfile):
d01bb5
+        self.fixture_disable_both_resources(mock_write_tmpfile)
d01bb5
+        self.config.runner.pcmk.simulate_cib(
d01bb5
+            self.tmpfile_new_cib.name,
d01bb5
+            self.tmpfile_transitions.name,
d01bb5
+            stdout="some stdout",
d01bb5
+            stderr="some stderr",
d01bb5
+            returncode=1,
d01bb5
+            resources=fixture_two_primitives_cib_disabled_both,
d01bb5
+        )
d01bb5
+        self.env_assist.assert_raise_library_error(
d01bb5
+            lambda: resource.disable_safe(
d01bb5
+                self.env_assist.get_env(),
d01bb5
+                ["A", "B"],
d01bb5
+                self.strict,
d01bb5
+                False,
d01bb5
+            ),
d01bb5
+            [
d01bb5
+                fixture.error(
d01bb5
+                    report_codes.CIB_SIMULATE_ERROR,
d01bb5
+                    reason="some stderr",
d01bb5
+                ),
d01bb5
+            ],
d01bb5
+            expected_in_processor=False
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_only_specified_resources_stopped(self, mock_write_tmpfile):
d01bb5
+        self.fixture_disable_both_resources(mock_write_tmpfile)
d01bb5
+        self.config.runner.pcmk.simulate_cib(
d01bb5
+            self.tmpfile_new_cib.name,
d01bb5
+            self.tmpfile_transitions.name,
d01bb5
+            stdout="simulate output",
d01bb5
+            resources=fixture_two_primitives_cib_disabled_both,
d01bb5
+        )
d01bb5
+        self.config.env.push_cib(
d01bb5
+            resources=fixture_two_primitives_cib_disabled_both
d01bb5
+        )
d01bb5
+        resource.disable_safe(
d01bb5
+            self.env_assist.get_env(),
d01bb5
+            ["A", "B"],
d01bb5
+            self.strict,
d01bb5
+            False,
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_other_resources_stopped(self, mock_write_tmpfile):
d01bb5
+        self.fixture_disable_both_resources(mock_write_tmpfile)
d01bb5
+        self.config.runner.pcmk.simulate_cib(
d01bb5
+            self.tmpfile_new_cib.name,
d01bb5
+            self.tmpfile_transitions.name,
d01bb5
+            stdout="simulate output",
d01bb5
+            resources=fixture_two_primitives_cib_disabled,
d01bb5
+        )
d01bb5
+        self.env_assist.assert_raise_library_error(
d01bb5
+            lambda: resource.disable_safe(
d01bb5
+                self.env_assist.get_env(),
d01bb5
+                ["A"],
d01bb5
+                self.strict,
d01bb5
+                False,
d01bb5
+            ),
d01bb5
+            [
d01bb5
+                fixture.error(
d01bb5
+                    report_codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES,
d01bb5
+                    disabled_resource_list=["A"],
d01bb5
+                    affected_resource_list=["B"],
d01bb5
+                    crm_simulate_plaintext_output="simulate output",
d01bb5
+                ),
d01bb5
+            ],
d01bb5
+            expected_in_processor=False
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_master_demoted(self, mock_write_tmpfile):
d01bb5
+        self.tmpfile_transitions.read.return_value = (
d01bb5
+            self.fixture_transitions_master_demoted
d01bb5
+        )
d01bb5
+        mock_write_tmpfile.side_effect = [
d01bb5
+            self.tmpfile_new_cib, self.tmpfile_transitions,
d01bb5
+            AssertionError("No other write_tmpfile call expected")
d01bb5
+        ]
d01bb5
+        (self.config
d01bb5
+            .runner.cib.load(resources=self.fixture_cib_with_master)
d01bb5
+            .runner.pcmk.load_state(
d01bb5
+                resources=self.fixture_status_with_master_managed
d01bb5
+            )
d01bb5
+            .runner.pcmk.simulate_cib(
d01bb5
+                self.tmpfile_new_cib.name,
d01bb5
+                self.tmpfile_transitions.name,
d01bb5
+                stdout="simulate output",
d01bb5
+                resources=self.fixture_cib_with_master_primitive_disabled,
d01bb5
+            )
d01bb5
+        )
d01bb5
+        self.env_assist.assert_raise_library_error(
d01bb5
+            lambda: resource.disable_safe(
d01bb5
+                self.env_assist.get_env(),
d01bb5
+                ["A"],
d01bb5
+                self.strict,
d01bb5
+                False,
d01bb5
+            ),
d01bb5
+            [
d01bb5
+                fixture.error(
d01bb5
+                    report_codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES,
d01bb5
+                    disabled_resource_list=["A"],
d01bb5
+                    affected_resource_list=["B"],
d01bb5
+                    crm_simulate_plaintext_output="simulate output",
d01bb5
+                ),
d01bb5
+            ],
d01bb5
+            expected_in_processor=False
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_wait_success(self, mock_write_tmpfile):
d01bb5
+        self.config.runner.pcmk.can_wait()
d01bb5
+        self.fixture_disable_both_resources(mock_write_tmpfile)
d01bb5
+        (self.config
d01bb5
+            .runner.pcmk.simulate_cib(
d01bb5
+                self.tmpfile_new_cib.name,
d01bb5
+                self.tmpfile_transitions.name,
d01bb5
+                stdout="simulate output",
d01bb5
+                resources=fixture_two_primitives_cib_disabled_both,
d01bb5
+            )
d01bb5
+            .env.push_cib(
d01bb5
+                resources=fixture_two_primitives_cib_disabled_both,
d01bb5
+                wait=TIMEOUT
d01bb5
+            )
d01bb5
+            .runner.pcmk.load_state(
d01bb5
+                name="runner.pcmk.load_state_2",
d01bb5
+                resources="""
d01bb5
+                    <resources>
d01bb5
+                        <resource id="A" managed="true" role="Stopped">
d01bb5
+                        </resource>
d01bb5
+                        <resource id="B" managed="true" role="Stopped">
d01bb5
+                        </resource>
d01bb5
+                    </resources>
d01bb5
+                """
d01bb5
+            )
d01bb5
+        )
d01bb5
+        resource.disable_safe(
d01bb5
+            self.env_assist.get_env(),
d01bb5
+            ["A", "B"],
d01bb5
+            self.strict,
d01bb5
+            TIMEOUT
d01bb5
+        )
d01bb5
+        self.env_assist.assert_reports([
d01bb5
+            fixture.report_resource_not_running("A"),
d01bb5
+            fixture.report_resource_not_running("B"),
d01bb5
+        ])
d01bb5
+
d01bb5
+    def test_inner_resources(self, mock_write_tmpfile):
d01bb5
+        cib_xml = """
d01bb5
+            <resources>
d01bb5
+                <primitive id="A" />
d01bb5
+                <clone id="B-clone">
d01bb5
+                    <primitive id="B" />
d01bb5
+                </clone>
d01bb5
+                <master id="C-master">
d01bb5
+                    <primitive id="C" />
d01bb5
+                </master>
d01bb5
+                <group id="D">
d01bb5
+                    <primitive id="D1" />
d01bb5
+                    <primitive id="D2" />
d01bb5
+                </group>
d01bb5
+                <clone id="E-clone">
d01bb5
+                    <group id="E">
d01bb5
+                        <primitive id="E1" />
d01bb5
+                        <primitive id="E2" />
d01bb5
+                    </group>
d01bb5
+                </clone>
d01bb5
+                <master id="F-master">
d01bb5
+                    <group id="F">
d01bb5
+                        <primitive id="F1" />
d01bb5
+                        <primitive id="F2" />
d01bb5
+                    </group>
d01bb5
+                </master>
d01bb5
+                <bundle id="G-bundle" />
d01bb5
+                <bundle id="H-bundle">
d01bb5
+                    <primitive id="H" />
d01bb5
+                </bundle>
d01bb5
+            </resources>
d01bb5
+        """
d01bb5
+        status_xml = """
d01bb5
+            <resources>
d01bb5
+                <resource id="A" managed="true" />
d01bb5
+                
d01bb5
+                    unique="false"
d01bb5
+                >
d01bb5
+                    <resource id="B" managed="true" />
d01bb5
+                    <resource id="B" managed="true" />
d01bb5
+                </clone>
d01bb5
+                
d01bb5
+                    unique="false"
d01bb5
+                >
d01bb5
+                    <resource id="C" managed="true" />
d01bb5
+                    <resource id="C" managed="true" />
d01bb5
+                </clone>
d01bb5
+                <group id="D" number_resources="2">
d01bb5
+                    <resource id="D1" managed="true" />
d01bb5
+                    <resource id="D2" managed="true" />
d01bb5
+                </group>
d01bb5
+                
d01bb5
+                    unique="false"
d01bb5
+                >
d01bb5
+                    <group id="E:0" number_resources="2">
d01bb5
+                        <resource id="E1" managed="true" />
d01bb5
+                        <resource id="E2" managed="true" />
d01bb5
+                    </group>
d01bb5
+                    <group id="E:1" number_resources="2">
d01bb5
+                        <resource id="E1" managed="true" />
d01bb5
+                        <resource id="E2" managed="true" />
d01bb5
+                    </group>
d01bb5
+                </clone>
d01bb5
+                
d01bb5
+                    unique="false"
d01bb5
+                >
d01bb5
+                    <group id="F:0" number_resources="2">
d01bb5
+                        <resource id="F1" managed="true" />
d01bb5
+                        <resource id="F2" managed="true" />
d01bb5
+                    </group>
d01bb5
+                    <group id="F:1" number_resources="2">
d01bb5
+                        <resource id="F1" managed="true" />
d01bb5
+                        <resource id="F2" managed="true" />
d01bb5
+                    </group>
d01bb5
+                </clone>
d01bb5
+                
d01bb5
+                    unique="false" managed="true" failed="false"
d01bb5
+                >
d01bb5
+                    <replica id="0">
d01bb5
+                        <resource id="H" />
d01bb5
+                    </replica>
d01bb5
+                    <replica id="1">
d01bb5
+                        <resource id="H" />
d01bb5
+                    </replica>
d01bb5
+                </bundle>
d01bb5
+            </resources>
d01bb5
+        """
d01bb5
+        synapses = []
d01bb5
+        index = 0
d01bb5
+        for res_name, is_clone in [
d01bb5
+            ("A", False),
d01bb5
+            ("B", True),
d01bb5
+            ("C", True),
d01bb5
+            ("D1", False),
d01bb5
+            ("D2", False),
d01bb5
+            ("E1", True),
d01bb5
+            ("E2", True),
d01bb5
+            ("F1", True),
d01bb5
+            ("F2", True),
d01bb5
+            ("H", False),
d01bb5
+        ]:
d01bb5
+            if is_clone:
d01bb5
+                synapses.append("""
d01bb5
+                  <synapse>
d01bb5
+                    <action_set>
d01bb5
+                      <rsc_op id="{index}" operation="stop" on_node="node1">
d01bb5
+                        <primitive id="{res_name}" long_id="{res_name}:0" />
d01bb5
+                      </rsc_op>
d01bb5
+                    </action_set>
d01bb5
+                  </synapse>
d01bb5
+                  <synapse>
d01bb5
+                    <action_set>
d01bb5
+                      <rsc_op id="{index_1}" operation="stop" on_node="node2">
d01bb5
+                        <primitive id="{res_name}" long_id="{res_name}:1" />
d01bb5
+                      </rsc_op>
d01bb5
+                    </action_set>
d01bb5
+                  </synapse>
d01bb5
+                """.format(
d01bb5
+                    index=index,
d01bb5
+                    index_1=index + 1,
d01bb5
+                    res_name=res_name,
d01bb5
+                ))
d01bb5
+                index += 2
d01bb5
+            else:
d01bb5
+                synapses.append("""
d01bb5
+                  <synapse>
d01bb5
+                    <action_set>
d01bb5
+                      <rsc_op id="{index}" operation="stop" on_node="node1">
d01bb5
+                        <primitive id="{res_name}" />
d01bb5
+                      </rsc_op>
d01bb5
+                    </action_set>
d01bb5
+                  </synapse>
d01bb5
+                """.format(
d01bb5
+                    index=index,
d01bb5
+                    res_name=res_name,
d01bb5
+                ))
d01bb5
+                index += 1
d01bb5
+        transitions_xml = (
d01bb5
+            "<transition_graph>" + "\n".join(synapses) + "</transition_graph>"
d01bb5
+        )
d01bb5
+
d01bb5
+        self.tmpfile_transitions.read.return_value = transitions_xml
d01bb5
+        mock_write_tmpfile.side_effect = [
d01bb5
+            self.tmpfile_new_cib, self.tmpfile_transitions,
d01bb5
+            AssertionError("No other write_tmpfile call expected")
d01bb5
+        ]
d01bb5
+        (self.config
d01bb5
+            .runner.cib.load(resources=cib_xml)
d01bb5
+            .runner.pcmk.load_state(resources=status_xml)
d01bb5
+        )
d01bb5
+        self.config.runner.pcmk.simulate_cib(
d01bb5
+            self.tmpfile_new_cib.name,
d01bb5
+            self.tmpfile_transitions.name,
d01bb5
+            stdout="simulate output",
d01bb5
+            resources="""
d01bb5
+                <resources>
d01bb5
+                    <primitive id="A" />
d01bb5
+                    <clone id="B-clone">
d01bb5
+                        <meta_attributes id="B-clone-meta_attributes">
d01bb5
+                            
d01bb5
+                                id="B-clone-meta_attributes-target-role"
d01bb5
+                            />
d01bb5
+                        </meta_attributes>
d01bb5
+                        <primitive id="B" />
d01bb5
+                    </clone>
d01bb5
+                    <master id="C-master">
d01bb5
+                        <meta_attributes id="C-master-meta_attributes">
d01bb5
+                            
d01bb5
+                                id="C-master-meta_attributes-target-role"
d01bb5
+                            />
d01bb5
+                        </meta_attributes>
d01bb5
+                        <primitive id="C" />
d01bb5
+                    </master>
d01bb5
+                    <group id="D">
d01bb5
+                        <meta_attributes id="D-meta_attributes">
d01bb5
+                            
d01bb5
+                                id="D-meta_attributes-target-role"
d01bb5
+                            />
d01bb5
+                        </meta_attributes>
d01bb5
+                        <primitive id="D1" />
d01bb5
+                        <primitive id="D2" />
d01bb5
+                    </group>
d01bb5
+                    <clone id="E-clone">
d01bb5
+                        <meta_attributes id="E-clone-meta_attributes">
d01bb5
+                            
d01bb5
+                                id="E-clone-meta_attributes-target-role"
d01bb5
+                            />
d01bb5
+                        </meta_attributes>
d01bb5
+                        <group id="E">
d01bb5
+                            <primitive id="E1" />
d01bb5
+                            <primitive id="E2" />
d01bb5
+                        </group>
d01bb5
+                    </clone>
d01bb5
+                    <master id="F-master">
d01bb5
+                        <meta_attributes id="F-master-meta_attributes">
d01bb5
+                            
d01bb5
+                                id="F-master-meta_attributes-target-role"
d01bb5
+                            />
d01bb5
+                        </meta_attributes>
d01bb5
+                        <group id="F">
d01bb5
+                            <primitive id="F1" />
d01bb5
+                            <primitive id="F2" />
d01bb5
+                        </group>
d01bb5
+                    </master>
d01bb5
+                    <bundle id="G-bundle" />
d01bb5
+                    <bundle id="H-bundle">
d01bb5
+                        <meta_attributes id="H-bundle-meta_attributes">
d01bb5
+                            
d01bb5
+                                id="H-bundle-meta_attributes-target-role"
d01bb5
+                            />
d01bb5
+                        </meta_attributes>
d01bb5
+                        <primitive id="H" />
d01bb5
+                    </bundle>
d01bb5
+                </resources>
d01bb5
+            """
d01bb5
+        )
d01bb5
+        self.env_assist.assert_raise_library_error(
d01bb5
+            lambda: resource.disable_safe(
d01bb5
+                self.env_assist.get_env(),
d01bb5
+                ["B-clone", "C-master", "D", "E-clone", "F-master", "H-bundle"],
d01bb5
+                self.strict,
d01bb5
+                False,
d01bb5
+            ),
d01bb5
+            [
d01bb5
+                fixture.error(
d01bb5
+                    report_codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES,
d01bb5
+                    disabled_resource_list=[
d01bb5
+                        "B-clone", "C-master", "D", "E-clone", "F-master",
d01bb5
+                        "H-bundle"
d01bb5
+                    ],
d01bb5
+                    affected_resource_list=["A"],
d01bb5
+                    crm_simulate_plaintext_output="simulate output",
d01bb5
+                ),
d01bb5
+            ],
d01bb5
+            expected_in_processor=False
d01bb5
+        )
d01bb5
+
d01bb5
+@mock.patch("pcs.lib.pacemaker.live.write_tmpfile")
d01bb5
+class DisableSafe(DisableSafeMixin, TestCase):
d01bb5
+    strict = False
d01bb5
+
d01bb5
+    def test_resources_migrated(self, mock_write_tmpfile):
d01bb5
+        self.fixture_migrate_one_resource(mock_write_tmpfile)
d01bb5
+        self.config.runner.pcmk.simulate_cib(
d01bb5
+            self.tmpfile_new_cib.name,
d01bb5
+            self.tmpfile_transitions.name,
d01bb5
+            stdout="simulate output",
d01bb5
+            resources=fixture_two_primitives_cib_disabled,
d01bb5
+        )
d01bb5
+        self.config.env.push_cib(resources=fixture_two_primitives_cib_disabled)
d01bb5
+        resource.disable_safe(
d01bb5
+            self.env_assist.get_env(),
d01bb5
+            ["A"],
d01bb5
+            self.strict,
d01bb5
+            False,
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_master_migrated(self, mock_write_tmpfile):
d01bb5
+        self.tmpfile_transitions.read.return_value = (
d01bb5
+            self.fixture_transitions_master_migrated
d01bb5
+        )
d01bb5
+        mock_write_tmpfile.side_effect = [
d01bb5
+            self.tmpfile_new_cib, self.tmpfile_transitions,
d01bb5
+            AssertionError("No other write_tmpfile call expected")
d01bb5
+        ]
d01bb5
+        (self.config
d01bb5
+            .runner.cib.load(resources=self.fixture_cib_with_master)
d01bb5
+            .runner.pcmk.load_state(
d01bb5
+                resources=self.fixture_status_with_master_managed
d01bb5
+            )
d01bb5
+            .runner.pcmk.simulate_cib(
d01bb5
+                self.tmpfile_new_cib.name,
d01bb5
+                self.tmpfile_transitions.name,
d01bb5
+                stdout="simulate output",
d01bb5
+                resources=self.fixture_cib_with_master_primitive_disabled,
d01bb5
+            )
d01bb5
+            .env.push_cib(
d01bb5
+                resources=self.fixture_cib_with_master_primitive_disabled
d01bb5
+            )
d01bb5
+        )
d01bb5
+        resource.disable_safe(
d01bb5
+            self.env_assist.get_env(),
d01bb5
+            ["A"],
d01bb5
+            self.strict,
d01bb5
+            False,
d01bb5
+        )
d01bb5
+
d01bb5
+@mock.patch("pcs.lib.pacemaker.live.write_tmpfile")
d01bb5
+class DisableSafeStrict(DisableSafeMixin, TestCase):
d01bb5
+    strict = True
d01bb5
+
d01bb5
+    def test_resources_migrated(self, mock_write_tmpfile):
d01bb5
+        self.fixture_migrate_one_resource(mock_write_tmpfile)
d01bb5
+        self.config.runner.pcmk.simulate_cib(
d01bb5
+            self.tmpfile_new_cib.name,
d01bb5
+            self.tmpfile_transitions.name,
d01bb5
+            stdout="simulate output",
d01bb5
+            resources=fixture_two_primitives_cib_disabled,
d01bb5
+        )
d01bb5
+        self.env_assist.assert_raise_library_error(
d01bb5
+            lambda: resource.disable_safe(
d01bb5
+                self.env_assist.get_env(),
d01bb5
+                ["A"],
d01bb5
+                self.strict,
d01bb5
+                False,
d01bb5
+            ),
d01bb5
+            [
d01bb5
+                fixture.error(
d01bb5
+                    report_codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES,
d01bb5
+                    disabled_resource_list=["A"],
d01bb5
+                    affected_resource_list=["B"],
d01bb5
+                    crm_simulate_plaintext_output="simulate output",
d01bb5
+                ),
d01bb5
+            ],
d01bb5
+            expected_in_processor=False
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_master_migrated(self, mock_write_tmpfile):
d01bb5
+        self.tmpfile_transitions.read.return_value = (
d01bb5
+            self.fixture_transitions_master_migrated
d01bb5
+        )
d01bb5
+        mock_write_tmpfile.side_effect = [
d01bb5
+            self.tmpfile_new_cib, self.tmpfile_transitions,
d01bb5
+            AssertionError("No other write_tmpfile call expected")
d01bb5
+        ]
d01bb5
+        (self.config
d01bb5
+            .runner.cib.load(resources=self.fixture_cib_with_master)
d01bb5
+            .runner.pcmk.load_state(
d01bb5
+                resources=self.fixture_status_with_master_managed
d01bb5
+            )
d01bb5
+            .runner.pcmk.simulate_cib(
d01bb5
+                self.tmpfile_new_cib.name,
d01bb5
+                self.tmpfile_transitions.name,
d01bb5
+                stdout="simulate output",
d01bb5
+                resources=self.fixture_cib_with_master_primitive_disabled,
d01bb5
+            )
d01bb5
+        )
d01bb5
+        self.env_assist.assert_raise_library_error(
d01bb5
+            lambda: resource.disable_safe(
d01bb5
+                self.env_assist.get_env(),
d01bb5
+                ["A"],
d01bb5
+                self.strict,
d01bb5
+                False,
d01bb5
+            ),
d01bb5
+            [
d01bb5
+                fixture.error(
d01bb5
+                    report_codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES,
d01bb5
+                    disabled_resource_list=["A"],
d01bb5
+                    affected_resource_list=["B"],
d01bb5
+                    crm_simulate_plaintext_output="simulate output",
d01bb5
+                ),
d01bb5
+            ],
d01bb5
+            expected_in_processor=False
d01bb5
+        )
d01bb5
diff --git a/pcs/lib/pacemaker/live.py b/pcs/lib/pacemaker/live.py
d01bb5
index 992cc639..bc444229 100644
d01bb5
--- a/pcs/lib/pacemaker/live.py
d01bb5
+++ b/pcs/lib/pacemaker/live.py
d01bb5
@@ -9,8 +9,9 @@ import os.path
d01bb5
 
d01bb5
 from pcs import settings
d01bb5
 from pcs.common.tools import (
d01bb5
+    format_environment_error,
d01bb5
     join_multilines,
d01bb5
-    xml_fromstring
d01bb5
+    xml_fromstring,
d01bb5
 )
d01bb5
 from pcs.lib import reports
d01bb5
 from pcs.lib.cib.tools import get_pacemaker_version_by_which_cib_was_validated
d01bb5
@@ -205,6 +206,69 @@ def _upgrade_cib(runner):
d01bb5
             reports.cib_upgrade_failed(join_multilines([stderr, stdout]))
d01bb5
         )
d01bb5
 
d01bb5
+def simulate_cib_xml(runner, cib_xml):
d01bb5
+    """
d01bb5
+    Run crm_simulate to get effects the cib would have on the live cluster
d01bb5
+
d01bb5
+    CommandRunner runner -- runner
d01bb5
+    string cib_xml -- CIB XML to simulate
d01bb5
+    """
d01bb5
+    try:
d01bb5
+        new_cib_file = write_tmpfile(None)
d01bb5
+        transitions_file = write_tmpfile(None)
d01bb5
+    except EnvironmentError as e:
d01bb5
+        raise LibraryError(
d01bb5
+            reports.cib_simulate_error(format_environment_error(e))
d01bb5
+        )
d01bb5
+
d01bb5
+    cmd = [
d01bb5
+        __exec("crm_simulate"),
d01bb5
+        "--simulate",
d01bb5
+        "--save-output", new_cib_file.name,
d01bb5
+        "--save-graph", transitions_file.name,
d01bb5
+        "--xml-pipe",
d01bb5
+    ]
d01bb5
+    stdout, stderr, retval = runner.run(cmd, stdin_string=cib_xml)
d01bb5
+    if retval != 0:
d01bb5
+        raise LibraryError(
d01bb5
+            reports.cib_simulate_error(stderr.strip())
d01bb5
+        )
d01bb5
+
d01bb5
+    try:
d01bb5
+        new_cib_file.seek(0)
d01bb5
+        transitions_file.seek(0)
d01bb5
+        new_cib_xml = new_cib_file.read()
d01bb5
+        transitions_xml = transitions_file.read()
d01bb5
+        new_cib_file.close()
d01bb5
+        transitions_file.close()
d01bb5
+        return stdout, transitions_xml, new_cib_xml
d01bb5
+    except EnvironmentError as e:
d01bb5
+        raise LibraryError(
d01bb5
+            reports.cib_simulate_error(format_environment_error(e))
d01bb5
+        )
d01bb5
+
d01bb5
+def simulate_cib(runner, cib):
d01bb5
+    """
d01bb5
+    Run crm_simulate to get effects the cib would have on the live cluster
d01bb5
+
d01bb5
+    CommandRunner runner -- runner
d01bb5
+    etree cib -- cib tree to simulate
d01bb5
+    """
d01bb5
+    cib_xml = etree_to_str(cib)
d01bb5
+    try:
d01bb5
+        plaintext_result, transitions_xml, new_cib_xml = simulate_cib_xml(
d01bb5
+            runner, cib_xml
d01bb5
+        )
d01bb5
+        return (
d01bb5
+            plaintext_result.strip(),
d01bb5
+            xml_fromstring(transitions_xml),
d01bb5
+            xml_fromstring(new_cib_xml)
d01bb5
+        )
d01bb5
+    except (etree.XMLSyntaxError, etree.DocumentInvalid) as e:
d01bb5
+        raise LibraryError(
d01bb5
+            reports.cib_simulate_error(str(e))
d01bb5
+        )
d01bb5
+
d01bb5
 ### wait for idle
d01bb5
 
d01bb5
 def has_wait_for_idle_support(runner):
d01bb5
diff --git a/pcs/lib/pacemaker/simulate.py b/pcs/lib/pacemaker/simulate.py
d01bb5
new file mode 100644
d01bb5
index 00000000..aff38058
d01bb5
--- /dev/null
d01bb5
+++ b/pcs/lib/pacemaker/simulate.py
d01bb5
@@ -0,0 +1,86 @@
d01bb5
+from collections import defaultdict
d01bb5
+
d01bb5
+def get_operations_from_transitions(transitions):
d01bb5
+    """
d01bb5
+    Extract resource operations from simulated transitions
d01bb5
+
d01bb5
+    etree transitions -- simulated transitions from crm_simulate
d01bb5
+    """
d01bb5
+    operation_list = []
d01bb5
+    watched_operations = {
d01bb5
+        "start", "stop", "promote", "demote", "migrate_from", "migrate_to"
d01bb5
+    }
d01bb5
+    for rsc_op in transitions.iterfind("synapse/action_set/rsc_op"):
d01bb5
+        operation = rsc_op.get("operation", "").lower()
d01bb5
+        if operation not in watched_operations:
d01bb5
+            continue
d01bb5
+        for primitive in rsc_op.iterfind("primitive"):
d01bb5
+            primitive_id = primitive.get("id")
d01bb5
+            operation_list.append((
d01bb5
+                int(rsc_op.get("id")),
d01bb5
+                {
d01bb5
+                    "primitive_id": primitive_id,
d01bb5
+                    "primitive_long_id": (
d01bb5
+                        primitive.get("long-id") or primitive_id
d01bb5
+                    ),
d01bb5
+                    "operation": operation,
d01bb5
+                    "on_node": rsc_op.get("on_node"),
d01bb5
+                }
d01bb5
+            ))
d01bb5
+    operation_list.sort(key=lambda x: x[0])
d01bb5
+    op_list = [op[1] for op in operation_list]
d01bb5
+    return op_list
d01bb5
+
d01bb5
+def get_resources_from_operations(operation_list, exclude=None):
d01bb5
+    """
d01bb5
+    Get names of all resources from the provided operation list
d01bb5
+
d01bb5
+    list operation_list -- result of get_operations_from_transitions
d01bb5
+    iterable exclude -- resources to exclude from the result
d01bb5
+    """
d01bb5
+    exclude = exclude or set()
d01bb5
+    return sorted({
d01bb5
+        op["primitive_id"]
d01bb5
+        for op in operation_list
d01bb5
+        if op["primitive_id"] not in exclude
d01bb5
+    })
d01bb5
+
d01bb5
+def get_resources_left_stopped(operation_list, exclude=None):
d01bb5
+    """
d01bb5
+    Get names of resources which are left stopped by the provided operation list
d01bb5
+
d01bb5
+    list operation_list -- result of get_operations_from_transitions
d01bb5
+    iterable exclude -- resources to exclude from the result
d01bb5
+    """
d01bb5
+    return _resources_with_imbalanced_operations(
d01bb5
+        operation_list, "stop", "start", exclude
d01bb5
+    )
d01bb5
+
d01bb5
+def get_resources_left_demoted(operation_list, exclude=None):
d01bb5
+    """
d01bb5
+    Get names of resources which are left demoted by the provided operation list
d01bb5
+
d01bb5
+    list operation_list -- result of get_operations_from_transitions
d01bb5
+    iterable exclude -- resources to exclude from the result
d01bb5
+    """
d01bb5
+    return _resources_with_imbalanced_operations(
d01bb5
+        operation_list, "demote", "promote", exclude
d01bb5
+    )
d01bb5
+
d01bb5
+def _resources_with_imbalanced_operations(
d01bb5
+    operation_list, increment_op, decrement_op, exclude
d01bb5
+):
d01bb5
+    exclude = exclude or set()
d01bb5
+    counter = defaultdict(int)
d01bb5
+    for res_op in operation_list:
d01bb5
+        resource = res_op["primitive_id"]
d01bb5
+        operation = res_op["operation"]
d01bb5
+        if operation == increment_op:
d01bb5
+            counter[resource] += 1
d01bb5
+        elif operation == decrement_op:
d01bb5
+            counter[resource] -= 1
d01bb5
+    return sorted([
d01bb5
+        resource
d01bb5
+        for resource, count in counter.items()
d01bb5
+        if count > 0 and resource not in exclude
d01bb5
+    ])
d01bb5
diff --git a/pcs/lib/pacemaker/test/test_live.py b/pcs/lib/pacemaker/test/test_live.py
d01bb5
index 904e3498..fad95037 100644
d01bb5
--- a/pcs/lib/pacemaker/test/test_live.py
d01bb5
+++ b/pcs/lib/pacemaker/test/test_live.py
d01bb5
@@ -16,7 +16,7 @@ from pcs.test.tools import fixture
d01bb5
 from pcs.test.tools.command_env import get_env_tools
d01bb5
 from pcs.test.tools.misc import get_test_resource as rc
d01bb5
 from pcs.test.tools.pcs_unittest import TestCase, mock
d01bb5
-from pcs.test.tools.xml import XmlManipulation
d01bb5
+from pcs.test.tools.xml import etree_to_str, XmlManipulation
d01bb5
 
d01bb5
 from pcs import settings
d01bb5
 from pcs.common import report_codes
d01bb5
@@ -435,6 +435,195 @@ class EnsureCibVersionTest(TestCase):
d01bb5
         mock_upgrade.assert_called_once_with(self.mock_runner)
d01bb5
         mock_get_cib.assert_called_once_with(self.mock_runner)
d01bb5
 
d01bb5
+
d01bb5
+@mock.patch("pcs.lib.pacemaker.live.write_tmpfile")
d01bb5
+class SimulateCibXml(LibraryPacemakerTest):
d01bb5
+    def test_success(self, mock_write_tmpfile):
d01bb5
+        tmpfile_new_cib = mock.MagicMock()
d01bb5
+        tmpfile_new_cib.name = rc("new_cib.tmp")
d01bb5
+        tmpfile_new_cib.read.return_value = "new cib data"
d01bb5
+        tmpfile_transitions = mock.MagicMock()
d01bb5
+        tmpfile_transitions.name = rc("transitions.tmp")
d01bb5
+        tmpfile_transitions.read.return_value = "transitions data"
d01bb5
+        mock_write_tmpfile.side_effect = [tmpfile_new_cib, tmpfile_transitions]
d01bb5
+
d01bb5
+        expected_stdout = "simulate output"
d01bb5
+        expected_stderr = ""
d01bb5
+        expected_retval = 0
d01bb5
+        mock_runner = get_runner(
d01bb5
+            expected_stdout,
d01bb5
+            expected_stderr,
d01bb5
+            expected_retval
d01bb5
+        )
d01bb5
+
d01bb5
+        result = lib.simulate_cib_xml(mock_runner, "<cib />")
d01bb5
+        self.assertEqual(result[0], expected_stdout)
d01bb5
+        self.assertEqual(result[1], "transitions data")
d01bb5
+        self.assertEqual(result[2], "new cib data")
d01bb5
+
d01bb5
+        mock_runner.run.assert_called_once_with(
d01bb5
+            [
d01bb5
+                self.path("crm_simulate"),
d01bb5
+                "--simulate",
d01bb5
+                "--save-output", tmpfile_new_cib.name,
d01bb5
+                "--save-graph", tmpfile_transitions.name,
d01bb5
+                "--xml-pipe",
d01bb5
+            ],
d01bb5
+            stdin_string="<cib />"
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_error_creating_cib(self, mock_write_tmpfile):
d01bb5
+        mock_write_tmpfile.side_effect = OSError(1, "some error")
d01bb5
+        mock_runner = get_runner()
d01bb5
+        assert_raise_library_error(
d01bb5
+            lambda: lib.simulate_cib_xml(mock_runner, "<cib />"),
d01bb5
+            fixture.error(
d01bb5
+                report_codes.CIB_SIMULATE_ERROR,
d01bb5
+                reason="some error",
d01bb5
+            ),
d01bb5
+        )
d01bb5
+        mock_runner.run.assert_not_called()
d01bb5
+
d01bb5
+    def test_error_creating_transitions(self, mock_write_tmpfile):
d01bb5
+        tmpfile_new_cib = mock.MagicMock()
d01bb5
+        mock_write_tmpfile.side_effect = [
d01bb5
+            tmpfile_new_cib,
d01bb5
+            OSError(1, "some error")
d01bb5
+        ]
d01bb5
+        mock_runner = get_runner()
d01bb5
+        assert_raise_library_error(
d01bb5
+            lambda: lib.simulate_cib_xml(mock_runner, "<cib />"),
d01bb5
+            fixture.error(
d01bb5
+                report_codes.CIB_SIMULATE_ERROR,
d01bb5
+                reason="some error",
d01bb5
+            ),
d01bb5
+        )
d01bb5
+        mock_runner.run.assert_not_called()
d01bb5
+
d01bb5
+    def test_error_running_simulate(self, mock_write_tmpfile):
d01bb5
+        tmpfile_new_cib = mock.MagicMock()
d01bb5
+        tmpfile_new_cib.name = rc("new_cib.tmp")
d01bb5
+        tmpfile_transitions = mock.MagicMock()
d01bb5
+        tmpfile_transitions.name = rc("transitions.tmp")
d01bb5
+        mock_write_tmpfile.side_effect = [tmpfile_new_cib, tmpfile_transitions]
d01bb5
+
d01bb5
+        expected_stdout = "some stdout"
d01bb5
+        expected_stderr = "some error"
d01bb5
+        expected_retval = 1
d01bb5
+        mock_runner = get_runner(
d01bb5
+            expected_stdout,
d01bb5
+            expected_stderr,
d01bb5
+            expected_retval
d01bb5
+        )
d01bb5
+
d01bb5
+        assert_raise_library_error(
d01bb5
+            lambda: lib.simulate_cib_xml(mock_runner, "<cib />"),
d01bb5
+            fixture.error(
d01bb5
+                report_codes.CIB_SIMULATE_ERROR,
d01bb5
+                reason="some error",
d01bb5
+            ),
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_error_reading_cib(self, mock_write_tmpfile):
d01bb5
+        tmpfile_new_cib = mock.MagicMock()
d01bb5
+        tmpfile_new_cib.name = rc("new_cib.tmp")
d01bb5
+        tmpfile_new_cib.read.side_effect = OSError(1, "some error")
d01bb5
+        tmpfile_transitions = mock.MagicMock()
d01bb5
+        tmpfile_transitions.name = rc("transitions.tmp")
d01bb5
+        mock_write_tmpfile.side_effect = [tmpfile_new_cib, tmpfile_transitions]
d01bb5
+
d01bb5
+        expected_stdout = "simulate output"
d01bb5
+        expected_stderr = ""
d01bb5
+        expected_retval = 0
d01bb5
+        mock_runner = get_runner(
d01bb5
+            expected_stdout,
d01bb5
+            expected_stderr,
d01bb5
+            expected_retval
d01bb5
+        )
d01bb5
+
d01bb5
+        assert_raise_library_error(
d01bb5
+            lambda: lib.simulate_cib_xml(mock_runner, "<cib />"),
d01bb5
+            fixture.error(
d01bb5
+                report_codes.CIB_SIMULATE_ERROR,
d01bb5
+                reason="some error",
d01bb5
+            ),
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_error_reading_transitions(self, mock_write_tmpfile):
d01bb5
+        tmpfile_new_cib = mock.MagicMock()
d01bb5
+        tmpfile_new_cib.name = rc("new_cib.tmp")
d01bb5
+        tmpfile_new_cib.read.return_value = "new cib data"
d01bb5
+        tmpfile_transitions = mock.MagicMock()
d01bb5
+        tmpfile_transitions.name = rc("transitions.tmp")
d01bb5
+        tmpfile_transitions.read.side_effect = OSError(1, "some error")
d01bb5
+        mock_write_tmpfile.side_effect = [tmpfile_new_cib, tmpfile_transitions]
d01bb5
+
d01bb5
+        expected_stdout = "simulate output"
d01bb5
+        expected_stderr = ""
d01bb5
+        expected_retval = 0
d01bb5
+        mock_runner = get_runner(
d01bb5
+            expected_stdout,
d01bb5
+            expected_stderr,
d01bb5
+            expected_retval
d01bb5
+        )
d01bb5
+
d01bb5
+        assert_raise_library_error(
d01bb5
+            lambda: lib.simulate_cib_xml(mock_runner, "<cib />"),
d01bb5
+            fixture.error(
d01bb5
+                report_codes.CIB_SIMULATE_ERROR,
d01bb5
+                reason="some error",
d01bb5
+            ),
d01bb5
+        )
d01bb5
+
d01bb5
+
d01bb5
+@mock.patch("pcs.lib.pacemaker.live.simulate_cib_xml")
d01bb5
+class SimulateCib(TestCase):
d01bb5
+    def setUp(self):
d01bb5
+        self.runner = "mock runner"
d01bb5
+        self.cib_xml = "<cib/>"
d01bb5
+        self.cib = etree.fromstring(self.cib_xml)
d01bb5
+        self.simulate_output = "  some output  "
d01bb5
+        self.transitions = "<transitions/>"
d01bb5
+        self.new_cib = "<new-cib/>"
d01bb5
+
d01bb5
+    def test_success(self, mock_simulate):
d01bb5
+        mock_simulate.return_value = (
d01bb5
+            self.simulate_output, self.transitions, self.new_cib
d01bb5
+        )
d01bb5
+        result = lib.simulate_cib(self.runner, self.cib)
d01bb5
+        self.assertEqual(result[0], "some output")
d01bb5
+        self.assertEqual(etree_to_str(result[1]), self.transitions)
d01bb5
+        self.assertEqual(etree_to_str(result[2]), self.new_cib)
d01bb5
+        mock_simulate.assert_called_once_with(self.runner, self.cib_xml)
d01bb5
+
d01bb5
+    def test_invalid_cib(self, mock_simulate):
d01bb5
+        mock_simulate.return_value = (
d01bb5
+            self.simulate_output, "bad transitions", self.new_cib
d01bb5
+        )
d01bb5
+        assert_raise_library_error(
d01bb5
+            lambda: lib.simulate_cib(self.runner, self.cib),
d01bb5
+            fixture.error(
d01bb5
+                report_codes.CIB_SIMULATE_ERROR,
d01bb5
+                reason=(
d01bb5
+                    "Start tag expected, '<' not found, line 1, column 1"
d01bb5
+                ),
d01bb5
+            ),
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_invalid_transitions(self, mock_simulate):
d01bb5
+        mock_simulate.return_value = (
d01bb5
+            self.simulate_output, self.transitions, "bad new cib"
d01bb5
+        )
d01bb5
+        assert_raise_library_error(
d01bb5
+            lambda: lib.simulate_cib(self.runner, self.cib),
d01bb5
+            fixture.error(
d01bb5
+                report_codes.CIB_SIMULATE_ERROR,
d01bb5
+                reason=(
d01bb5
+                    "Start tag expected, '<' not found, line 1, column 1"
d01bb5
+                ),
d01bb5
+            ),
d01bb5
+        )
d01bb5
+
d01bb5
 class GetLocalNodeStatusTest(LibraryPacemakerNodeStatusTest):
d01bb5
     def test_offline(self):
d01bb5
         expected_stdout = "some info"
d01bb5
diff --git a/pcs/lib/pacemaker/test/test_simulate.py b/pcs/lib/pacemaker/test/test_simulate.py
d01bb5
new file mode 100644
d01bb5
index 00000000..2fe0789a
d01bb5
--- /dev/null
d01bb5
+++ b/pcs/lib/pacemaker/test/test_simulate.py
d01bb5
@@ -0,0 +1,380 @@
d01bb5
+from __future__ import (
d01bb5
+    absolute_import,
d01bb5
+    division,
d01bb5
+    print_function,
d01bb5
+)
d01bb5
+
d01bb5
+from lxml import etree
d01bb5
+
d01bb5
+from pcs.lib.pacemaker import simulate
d01bb5
+
d01bb5
+from pcs.test.tools.pcs_unittest import TestCase
d01bb5
+from pcs.test.tools.misc import get_test_resource as rc
d01bb5
+
d01bb5
+
d01bb5
+class GetOperationsFromTransitions(TestCase):
d01bb5
+    def test_transitions1(self):
d01bb5
+        transitions = etree.parse(rc("transitions01.xml"))
d01bb5
+        self.assertEqual(
d01bb5
+            [
d01bb5
+                {
d01bb5
+                    "primitive_id": "dummy",
d01bb5
+                    "primitive_long_id": "dummy",
d01bb5
+                    "operation": "stop",
d01bb5
+                    "on_node": "rh7-3",
d01bb5
+                },
d01bb5
+                {
d01bb5
+                    "primitive_id": "dummy",
d01bb5
+                    "primitive_long_id": "dummy",
d01bb5
+                    "operation": "start",
d01bb5
+                    "on_node": "rh7-2",
d01bb5
+                },
d01bb5
+                {
d01bb5
+                    "primitive_id": "d0",
d01bb5
+                    "primitive_long_id": "d0:1",
d01bb5
+                    "operation": "stop",
d01bb5
+                    "on_node": "rh7-1",
d01bb5
+                },
d01bb5
+                {
d01bb5
+                    "primitive_id": "d0",
d01bb5
+                    "primitive_long_id": "d0:1",
d01bb5
+                    "operation": "start",
d01bb5
+                    "on_node": "rh7-2",
d01bb5
+                },
d01bb5
+                {
d01bb5
+                    "primitive_id": "state",
d01bb5
+                    "primitive_long_id": "state:0",
d01bb5
+                    "operation": "stop",
d01bb5
+                    "on_node": "rh7-3",
d01bb5
+                },
d01bb5
+                {
d01bb5
+                    "primitive_id": "state",
d01bb5
+                    "primitive_long_id": "state:0",
d01bb5
+                    "operation": "start",
d01bb5
+                    "on_node": "rh7-2",
d01bb5
+                },
d01bb5
+            ],
d01bb5
+            simulate.get_operations_from_transitions(transitions)
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_transitions2(self):
d01bb5
+        transitions = etree.parse(rc("transitions02.xml"))
d01bb5
+        self.assertEqual(
d01bb5
+            [
d01bb5
+                {
d01bb5
+                    "primitive_id": "RemoteNode",
d01bb5
+                    "primitive_long_id": "RemoteNode",
d01bb5
+                    "operation": "stop",
d01bb5
+                    "on_node": "virt-143",
d01bb5
+                },
d01bb5
+                {
d01bb5
+                    "primitive_id": "RemoteNode",
d01bb5
+                    "primitive_long_id": "RemoteNode",
d01bb5
+                    "operation": "migrate_to",
d01bb5
+                    "on_node": "virt-143",
d01bb5
+                },
d01bb5
+                {
d01bb5
+                    "primitive_id": "RemoteNode",
d01bb5
+                    "primitive_long_id": "RemoteNode",
d01bb5
+                    "operation": "migrate_from",
d01bb5
+                    "on_node": "virt-142",
d01bb5
+                },
d01bb5
+                {
d01bb5
+                    "primitive_id": "dummy8",
d01bb5
+                    "primitive_long_id": "dummy8",
d01bb5
+                    "operation": "stop",
d01bb5
+                    "on_node": "virt-143",
d01bb5
+                },
d01bb5
+                {
d01bb5
+                    "primitive_id": "dummy8",
d01bb5
+                    "primitive_long_id": "dummy8",
d01bb5
+                    "operation": "start",
d01bb5
+                    "on_node": "virt-142",
d01bb5
+                }
d01bb5
+            ],
d01bb5
+            simulate.get_operations_from_transitions(transitions)
d01bb5
+        )
d01bb5
+
d01bb5
+class GetResourcesFromOperations(TestCase):
d01bb5
+    operations = [
d01bb5
+            {
d01bb5
+                "primitive_id": "dummy2",
d01bb5
+                "primitive_long_id": "dummy2:1",
d01bb5
+                "operation": "stop",
d01bb5
+                "on_node": "node1",
d01bb5
+            },
d01bb5
+            {
d01bb5
+                "primitive_id": "dummy1",
d01bb5
+                "primitive_long_id": "dummy1",
d01bb5
+                "operation": "stop",
d01bb5
+                "on_node": "node3",
d01bb5
+            },
d01bb5
+            {
d01bb5
+                "primitive_id": "dummy1",
d01bb5
+                "primitive_long_id": "dummy1",
d01bb5
+                "operation": "start",
d01bb5
+                "on_node": "node2",
d01bb5
+            },
d01bb5
+    ]
d01bb5
+    def test_no_operations(self):
d01bb5
+        self.assertEqual(
d01bb5
+            [],
d01bb5
+            simulate.get_resources_from_operations(
d01bb5
+                []
d01bb5
+            )
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_no_operations_exclude(self):
d01bb5
+        self.assertEqual(
d01bb5
+            [],
d01bb5
+            simulate.get_resources_from_operations(
d01bb5
+                [], exclude={"dummy1"}
d01bb5
+            )
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_some_operations(self):
d01bb5
+        self.assertEqual(
d01bb5
+            ["dummy1", "dummy2"],
d01bb5
+            simulate.get_resources_from_operations(
d01bb5
+                self.operations
d01bb5
+            )
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_some_operations_exclude(self):
d01bb5
+        self.assertEqual(
d01bb5
+            ["dummy2"],
d01bb5
+            simulate.get_resources_from_operations(
d01bb5
+                self.operations, exclude={"dummy1", "dummy2:1", "dummyX"}
d01bb5
+            )
d01bb5
+        )
d01bb5
+
d01bb5
+class GetResourcesLeftStoppedDemotedMixin(object):
d01bb5
+    def test_no_operations(self):
d01bb5
+        self.assertEqual([], self.call([]))
d01bb5
+
d01bb5
+    def test_down(self):
d01bb5
+        self.assertEqual(
d01bb5
+            ["dummy"],
d01bb5
+            self.call([
d01bb5
+                {
d01bb5
+                    "primitive_id": "dummy",
d01bb5
+                    "primitive_long_id": "dummy",
d01bb5
+                    "operation": self.action_down,
d01bb5
+                    "on_node": "node3",
d01bb5
+                },
d01bb5
+            ])
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_up(self):
d01bb5
+        self.assertEqual(
d01bb5
+            [],
d01bb5
+            self.call([
d01bb5
+                {
d01bb5
+                    "primitive_id": "dummy",
d01bb5
+                    "primitive_long_id": "dummy",
d01bb5
+                    "operation": self.action_up,
d01bb5
+                    "on_node": "node3",
d01bb5
+                },
d01bb5
+            ])
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_down_up(self):
d01bb5
+        self.assertEqual(
d01bb5
+            [],
d01bb5
+            self.call([
d01bb5
+                {
d01bb5
+                    "primitive_id": "dummy",
d01bb5
+                    "primitive_long_id": "dummy",
d01bb5
+                    "operation": self.action_down,
d01bb5
+                    "on_node": "node2",
d01bb5
+                },
d01bb5
+                {
d01bb5
+                    "primitive_id": "dummy",
d01bb5
+                    "primitive_long_id": "dummy",
d01bb5
+                    "operation": self.action_up,
d01bb5
+                    "on_node": "node3",
d01bb5
+                },
d01bb5
+            ])
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_up_down(self):
d01bb5
+        self.assertEqual(
d01bb5
+            [],
d01bb5
+            self.call([
d01bb5
+                {
d01bb5
+                    "primitive_id": "dummy",
d01bb5
+                    "primitive_long_id": "dummy",
d01bb5
+                    "operation": self.action_up,
d01bb5
+                    "on_node": "node2",
d01bb5
+                },
d01bb5
+                {
d01bb5
+                    "primitive_id": "dummy",
d01bb5
+                    "primitive_long_id": "dummy",
d01bb5
+                    "operation": self.action_down,
d01bb5
+                    "on_node": "node3",
d01bb5
+                },
d01bb5
+            ])
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_mixed(self):
d01bb5
+        self.assertEqual(
d01bb5
+            ["dummy1", "dummy2"],
d01bb5
+            self.call([
d01bb5
+                {
d01bb5
+                    "primitive_id": "dummy1",
d01bb5
+                    "primitive_long_id": "dummy1",
d01bb5
+                    "operation": self.action_down,
d01bb5
+                    "on_node": "node3",
d01bb5
+                },
d01bb5
+                {
d01bb5
+                    "primitive_id": "dummy2",
d01bb5
+                    "primitive_long_id": "dummy2",
d01bb5
+                    "operation": self.action_down,
d01bb5
+                    "on_node": "node3",
d01bb5
+                },
d01bb5
+                {
d01bb5
+                    "primitive_id": "dummy3",
d01bb5
+                    "primitive_long_id": "dummy3",
d01bb5
+                    "operation": self.action_down,
d01bb5
+                    "on_node": "node3",
d01bb5
+                },
d01bb5
+                {
d01bb5
+                    "primitive_id": "dummy3",
d01bb5
+                    "primitive_long_id": "dummy3",
d01bb5
+                    "operation": self.action_up,
d01bb5
+                    "on_node": "node2",
d01bb5
+                },
d01bb5
+            ])
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_exclude(self):
d01bb5
+        self.assertEqual(
d01bb5
+            ["dummy2"],
d01bb5
+            self.call(
d01bb5
+                [
d01bb5
+                    {
d01bb5
+                        "primitive_id": "dummy1",
d01bb5
+                        "primitive_long_id": "dummy1",
d01bb5
+                        "operation": self.action_down,
d01bb5
+                        "on_node": "node3",
d01bb5
+                    },
d01bb5
+                    {
d01bb5
+                        "primitive_id": "dummy2",
d01bb5
+                        "primitive_long_id": "dummy2",
d01bb5
+                        "operation": self.action_down,
d01bb5
+                        "on_node": "node3",
d01bb5
+                    },
d01bb5
+                ],
d01bb5
+                exclude={"dummy1", "dummyX"}
d01bb5
+            )
d01bb5
+        )
d01bb5
+
d01bb5
+class GetResourcesLeftStopped(GetResourcesLeftStoppedDemotedMixin, TestCase):
d01bb5
+    action_up = "start"
d01bb5
+    action_down = "stop"
d01bb5
+    call = staticmethod(simulate.get_resources_left_stopped)
d01bb5
+
d01bb5
+    def test_clone_move(self):
d01bb5
+        self.assertEqual(
d01bb5
+            [],
d01bb5
+            self.call([
d01bb5
+                {
d01bb5
+                    "primitive_id": "dummy1",
d01bb5
+                    "primitive_long_id": "dummy1:0",
d01bb5
+                    "operation": self.action_down,
d01bb5
+                    "on_node": "node3",
d01bb5
+                },
d01bb5
+                {
d01bb5
+                    "primitive_id": "dummy1",
d01bb5
+                    "primitive_long_id": "dummy1:1",
d01bb5
+                    "operation": self.action_down,
d01bb5
+                    "on_node": "node1",
d01bb5
+                },
d01bb5
+                {
d01bb5
+                    "primitive_id": "dummy1",
d01bb5
+                    "primitive_long_id": "dummy1:0",
d01bb5
+                    "operation": self.action_up,
d01bb5
+                    "on_node": "node2",
d01bb5
+                },
d01bb5
+                {
d01bb5
+                    "primitive_id": "dummy1",
d01bb5
+                    "primitive_long_id": "dummy1:1",
d01bb5
+                    "operation": self.action_up,
d01bb5
+                    "on_node": "node4",
d01bb5
+                },
d01bb5
+            ])
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_clone_stop(self):
d01bb5
+        self.assertEqual(
d01bb5
+            ["dummy1"],
d01bb5
+            self.call([
d01bb5
+                {
d01bb5
+                    "primitive_id": "dummy1",
d01bb5
+                    "primitive_long_id": "dummy1:0",
d01bb5
+                    "operation": self.action_down,
d01bb5
+                    "on_node": "node3",
d01bb5
+                },
d01bb5
+                {
d01bb5
+                    "primitive_id": "dummy1",
d01bb5
+                    "primitive_long_id": "dummy1:1",
d01bb5
+                    "operation": self.action_down,
d01bb5
+                    "on_node": "node1",
d01bb5
+                },
d01bb5
+                {
d01bb5
+                    "primitive_id": "dummy1",
d01bb5
+                    "primitive_long_id": "dummy1:1",
d01bb5
+                    "operation": self.action_up,
d01bb5
+                    "on_node": "node4",
d01bb5
+                },
d01bb5
+            ])
d01bb5
+        )
d01bb5
+
d01bb5
+class GetResourcesLeftDemoted(GetResourcesLeftStoppedDemotedMixin, TestCase):
d01bb5
+    action_up = "promote"
d01bb5
+    action_down = "demote"
d01bb5
+    call = staticmethod(simulate.get_resources_left_demoted)
d01bb5
+
d01bb5
+    def test_master_move(self):
d01bb5
+        self.assertEqual(
d01bb5
+            [],
d01bb5
+            self.call([
d01bb5
+                {
d01bb5
+                    "primitive_id": "dummy1",
d01bb5
+                    "primitive_long_id": "dummy1:0",
d01bb5
+                    "operation": self.action_down,
d01bb5
+                    "on_node": "node3",
d01bb5
+                },
d01bb5
+                {
d01bb5
+                    "primitive_id": "dummy1",
d01bb5
+                    "primitive_long_id": "dummy1:1",
d01bb5
+                    "operation": self.action_up,
d01bb5
+                    "on_node": "node4",
d01bb5
+                },
d01bb5
+            ])
d01bb5
+        )
d01bb5
+
d01bb5
+    def test_master_stop(self):
d01bb5
+        self.assertEqual(
d01bb5
+            ["dummy1"],
d01bb5
+            self.call([
d01bb5
+                {
d01bb5
+                    "primitive_id": "dummy1",
d01bb5
+                    "primitive_long_id": "dummy1:0",
d01bb5
+                    "operation": self.action_down,
d01bb5
+                    "on_node": "node3",
d01bb5
+                },
d01bb5
+                {
d01bb5
+                    "primitive_id": "dummy1",
d01bb5
+                    "primitive_long_id": "dummy1:1",
d01bb5
+                    "operation": self.action_up,
d01bb5
+                    "on_node": "node4",
d01bb5
+                },
d01bb5
+                {
d01bb5
+                    "primitive_id": "dummy1",
d01bb5
+                    "primitive_long_id": "dummy1:2",
d01bb5
+                    "operation": self.action_down,
d01bb5
+                    "on_node": "node1",
d01bb5
+                },
d01bb5
+            ])
d01bb5
+        )
d01bb5
diff --git a/pcs/lib/reports.py b/pcs/lib/reports.py
d01bb5
index 1408f620..3c5380a7 100644
d01bb5
--- a/pcs/lib/reports.py
d01bb5
+++ b/pcs/lib/reports.py
d01bb5
@@ -1406,6 +1406,19 @@ def cib_diff_error(reason, cib_old, cib_new):
d01bb5
         }
d01bb5
     )
d01bb5
 
d01bb5
+def cib_simulate_error(reason):
d01bb5
+    """
d01bb5
+    cannot simulate effects a CIB would have on a live cluster
d01bb5
+
d01bb5
+    string reason -- error description
d01bb5
+    """
d01bb5
+    return ReportItem.error(
d01bb5
+        report_codes.CIB_SIMULATE_ERROR,
d01bb5
+        info={
d01bb5
+            "reason": reason,
d01bb5
+        }
d01bb5
+    )
d01bb5
+
d01bb5
 def cib_push_forced_full_due_to_crm_feature_set(required_set, current_set):
d01bb5
     """
d01bb5
     Pcs uses the "push full CIB" approach so race conditions may occur.
d01bb5
@@ -3021,3 +3034,25 @@ def fence_history_not_supported():
d01bb5
     return ReportItem.error(
d01bb5
         report_codes.FENCE_HISTORY_NOT_SUPPORTED
d01bb5
     )
d01bb5
+
d01bb5
+def resource_disable_affects_other_resources(
d01bb5
+    disabled_resource_list,
d01bb5
+    affected_resource_list,
d01bb5
+    crm_simulate_plaintext_output
d01bb5
+):
d01bb5
+    """
d01bb5
+    User requested disabling resources without affecting other resources but
d01bb5
+    some resources would be affected
d01bb5
+
d01bb5
+    iterable disabled_resource_list -- list of resources to disable
d01bb5
+    iterable affected_resource_list -- other affected resources
d01bb5
+    string crm_simulate_plaintext_output -- plaintext output from pacemaker
d01bb5
+    """
d01bb5
+    return ReportItem.error(
d01bb5
+        report_codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES,
d01bb5
+        info={
d01bb5
+            "disabled_resource_list": sorted(disabled_resource_list),
d01bb5
+            "affected_resource_list": sorted(affected_resource_list),
d01bb5
+            "crm_simulate_plaintext_output": crm_simulate_plaintext_output,
d01bb5
+        }
d01bb5
+    )
d01bb5
diff --git a/pcs/lib/tools.py b/pcs/lib/tools.py
d01bb5
index 0d0dc461..55983af9 100644
d01bb5
--- a/pcs/lib/tools.py
d01bb5
+++ b/pcs/lib/tools.py
d01bb5
@@ -64,6 +64,7 @@ def write_tmpfile(data, binary=False):
d01bb5
     """
d01bb5
     mode = "w+b" if binary else "w+"
d01bb5
     tmpfile = tempfile.NamedTemporaryFile(mode=mode, suffix=".pcs")
d01bb5
-    tmpfile.write(data)
d01bb5
-    tmpfile.flush()
d01bb5
+    if data is not None:
d01bb5
+        tmpfile.write(data)
d01bb5
+        tmpfile.flush()
d01bb5
     return tmpfile
d01bb5
diff --git a/pcs/pcs.8 b/pcs/pcs.8
d01bb5
index 0e8b15f7..596ceb7a 100644
d01bb5
--- a/pcs/pcs.8
d01bb5
+++ b/pcs/pcs.8
d01bb5
@@ -90,8 +90,27 @@ Deletes the resource, group, master or clone (and all resources within the group
d01bb5
 enable <resource id>... [\fB\-\-wait\fR[=n]]
d01bb5
 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.
d01bb5
 .TP
d01bb5
-disable <resource id>... [\fB\-\-wait\fR[=n]]
d01bb5
-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.
d01bb5
+disable <resource id>... [\fB\-\-safe\fR [\fB\-\-no\-strict\fR]] [\fB\-\-simulate\fR] [\fB\-\-wait\fR[=n]]
d01bb5
+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.
d01bb5
+.br
d01bb5
+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.
d01bb5
+.br
d01bb5
+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.
d01bb5
+.br
d01bb5
+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.
d01bb5
+.br
d01bb5
+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.
d01bb5
+.TP
d01bb5
+safe\-disable <resource id>... [\fB\-\-no\-strict\fR] [\fB\-\-simulate\fR] [\fB\-\-wait\fR[=n]] [\fB\-\-force\fR]
d01bb5
+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.
d01bb5
+.br
d01bb5
+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.
d01bb5
+.br
d01bb5
+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.
d01bb5
+.br
d01bb5
+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.
d01bb5
+.br
d01bb5
+If \fB\-\-force\fR is specified, checks for safe disable will be skipped.
d01bb5
 .TP
d01bb5
 restart <resource id> [node] [\fB\-\-wait\fR=n]
d01bb5
 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.
d01bb5
diff --git a/pcs/resource.py b/pcs/resource.py
d01bb5
index 27af2405..51233a12 100644
d01bb5
--- a/pcs/resource.py
d01bb5
+++ b/pcs/resource.py
d01bb5
@@ -137,6 +137,8 @@ def resource_cmd(argv):
d01bb5
             resource_enable_cmd(lib, argv_next, modifiers)
d01bb5
         elif sub_cmd == "disable":
d01bb5
             resource_disable_cmd(lib, argv_next, modifiers)
d01bb5
+        elif sub_cmd == "safe-disable":
d01bb5
+            resource_safe_disable_cmd(lib, argv_next, modifiers)
d01bb5
         elif sub_cmd == "restart":
d01bb5
             resource_restart(argv_next)
d01bb5
         elif sub_cmd == "debug-start":
d01bb5
@@ -2034,11 +2036,53 @@ def resource_show(argv, stonith=False):
d01bb5
             utils.err("unable to find resource '"+arg+"'")
d01bb5
         resource_found = False
d01bb5
 
d01bb5
+
d01bb5
 def resource_disable_cmd(lib, argv, modifiers):
d01bb5
-    if len(argv) < 1:
d01bb5
-        utils.err("You must specify resource(s) to disable")
d01bb5
-    resources = argv
d01bb5
-    lib.resource.disable(resources, modifiers["wait"])
d01bb5
+    """
d01bb5
+    Options:
d01bb5
+      * -f - CIB file
d01bb5
+      * --safe - only disable if no other resource gets stopped or demoted
d01bb5
+      * --simulate - do not push the CIB, print its effects
d01bb5
+      * --no-strict - allow disable if other resource is affected
d01bb5
+      * --wait
d01bb5
+    """
d01bb5
+    if not argv:
d01bb5
+        raise CmdLineInputError("You must specify resource(s) to disable")
d01bb5
+
d01bb5
+    if modifiers["simulate"]:
d01bb5
+        print(lib.resource.disable_simulate(argv))
d01bb5
+        return
d01bb5
+    if modifiers["safe"] or modifiers["no-strict"]:
d01bb5
+        lib.resource.disable_safe(
d01bb5
+            argv,
d01bb5
+            not modifiers["no-strict"],
d01bb5
+            modifiers["wait"],
d01bb5
+        )
d01bb5
+        return
d01bb5
+    lib.resource.disable(argv, modifiers["wait"])
d01bb5
+
d01bb5
+
d01bb5
+def resource_safe_disable_cmd(lib, argv, modifiers):
d01bb5
+    """
d01bb5
+    Options:
d01bb5
+      * --force - skip checks for safe resource disable
d01bb5
+      * --no-strict - allow disable if other resource is affected
d01bb5
+      * --simulate - do not push the CIB, print its effects
d01bb5
+      * --wait
d01bb5
+    """
d01bb5
+    if modifiers["safe"]:
d01bb5
+        raise CmdLineInputError(
d01bb5
+            "Option '--safe' is not supported in this command"
d01bb5
+        )
d01bb5
+    if modifiers["force"]:
d01bb5
+        warn(
d01bb5
+            "option '--force' is specified therefore checks for disabling "
d01bb5
+            "resource safely will be skipped"
d01bb5
+        )
d01bb5
+    elif not modifiers["simulate"]:
d01bb5
+        modifiers["safe"] = True
d01bb5
+    resource_disable_cmd(lib, argv, modifiers)
d01bb5
+
d01bb5
 
d01bb5
 def resource_enable_cmd(lib, argv, modifiers):
d01bb5
     if len(argv) < 1:
d01bb5
diff --git a/pcs/test/test_resource.py b/pcs/test/test_resource.py
d01bb5
index 20f6af73..2a110683 100644
d01bb5
--- a/pcs/test/test_resource.py
d01bb5
+++ b/pcs/test/test_resource.py
d01bb5
@@ -32,6 +32,7 @@ from pcs.test.bin_mock import get_mock_settings
d01bb5
 
d01bb5
 from pcs import utils
d01bb5
 from pcs import resource
d01bb5
+from pcs.cli.common.errors import CmdLineInputError
d01bb5
 
d01bb5
 empty_cib = rc("cib-empty.xml")
d01bb5
 temp_cib = rc("temp-cib.xml")
d01bb5
@@ -6450,3 +6451,197 @@ class FailcountShow(TestCase):
d01bb5
             ),
d01bb5
             full=True
d01bb5
         )
d01bb5
+
d01bb5
+
d01bb5
+class ResourceDisable(TestCase):
d01bb5
+    def setUp(self):
d01bb5
+        self.lib = mock.Mock(spec_set=["resource"])
d01bb5
+        self.resource = mock.Mock(
d01bb5
+            spec_set=["disable", "disable_safe", "disable_simulate"]
d01bb5
+        )
d01bb5
+        self.lib.resource = self.resource
d01bb5
+
d01bb5
+    def run_cmd(self, argv, modifiers=None):
d01bb5
+        default_modifiers = {
d01bb5
+            "safe": False,
d01bb5
+            "simulate": False,
d01bb5
+            "no-strict": False,
d01bb5
+            "wait": False,
d01bb5
+        }
d01bb5
+        if modifiers:
d01bb5
+            default_modifiers.update(modifiers)
d01bb5
+        resource.resource_disable_cmd(self.lib, argv, default_modifiers)
d01bb5
+
d01bb5
+    def test_no_args(self):
d01bb5
+        with self.assertRaises(CmdLineInputError) as cm:
d01bb5
+            self.run_cmd([])
d01bb5
+        self.assertEqual(
d01bb5
+            cm.exception.message,
d01bb5
+            "You must specify resource(s) to disable"
d01bb5
+        )
d01bb5
+        self.resource.disable.assert_not_called()
d01bb5
+        self.resource.disable_safe.assert_not_called()
d01bb5
+        self.resource.disable_simulate.assert_not_called()
d01bb5
+
d01bb5
+    def test_one_resource(self):
d01bb5
+        self.run_cmd(["R1"])
d01bb5
+        self.resource.disable.assert_called_once_with(["R1"], False)
d01bb5
+        self.resource.disable_safe.assert_not_called()
d01bb5
+        self.resource.disable_simulate.assert_not_called()
d01bb5
+
d01bb5
+    def test_more_resources(self):
d01bb5
+        self.run_cmd(["R1", "R2"])
d01bb5
+        self.resource.disable.assert_called_once_with(["R1", "R2"], False)
d01bb5
+        self.resource.disable_safe.assert_not_called()
d01bb5
+        self.resource.disable_simulate.assert_not_called()
d01bb5
+
d01bb5
+    def test_safe(self):
d01bb5
+        self.run_cmd(["R1", "R2"], dict(safe=True))
d01bb5
+        self.resource.disable_safe.assert_called_once_with(
d01bb5
+            ["R1", "R2"], True, False
d01bb5
+        )
d01bb5
+        self.resource.disable.assert_not_called()
d01bb5
+        self.resource.disable_simulate.assert_not_called()
d01bb5
+
d01bb5
+    def test_safe_wait(self):
d01bb5
+        self.run_cmd(["R1", "R2"], dict(safe=True, wait="10"))
d01bb5
+        self.resource.disable_safe.assert_called_once_with(
d01bb5
+            ["R1", "R2"], True, "10"
d01bb5
+        )
d01bb5
+        self.resource.disable.assert_not_called()
d01bb5
+        self.resource.disable_simulate.assert_not_called()
d01bb5
+
d01bb5
+    def test_safe_no_strict(self):
d01bb5
+        self.run_cmd(["R1", "R2"], {"no-strict": True})
d01bb5
+        self.resource.disable_safe.assert_called_once_with(
d01bb5
+            ["R1", "R2"], False, False
d01bb5
+        )
d01bb5
+        self.resource.disable.assert_not_called()
d01bb5
+        self.resource.disable_simulate.assert_not_called()
d01bb5
+
d01bb5
+    def test_safe_no_strict_wait(self):
d01bb5
+        self.run_cmd(["R1", "R2"], {"no-strict": True, "wait": "10"})
d01bb5
+        self.resource.disable_safe.assert_called_once_with(
d01bb5
+            ["R1", "R2"], False, "10"
d01bb5
+        )
d01bb5
+        self.resource.disable.assert_not_called()
d01bb5
+        self.resource.disable_simulate.assert_not_called()
d01bb5
+
d01bb5
+    @mock.patch("pcs.resource.print")
d01bb5
+    def test_simulate(self, mock_print):
d01bb5
+        self.resource.disable_simulate.return_value = "simulate output"
d01bb5
+        self.run_cmd(["R1", "R2"], dict(simulate=True))
d01bb5
+        self.resource.disable_simulate.assert_called_once_with(["R1", "R2"])
d01bb5
+        self.resource.disable.assert_not_called()
d01bb5
+        self.resource.disable_safe.assert_not_called()
d01bb5
+        mock_print.assert_called_once_with("simulate output")
d01bb5
+
d01bb5
+    def test_wait(self):
d01bb5
+        self.run_cmd(["R1", "R2"], dict(wait="10"))
d01bb5
+        self.resource.disable.assert_called_once_with(["R1", "R2"], "10")
d01bb5
+        self.resource.disable_safe.assert_not_called()
d01bb5
+        self.resource.disable_simulate.assert_not_called()
d01bb5
+
d01bb5
+
d01bb5
+class ResourceSafeDisable(TestCase):
d01bb5
+    def setUp(self):
d01bb5
+        self.lib = mock.Mock(spec_set=["resource"])
d01bb5
+        self.resource = mock.Mock(
d01bb5
+            spec_set=["disable", "disable_safe", "disable_simulate"]
d01bb5
+        )
d01bb5
+        self.lib.resource = self.resource
d01bb5
+        self.force_warning = (
d01bb5
+            "option '--force' is specified therefore checks for disabling "
d01bb5
+            "resource safely will be skipped"
d01bb5
+        )
d01bb5
+
d01bb5
+    def run_cmd(self, argv, modifiers=None):
d01bb5
+        default_modifiers = {
d01bb5
+            "safe": False,
d01bb5
+            "simulate": False,
d01bb5
+            "no-strict": False,
d01bb5
+            "wait": False,
d01bb5
+            "force": False,
d01bb5
+        }
d01bb5
+        if modifiers:
d01bb5
+            default_modifiers.update(modifiers)
d01bb5
+        resource.resource_safe_disable_cmd(self.lib, argv, default_modifiers)
d01bb5
+
d01bb5
+    def test_no_args(self):
d01bb5
+        with self.assertRaises(CmdLineInputError) as cm:
d01bb5
+            self.run_cmd([])
d01bb5
+        self.assertEqual(
d01bb5
+            cm.exception.message,
d01bb5
+            "You must specify resource(s) to disable"
d01bb5
+        )
d01bb5
+        self.resource.disable.assert_not_called()
d01bb5
+        self.resource.disable_safe.assert_not_called()
d01bb5
+        self.resource.disable_simulate.assert_not_called()
d01bb5
+
d01bb5
+    def test_one_resource(self):
d01bb5
+        self.run_cmd(["R1"])
d01bb5
+        self.resource.disable_safe.assert_called_once_with(["R1"], True, False)
d01bb5
+        self.resource.disable.assert_not_called()
d01bb5
+        self.resource.disable_simulate.assert_not_called()
d01bb5
+
d01bb5
+    def test_more_resources(self):
d01bb5
+        self.run_cmd(["R1", "R2"])
d01bb5
+        self.resource.disable_safe.assert_called_once_with(
d01bb5
+            ["R1", "R2"], True, False
d01bb5
+        )
d01bb5
+        self.resource.disable.assert_not_called()
d01bb5
+        self.resource.disable_simulate.assert_not_called()
d01bb5
+
d01bb5
+    def test_wait(self):
d01bb5
+        self.run_cmd(["R1", "R2"], dict(wait="10"))
d01bb5
+        self.resource.disable_safe.assert_called_once_with(
d01bb5
+            ["R1", "R2"], True, "10"
d01bb5
+        )
d01bb5
+        self.resource.disable.assert_not_called()
d01bb5
+        self.resource.disable_simulate.assert_not_called()
d01bb5
+
d01bb5
+    def test_no_strict(self):
d01bb5
+        self.run_cmd(["R1", "R2"], {"no-strict": True})
d01bb5
+        self.resource.disable_safe.assert_called_once_with(
d01bb5
+            ["R1", "R2"], False, False
d01bb5
+        )
d01bb5
+        self.resource.disable.assert_not_called()
d01bb5
+        self.resource.disable_simulate.assert_not_called()
d01bb5
+
d01bb5
+    def test_no_strict_wait(self):
d01bb5
+        self.run_cmd(["R1", "R2"], {"no-strict": True, "wait": "10"})
d01bb5
+        self.resource.disable_safe.assert_called_once_with(
d01bb5
+            ["R1", "R2"], False, "10"
d01bb5
+        )
d01bb5
+        self.resource.disable.assert_not_called()
d01bb5
+        self.resource.disable_simulate.assert_not_called()
d01bb5
+
d01bb5
+    @mock.patch("pcs.resource.warn")
d01bb5
+    def test_force(self, mock_warn):
d01bb5
+        self.run_cmd(["R1", "R2"], {"force": True})
d01bb5
+        self.resource.disable.assert_called_once_with(
d01bb5
+            ["R1", "R2"], False
d01bb5
+        )
d01bb5
+        self.resource.disable_safe.assert_not_called()
d01bb5
+        self.resource.disable_simulate.assert_not_called()
d01bb5
+        mock_warn.assert_called_once_with(self.force_warning)
d01bb5
+
d01bb5
+    @mock.patch("pcs.resource.warn")
d01bb5
+    def test_force_wait(self, mock_warn):
d01bb5
+        self.run_cmd(["R1", "R2"], {"force": True, "wait": "10"})
d01bb5
+        self.resource.disable.assert_called_once_with(
d01bb5
+            ["R1", "R2"], "10"
d01bb5
+        )
d01bb5
+        self.resource.disable_safe.assert_not_called()
d01bb5
+        self.resource.disable_simulate.assert_not_called()
d01bb5
+        mock_warn.assert_called_once_with(self.force_warning)
d01bb5
+
d01bb5
+
d01bb5
+    @mock.patch("pcs.resource.print")
d01bb5
+    def test_simulate(self, mock_print):
d01bb5
+        self.resource.disable_simulate.return_value = "simulate output"
d01bb5
+        self.run_cmd(["R1", "R2"], dict(simulate=True))
d01bb5
+        self.resource.disable_simulate.assert_called_once_with(["R1", "R2"])
d01bb5
+        self.resource.disable.assert_not_called()
d01bb5
+        self.resource.disable_safe.assert_not_called()
d01bb5
+        mock_print.assert_called_once_with("simulate output")
d01bb5
diff --git a/pcs/test/tools/command_env/config_runner_pcmk.py b/pcs/test/tools/command_env/config_runner_pcmk.py
d01bb5
index e9cc6397..e561dd5b 100644
d01bb5
--- a/pcs/test/tools/command_env/config_runner_pcmk.py
d01bb5
+++ b/pcs/test/tools/command_env/config_runner_pcmk.py
d01bb5
@@ -7,8 +7,12 @@ import os
d01bb5
 
d01bb5
 from lxml import etree
d01bb5
 
d01bb5
-from pcs.test.tools.command_env.mock_runner import Call as RunnerCall
d01bb5
+from pcs.test.tools.command_env.mock_runner import (
d01bb5
+    Call as RunnerCall,
d01bb5
+    CheckStdinEqualXml,
d01bb5
+)
d01bb5
 from pcs.test.tools.fixture import complete_state_resources
d01bb5
+from pcs.test.tools.fixture_cib import modify_cib
d01bb5
 from pcs.test.tools.misc import get_test_resource as rc
d01bb5
 from pcs.test.tools.xml import etree_to_str
d01bb5
 
d01bb5
@@ -340,3 +344,52 @@ class PcmkShortcuts(object):
d01bb5
             name,
d01bb5
             RunnerCall("crm_node --force --remove {0}".format(node_name)),
d01bb5
         )
d01bb5
+
d01bb5
+    def simulate_cib(
d01bb5
+        self, new_cib_filepath, transitions_filepath,
d01bb5
+        cib_modifiers=None, cib_load_name="runner.cib.load",
d01bb5
+        stdout="", stderr="", returncode=0,
d01bb5
+        name="runner.pcmk.simulate_cib",
d01bb5
+        **modifier_shortcuts
d01bb5
+    ):
d01bb5
+        """
d01bb5
+        Create a call for simulating effects of cib changes
d01bb5
+
d01bb5
+        string new_cib_filepath -- a temp file for storing a new cib
d01bb5
+        string transitions_filepath -- a temp file for storing transitions
d01bb5
+        list of callable modifiers -- every callable takes etree.Element and
d01bb5
+            returns new etree.Element with desired modification
d01bb5
+        string cib_load_name -- key of a call from whose stdout the cib is taken
d01bb5
+        string stdout -- pacemaker's stdout
d01bb5
+        string stderr -- pacemaker's stderr
d01bb5
+        int returncode -- pacemaker's returncode
d01bb5
+        string name -- key of the call
d01bb5
+        dict modifier_shortcuts -- a new modifier is generated from each
d01bb5
+            modifier shortcut.
d01bb5
+            As key there can be keys of MODIFIER_GENERATORS.
d01bb5
+            Value is passed into appropriate generator from MODIFIER_GENERATORS.
d01bb5
+            For details see pcs_test.tools.fixture_cib (mainly the variable
d01bb5
+            MODIFIER_GENERATORS - please refer it when you are adding params
d01bb5
+            here)
d01bb5
+        """
d01bb5
+        cib_xml = modify_cib(
d01bb5
+            self.__calls.get(cib_load_name).stdout,
d01bb5
+            cib_modifiers,
d01bb5
+            **modifier_shortcuts
d01bb5
+        )
d01bb5
+        cmd = [
d01bb5
+            "crm_simulate", "--simulate",
d01bb5
+            "--save-output", new_cib_filepath,
d01bb5
+            "--save-graph", transitions_filepath,
d01bb5
+            "--xml-pipe",
d01bb5
+        ]
d01bb5
+        self.__calls.place(
d01bb5
+            name,
d01bb5
+            RunnerCall(
d01bb5
+                " ".join(cmd),
d01bb5
+                stdout=stdout,
d01bb5
+                stderr=stderr,
d01bb5
+                returncode=returncode,
d01bb5
+                check_stdin=CheckStdinEqualXml(cib_xml),
d01bb5
+            ),
d01bb5
+        )
d01bb5
diff --git a/pcs/test/tools/command_env/mock_runner.py b/pcs/test/tools/command_env/mock_runner.py
d01bb5
index 393e4995..41a09ea4 100644
d01bb5
--- a/pcs/test/tools/command_env/mock_runner.py
d01bb5
+++ b/pcs/test/tools/command_env/mock_runner.py
d01bb5
@@ -12,6 +12,22 @@ from pcs.test.tools.assertions import assert_xml_equal
d01bb5
 
d01bb5
 CALL_TYPE_RUNNER = "CALL_TYPE_RUNNER"
d01bb5
 
d01bb5
+
d01bb5
+class CheckStdinEqualXml(object):
d01bb5
+    def __init__(self, expected_stdin):
d01bb5
+        self.expected_stdin = expected_stdin
d01bb5
+
d01bb5
+    def __call__(self, stdin, command, order_num):
d01bb5
+        assert_xml_equal(
d01bb5
+            self.expected_stdin,
d01bb5
+            stdin,
d01bb5
+            (
d01bb5
+                "Trying to run command no. {0}"
d01bb5
+                "\n\n    '{1}'\n\nwith expected xml stdin.\n"
d01bb5
+            ).format(order_num, command)
d01bb5
+        )
d01bb5
+
d01bb5
+
d01bb5
 def create_check_stdin_xml(expected_stdin):
d01bb5
     def stdin_xml_check(stdin, command, order_num):
d01bb5
         assert_xml_equal(
d01bb5
@@ -62,6 +78,7 @@ COMMAND_COMPLETIONS = {
d01bb5
     "crm_mon": path.join(settings.pacemaker_binaries, "crm_mon"),
d01bb5
     "crm_node": path.join(settings.pacemaker_binaries, "crm_node"),
d01bb5
     "crm_resource": path.join(settings.pacemaker_binaries, "crm_resource"),
d01bb5
+    "crm_simulate": path.join(settings.pacemaker_binaries, "crm_simulate"),
d01bb5
     "crm_verify": path.join(settings.pacemaker_binaries, "crm_verify"),
d01bb5
     "sbd": settings.sbd_binary,
d01bb5
 }
d01bb5
diff --git a/pcs/usage.py b/pcs/usage.py
d01bb5
index 582bc53f..acad3730 100644
d01bb5
--- a/pcs/usage.py
d01bb5
+++ b/pcs/usage.py
d01bb5
@@ -252,14 +252,40 @@ Commands:
d01bb5
         started, or 1 if the resources have not yet started. If 'n' is not
d01bb5
         specified it defaults to 60 minutes.
d01bb5
 
d01bb5
-    disable <resource id>... [--wait[=n]]
d01bb5
+    disable <resource id>... [--safe [--no-strict]] [--simulate] [--wait[=n]]
d01bb5
         Attempt to stop the resources if they are running and forbid the
d01bb5
         cluster from starting them again. Depending on the rest of the
d01bb5
         configuration (constraints, options, failures, etc), the resources may
d01bb5
-        remain started. If --wait is specified, pcs will wait up to 'n' seconds
d01bb5
-        for the resources to stop and then return 0 if the resources are
d01bb5
-        stopped or 1 if the resources have not stopped. If 'n' is not specified
d01bb5
-        it defaults to 60 minutes.
d01bb5
+        remain started.
d01bb5
+        If --safe is specified, no changes to the cluster configuration will be
d01bb5
+        made if other than specified resources would be affected in any way.
d01bb5
+        If --no-strict is specified, no changes to the cluster configuration
d01bb5
+        will be made if other than specified resources would get stopped or
d01bb5
+        demoted. Moving resources between nodes is allowed.
d01bb5
+        If --simulate is specified, no changes to the cluster configuration
d01bb5
+        will be made and the effect of the changes will be printed instead.
d01bb5
+        If --wait is specified, pcs will wait up to 'n' seconds for the
d01bb5
+        resources to stop and then return 0 if the resources are stopped or 1
d01bb5
+        if the resources have not stopped. If 'n' is not specified it defaults
d01bb5
+        to 60 minutes.
d01bb5
+
d01bb5
+    safe-disable <resource id>... [--no-strict] [--simulate] [--wait[=n]]
d01bb5
+            [--force]
d01bb5
+        Attempt to stop the resources if they are running and forbid the
d01bb5
+        cluster from starting them again. Depending on the rest of the
d01bb5
+        configuration (constraints, options, failures, etc), the resources may
d01bb5
+        remain started. No changes to the cluster configuration will be
d01bb5
+        made if other than specified resources would be affected in any way.
d01bb5
+        If --no-strict is specified, no changes to the cluster configuration
d01bb5
+        will be made if other than specified resources would get stopped or
d01bb5
+        demoted. Moving resources between nodes is allowed.
d01bb5
+        If --simulate is specified, no changes to the cluster configuration
d01bb5
+        will be made and the effect of the changes will be printed instead.
d01bb5
+        If --wait is specified, pcs will wait up to 'n' seconds for the
d01bb5
+        resources to stop and then return 0 if the resources are stopped or 1
d01bb5
+        if the resources have not stopped. If 'n' is not specified it defaults
d01bb5
+        to 60 minutes.
d01bb5
+        If --force is specified, checks for safe disable will be skipped.
d01bb5
 
d01bb5
     restart <resource id> [node] [--wait=n]
d01bb5
         Restart the resource specified. If a node is specified and if the
d01bb5
diff --git a/pcs/utils.py b/pcs/utils.py
d01bb5
index 11e3b361..66f7ebf1 100644
d01bb5
--- a/pcs/utils.py
d01bb5
+++ b/pcs/utils.py
d01bb5
@@ -2973,6 +2973,9 @@ def get_modifiers():
d01bb5
         "monitor": "--monitor" in pcs_options,
d01bb5
         "name": pcs_options.get("--name", None),
d01bb5
         "no-default-ops": "--no-default-ops" in pcs_options,
d01bb5
+        "no-strict": "--no-strict" in pcs_options,
d01bb5
+        "safe": "--safe" in pcs_options,
d01bb5
+        "simulate": "--simulate" in pcs_options,
d01bb5
         "skip_offline_nodes": "--skip-offline" in pcs_options,
d01bb5
         "start": "--start" in pcs_options,
d01bb5
         "wait": pcs_options.get("--wait", False),
d01bb5
diff --git a/pcsd/capabilities.xml b/pcsd/capabilities.xml
d01bb5
index bc1d69c8..322ae300 100644
d01bb5
--- a/pcsd/capabilities.xml
d01bb5
+++ b/pcsd/capabilities.xml
d01bb5
@@ -1134,6 +1134,20 @@
d01bb5
         pcs commands: resource disable --wait, resource enable --wait
d01bb5
       </description>
d01bb5
     </capability>
d01bb5
+    <capability id="pcmk.resource.disable.safe" in-pcs="1" in-pcsd="0">
d01bb5
+      <description>
d01bb5
+        Do not disable resources if other resources would be affected.
d01bb5
+
d01bb5
+        pcs commands: resource disable --safe [--no-strict]
d01bb5
+      </description>
d01bb5
+    </capability>
d01bb5
+    <capability id="pcmk.resource.disable.simulate" in-pcs="1" in-pcsd="0">
d01bb5
+      <description>
d01bb5
+        Show effects caused by disabling resources.
d01bb5
+
d01bb5
+        pcs commands: resource disable --simulate
d01bb5
+      </description>
d01bb5
+    </capability>
d01bb5
     <capability id="pcmk.resource.manage-unmanage" in-pcs="1" in-pcsd="1">
d01bb5
       <description>
d01bb5
         Put a resource into unmanaged and managed mode.
d01bb5
-- 
d01bb5
2.20.1
d01bb5