Blame SOURCES/bz1158805-02-add-support-for-qdeviceqnetd-provided-by-corosync.patch

15f218
From bc599f0f30c039a72540002d9a41a93c15626837 Mon Sep 17 00:00:00 2001
15f218
From: Ivan Devat <idevat@redhat.com>
15f218
Date: Wed, 14 Sep 2016 09:04:57 +0200
15f218
Subject: [PATCH] squash bz1158805 Add support for qdevice/qnetd pro
15f218
15f218
9c7f37ef37bb lib: do not merge external processes' stdout and stderr
15f218
15f218
db3ada5e27aa warn on stopping/destroying currently used qdevice
15f218
15f218
18df73397b54 handle SBD when removing qdevice from a cluster
15f218
15f218
766f86954b46 Allow to re-run "cluster node add" if failed due to qdevice
15f218
---
15f218
 pcs/cluster.py                               |  44 ++--
15f218
 pcs/common/report_codes.py                   |   6 +-
15f218
 pcs/common/tools.py                          |   3 +
15f218
 pcs/lib/booth/status.py                      |  27 +-
15f218
 pcs/lib/booth/test/test_status.py            |  26 +-
15f218
 pcs/lib/cib/tools.py                         |   7 +-
15f218
 pcs/lib/commands/booth.py                    |   5 +-
15f218
 pcs/lib/commands/qdevice.py                  |  35 ++-
15f218
 pcs/lib/commands/quorum.py                   |  12 +-
15f218
 pcs/lib/commands/test/test_booth.py          |   4 +-
15f218
 pcs/lib/corosync/live.py                     |  26 +-
15f218
 pcs/lib/corosync/qdevice_client.py           |   9 +-
15f218
 pcs/lib/corosync/qdevice_net.py              |  77 ++++--
15f218
 pcs/lib/external.py                          | 105 +++++---
15f218
 pcs/lib/pacemaker.py                         |  71 ++++--
15f218
 pcs/lib/reports.py                           |  97 ++++----
15f218
 pcs/lib/resource_agent.py                    |  31 ++-
15f218
 pcs/lib/sbd.py                               |   4 +-
15f218
 pcs/qdevice.py                               |   4 +-
15f218
 pcs/test/resources/corosync-qdevice.conf     |  34 +++
15f218
 pcs/test/test_common_tools.py                |  32 +++
15f218
 pcs/test/test_lib_cib_tools.py               |  10 +-
15f218
 pcs/test/test_lib_commands_qdevice.py        | 155 +++++++++++-
15f218
 pcs/test/test_lib_commands_quorum.py         | 105 +++++++-
15f218
 pcs/test/test_lib_corosync_live.py           |  30 ++-
15f218
 pcs/test/test_lib_corosync_qdevice_client.py |   8 +-
15f218
 pcs/test/test_lib_corosync_qdevice_net.py    | 110 +++++---
15f218
 pcs/test/test_lib_external.py                | 167 +++++++------
15f218
 pcs/test/test_lib_pacemaker.py               | 359 ++++++++++++++++++---------
15f218
 pcs/test/test_lib_resource_agent.py          |  39 ++-
15f218
 pcs/test/test_lib_sbd.py                     |  12 +-
15f218
 31 files changed, 1166 insertions(+), 488 deletions(-)
15f218
 create mode 100644 pcs/test/resources/corosync-qdevice.conf
15f218
15f218
diff --git a/pcs/cluster.py b/pcs/cluster.py
15f218
index 577e08e..e5ad1ec 100644
15f218
--- a/pcs/cluster.py
15f218
+++ b/pcs/cluster.py
15f218
@@ -1414,7 +1414,6 @@ def cluster_node(argv):
15f218
                 "cluster is not configured for RRP, "
15f218
                 "you must not specify ring 1 address for the node"
15f218
             )
15f218
-        corosync_conf = None
15f218
         (canAdd, error) =  utils.canAddNodeToCluster(node0)
15f218
         if not canAdd:
15f218
             utils.err("Unable to add '%s' to cluster: %s" % (node0, error))
15f218
@@ -1422,7 +1421,29 @@ def cluster_node(argv):
15f218
         report_processor = lib_env.report_processor
15f218
         node_communicator = lib_env.node_communicator()
15f218
         node_addr = NodeAddresses(node0, node1)
15f218
+
15f218
+        # First set up everything else than corosync. Once the new node is
15f218
+        # present in corosync.conf / cluster.conf, it's considered part of a
15f218
+        # cluster and the node add command cannot be run again. So we need to
15f218
+        # minimize the amout of actions (and therefore possible failures) after
15f218
+        # adding the node to corosync.
15f218
         try:
15f218
+            # qdevice setup
15f218
+            if not utils.is_rhel6():
15f218
+                conf_facade = corosync_conf_facade.from_string(
15f218
+                    utils.getCorosyncConf()
15f218
+                )
15f218
+                qdevice_model, qdevice_model_options, _ = conf_facade.get_quorum_device_settings()
15f218
+                if qdevice_model == "net":
15f218
+                    _add_device_model_net(
15f218
+                        lib_env,
15f218
+                        qdevice_model_options["host"],
15f218
+                        conf_facade.get_cluster_name(),
15f218
+                        [node_addr],
15f218
+                        skip_offline_nodes=False
15f218
+                    )
15f218
+
15f218
+            # sbd setup
15f218
             if lib_sbd.is_sbd_enabled(utils.cmd_runner()):
15f218
                 if "--watchdog" not in utils.pcs_options:
15f218
                     watchdog = settings.sbd_watchdog_default
15f218
@@ -1463,6 +1484,7 @@ def cluster_node(argv):
15f218
                     report_processor, node_communicator, node_addr
15f218
                 )
15f218
 
15f218
+            # booth setup
15f218
             booth_sync.send_all_config_to_node(
15f218
                 node_communicator,
15f218
                 report_processor,
15f218
@@ -1477,6 +1499,8 @@ def cluster_node(argv):
15f218
                 [node_communicator_exception_to_report_item(e)]
15f218
             )
15f218
 
15f218
+        # Now add the new node to corosync.conf / cluster.conf
15f218
+        corosync_conf = None
15f218
         for my_node in utils.getNodesFromCorosyncConf():
15f218
             retval, output = utils.addLocalNode(my_node, node0, node1)
15f218
             if retval != 0:
15f218
@@ -1512,24 +1536,6 @@ def cluster_node(argv):
15f218
                 except:
15f218
                     utils.err('Unable to communicate with pcsd')
15f218
 
15f218
-            # set qdevice-net certificates if needed
15f218
-            if not utils.is_rhel6():
15f218
-                try:
15f218
-                    conf_facade = corosync_conf_facade.from_string(
15f218
-                        corosync_conf
15f218
-                    )
15f218
-                    qdevice_model, qdevice_model_options, _ = conf_facade.get_quorum_device_settings()
15f218
-                    if qdevice_model == "net":
15f218
-                        _add_device_model_net(
15f218
-                            lib_env,
15f218
-                            qdevice_model_options["host"],
15f218
-                            conf_facade.get_cluster_name(),
15f218
-                            [node_addr],
15f218
-                            skip_offline_nodes=False
15f218
-                        )
15f218
-                except LibraryError as e:
15f218
-                    process_library_reports(e.args)
15f218
-
15f218
             print("Setting up corosync...")
15f218
             utils.setCorosyncConfig(node0, corosync_conf)
15f218
             if "--enable" in utils.pcs_options:
15f218
diff --git a/pcs/common/report_codes.py b/pcs/common/report_codes.py
15f218
index e6a86ec..23e931f 100644
15f218
--- a/pcs/common/report_codes.py
15f218
+++ b/pcs/common/report_codes.py
15f218
@@ -8,17 +8,18 @@ from __future__ import (
15f218
 # force cathegories
15f218
 FORCE_ACTIVE_RRP = "ACTIVE_RRP"
15f218
 FORCE_ALERT_RECIPIENT_VALUE_NOT_UNIQUE = "FORCE_ALERT_RECIPIENT_VALUE_NOT_UNIQUE"
15f218
-FORCE_BOOTH_REMOVE_FROM_CIB = "FORCE_BOOTH_REMOVE_FROM_CIB"
15f218
 FORCE_BOOTH_DESTROY = "FORCE_BOOTH_DESTROY"
15f218
+FORCE_BOOTH_REMOVE_FROM_CIB = "FORCE_BOOTH_REMOVE_FROM_CIB"
15f218
 FORCE_CONSTRAINT_DUPLICATE = "CONSTRAINT_DUPLICATE"
15f218
 FORCE_CONSTRAINT_MULTIINSTANCE_RESOURCE = "CONSTRAINT_MULTIINSTANCE_RESOURCE"
15f218
 FORCE_FILE_OVERWRITE = "FORCE_FILE_OVERWRITE"
15f218
 FORCE_LOAD_THRESHOLD = "LOAD_THRESHOLD"
15f218
+FORCE_METADATA_ISSUE = "METADATA_ISSUE"
15f218
 FORCE_OPTIONS = "OPTIONS"
15f218
 FORCE_QDEVICE_MODEL = "QDEVICE_MODEL"
15f218
+FORCE_QDEVICE_USED = "QDEVICE_USED"
15f218
 FORCE_UNKNOWN_AGENT = "UNKNOWN_AGENT"
15f218
 FORCE_UNSUPPORTED_AGENT = "UNSUPPORTED_AGENT"
15f218
-FORCE_METADATA_ISSUE = "METADATA_ISSUE"
15f218
 SKIP_OFFLINE_NODES = "SKIP_OFFLINE_NODES"
15f218
 SKIP_UNREADABLE_CONFIG = "SKIP_UNREADABLE_CONFIG"
15f218
 
15f218
@@ -135,6 +136,7 @@ QDEVICE_NOT_DEFINED = "QDEVICE_NOT_DEFINED"
15f218
 QDEVICE_NOT_INITIALIZED = "QDEVICE_NOT_INITIALIZED"
15f218
 QDEVICE_CLIENT_RELOAD_STARTED = "QDEVICE_CLIENT_RELOAD_STARTED"
15f218
 QDEVICE_REMOVE_OR_CLUSTER_STOP_NEEDED = "QDEVICE_REMOVE_OR_CLUSTER_STOP_NEEDED"
15f218
+QDEVICE_USED_BY_CLUSTERS = "QDEVICE_USED_BY_CLUSTERS"
15f218
 REQUIRED_OPTION_IS_MISSING = "REQUIRED_OPTION_IS_MISSING"
15f218
 RESOURCE_CLEANUP_ERROR = "RESOURCE_CLEANUP_ERROR"
15f218
 RESOURCE_CLEANUP_TOO_TIME_CONSUMING = 'RESOURCE_CLEANUP_TOO_TIME_CONSUMING'
15f218
diff --git a/pcs/common/tools.py b/pcs/common/tools.py
15f218
index 275f6b9..01194a5 100644
15f218
--- a/pcs/common/tools.py
15f218
+++ b/pcs/common/tools.py
15f218
@@ -38,3 +38,6 @@ def format_environment_error(e):
15f218
     if e.filename:
15f218
         return "{0}: '{1}'".format(e.strerror, e.filename)
15f218
     return e.strerror
15f218
+
15f218
+def join_multilines(strings):
15f218
+    return "\n".join([a.strip() for a in strings if a.strip()])
15f218
diff --git a/pcs/lib/booth/status.py b/pcs/lib/booth/status.py
15f218
index 4b93161..87cdc05 100644
15f218
--- a/pcs/lib/booth/status.py
15f218
+++ b/pcs/lib/booth/status.py
15f218
@@ -6,6 +6,7 @@ from __future__ import (
15f218
 )
15f218
 
15f218
 from pcs import settings
15f218
+from pcs.common.tools import join_multilines
15f218
 from pcs.lib.booth import reports
15f218
 from pcs.lib.errors import LibraryError
15f218
 
15f218
@@ -14,28 +15,36 @@ def get_daemon_status(runner, name=None):
15f218
     cmd = [settings.booth_binary, "status"]
15f218
     if name:
15f218
         cmd += ["-c", name]
15f218
-    output, return_value = runner.run(cmd)
15f218
+    stdout, stderr, return_value = runner.run(cmd)
15f218
     # 7 means that there is no booth instance running
15f218
     if return_value not in [0, 7]:
15f218
-        raise LibraryError(reports.booth_daemon_status_error(output))
15f218
-    return output
15f218
+        raise LibraryError(
15f218
+            reports.booth_daemon_status_error(join_multilines([stderr, stdout]))
15f218
+        )
15f218
+    return stdout
15f218
 
15f218
 
15f218
 def get_tickets_status(runner, name=None):
15f218
     cmd = [settings.booth_binary, "list"]
15f218
     if name:
15f218
         cmd += ["-c", name]
15f218
-    output, return_value = runner.run(cmd)
15f218
+    stdout, stderr, return_value = runner.run(cmd)
15f218
     if return_value != 0:
15f218
-        raise LibraryError(reports.booth_tickets_status_error(output))
15f218
-    return output
15f218
+        raise LibraryError(
15f218
+            reports.booth_tickets_status_error(
15f218
+                join_multilines([stderr, stdout])
15f218
+            )
15f218
+        )
15f218
+    return stdout
15f218
 
15f218
 
15f218
 def get_peers_status(runner, name=None):
15f218
     cmd = [settings.booth_binary, "peers"]
15f218
     if name:
15f218
         cmd += ["-c", name]
15f218
-    output, return_value = runner.run(cmd)
15f218
+    stdout, stderr, return_value = runner.run(cmd)
15f218
     if return_value != 0:
15f218
-        raise LibraryError(reports.booth_peers_status_error(output))
15f218
-    return output
15f218
+        raise LibraryError(
15f218
+            reports.booth_peers_status_error(join_multilines([stderr, stdout]))
15f218
+        )
15f218
+    return stdout
15f218
diff --git a/pcs/lib/booth/test/test_status.py b/pcs/lib/booth/test/test_status.py
15f218
index d47ffca..dfb7354 100644
15f218
--- a/pcs/lib/booth/test/test_status.py
15f218
+++ b/pcs/lib/booth/test/test_status.py
15f218
@@ -30,34 +30,34 @@ class GetDaemonStatusTest(TestCase):
15f218
         self.mock_run = mock.MagicMock(spec_set=CommandRunner)
15f218
 
15f218
     def test_no_name(self):
15f218
-        self.mock_run.run.return_value = ("output", 0)
15f218
+        self.mock_run.run.return_value = ("output", "", 0)
15f218
         self.assertEqual("output", lib.get_daemon_status(self.mock_run))
15f218
         self.mock_run.run.assert_called_once_with(
15f218
             [settings.booth_binary, "status"]
15f218
         )
15f218
 
15f218
     def test_with_name(self):
15f218
-        self.mock_run.run.return_value = ("output", 0)
15f218
+        self.mock_run.run.return_value = ("output", "", 0)
15f218
         self.assertEqual("output", lib.get_daemon_status(self.mock_run, "name"))
15f218
         self.mock_run.run.assert_called_once_with(
15f218
             [settings.booth_binary, "status", "-c", "name"]
15f218
         )
15f218
 
15f218
     def test_daemon_not_running(self):
15f218
-        self.mock_run.run.return_value = ("", 7)
15f218
+        self.mock_run.run.return_value = ("", "error", 7)
15f218
         self.assertEqual("", lib.get_daemon_status(self.mock_run))
15f218
         self.mock_run.run.assert_called_once_with(
15f218
             [settings.booth_binary, "status"]
15f218
         )
15f218
 
15f218
     def test_failure(self):
15f218
-        self.mock_run.run.return_value = ("out", 1)
15f218
+        self.mock_run.run.return_value = ("out", "error", 1)
15f218
         assert_raise_library_error(
15f218
             lambda: lib.get_daemon_status(self.mock_run),
15f218
             (
15f218
                 Severities.ERROR,
15f218
                 report_codes.BOOTH_DAEMON_STATUS_ERROR,
15f218
-                {"reason": "out"}
15f218
+                {"reason": "error\nout"}
15f218
             )
15f218
         )
15f218
         self.mock_run.run.assert_called_once_with(
15f218
@@ -70,14 +70,14 @@ class GetTicketsStatusTest(TestCase):
15f218
         self.mock_run = mock.MagicMock(spec_set=CommandRunner)
15f218
 
15f218
     def test_no_name(self):
15f218
-        self.mock_run.run.return_value = ("output", 0)
15f218
+        self.mock_run.run.return_value = ("output", "", 0)
15f218
         self.assertEqual("output", lib.get_tickets_status(self.mock_run))
15f218
         self.mock_run.run.assert_called_once_with(
15f218
             [settings.booth_binary, "list"]
15f218
         )
15f218
 
15f218
     def test_with_name(self):
15f218
-        self.mock_run.run.return_value = ("output", 0)
15f218
+        self.mock_run.run.return_value = ("output", "", 0)
15f218
         self.assertEqual(
15f218
             "output", lib.get_tickets_status(self.mock_run, "name")
15f218
         )
15f218
@@ -86,14 +86,14 @@ class GetTicketsStatusTest(TestCase):
15f218
         )
15f218
 
15f218
     def test_failure(self):
15f218
-        self.mock_run.run.return_value = ("out", 1)
15f218
+        self.mock_run.run.return_value = ("out", "error", 1)
15f218
         assert_raise_library_error(
15f218
             lambda: lib.get_tickets_status(self.mock_run),
15f218
             (
15f218
                 Severities.ERROR,
15f218
                 report_codes.BOOTH_TICKET_STATUS_ERROR,
15f218
                 {
15f218
-                    "reason": "out"
15f218
+                    "reason": "error\nout"
15f218
                 }
15f218
             )
15f218
         )
15f218
@@ -107,28 +107,28 @@ class GetPeersStatusTest(TestCase):
15f218
         self.mock_run = mock.MagicMock(spec_set=CommandRunner)
15f218
 
15f218
     def test_no_name(self):
15f218
-        self.mock_run.run.return_value = ("output", 0)
15f218
+        self.mock_run.run.return_value = ("output", "", 0)
15f218
         self.assertEqual("output", lib.get_peers_status(self.mock_run))
15f218
         self.mock_run.run.assert_called_once_with(
15f218
             [settings.booth_binary, "peers"]
15f218
         )
15f218
 
15f218
     def test_with_name(self):
15f218
-        self.mock_run.run.return_value = ("output", 0)
15f218
+        self.mock_run.run.return_value = ("output", "", 0)
15f218
         self.assertEqual("output", lib.get_peers_status(self.mock_run, "name"))
15f218
         self.mock_run.run.assert_called_once_with(
15f218
             [settings.booth_binary, "peers", "-c", "name"]
15f218
         )
15f218
 
15f218
     def test_failure(self):
15f218
-        self.mock_run.run.return_value = ("out", 1)
15f218
+        self.mock_run.run.return_value = ("out", "error", 1)
15f218
         assert_raise_library_error(
15f218
             lambda: lib.get_peers_status(self.mock_run),
15f218
             (
15f218
                 Severities.ERROR,
15f218
                 report_codes.BOOTH_PEERS_STATUS_ERROR,
15f218
                 {
15f218
-                    "reason": "out"
15f218
+                    "reason": "error\nout"
15f218
                 }
15f218
             )
15f218
         )
15f218
diff --git a/pcs/lib/cib/tools.py b/pcs/lib/cib/tools.py
15f218
index 8141360..6285931 100644
15f218
--- a/pcs/lib/cib/tools.py
15f218
+++ b/pcs/lib/cib/tools.py
15f218
@@ -11,6 +11,7 @@ import tempfile
15f218
 from lxml import etree
15f218
 
15f218
 from pcs import settings
15f218
+from pcs.common.tools import join_multilines
15f218
 from pcs.lib import reports
15f218
 from pcs.lib.errors import LibraryError
15f218
 from pcs.lib.pacemaker_values import validate_id
15f218
@@ -181,7 +182,7 @@ def upgrade_cib(cib, runner):
15f218
         temp_file = tempfile.NamedTemporaryFile("w+", suffix=".pcs")
15f218
         temp_file.write(etree.tostring(cib).decode())
15f218
         temp_file.flush()
15f218
-        output, retval = runner.run(
15f218
+        stdout, stderr, retval = runner.run(
15f218
             [
15f218
                 os.path.join(settings.pacemaker_binaries, "cibadmin"),
15f218
                 "--upgrade",
15f218
@@ -192,7 +193,9 @@ def upgrade_cib(cib, runner):
15f218
 
15f218
         if retval != 0:
15f218
             temp_file.close()
15f218
-            raise LibraryError(reports.cib_upgrade_failed(output))
15f218
+            raise LibraryError(
15f218
+                reports.cib_upgrade_failed(join_multilines([stderr, stdout]))
15f218
+            )
15f218
 
15f218
         temp_file.seek(0)
15f218
         return etree.fromstring(temp_file.read())
15f218
diff --git a/pcs/lib/commands/booth.py b/pcs/lib/commands/booth.py
15f218
index 7a3d348..bea966c 100644
15f218
--- a/pcs/lib/commands/booth.py
15f218
+++ b/pcs/lib/commands/booth.py
15f218
@@ -10,6 +10,7 @@ import os.path
15f218
 from functools import partial
15f218
 
15f218
 from pcs import settings
15f218
+from pcs.common.tools import join_multilines
15f218
 from pcs.lib import external, reports
15f218
 from pcs.lib.booth import (
15f218
     config_exchange,
15f218
@@ -185,7 +186,7 @@ def ticket_operation(operation, env, name, ticket, site_ip):
15f218
             )
15f218
         site_ip = site_ip_list[0]
15f218
 
15f218
-    command_output, return_code = env.cmd_runner().run([
15f218
+    stdout, stderr, return_code = env.cmd_runner().run([
15f218
         settings.booth_binary, operation,
15f218
         "-s", site_ip,
15f218
         ticket
15f218
@@ -195,7 +196,7 @@ def ticket_operation(operation, env, name, ticket, site_ip):
15f218
         raise LibraryError(
15f218
             booth_reports.booth_ticket_operation_failed(
15f218
                 operation,
15f218
-                command_output,
15f218
+                join_multilines([stderr, stdout]),
15f218
                 site_ip,
15f218
                 ticket
15f218
             )
15f218
diff --git a/pcs/lib/commands/qdevice.py b/pcs/lib/commands/qdevice.py
15f218
index 1d1d85f..ca0ae86 100644
15f218
--- a/pcs/lib/commands/qdevice.py
15f218
+++ b/pcs/lib/commands/qdevice.py
15f218
@@ -8,9 +8,10 @@ from __future__ import (
15f218
 import base64
15f218
 import binascii
15f218
 
15f218
+from pcs.common import report_codes
15f218
 from pcs.lib import external, reports
15f218
 from pcs.lib.corosync import qdevice_net
15f218
-from pcs.lib.errors import LibraryError
15f218
+from pcs.lib.errors import LibraryError, ReportItemSeverity
15f218
 
15f218
 
15f218
 def qdevice_setup(lib_env, model, enable, start):
15f218
@@ -31,13 +32,20 @@ def qdevice_setup(lib_env, model, enable, start):
15f218
     if start:
15f218
         _service_start(lib_env, qdevice_net.qdevice_start)
15f218
 
15f218
-def qdevice_destroy(lib_env, model):
15f218
+def qdevice_destroy(lib_env, model, proceed_if_used=False):
15f218
     """
15f218
     Stop and disable qdevice on local host and remove its configuration
15f218
     string model qdevice model to destroy
15f218
+    bool procced_if_used destroy qdevice even if it is used by clusters
15f218
     """
15f218
     _ensure_not_cman(lib_env)
15f218
     _check_model(model)
15f218
+    _check_qdevice_not_used(
15f218
+        lib_env.report_processor,
15f218
+        lib_env.cmd_runner(),
15f218
+        model,
15f218
+        proceed_if_used
15f218
+    )
15f218
     _service_stop(lib_env, qdevice_net.qdevice_stop)
15f218
     _service_disable(lib_env, qdevice_net.qdevice_disable)
15f218
     qdevice_net.qdevice_destroy()
15f218
@@ -83,12 +91,20 @@ def qdevice_start(lib_env, model):
15f218
     _check_model(model)
15f218
     _service_start(lib_env, qdevice_net.qdevice_start)
15f218
 
15f218
-def qdevice_stop(lib_env, model):
15f218
+def qdevice_stop(lib_env, model, proceed_if_used=False):
15f218
     """
15f218
     stop qdevice now on local host
15f218
+    string model qdevice model to destroy
15f218
+    bool procced_if_used stop qdevice even if it is used by clusters
15f218
     """
15f218
     _ensure_not_cman(lib_env)
15f218
     _check_model(model)
15f218
+    _check_qdevice_not_used(
15f218
+        lib_env.report_processor,
15f218
+        lib_env.cmd_runner(),
15f218
+        model,
15f218
+        proceed_if_used
15f218
+    )
15f218
     _service_stop(lib_env, qdevice_net.qdevice_stop)
15f218
 
15f218
 def qdevice_kill(lib_env, model):
15f218
@@ -176,6 +192,19 @@ def _check_model(model):
15f218
             reports.invalid_option_value("model", model, ["net"])
15f218
         )
15f218
 
15f218
+def _check_qdevice_not_used(reporter, runner, model, force=False):
15f218
+    _check_model(model)
15f218
+    connected_clusters = []
15f218
+    if model == "net":
15f218
+        status = qdevice_net.qdevice_status_cluster_text(runner)
15f218
+        connected_clusters = qdevice_net.qdevice_connected_clusters(status)
15f218
+    if connected_clusters:
15f218
+        reporter.process(reports.qdevice_used_by_clusters(
15f218
+            connected_clusters,
15f218
+            ReportItemSeverity.WARNING if force else ReportItemSeverity.ERROR,
15f218
+            None if force else report_codes.FORCE_QDEVICE_USED
15f218
+        ))
15f218
+
15f218
 def _service_start(lib_env, func):
15f218
     lib_env.report_processor.process(
15f218
         reports.service_start_started("quorum device")
15f218
diff --git a/pcs/lib/commands/quorum.py b/pcs/lib/commands/quorum.py
15f218
index 7fb7bb4..8390fc6 100644
15f218
--- a/pcs/lib/commands/quorum.py
15f218
+++ b/pcs/lib/commands/quorum.py
15f218
@@ -283,14 +283,23 @@ def remove_device(lib_env, skip_offline_nodes=False):
15f218
     cfg = lib_env.get_corosync_conf()
15f218
     model, dummy_options, dummy_options = cfg.get_quorum_device_settings()
15f218
     cfg.remove_quorum_device()
15f218
+
15f218
+    if lib_env.is_corosync_conf_live:
15f218
+        # fix quorum options for SBD to work properly
15f218
+        if sbd.atb_has_to_be_enabled(lib_env.cmd_runner(), cfg):
15f218
+            lib_env.report_processor.process(reports.sbd_requires_atb())
15f218
+            cfg.set_quorum_options(
15f218
+                lib_env.report_processor, {"auto_tie_breaker": "1"}
15f218
+            )
15f218
+
15f218
     lib_env.push_corosync_conf(cfg, skip_offline_nodes)
15f218
 
15f218
     if lib_env.is_corosync_conf_live:
15f218
+        communicator = lib_env.node_communicator()
15f218
         # disable qdevice
15f218
         lib_env.report_processor.process(
15f218
             reports.service_disable_started("corosync-qdevice")
15f218
         )
15f218
-        communicator = lib_env.node_communicator()
15f218
         parallel_nodes_communication_helper(
15f218
             qdevice_client.remote_client_disable,
15f218
             [
15f218
@@ -304,7 +313,6 @@ def remove_device(lib_env, skip_offline_nodes=False):
15f218
         lib_env.report_processor.process(
15f218
             reports.service_stop_started("corosync-qdevice")
15f218
         )
15f218
-        communicator = lib_env.node_communicator()
15f218
         parallel_nodes_communication_helper(
15f218
             qdevice_client.remote_client_stop,
15f218
             [
15f218
diff --git a/pcs/lib/commands/test/test_booth.py b/pcs/lib/commands/test/test_booth.py
15f218
index 08d2c79..6bcab2b 100644
15f218
--- a/pcs/lib/commands/test/test_booth.py
15f218
+++ b/pcs/lib/commands/test/test_booth.py
15f218
@@ -520,7 +520,7 @@ class TicketOperationTest(TestCase):
15f218
         )
15f218
 
15f218
     def test_raises_when_command_fail(self):
15f218
-        mock_run = mock.Mock(return_value=("some message", 1))
15f218
+        mock_run = mock.Mock(return_value=("some message", "error", 1))
15f218
         mock_env = mock.MagicMock(
15f218
             cmd_runner=mock.Mock(return_value=mock.MagicMock(run=mock_run))
15f218
         )
15f218
@@ -533,7 +533,7 @@ class TicketOperationTest(TestCase):
15f218
                 report_codes.BOOTH_TICKET_OPERATION_FAILED,
15f218
                 {
15f218
                     "operation": "grant",
15f218
-                    "reason": "some message",
15f218
+                    "reason": "error\nsome message",
15f218
                     "site_ip": "1.2.3.4",
15f218
                     "ticket_name": "ABC",
15f218
                 }
15f218
diff --git a/pcs/lib/corosync/live.py b/pcs/lib/corosync/live.py
15f218
index 1e68c31..67aa0e4 100644
15f218
--- a/pcs/lib/corosync/live.py
15f218
+++ b/pcs/lib/corosync/live.py
15f218
@@ -8,6 +8,7 @@ from __future__ import (
15f218
 import os.path
15f218
 
15f218
 from pcs import settings
15f218
+from pcs.common.tools import join_multilines
15f218
 from pcs.lib import reports
15f218
 from pcs.lib.errors import LibraryError
15f218
 from pcs.lib.external import NodeCommunicator
15f218
@@ -41,42 +42,39 @@ def reload_config(runner):
15f218
     """
15f218
     Ask corosync to reload its configuration
15f218
     """
15f218
-    output, retval = runner.run([
15f218
+    stdout, stderr, retval = runner.run([
15f218
         os.path.join(settings.corosync_binaries, "corosync-cfgtool"),
15f218
         "-R"
15f218
     ])
15f218
-    if retval != 0 or "invalid option" in output:
15f218
-        raise LibraryError(
15f218
-            reports.corosync_config_reload_error(output.rstrip())
15f218
-        )
15f218
+    message = join_multilines([stderr, stdout])
15f218
+    if retval != 0 or "invalid option" in message:
15f218
+        raise LibraryError(reports.corosync_config_reload_error(message))
15f218
 
15f218
 def get_quorum_status_text(runner):
15f218
     """
15f218
     Get runtime quorum status from the local node
15f218
     """
15f218
-    output, retval = runner.run([
15f218
+    stdout, stderr, retval = runner.run([
15f218
         os.path.join(settings.corosync_binaries, "corosync-quorumtool"),
15f218
         "-p"
15f218
     ])
15f218
     # retval is 0 on success if node is not in partition with quorum
15f218
     # retval is 1 on error OR on success if node has quorum
15f218
-    if retval not in [0, 1]:
15f218
-        raise LibraryError(
15f218
-            reports.corosync_quorum_get_status_error(output)
15f218
-        )
15f218
-    return output
15f218
+    if retval not in [0, 1] or stderr.strip():
15f218
+        raise LibraryError(reports.corosync_quorum_get_status_error(stderr))
15f218
+    return stdout
15f218
 
15f218
 def set_expected_votes(runner, votes):
15f218
     """
15f218
     set expected votes in live cluster to specified value
15f218
     """
15f218
-    output, retval = runner.run([
15f218
+    stdout, stderr, retval = runner.run([
15f218
         os.path.join(settings.corosync_binaries, "corosync-quorumtool"),
15f218
         # format votes to handle the case where they are int
15f218
         "-e", "{0}".format(votes)
15f218
     ])
15f218
     if retval != 0:
15f218
         raise LibraryError(
15f218
-            reports.corosync_quorum_set_expected_votes_error(output)
15f218
+            reports.corosync_quorum_set_expected_votes_error(stderr)
15f218
         )
15f218
-    return output
15f218
+    return stdout
15f218
diff --git a/pcs/lib/corosync/qdevice_client.py b/pcs/lib/corosync/qdevice_client.py
15f218
index 98fbb0e..c9d0095 100644
15f218
--- a/pcs/lib/corosync/qdevice_client.py
15f218
+++ b/pcs/lib/corosync/qdevice_client.py
15f218
@@ -8,6 +8,7 @@ from __future__ import (
15f218
 import os.path
15f218
 
15f218
 from pcs import settings
15f218
+from pcs.common.tools import join_multilines
15f218
 from pcs.lib import reports
15f218
 from pcs.lib.errors import LibraryError
15f218
 
15f218
@@ -23,12 +24,14 @@ def get_status_text(runner, verbose=False):
15f218
     ]
15f218
     if verbose:
15f218
         cmd.append("-v")
15f218
-    output, retval = runner.run(cmd)
15f218
+    stdout, stderr, retval = runner.run(cmd)
15f218
     if retval != 0:
15f218
         raise LibraryError(
15f218
-            reports.corosync_quorum_get_status_error(output)
15f218
+            reports.corosync_quorum_get_status_error(
15f218
+                join_multilines([stderr, stdout])
15f218
+            )
15f218
         )
15f218
-    return output
15f218
+    return stdout
15f218
 
15f218
 def remote_client_enable(reporter, node_communicator, node):
15f218
     """
15f218
diff --git a/pcs/lib/corosync/qdevice_net.py b/pcs/lib/corosync/qdevice_net.py
15f218
index 4054592..200e45a 100644
15f218
--- a/pcs/lib/corosync/qdevice_net.py
15f218
+++ b/pcs/lib/corosync/qdevice_net.py
15f218
@@ -15,6 +15,7 @@ import shutil
15f218
 import tempfile
15f218
 
15f218
 from pcs import settings
15f218
+from pcs.common.tools import join_multilines
15f218
 from pcs.lib import external, reports
15f218
 from pcs.lib.errors import LibraryError
15f218
 
15f218
@@ -41,12 +42,15 @@ def qdevice_setup(runner):
15f218
     if external.is_dir_nonempty(settings.corosync_qdevice_net_server_certs_dir):
15f218
         raise LibraryError(reports.qdevice_already_initialized(__model))
15f218
 
15f218
-    output, retval = runner.run([
15f218
+    stdout, stderr, retval = runner.run([
15f218
         __qnetd_certutil, "-i"
15f218
     ])
15f218
     if retval != 0:
15f218
         raise LibraryError(
15f218
-            reports.qdevice_initialization_error(__model, output.rstrip())
15f218
+            reports.qdevice_initialization_error(
15f218
+                __model,
15f218
+                join_multilines([stderr, stdout])
15f218
+            )
15f218
         )
15f218
 
15f218
 def qdevice_initialized():
15f218
@@ -78,10 +82,15 @@ def qdevice_status_generic_text(runner, verbose=False):
15f218
     cmd = [__qnetd_tool, "-s"]
15f218
     if verbose:
15f218
         cmd.append("-v")
15f218
-    output, retval = runner.run(cmd)
15f218
+    stdout, stderr, retval = runner.run(cmd)
15f218
     if retval != 0:
15f218
-        raise LibraryError(reports.qdevice_get_status_error(__model, output))
15f218
-    return output
15f218
+        raise LibraryError(
15f218
+            reports.qdevice_get_status_error(
15f218
+                __model,
15f218
+                join_multilines([stderr, stdout])
15f218
+            )
15f218
+        )
15f218
+    return stdout
15f218
 
15f218
 def qdevice_status_cluster_text(runner, cluster=None, verbose=False):
15f218
     """
15f218
@@ -94,10 +103,24 @@ def qdevice_status_cluster_text(runner, cluster=None, verbose=False):
15f218
         cmd.append("-v")
15f218
     if cluster:
15f218
         cmd.extend(["-c", cluster])
15f218
-    output, retval = runner.run(cmd)
15f218
+    stdout, stderr, retval = runner.run(cmd)
15f218
     if retval != 0:
15f218
-        raise LibraryError(reports.qdevice_get_status_error(__model, output))
15f218
-    return output
15f218
+        raise LibraryError(
15f218
+            reports.qdevice_get_status_error(
15f218
+                __model,
15f218
+                join_multilines([stderr, stdout])
15f218
+            )
15f218
+        )
15f218
+    return stdout
15f218
+
15f218
+def qdevice_connected_clusters(status_cluster_text):
15f218
+    connected_clusters = []
15f218
+    regexp = re.compile(r'^Cluster "(?P<cluster>[^"]+)":$')
15f218
+    for line in status_cluster_text.splitlines():
15f218
+        match = regexp.search(line)
15f218
+        if match:
15f218
+            connected_clusters.append(match.group("cluster"))
15f218
+    return connected_clusters
15f218
 
15f218
 def qdevice_enable(runner):
15f218
     """
15f218
@@ -143,17 +166,19 @@ def qdevice_sign_certificate_request(runner, cert_request, cluster_name):
15f218
         reports.qdevice_certificate_sign_error
15f218
     )
15f218
     # sign the request
15f218
-    output, retval = runner.run([
15f218
+    stdout, stderr, retval = runner.run([
15f218
         __qnetd_certutil, "-s", "-c", tmpfile.name, "-n", cluster_name
15f218
     ])
15f218
     tmpfile.close() # temp file is deleted on close
15f218
     if retval != 0:
15f218
         raise LibraryError(
15f218
-            reports.qdevice_certificate_sign_error(output.strip())
15f218
+            reports.qdevice_certificate_sign_error(
15f218
+                join_multilines([stderr, stdout])
15f218
+            )
15f218
         )
15f218
     # get signed certificate, corosync tool only works with files
15f218
     return _get_output_certificate(
15f218
-        output,
15f218
+        stdout,
15f218
         reports.qdevice_certificate_sign_error
15f218
     )
15f218
 
15f218
@@ -181,12 +206,15 @@ def client_setup(runner, ca_certificate):
15f218
             reports.qdevice_initialization_error(__model, e.strerror)
15f218
         )
15f218
     # initialize client's certificate storage
15f218
-    output, retval = runner.run([
15f218
+    stdout, stderr, retval = runner.run([
15f218
         __qdevice_certutil, "-i", "-c", ca_file_path
15f218
     ])
15f218
     if retval != 0:
15f218
         raise LibraryError(
15f218
-            reports.qdevice_initialization_error(__model, output.rstrip())
15f218
+            reports.qdevice_initialization_error(
15f218
+                __model,
15f218
+                join_multilines([stderr, stdout])
15f218
+            )
15f218
         )
15f218
 
15f218
 def client_initialized():
15f218
@@ -217,15 +245,18 @@ def client_generate_certificate_request(runner, cluster_name):
15f218
     """
15f218
     if not client_initialized():
15f218
         raise LibraryError(reports.qdevice_not_initialized(__model))
15f218
-    output, retval = runner.run([
15f218
+    stdout, stderr, retval = runner.run([
15f218
         __qdevice_certutil, "-r", "-n", cluster_name
15f218
     ])
15f218
     if retval != 0:
15f218
         raise LibraryError(
15f218
-            reports.qdevice_initialization_error(__model, output.rstrip())
15f218
+            reports.qdevice_initialization_error(
15f218
+                __model,
15f218
+                join_multilines([stderr, stdout])
15f218
+            )
15f218
         )
15f218
     return _get_output_certificate(
15f218
-        output,
15f218
+        stdout,
15f218
         functools.partial(reports.qdevice_initialization_error, __model)
15f218
     )
15f218
 
15f218
@@ -243,17 +274,19 @@ def client_cert_request_to_pk12(runner, cert_request):
15f218
         reports.qdevice_certificate_import_error
15f218
     )
15f218
     # transform it
15f218
-    output, retval = runner.run([
15f218
+    stdout, stderr, retval = runner.run([
15f218
         __qdevice_certutil, "-M", "-c", tmpfile.name
15f218
     ])
15f218
     tmpfile.close() # temp file is deleted on close
15f218
     if retval != 0:
15f218
         raise LibraryError(
15f218
-            reports.qdevice_certificate_import_error(output)
15f218
+            reports.qdevice_certificate_import_error(
15f218
+                join_multilines([stderr, stdout])
15f218
+            )
15f218
         )
15f218
     # get resulting pk12, corosync tool only works with files
15f218
     return _get_output_certificate(
15f218
-        output,
15f218
+        stdout,
15f218
         reports.qdevice_certificate_import_error
15f218
     )
15f218
 
15f218
@@ -268,13 +301,15 @@ def client_import_certificate_and_key(runner, pk12_certificate):
15f218
         pk12_certificate,
15f218
         reports.qdevice_certificate_import_error
15f218
     )
15f218
-    output, retval = runner.run([
15f218
+    stdout, stderr, retval = runner.run([
15f218
         __qdevice_certutil, "-m", "-c", tmpfile.name
15f218
     ])
15f218
     tmpfile.close() # temp file is deleted on close
15f218
     if retval != 0:
15f218
         raise LibraryError(
15f218
-            reports.qdevice_certificate_import_error(output)
15f218
+            reports.qdevice_certificate_import_error(
15f218
+                join_multilines([stderr, stdout])
15f218
+            )
15f218
         )
15f218
 
15f218
 def remote_qdevice_get_ca_certificate(node_communicator, host):
15f218
diff --git a/pcs/lib/external.py b/pcs/lib/external.py
15f218
index 08bf2bb..074d2aa 100644
15f218
--- a/pcs/lib/external.py
15f218
+++ b/pcs/lib/external.py
15f218
@@ -47,14 +47,15 @@ except ImportError:
15f218
         URLError as urllib_URLError
15f218
     )
15f218
 
15f218
-from pcs.lib import reports
15f218
-from pcs.lib.errors import LibraryError, ReportItemSeverity
15f218
+from pcs import settings
15f218
 from pcs.common import report_codes
15f218
 from pcs.common.tools import (
15f218
+    join_multilines,
15f218
     simple_cache,
15f218
     run_parallel as tools_run_parallel,
15f218
 )
15f218
-from pcs import settings
15f218
+from pcs.lib import reports
15f218
+from pcs.lib.errors import LibraryError, ReportItemSeverity
15f218
 
15f218
 
15f218
 class ManageServiceError(Exception):
15f218
@@ -138,13 +139,17 @@ def disable_service(runner, service, instance=None):
15f218
     if not is_service_installed(runner, service):
15f218
         return
15f218
     if is_systemctl():
15f218
-        output, retval = runner.run([
15f218
+        stdout, stderr, retval = runner.run([
15f218
             "systemctl", "disable", _get_service_name(service, instance)
15f218
         ])
15f218
     else:
15f218
-        output, retval = runner.run(["chkconfig", service, "off"])
15f218
+        stdout, stderr, retval = runner.run(["chkconfig", service, "off"])
15f218
     if retval != 0:
15f218
-        raise DisableServiceError(service, output.rstrip(), instance)
15f218
+        raise DisableServiceError(
15f218
+            service,
15f218
+            join_multilines([stderr, stdout]),
15f218
+            instance
15f218
+        )
15f218
 
15f218
 
15f218
 def enable_service(runner, service, instance=None):
15f218
@@ -158,13 +163,17 @@ def enable_service(runner, service, instance=None):
15f218
         If None no instance name will be used.
15f218
     """
15f218
     if is_systemctl():
15f218
-        output, retval = runner.run([
15f218
+        stdout, stderr, retval = runner.run([
15f218
             "systemctl", "enable", _get_service_name(service, instance)
15f218
         ])
15f218
     else:
15f218
-        output, retval = runner.run(["chkconfig", service, "on"])
15f218
+        stdout, stderr, retval = runner.run(["chkconfig", service, "on"])
15f218
     if retval != 0:
15f218
-        raise EnableServiceError(service, output.rstrip(), instance)
15f218
+        raise EnableServiceError(
15f218
+            service,
15f218
+            join_multilines([stderr, stdout]),
15f218
+            instance
15f218
+        )
15f218
 
15f218
 
15f218
 def start_service(runner, service, instance=None):
15f218
@@ -176,13 +185,17 @@ def start_service(runner, service, instance=None):
15f218
         If None no instance name will be used.
15f218
     """
15f218
     if is_systemctl():
15f218
-        output, retval = runner.run([
15f218
+        stdout, stderr, retval = runner.run([
15f218
             "systemctl", "start", _get_service_name(service, instance)
15f218
         ])
15f218
     else:
15f218
-        output, retval = runner.run(["service", service, "start"])
15f218
+        stdout, stderr, retval = runner.run(["service", service, "start"])
15f218
     if retval != 0:
15f218
-        raise StartServiceError(service, output.rstrip(), instance)
15f218
+        raise StartServiceError(
15f218
+            service,
15f218
+            join_multilines([stderr, stdout]),
15f218
+            instance
15f218
+        )
15f218
 
15f218
 
15f218
 def stop_service(runner, service, instance=None):
15f218
@@ -194,13 +207,17 @@ def stop_service(runner, service, instance=None):
15f218
         If None no instance name will be used.
15f218
     """
15f218
     if is_systemctl():
15f218
-        output, retval = runner.run([
15f218
+        stdout, stderr, retval = runner.run([
15f218
             "systemctl", "stop", _get_service_name(service, instance)
15f218
         ])
15f218
     else:
15f218
-        output, retval = runner.run(["service", service, "stop"])
15f218
+        stdout, stderr, retval = runner.run(["service", service, "stop"])
15f218
     if retval != 0:
15f218
-        raise StopServiceError(service, output.rstrip(), instance)
15f218
+        raise StopServiceError(
15f218
+            service,
15f218
+            join_multilines([stderr, stdout]),
15f218
+            instance
15f218
+        )
15f218
 
15f218
 
15f218
 def kill_services(runner, services):
15f218
@@ -210,15 +227,16 @@ def kill_services(runner, services):
15f218
     iterable services service names
15f218
     """
15f218
     # make killall not report that a process is not running
15f218
-    output, retval = runner.run(
15f218
+    stdout, stderr, retval = runner.run(
15f218
         ["killall", "--quiet", "--signal", "9", "--"] + list(services)
15f218
     )
15f218
     # If a process isn't running, killall will still return 1 even with --quiet.
15f218
     # We don't consider that an error, so we check for output string as well.
15f218
     # If it's empty, no actuall error happened.
15f218
     if retval != 0:
15f218
-        if output.strip():
15f218
-            raise KillServicesError(list(services), output.rstrip())
15f218
+        message = join_multilines([stderr, stdout])
15f218
+        if message:
15f218
+            raise KillServicesError(list(services), message)
15f218
 
15f218
 
15f218
 def is_service_enabled(runner, service, instance=None):
15f218
@@ -229,11 +247,11 @@ def is_service_enabled(runner, service, instance=None):
15f218
     service -- name of service
15f218
     """
15f218
     if is_systemctl():
15f218
-        _, retval = runner.run(
15f218
+        dummy_stdout, dummy_stderr, retval = runner.run(
15f218
             ["systemctl", "is-enabled", _get_service_name(service, instance)]
15f218
         )
15f218
     else:
15f218
-        _, retval = runner.run(["chkconfig", service])
15f218
+        dummy_stdout, dummy_stderr, retval = runner.run(["chkconfig", service])
15f218
 
15f218
     return retval == 0
15f218
 
15f218
@@ -246,13 +264,15 @@ def is_service_running(runner, service, instance=None):
15f218
     service -- name of service
15f218
     """
15f218
     if is_systemctl():
15f218
-        _, retval = runner.run([
15f218
+        dummy_stdout, dummy_stderr, retval = runner.run([
15f218
             "systemctl",
15f218
             "is-active",
15f218
             _get_service_name(service, instance)
15f218
         ])
15f218
     else:
15f218
-        _, retval = runner.run(["service", service, "status"])
15f218
+        dummy_stdout, dummy_stderr, retval = runner.run(
15f218
+            ["service", service, "status"]
15f218
+        )
15f218
 
15f218
     return retval == 0
15f218
 
15f218
@@ -279,12 +299,12 @@ def get_non_systemd_services(runner):
15f218
     if is_systemctl():
15f218
         return []
15f218
 
15f218
-    output, return_code = runner.run(["chkconfig"], ignore_stderr=True)
15f218
+    stdout, dummy_stderr, return_code = runner.run(["chkconfig"])
15f218
     if return_code != 0:
15f218
         return []
15f218
 
15f218
     service_list = []
15f218
-    for service in output.splitlines():
15f218
+    for service in stdout.splitlines():
15f218
         service = service.split(" ", 1)[0]
15f218
         if service:
15f218
             service_list.append(service)
15f218
@@ -300,12 +320,14 @@ def get_systemd_services(runner):
15f218
     if not is_systemctl():
15f218
         return []
15f218
 
15f218
-    output, return_code = runner.run(["systemctl", "list-unit-files", "--full"])
15f218
+    stdout, dummy_stderr, return_code = runner.run([
15f218
+        "systemctl", "list-unit-files", "--full"
15f218
+    ])
15f218
     if return_code != 0:
15f218
         return []
15f218
 
15f218
     service_list = []
15f218
-    for service in output.splitlines():
15f218
+    for service in stdout.splitlines():
15f218
         match = re.search(r'^([\S]*)\.service', service)
15f218
         if match:
15f218
             service_list.append(match.group(1))
15f218
@@ -322,13 +344,13 @@ def is_cman_cluster(runner):
15f218
     # - corosync1 runs with cman on rhel6
15f218
     # - corosync1 can be used without cman, but we don't support it anyways
15f218
     # - corosync2 is the default result if errors occur
15f218
-    output, retval = runner.run([
15f218
+    stdout, dummy_stderr, retval = runner.run([
15f218
         os.path.join(settings.corosync_binaries, "corosync"),
15f218
         "-v"
15f218
     ])
15f218
     if retval != 0:
15f218
         return False
15f218
-    match = re.search(r"version\D+(\d+)", output)
15f218
+    match = re.search(r"version\D+(\d+)", stdout)
15f218
     return match is not None and match.group(1) == "1"
15f218
 
15f218
 
15f218
@@ -340,8 +362,7 @@ class CommandRunner(object):
15f218
         self._python2 = sys.version[0] == "2"
15f218
 
15f218
     def run(
15f218
-        self, args, ignore_stderr=False, stdin_string=None, env_extend=None,
15f218
-        binary_output=False
15f218
+        self, args, stdin_string=None, env_extend=None, binary_output=False
15f218
     ):
15f218
         #Reset environment variables by empty dict is desired here.  We need to
15f218
         #get rid of defaults - we do not know the context and environment of the
15f218
@@ -364,9 +385,7 @@ class CommandRunner(object):
15f218
                 # Some commands react differently if they get anything via stdin
15f218
                 stdin=(subprocess.PIPE if stdin_string is not None else None),
15f218
                 stdout=subprocess.PIPE,
15f218
-                stderr=(
15f218
-                    subprocess.PIPE if ignore_stderr else subprocess.STDOUT
15f218
-                ),
15f218
+                stderr=subprocess.PIPE,
15f218
                 preexec_fn=(
15f218
                     lambda: signal.signal(signal.SIGPIPE, signal.SIG_DFL)
15f218
                 ),
15f218
@@ -376,7 +395,7 @@ class CommandRunner(object):
15f218
                 # decodes newlines and in python3 also converts bytes to str
15f218
                 universal_newlines=(not self._python2 and not binary_output)
15f218
             )
15f218
-            output, dummy_stderror = process.communicate(stdin_string)
15f218
+            out_std, out_err = process.communicate(stdin_string)
15f218
             retval = process.returncode
15f218
         except OSError as e:
15f218
             raise LibraryError(
15f218
@@ -386,13 +405,19 @@ class CommandRunner(object):
15f218
         self._logger.debug(
15f218
             (
15f218
                 "Finished running: {args}\nReturn value: {retval}"
15f218
-                + "\n--Debug Output Start--\n{output}\n--Debug Output End--"
15f218
-            ).format(args=log_args, retval=retval, output=output)
15f218
-        )
15f218
-        self._reporter.process(
15f218
-            reports.run_external_process_finished(log_args, retval, output)
15f218
+                + "\n--Debug Stdout Start--\n{out_std}\n--Debug Stdout End--"
15f218
+                + "\n--Debug Stderr Start--\n{out_err}\n--Debug Stderr End--"
15f218
+            ).format(
15f218
+                args=log_args,
15f218
+                retval=retval,
15f218
+                out_std=out_std,
15f218
+                out_err=out_err
15f218
+            )
15f218
         )
15f218
-        return output, retval
15f218
+        self._reporter.process(reports.run_external_process_finished(
15f218
+            log_args, retval, out_std, out_err
15f218
+        ))
15f218
+        return out_std, out_err, retval
15f218
 
15f218
 
15f218
 class NodeCommunicationException(Exception):
15f218
diff --git a/pcs/lib/pacemaker.py b/pcs/lib/pacemaker.py
15f218
index fd6f97b..6747b22 100644
15f218
--- a/pcs/lib/pacemaker.py
15f218
+++ b/pcs/lib/pacemaker.py
15f218
@@ -9,6 +9,7 @@ import os.path
15f218
 from lxml import etree
15f218
 
15f218
 from pcs import settings
15f218
+from pcs.common.tools import join_multilines
15f218
 from pcs.lib import reports
15f218
 from pcs.lib.errors import LibraryError
15f218
 from pcs.lib.pacemaker_state import ClusterState
15f218
@@ -26,28 +27,33 @@ def __exec(name):
15f218
     return os.path.join(settings.pacemaker_binaries, name)
15f218
 
15f218
 def get_cluster_status_xml(runner):
15f218
-    output, retval = runner.run(
15f218
+    stdout, stderr, retval = runner.run(
15f218
         [__exec("crm_mon"), "--one-shot", "--as-xml", "--inactive"]
15f218
     )
15f218
     if retval != 0:
15f218
         raise CrmMonErrorException(
15f218
-            reports.cluster_state_cannot_load(retval, output)
15f218
+            reports.cluster_state_cannot_load(join_multilines([stderr, stdout]))
15f218
         )
15f218
-    return output
15f218
+    return stdout
15f218
 
15f218
 def get_cib_xml(runner, scope=None):
15f218
     command = [__exec("cibadmin"), "--local", "--query"]
15f218
     if scope:
15f218
         command.append("--scope={0}".format(scope))
15f218
-    output, retval = runner.run(command)
15f218
+    stdout, stderr, retval = runner.run(command)
15f218
     if retval != 0:
15f218
         if retval == __EXITCODE_CIB_SCOPE_VALID_BUT_NOT_PRESENT and scope:
15f218
             raise LibraryError(
15f218
-                reports.cib_load_error_scope_missing(scope, retval, output)
15f218
+                reports.cib_load_error_scope_missing(
15f218
+                    scope,
15f218
+                    join_multilines([stderr, stdout])
15f218
+                )
15f218
             )
15f218
         else:
15f218
-            raise LibraryError(reports.cib_load_error(retval, output))
15f218
-    return output
15f218
+            raise LibraryError(
15f218
+                reports.cib_load_error(join_multilines([stderr, stdout]))
15f218
+            )
15f218
+    return stdout
15f218
 
15f218
 def get_cib(xml):
15f218
     try:
15f218
@@ -59,9 +65,9 @@ def replace_cib_configuration_xml(runner, xml, cib_upgraded=False):
15f218
     cmd = [__exec("cibadmin"), "--replace",  "--verbose", "--xml-pipe"]
15f218
     if not cib_upgraded:
15f218
         cmd += ["--scope", "configuration"]
15f218
-    output, retval = runner.run(cmd, stdin_string=xml)
15f218
+    stdout, stderr, retval = runner.run(cmd, stdin_string=xml)
15f218
     if retval != 0:
15f218
-        raise LibraryError(reports.cib_push_error(retval, output))
15f218
+        raise LibraryError(reports.cib_push_error(stderr, stdout))
15f218
 
15f218
 def replace_cib_configuration(runner, tree, cib_upgraded=False):
15f218
     #etree returns bytes: b'xml'
15f218
@@ -108,13 +114,18 @@ def resource_cleanup(runner, resource=None, node=None, force=False):
15f218
     if node:
15f218
         cmd.extend(["--node", node])
15f218
 
15f218
-    output, retval = runner.run(cmd)
15f218
+    stdout, stderr, retval = runner.run(cmd)
15f218
 
15f218
     if retval != 0:
15f218
         raise LibraryError(
15f218
-            reports.resource_cleanup_error(retval, output, resource, node)
15f218
+            reports.resource_cleanup_error(
15f218
+                join_multilines([stderr, stdout]),
15f218
+                resource,
15f218
+                node
15f218
+            )
15f218
         )
15f218
-    return output
15f218
+    # usefull output (what has been done) goes to stderr
15f218
+    return join_multilines([stdout, stderr])
15f218
 
15f218
 def nodes_standby(runner, node_list=None, all_nodes=False):
15f218
     return __nodes_standby_unstandby(runner, True, node_list, all_nodes)
15f218
@@ -124,8 +135,11 @@ def nodes_unstandby(runner, node_list=None, all_nodes=False):
15f218
 
15f218
 def has_resource_wait_support(runner):
15f218
     # returns 1 on success so we don't care about retval
15f218
-    output, dummy_retval = runner.run([__exec("crm_resource"), "-?"])
15f218
-    return "--wait" in output
15f218
+    stdout, stderr, dummy_retval = runner.run(
15f218
+        [__exec("crm_resource"), "-?"]
15f218
+    )
15f218
+    # help goes to stderr but we check stdout as well if that gets changed
15f218
+    return "--wait" in stderr or "--wait" in stdout
15f218
 
15f218
 def ensure_resource_wait_support(runner):
15f218
     if not has_resource_wait_support(runner):
15f218
@@ -135,15 +149,22 @@ def wait_for_resources(runner, timeout=None):
15f218
     args = [__exec("crm_resource"), "--wait"]
15f218
     if timeout is not None:
15f218
         args.append("--timeout={0}".format(timeout))
15f218
-    output, retval = runner.run(args)
15f218
+    stdout, stderr, retval = runner.run(args)
15f218
     if retval != 0:
15f218
+        # Usefull info goes to stderr - not only error messages, a list of
15f218
+        # pending actions in case of timeout goes there as well.
15f218
+        # We use stdout just to be sure if that's get changed.
15f218
         if retval == __EXITCODE_WAIT_TIMEOUT:
15f218
             raise LibraryError(
15f218
-                reports.resource_wait_timed_out(retval, output.strip())
15f218
+                reports.resource_wait_timed_out(
15f218
+                    join_multilines([stderr, stdout])
15f218
+                )
15f218
             )
15f218
         else:
15f218
             raise LibraryError(
15f218
-                reports.resource_wait_error(retval, output.strip())
15f218
+                reports.resource_wait_error(
15f218
+                    join_multilines([stderr, stdout])
15f218
+                )
15f218
             )
15f218
 
15f218
 def __nodes_standby_unstandby(
15f218
@@ -178,9 +199,11 @@ def __nodes_standby_unstandby(
15f218
         cmd_list.append(cmd_template)
15f218
     report = []
15f218
     for cmd in cmd_list:
15f218
-        output, retval = runner.run(cmd)
15f218
+        stdout, stderr, retval = runner.run(cmd)
15f218
         if retval != 0:
15f218
-            report.append(reports.common_error(output))
15f218
+            report.append(
15f218
+                reports.common_error(join_multilines([stderr, stdout]))
15f218
+            )
15f218
     if report:
15f218
         raise LibraryError(*report)
15f218
 
15f218
@@ -189,21 +212,23 @@ def __get_local_node_name(runner):
15f218
     # but it returns false names when cluster is not running (or we are on
15f218
     # a remote node). Getting node id first is reliable since it fails in those
15f218
     # cases.
15f218
-    output, retval = runner.run([__exec("crm_node"), "--cluster-id"])
15f218
+    stdout, dummy_stderr, retval = runner.run(
15f218
+        [__exec("crm_node"), "--cluster-id"]
15f218
+    )
15f218
     if retval != 0:
15f218
         raise LibraryError(
15f218
             reports.pacemaker_local_node_name_not_found("node id not found")
15f218
         )
15f218
-    node_id = output.strip()
15f218
+    node_id = stdout.strip()
15f218
 
15f218
-    output, retval = runner.run(
15f218
+    stdout, dummy_stderr, retval = runner.run(
15f218
         [__exec("crm_node"), "--name-for-id={0}".format(node_id)]
15f218
     )
15f218
     if retval != 0:
15f218
         raise LibraryError(
15f218
             reports.pacemaker_local_node_name_not_found("node name not found")
15f218
         )
15f218
-    node_name = output.strip()
15f218
+    node_name = stdout.strip()
15f218
 
15f218
     if node_name == "(null)":
15f218
         raise LibraryError(
15f218
diff --git a/pcs/lib/reports.py b/pcs/lib/reports.py
15f218
index a701679..b9e9a66 100644
15f218
--- a/pcs/lib/reports.py
15f218
+++ b/pcs/lib/reports.py
15f218
@@ -262,21 +262,24 @@ def run_external_process_started(command, stdin):
15f218
         }
15f218
     )
15f218
 
15f218
-def run_external_process_finished(command, retval, stdout):
15f218
+def run_external_process_finished(command, retval, stdout, stderr):
15f218
     """
15f218
     information about result of running an external process
15f218
     command string the external process command
15f218
     retval external process's return (exit) code
15f218
     stdout string external process's stdout
15f218
+    stderr string external process's stderr
15f218
     """
15f218
     return ReportItem.debug(
15f218
         report_codes.RUN_EXTERNAL_PROCESS_FINISHED,
15f218
         "Finished running: {command}\nReturn value: {return_value}"
15f218
-        + "\n--Debug Output Start--\n{stdout}\n--Debug Output End--\n",
15f218
+        + "\n--Debug Stdout Start--\n{stdout}\n--Debug Stdout End--"
15f218
+        + "\n--Debug Stderr Start--\n{stderr}\n--Debug Stderr End--\n",
15f218
         info={
15f218
             "command": command,
15f218
             "return_value": retval,
15f218
             "stdout": stdout,
15f218
+            "stderr": stderr,
15f218
         }
15f218
     )
15f218
 
15f218
@@ -854,6 +857,23 @@ def qdevice_get_status_error(model, reason):
15f218
         }
15f218
     )
15f218
 
15f218
+def qdevice_used_by_clusters(
15f218
+    clusters, severity=ReportItemSeverity.ERROR, forceable=None
15f218
+):
15f218
+    """
15f218
+    Qdevice is currently being used by clusters, cannot stop it unless forced
15f218
+    """
15f218
+    return ReportItem(
15f218
+        report_codes.QDEVICE_USED_BY_CLUSTERS,
15f218
+        severity,
15f218
+        "Quorum device is currently being used by cluster(s): {clusters_str}",
15f218
+        info={
15f218
+            "clusters": clusters,
15f218
+            "clusters_str": ", ".join(clusters),
15f218
+        },
15f218
+        forceable=forceable
15f218
+    )
15f218
+
15f218
 def cman_unsupported_command():
15f218
     """
15f218
     requested library command is not available as local cluster is CMAN based
15f218
@@ -903,35 +923,31 @@ def resource_does_not_exist(resource_id):
15f218
         }
15f218
     )
15f218
 
15f218
-def cib_load_error(retval, stdout):
15f218
+def cib_load_error(reason):
15f218
     """
15f218
     cannot load cib from cibadmin, cibadmin exited with non-zero code
15f218
-    retval external process's return (exit) code
15f218
-    stdout string external process's stdout
15f218
+    string reason error description
15f218
     """
15f218
     return ReportItem.error(
15f218
         report_codes.CIB_LOAD_ERROR,
15f218
         "unable to get cib",
15f218
         info={
15f218
-            "return_value": retval,
15f218
-            "stdout": stdout,
15f218
+            "reason": reason,
15f218
         }
15f218
     )
15f218
 
15f218
-def cib_load_error_scope_missing(scope, retval, stdout):
15f218
+def cib_load_error_scope_missing(scope, reason):
15f218
     """
15f218
     cannot load cib from cibadmin, specified scope is missing in the cib
15f218
     scope string requested cib scope
15f218
-    retval external process's return (exit) code
15f218
-    stdout string external process's stdout
15f218
+    string reason error description
15f218
     """
15f218
     return ReportItem.error(
15f218
         report_codes.CIB_LOAD_ERROR_SCOPE_MISSING,
15f218
         "unable to get cib, scope '{scope}' not present in cib",
15f218
         info={
15f218
             "scope": scope,
15f218
-            "return_value": retval,
15f218
-            "stdout": stdout,
15f218
+            "reason": reason,
15f218
         }
15f218
     )
15f218
 
15f218
@@ -957,33 +973,31 @@ def cib_missing_mandatory_section(section_name):
15f218
         }
15f218
     )
15f218
 
15f218
-def cib_push_error(retval, stdout):
15f218
+def cib_push_error(reason, pushed_cib):
15f218
     """
15f218
     cannot push cib to cibadmin, cibadmin exited with non-zero code
15f218
-    retval external process's return (exit) code
15f218
-    stdout string external process's stdout
15f218
+    string reason error description
15f218
+    string pushed_cib cib which failed to be pushed
15f218
     """
15f218
     return ReportItem.error(
15f218
         report_codes.CIB_PUSH_ERROR,
15f218
-        "Unable to update cib\n{stdout}",
15f218
+        "Unable to update cib\n{reason}\n{pushed_cib}",
15f218
         info={
15f218
-            "return_value": retval,
15f218
-            "stdout": stdout,
15f218
+            "reason": reason,
15f218
+            "pushed_cib": pushed_cib,
15f218
         }
15f218
     )
15f218
 
15f218
-def cluster_state_cannot_load(retval, stdout):
15f218
+def cluster_state_cannot_load(reason):
15f218
     """
15f218
     cannot load cluster status from crm_mon, crm_mon exited with non-zero code
15f218
-    retval external process's return (exit) code
15f218
-    stdout string external process's stdout
15f218
+    string reason error description
15f218
     """
15f218
     return ReportItem.error(
15f218
         report_codes.CRM_MON_ERROR,
15f218
         "error running crm_mon, is pacemaker running?",
15f218
         info={
15f218
-            "return_value": retval,
15f218
-            "stdout": stdout,
15f218
+            "reason": reason,
15f218
         }
15f218
     )
15f218
 
15f218
@@ -1005,57 +1019,50 @@ def resource_wait_not_supported():
15f218
         "crm_resource does not support --wait, please upgrade pacemaker"
15f218
     )
15f218
 
15f218
-def resource_wait_timed_out(retval, stdout):
15f218
+def resource_wait_timed_out(reason):
15f218
     """
15f218
     waiting for resources (crm_resource --wait) failed, timeout expired
15f218
-    retval external process's return (exit) code
15f218
-    stdout string external process's stdout
15f218
+    string reason error description
15f218
     """
15f218
     return ReportItem.error(
15f218
         report_codes.RESOURCE_WAIT_TIMED_OUT,
15f218
-        "waiting timeout\n\n{stdout}",
15f218
+        "waiting timeout\n\n{reason}",
15f218
         info={
15f218
-            "return_value": retval,
15f218
-            "stdout": stdout,
15f218
+            "reason": reason,
15f218
         }
15f218
     )
15f218
 
15f218
-def resource_wait_error(retval, stdout):
15f218
+def resource_wait_error(reason):
15f218
     """
15f218
     waiting for resources (crm_resource --wait) failed
15f218
-    retval external process's return (exit) code
15f218
-    stdout string external process's stdout
15f218
+    string reason error description
15f218
     """
15f218
     return ReportItem.error(
15f218
         report_codes.RESOURCE_WAIT_ERROR,
15f218
-        "{stdout}",
15f218
+        "{reason}",
15f218
         info={
15f218
-            "return_value": retval,
15f218
-            "stdout": stdout,
15f218
+            "reason": reason,
15f218
         }
15f218
     )
15f218
 
15f218
-def resource_cleanup_error(retval, stdout, resource=None, node=None):
15f218
+def resource_cleanup_error(reason, resource=None, node=None):
15f218
     """
15f218
     an error occured when deleting resource history in pacemaker
15f218
-    retval external process's return (exit) code
15f218
-    stdout string external process's stdout
15f218
-    resource string resource which has been cleaned up
15f218
-    node string node which has been cleaned up
15f218
+    string reason error description
15f218
+    string resource resource which has been cleaned up
15f218
+    string node node which has been cleaned up
15f218
     """
15f218
     if resource:
15f218
-        text = "Unable to cleanup resource: {resource}\n{stdout}"
15f218
+        text = "Unable to cleanup resource: {resource}\n{reason}"
15f218
     else:
15f218
         text = (
15f218
-            "Unexpected error occured. 'crm_resource -C' err_code: "
15f218
-            + "{return_value}\n{stdout}"
15f218
+            "Unexpected error occured. 'crm_resource -C' error:\n{reason}"
15f218
         )
15f218
     return ReportItem.error(
15f218
         report_codes.RESOURCE_CLEANUP_ERROR,
15f218
         text,
15f218
         info={
15f218
-            "return_value": retval,
15f218
-            "stdout": stdout,
15f218
+            "reason": reason,
15f218
             "resource": resource,
15f218
             "node": node,
15f218
         }
15f218
diff --git a/pcs/lib/resource_agent.py b/pcs/lib/resource_agent.py
15f218
index ea93875..d49b5c0 100644
15f218
--- a/pcs/lib/resource_agent.py
15f218
+++ b/pcs/lib/resource_agent.py
15f218
@@ -125,14 +125,14 @@ def _get_pcmk_advanced_stonith_parameters(runner):
15f218
     """
15f218
     @simple_cache
15f218
     def __get_stonithd_parameters():
15f218
-        output, retval = runner.run(
15f218
-            [settings.stonithd_binary, "metadata"], ignore_stderr=True
15f218
+        stdout, stderr, dummy_retval = runner.run(
15f218
+            [settings.stonithd_binary, "metadata"]
15f218
         )
15f218
-        if output.strip() == "":
15f218
-            raise UnableToGetAgentMetadata("stonithd", output)
15f218
+        if stdout.strip() == "":
15f218
+            raise UnableToGetAgentMetadata("stonithd", stderr)
15f218
 
15f218
         try:
15f218
-            params = _get_agent_parameters(etree.fromstring(output))
15f218
+            params = _get_agent_parameters(etree.fromstring(stdout))
15f218
             for param in params:
15f218
                 param["longdesc"] = "{0}\n{1}".format(
15f218
                     param["shortdesc"], param["longdesc"]
15f218
@@ -166,15 +166,15 @@ def get_fence_agent_metadata(runner, fence_agent):
15f218
     ):
15f218
         raise AgentNotFound(fence_agent)
15f218
 
15f218
-    output, retval = runner.run(
15f218
-        [script_path, "-o", "metadata"], ignore_stderr=True
15f218
+    stdout, stderr, dummy_retval = runner.run(
15f218
+        [script_path, "-o", "metadata"]
15f218
     )
15f218
 
15f218
-    if output.strip() == "":
15f218
-        raise UnableToGetAgentMetadata(fence_agent, output)
15f218
+    if stdout.strip() == "":
15f218
+        raise UnableToGetAgentMetadata(fence_agent, stderr)
15f218
 
15f218
     try:
15f218
-        return etree.fromstring(output)
15f218
+        return etree.fromstring(stdout)
15f218
     except etree.XMLSyntaxError as e:
15f218
         raise UnableToGetAgentMetadata(fence_agent, str(e))
15f218
 
15f218
@@ -219,17 +219,16 @@ def _get_ocf_resource_agent_metadata(runner, provider, agent):
15f218
     if not __is_path_abs(script_path) or not is_path_runnable(script_path):
15f218
         raise AgentNotFound(agent_name)
15f218
 
15f218
-    output, retval = runner.run(
15f218
+    stdout, stderr, dummy_retval = runner.run(
15f218
         [script_path, "meta-data"],
15f218
-        env_extend={"OCF_ROOT": settings.ocf_root},
15f218
-        ignore_stderr=True
15f218
+        env_extend={"OCF_ROOT": settings.ocf_root}
15f218
     )
15f218
 
15f218
-    if output.strip() == "":
15f218
-        raise UnableToGetAgentMetadata(agent_name, output)
15f218
+    if stdout.strip() == "":
15f218
+        raise UnableToGetAgentMetadata(agent_name, stderr)
15f218
 
15f218
     try:
15f218
-        return etree.fromstring(output)
15f218
+        return etree.fromstring(stdout)
15f218
     except etree.XMLSyntaxError as e:
15f218
         raise UnableToGetAgentMetadata(agent_name, str(e))
15f218
 
15f218
diff --git a/pcs/lib/sbd.py b/pcs/lib/sbd.py
15f218
index 39de740..9b57400 100644
15f218
--- a/pcs/lib/sbd.py
15f218
+++ b/pcs/lib/sbd.py
15f218
@@ -115,11 +115,11 @@ def atb_has_to_be_enabled(runner, corosync_conf_facade, node_number_modifier=0):
15f218
         node.
15f218
     """
15f218
     return (
15f218
+        not corosync_conf_facade.is_enabled_auto_tie_breaker()
15f218
+        and
15f218
         is_auto_tie_breaker_needed(
15f218
             runner, corosync_conf_facade, node_number_modifier
15f218
         )
15f218
-        and
15f218
-        not corosync_conf_facade.is_enabled_auto_tie_breaker()
15f218
     )
15f218
 
15f218
 
15f218
diff --git a/pcs/qdevice.py b/pcs/qdevice.py
15f218
index 0037704..2591bae 100644
15f218
--- a/pcs/qdevice.py
15f218
+++ b/pcs/qdevice.py
15f218
@@ -92,7 +92,7 @@ def qdevice_destroy_cmd(lib, argv, modifiers):
15f218
     if len(argv) != 1:
15f218
         raise CmdLineInputError()
15f218
     model = argv[0]
15f218
-    lib.qdevice.destroy(model)
15f218
+    lib.qdevice.destroy(model, modifiers["force"])
15f218
 
15f218
 def qdevice_start_cmd(lib, argv, modifiers):
15f218
     if len(argv) != 1:
15f218
@@ -104,7 +104,7 @@ def qdevice_stop_cmd(lib, argv, modifiers):
15f218
     if len(argv) != 1:
15f218
         raise CmdLineInputError()
15f218
     model = argv[0]
15f218
-    lib.qdevice.stop(model)
15f218
+    lib.qdevice.stop(model, modifiers["force"])
15f218
 
15f218
 def qdevice_kill_cmd(lib, argv, modifiers):
15f218
     if len(argv) != 1:
15f218
diff --git a/pcs/test/resources/corosync-qdevice.conf b/pcs/test/resources/corosync-qdevice.conf
15f218
new file mode 100644
15f218
index 0000000..38998e7
15f218
--- /dev/null
15f218
+++ b/pcs/test/resources/corosync-qdevice.conf
15f218
@@ -0,0 +1,34 @@
15f218
+totem {
15f218
+    version: 2
15f218
+    secauth: off
15f218
+    cluster_name: test99
15f218
+    transport: udpu
15f218
+}
15f218
+
15f218
+nodelist {
15f218
+    node {
15f218
+        ring0_addr: rh7-1
15f218
+        nodeid: 1
15f218
+    }
15f218
+
15f218
+    node {
15f218
+        ring0_addr: rh7-2
15f218
+        nodeid: 2
15f218
+    }
15f218
+}
15f218
+
15f218
+quorum {
15f218
+    provider: corosync_votequorum
15f218
+
15f218
+    device {
15f218
+        model: net
15f218
+
15f218
+        net {
15f218
+            host: 127.0.0.1
15f218
+        }
15f218
+    }
15f218
+}
15f218
+
15f218
+logging {
15f218
+    to_syslog: yes
15f218
+}
15f218
diff --git a/pcs/test/test_common_tools.py b/pcs/test/test_common_tools.py
15f218
index 5290e6d..d9b6af3 100644
15f218
--- a/pcs/test/test_common_tools.py
15f218
+++ b/pcs/test/test_common_tools.py
15f218
@@ -63,3 +63,35 @@ class RunParallelTestCase(TestCase):
15f218
         elapsed_time = finish_time - start_time
15f218
         self.assertTrue(elapsed_time > x)
15f218
         self.assertTrue(elapsed_time < sum([i + 1 for i in range(x)]))
15f218
+
15f218
+
15f218
+class JoinMultilinesTest(TestCase):
15f218
+    def test_empty_input(self):
15f218
+        self.assertEqual(
15f218
+            "",
15f218
+            tools.join_multilines([])
15f218
+        )
15f218
+
15f218
+    def test_two_strings(self):
15f218
+        self.assertEqual(
15f218
+            "a\nb",
15f218
+            tools.join_multilines(["a", "b"])
15f218
+        )
15f218
+
15f218
+    def test_strip(self):
15f218
+        self.assertEqual(
15f218
+            "a\nb",
15f218
+            tools.join_multilines(["  a\n", "  b\n"])
15f218
+        )
15f218
+
15f218
+    def test_skip_empty(self):
15f218
+        self.assertEqual(
15f218
+            "a\nb",
15f218
+            tools.join_multilines(["  a\n", "   \n", "  b\n"])
15f218
+        )
15f218
+
15f218
+    def test_multiline(self):
15f218
+        self.assertEqual(
15f218
+            "a\nA\nb\nB",
15f218
+            tools.join_multilines(["a\nA\n", "b\nB\n"])
15f218
+        )
15f218
diff --git a/pcs/test/test_lib_cib_tools.py b/pcs/test/test_lib_cib_tools.py
15f218
index ffc2642..ec9c312 100644
15f218
--- a/pcs/test/test_lib_cib_tools.py
15f218
+++ b/pcs/test/test_lib_cib_tools.py
15f218
@@ -383,7 +383,7 @@ class UpgradeCibTest(TestCase):
15f218
         mock_file.name = "mock_file_name"
15f218
         mock_file.read.return_value = "<cib/>"
15f218
         mock_named_file.return_value = mock_file
15f218
-        self.mock_runner.run.return_value = ("", 0)
15f218
+        self.mock_runner.run.return_value = ("", "", 0)
15f218
         assert_xml_equal(
15f218
             "<cib/>",
15f218
             etree.tostring(
15f218
@@ -408,13 +408,15 @@ class UpgradeCibTest(TestCase):
15f218
         mock_file = mock.MagicMock()
15f218
         mock_file.name = "mock_file_name"
15f218
         mock_named_file.return_value = mock_file
15f218
-        self.mock_runner.run.return_value = ("reason", 1)
15f218
+        self.mock_runner.run.return_value = ("some info", "some error", 1)
15f218
         assert_raise_library_error(
15f218
             lambda: lib.upgrade_cib(etree.XML("<old_cib/>"), self.mock_runner),
15f218
             (
15f218
                 severities.ERROR,
15f218
                 report_codes.CIB_UPGRADE_FAILED,
15f218
-                {"reason": "reason"}
15f218
+                {
15f218
+                    "reason": "some error\nsome info",
15f218
+                }
15f218
             )
15f218
         )
15f218
         mock_named_file.assert_called_once_with("w+", suffix=".pcs")
15f218
@@ -434,7 +436,7 @@ class UpgradeCibTest(TestCase):
15f218
         mock_file.name = "mock_file_name"
15f218
         mock_file.read.return_value = "not xml"
15f218
         mock_named_file.return_value = mock_file
15f218
-        self.mock_runner.run.return_value = ("", 0)
15f218
+        self.mock_runner.run.return_value = ("", "", 0)
15f218
         assert_raise_library_error(
15f218
             lambda: lib.upgrade_cib(etree.XML("<old_cib/>"), self.mock_runner),
15f218
             (
15f218
diff --git a/pcs/test/test_lib_commands_qdevice.py b/pcs/test/test_lib_commands_qdevice.py
15f218
index 10841e9..756afa8 100644
15f218
--- a/pcs/test/test_lib_commands_qdevice.py
15f218
+++ b/pcs/test/test_lib_commands_qdevice.py
15f218
@@ -345,6 +345,7 @@ class QdeviceNetSetupTest(QdeviceTestCase):
15f218
         )
15f218
 
15f218
 
15f218
+@mock.patch("pcs.lib.corosync.qdevice_net.qdevice_status_cluster_text")
15f218
 @mock.patch("pcs.lib.external.stop_service")
15f218
 @mock.patch("pcs.lib.external.disable_service")
15f218
 @mock.patch("pcs.lib.commands.qdevice.qdevice_net.qdevice_destroy")
15f218
@@ -355,7 +356,11 @@ class QdeviceNetSetupTest(QdeviceTestCase):
15f218
     lambda self: "mock_runner"
15f218
 )
15f218
 class QdeviceNetDestroyTest(QdeviceTestCase):
15f218
-    def test_success(self, mock_net_destroy, mock_net_disable, mock_net_stop):
15f218
+    def test_success_not_used(
15f218
+        self, mock_net_destroy, mock_net_disable, mock_net_stop, mock_status
15f218
+    ):
15f218
+        mock_status.return_value = ""
15f218
+
15f218
         lib.qdevice_destroy(self.lib_env, "net")
15f218
 
15f218
         mock_net_stop.assert_called_once_with("mock_runner", "corosync-qnetd")
15f218
@@ -398,9 +403,85 @@ class QdeviceNetDestroyTest(QdeviceTestCase):
15f218
             ]
15f218
         )
15f218
 
15f218
+    def test_success_used_forced(
15f218
+        self, mock_net_destroy, mock_net_disable, mock_net_stop, mock_status
15f218
+    ):
15f218
+        mock_status.return_value = 'Cluster "a_cluster":\n'
15f218
+
15f218
+        lib.qdevice_destroy(self.lib_env, "net", proceed_if_used=True)
15f218
+
15f218
+        mock_net_stop.assert_called_once_with("mock_runner", "corosync-qnetd")
15f218
+        mock_net_disable.assert_called_once_with(
15f218
+            "mock_runner",
15f218
+            "corosync-qnetd"
15f218
+        )
15f218
+        mock_net_destroy.assert_called_once_with()
15f218
+        assert_report_item_list_equal(
15f218
+            self.mock_reporter.report_item_list,
15f218
+            [
15f218
+                (
15f218
+                    severity.WARNING,
15f218
+                    report_codes.QDEVICE_USED_BY_CLUSTERS,
15f218
+                    {
15f218
+                        "clusters": ["a_cluster"],
15f218
+                    }
15f218
+                ),
15f218
+                (
15f218
+                    severity.INFO,
15f218
+                    report_codes.SERVICE_STOP_STARTED,
15f218
+                    {
15f218
+                        "service": "quorum device",
15f218
+                    }
15f218
+                ),
15f218
+                (
15f218
+                    severity.INFO,
15f218
+                    report_codes.SERVICE_STOP_SUCCESS,
15f218
+                    {
15f218
+                        "service": "quorum device",
15f218
+                    }
15f218
+                ),
15f218
+                (
15f218
+                    severity.INFO,
15f218
+                    report_codes.SERVICE_DISABLE_SUCCESS,
15f218
+                    {
15f218
+                        "service": "quorum device",
15f218
+                    }
15f218
+                ),
15f218
+                (
15f218
+                    severity.INFO,
15f218
+                    report_codes.QDEVICE_DESTROY_SUCCESS,
15f218
+                    {
15f218
+                        "model": "net",
15f218
+                    }
15f218
+                )
15f218
+            ]
15f218
+        )
15f218
+
15f218
+    def test_used_not_forced(
15f218
+        self, mock_net_destroy, mock_net_disable, mock_net_stop, mock_status
15f218
+    ):
15f218
+        mock_status.return_value = 'Cluster "a_cluster":\n'
15f218
+
15f218
+        assert_raise_library_error(
15f218
+            lambda: lib.qdevice_destroy(self.lib_env, "net"),
15f218
+            (
15f218
+                severity.ERROR,
15f218
+                report_codes.QDEVICE_USED_BY_CLUSTERS,
15f218
+                {
15f218
+                    "clusters": ["a_cluster"],
15f218
+                },
15f218
+                report_codes.FORCE_QDEVICE_USED
15f218
+            ),
15f218
+        )
15f218
+
15f218
+        mock_net_stop.assert_not_called()
15f218
+        mock_net_disable.assert_not_called()
15f218
+        mock_net_destroy.assert_not_called()
15f218
+
15f218
     def test_stop_failed(
15f218
-        self, mock_net_destroy, mock_net_disable, mock_net_stop
15f218
+        self, mock_net_destroy, mock_net_disable, mock_net_stop, mock_status
15f218
     ):
15f218
+        mock_status.return_value = ""
15f218
         mock_net_stop.side_effect = StopServiceError(
15f218
             "test service",
15f218
             "test error"
15f218
@@ -435,8 +516,9 @@ class QdeviceNetDestroyTest(QdeviceTestCase):
15f218
         )
15f218
 
15f218
     def test_disable_failed(
15f218
-        self, mock_net_destroy, mock_net_disable, mock_net_stop
15f218
+        self, mock_net_destroy, mock_net_disable, mock_net_stop, mock_status
15f218
     ):
15f218
+        mock_status.return_value = ""
15f218
         mock_net_disable.side_effect = DisableServiceError(
15f218
             "test service",
15f218
             "test error"
15f218
@@ -481,8 +563,9 @@ class QdeviceNetDestroyTest(QdeviceTestCase):
15f218
         )
15f218
 
15f218
     def test_destroy_failed(
15f218
-        self, mock_net_destroy, mock_net_disable, mock_net_stop
15f218
+        self, mock_net_destroy, mock_net_disable, mock_net_stop, mock_status
15f218
     ):
15f218
+        mock_status.return_value = ""
15f218
         mock_net_destroy.side_effect = LibraryError("mock_report_item")
15f218
 
15f218
         self.assertRaises(
15f218
@@ -755,6 +838,7 @@ class QdeviceNetStartTest(QdeviceTestCase):
15f218
         )
15f218
 
15f218
 
15f218
+@mock.patch("pcs.lib.corosync.qdevice_net.qdevice_status_cluster_text")
15f218
 @mock.patch("pcs.lib.external.stop_service")
15f218
 @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
15f218
 @mock.patch.object(
15f218
@@ -763,13 +847,49 @@ class QdeviceNetStartTest(QdeviceTestCase):
15f218
     lambda self: "mock_runner"
15f218
 )
15f218
 class QdeviceNetStopTest(QdeviceTestCase):
15f218
-    def test_success(self, mock_net_stop):
15f218
-        lib.qdevice_stop(self.lib_env, "net")
15f218
+    def test_success_not_used(self, mock_net_stop, mock_status):
15f218
+        mock_status.return_value = ""
15f218
+
15f218
+        lib.qdevice_stop(self.lib_env, "net", proceed_if_used=False)
15f218
+
15f218
+        mock_net_stop.assert_called_once_with("mock_runner", "corosync-qnetd")
15f218
+        assert_report_item_list_equal(
15f218
+            self.mock_reporter.report_item_list,
15f218
+            [
15f218
+                (
15f218
+                    severity.INFO,
15f218
+                    report_codes.SERVICE_STOP_STARTED,
15f218
+                    {
15f218
+                        "service": "quorum device",
15f218
+                    }
15f218
+                ),
15f218
+                (
15f218
+                    severity.INFO,
15f218
+                    report_codes.SERVICE_STOP_SUCCESS,
15f218
+                    {
15f218
+                        "service": "quorum device",
15f218
+                    }
15f218
+                )
15f218
+            ]
15f218
+        )
15f218
+
15f218
+    def test_success_used_forced(self, mock_net_stop, mock_status):
15f218
+        mock_status.return_value = 'Cluster "a_cluster":\n'
15f218
+
15f218
+        lib.qdevice_stop(self.lib_env, "net", proceed_if_used=True)
15f218
+
15f218
         mock_net_stop.assert_called_once_with("mock_runner", "corosync-qnetd")
15f218
         assert_report_item_list_equal(
15f218
             self.mock_reporter.report_item_list,
15f218
             [
15f218
                 (
15f218
+                    severity.WARNING,
15f218
+                    report_codes.QDEVICE_USED_BY_CLUSTERS,
15f218
+                    {
15f218
+                        "clusters": ["a_cluster"],
15f218
+                    }
15f218
+                ),
15f218
+                (
15f218
                     severity.INFO,
15f218
                     report_codes.SERVICE_STOP_STARTED,
15f218
                     {
15f218
@@ -786,7 +906,28 @@ class QdeviceNetStopTest(QdeviceTestCase):
15f218
             ]
15f218
         )
15f218
 
15f218
-    def test_failed(self, mock_net_stop):
15f218
+    def test_used_not_forced(self, mock_net_stop, mock_status):
15f218
+        mock_status.return_value = 'Cluster "a_cluster":\n'
15f218
+
15f218
+        assert_raise_library_error(
15f218
+            lambda: lib.qdevice_stop(
15f218
+                self.lib_env,
15f218
+                "net",
15f218
+                proceed_if_used=False
15f218
+            ),
15f218
+            (
15f218
+                severity.ERROR,
15f218
+                report_codes.QDEVICE_USED_BY_CLUSTERS,
15f218
+                {
15f218
+                    "clusters": ["a_cluster"],
15f218
+                },
15f218
+                report_codes.FORCE_QDEVICE_USED
15f218
+            ),
15f218
+        )
15f218
+        mock_net_stop.assert_not_called()
15f218
+
15f218
+    def test_failed(self, mock_net_stop, mock_status):
15f218
+        mock_status.return_value = ""
15f218
         mock_net_stop.side_effect = StopServiceError(
15f218
             "test service",
15f218
             "test error"
15f218
diff --git a/pcs/test/test_lib_commands_quorum.py b/pcs/test/test_lib_commands_quorum.py
15f218
index d7701af..1487eb4 100644
15f218
--- a/pcs/test/test_lib_commands_quorum.py
15f218
+++ b/pcs/test/test_lib_commands_quorum.py
15f218
@@ -1579,10 +1579,14 @@ class RemoveDeviceTest(TestCase, CmanMixin):
15f218
         mock_remote_stop.assert_not_called()
15f218
 
15f218
     @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
15f218
-    def test_success(
15f218
+    @mock.patch("pcs.lib.sbd.is_sbd_installed", lambda self: True)
15f218
+    @mock.patch("pcs.lib.sbd.is_sbd_enabled", lambda self: True)
15f218
+    def test_success_3nodes_sbd(
15f218
         self, mock_remote_stop, mock_remote_disable, mock_remove_net,
15f218
         mock_get_corosync, mock_push_corosync
15f218
     ):
15f218
+        # nothing special needs to be done in regards of SBD if a cluster
15f218
+        # consists of odd number of nodes
15f218
         original_conf = open(rc("corosync-3nodes-qdevice.conf")).read()
15f218
         no_device_conf = open(rc("corosync-3nodes.conf")).read()
15f218
         mock_get_corosync.return_value = original_conf
15f218
@@ -1619,10 +1623,106 @@ class RemoveDeviceTest(TestCase, CmanMixin):
15f218
         self.assertEqual(3, len(mock_remote_stop.mock_calls))
15f218
 
15f218
     @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
15f218
-    def test_success_file(
15f218
+    @mock.patch("pcs.lib.sbd.is_sbd_installed", lambda self: False)
15f218
+    @mock.patch("pcs.lib.sbd.is_sbd_enabled", lambda self: False)
15f218
+    def test_success_2nodes_no_sbd(
15f218
+        self, mock_remote_stop, mock_remote_disable, mock_remove_net,
15f218
+        mock_get_corosync, mock_push_corosync
15f218
+    ):
15f218
+        # cluster consists of two nodes, two_node must be set
15f218
+        original_conf = open(rc("corosync-qdevice.conf")).read()
15f218
+        no_device_conf = open(rc("corosync.conf")).read()
15f218
+        mock_get_corosync.return_value = original_conf
15f218
+        lib_env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
15f218
+
15f218
+        lib.remove_device(lib_env)
15f218
+
15f218
+        self.assertEqual(1, len(mock_push_corosync.mock_calls))
15f218
+        ac(
15f218
+            mock_push_corosync.mock_calls[0][1][0].config.export(),
15f218
+            no_device_conf
15f218
+        )
15f218
+        assert_report_item_list_equal(
15f218
+            self.mock_reporter.report_item_list,
15f218
+            [
15f218
+                (
15f218
+                    severity.INFO,
15f218
+                    report_codes.SERVICE_DISABLE_STARTED,
15f218
+                    {
15f218
+                        "service": "corosync-qdevice",
15f218
+                    }
15f218
+                ),
15f218
+                (
15f218
+                    severity.INFO,
15f218
+                    report_codes.SERVICE_STOP_STARTED,
15f218
+                    {
15f218
+                        "service": "corosync-qdevice",
15f218
+                    }
15f218
+                ),
15f218
+            ]
15f218
+        )
15f218
+        self.assertEqual(1, len(mock_remove_net.mock_calls))
15f218
+        self.assertEqual(2, len(mock_remote_disable.mock_calls))
15f218
+        self.assertEqual(2, len(mock_remote_stop.mock_calls))
15f218
+
15f218
+    @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
15f218
+    @mock.patch("pcs.lib.sbd.is_sbd_installed", lambda self: True)
15f218
+    @mock.patch("pcs.lib.sbd.is_sbd_enabled", lambda self: True)
15f218
+    def test_success_2nodes_sbd(
15f218
         self, mock_remote_stop, mock_remote_disable, mock_remove_net,
15f218
         mock_get_corosync, mock_push_corosync
15f218
     ):
15f218
+        # cluster consists of two nodes, but SBD is in use
15f218
+        # auto tie breaker must be enabled
15f218
+        original_conf = open(rc("corosync-qdevice.conf")).read()
15f218
+        no_device_conf = open(rc("corosync.conf")).read().replace(
15f218
+            "two_node: 1",
15f218
+            "auto_tie_breaker: 1"
15f218
+        )
15f218
+        mock_get_corosync.return_value = original_conf
15f218
+        lib_env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
15f218
+
15f218
+        lib.remove_device(lib_env)
15f218
+
15f218
+        self.assertEqual(1, len(mock_push_corosync.mock_calls))
15f218
+        ac(
15f218
+            mock_push_corosync.mock_calls[0][1][0].config.export(),
15f218
+            no_device_conf
15f218
+        )
15f218
+        assert_report_item_list_equal(
15f218
+            self.mock_reporter.report_item_list,
15f218
+            [
15f218
+                (
15f218
+                    severity.WARNING,
15f218
+                    report_codes.SBD_REQUIRES_ATB,
15f218
+                    {}
15f218
+                ),
15f218
+                (
15f218
+                    severity.INFO,
15f218
+                    report_codes.SERVICE_DISABLE_STARTED,
15f218
+                    {
15f218
+                        "service": "corosync-qdevice",
15f218
+                    }
15f218
+                ),
15f218
+                (
15f218
+                    severity.INFO,
15f218
+                    report_codes.SERVICE_STOP_STARTED,
15f218
+                    {
15f218
+                        "service": "corosync-qdevice",
15f218
+                    }
15f218
+                ),
15f218
+            ]
15f218
+        )
15f218
+        self.assertEqual(1, len(mock_remove_net.mock_calls))
15f218
+        self.assertEqual(2, len(mock_remote_disable.mock_calls))
15f218
+        self.assertEqual(2, len(mock_remote_stop.mock_calls))
15f218
+
15f218
+    @mock.patch("pcs.lib.sbd.atb_has_to_be_enabled")
15f218
+    @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
15f218
+    def test_success_file(
15f218
+        self, mock_atb_check, mock_remote_stop, mock_remote_disable,
15f218
+        mock_remove_net, mock_get_corosync, mock_push_corosync
15f218
+    ):
15f218
         original_conf = open(rc("corosync-3nodes-qdevice.conf")).read()
15f218
         no_device_conf = open(rc("corosync-3nodes.conf")).read()
15f218
         mock_get_corosync.return_value = original_conf
15f218
@@ -1643,6 +1743,7 @@ class RemoveDeviceTest(TestCase, CmanMixin):
15f218
         mock_remove_net.assert_not_called()
15f218
         mock_remote_disable.assert_not_called()
15f218
         mock_remote_stop.assert_not_called()
15f218
+        mock_atb_check.assert_not_called()
15f218
 
15f218
 
15f218
 @mock.patch("pcs.lib.commands.quorum.qdevice_net.remote_client_destroy")
15f218
diff --git a/pcs/test/test_lib_corosync_live.py b/pcs/test/test_lib_corosync_live.py
15f218
index 3173195..f03d78b 100644
15f218
--- a/pcs/test/test_lib_corosync_live.py
15f218
+++ b/pcs/test/test_lib_corosync_live.py
15f218
@@ -69,9 +69,10 @@ class ReloadConfigTest(TestCase):
15f218
 
15f218
     def test_success(self):
15f218
         cmd_retval = 0
15f218
-        cmd_output = "cmd output"
15f218
+        cmd_stdout = "cmd output"
15f218
+        cmd_stderr = ""
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (cmd_output, cmd_retval)
15f218
+        mock_runner.run.return_value = (cmd_stdout, cmd_stderr, cmd_retval)
15f218
 
15f218
         lib.reload_config(mock_runner)
15f218
 
15f218
@@ -81,9 +82,10 @@ class ReloadConfigTest(TestCase):
15f218
 
15f218
     def test_error(self):
15f218
         cmd_retval = 1
15f218
-        cmd_output = "cmd output"
15f218
+        cmd_stdout = "cmd output"
15f218
+        cmd_stderr = "cmd error"
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (cmd_output, cmd_retval)
15f218
+        mock_runner.run.return_value = (cmd_stdout, cmd_stderr, cmd_retval)
15f218
 
15f218
         assert_raise_library_error(
15f218
             lambda: lib.reload_config(mock_runner),
15f218
@@ -91,7 +93,7 @@ class ReloadConfigTest(TestCase):
15f218
                 severity.ERROR,
15f218
                 report_codes.COROSYNC_CONFIG_RELOAD_ERROR,
15f218
                 {
15f218
-                    "reason": cmd_output,
15f218
+                    "reason": "\n".join([cmd_stderr, cmd_stdout]),
15f218
                 }
15f218
             )
15f218
         )
15f218
@@ -107,7 +109,7 @@ class GetQuorumStatusTextTest(TestCase):
15f218
         self.quorum_tool = "/usr/sbin/corosync-quorumtool"
15f218
 
15f218
     def test_success(self):
15f218
-        self.mock_runner.run.return_value = ("status info", 0)
15f218
+        self.mock_runner.run.return_value = ("status info", "", 0)
15f218
         self.assertEqual(
15f218
             "status info",
15f218
             lib.get_quorum_status_text(self.mock_runner)
15f218
@@ -117,7 +119,7 @@ class GetQuorumStatusTextTest(TestCase):
15f218
         ])
15f218
 
15f218
     def test_success_with_retval_1(self):
15f218
-        self.mock_runner.run.return_value = ("status info", 1)
15f218
+        self.mock_runner.run.return_value = ("status info", "", 1)
15f218
         self.assertEqual(
15f218
             "status info",
15f218
             lib.get_quorum_status_text(self.mock_runner)
15f218
@@ -127,7 +129,7 @@ class GetQuorumStatusTextTest(TestCase):
15f218
         ])
15f218
 
15f218
     def test_error(self):
15f218
-        self.mock_runner.run.return_value = ("status error", 2)
15f218
+        self.mock_runner.run.return_value = ("some info", "status error", 2)
15f218
         assert_raise_library_error(
15f218
             lambda: lib.get_quorum_status_text(self.mock_runner),
15f218
             (
15f218
@@ -152,9 +154,10 @@ class SetExpectedVotesTest(TestCase):
15f218
 
15f218
     def test_success(self):
15f218
         cmd_retval = 0
15f218
-        cmd_output = "cmd output"
15f218
+        cmd_stdout = "cmd output"
15f218
+        cmd_stderr = ""
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (cmd_output, cmd_retval)
15f218
+        mock_runner.run.return_value = (cmd_stdout, cmd_stderr, cmd_retval)
15f218
 
15f218
         lib.set_expected_votes(mock_runner, 3)
15f218
 
15f218
@@ -164,9 +167,10 @@ class SetExpectedVotesTest(TestCase):
15f218
 
15f218
     def test_error(self):
15f218
         cmd_retval = 1
15f218
-        cmd_output = "cmd output"
15f218
+        cmd_stdout = "cmd output"
15f218
+        cmd_stderr = "cmd stderr"
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (cmd_output, cmd_retval)
15f218
+        mock_runner.run.return_value = (cmd_stdout, cmd_stderr, cmd_retval)
15f218
 
15f218
         assert_raise_library_error(
15f218
             lambda: lib.set_expected_votes(mock_runner, 3),
15f218
@@ -174,7 +178,7 @@ class SetExpectedVotesTest(TestCase):
15f218
                 severity.ERROR,
15f218
                 report_codes.COROSYNC_QUORUM_SET_EXPECTED_VOTES_ERROR,
15f218
                 {
15f218
-                    "reason": cmd_output,
15f218
+                    "reason": cmd_stderr,
15f218
                 }
15f218
             )
15f218
         )
15f218
diff --git a/pcs/test/test_lib_corosync_qdevice_client.py b/pcs/test/test_lib_corosync_qdevice_client.py
15f218
index 0b5bd67..8c32c36 100644
15f218
--- a/pcs/test/test_lib_corosync_qdevice_client.py
15f218
+++ b/pcs/test/test_lib_corosync_qdevice_client.py
15f218
@@ -23,7 +23,7 @@ class GetStatusTextTest(TestCase):
15f218
         self.qdevice_tool = "/usr/sbin/corosync-qdevice-tool"
15f218
 
15f218
     def test_success(self):
15f218
-        self.mock_runner.run.return_value = ("status info", 0)
15f218
+        self.mock_runner.run.return_value = ("status info", "", 0)
15f218
         self.assertEqual(
15f218
             "status info",
15f218
             lib.get_status_text(self.mock_runner)
15f218
@@ -33,7 +33,7 @@ class GetStatusTextTest(TestCase):
15f218
         ])
15f218
 
15f218
     def test_success_verbose(self):
15f218
-        self.mock_runner.run.return_value = ("status info", 0)
15f218
+        self.mock_runner.run.return_value = ("status info", "", 0)
15f218
         self.assertEqual(
15f218
             "status info",
15f218
             lib.get_status_text(self.mock_runner, True)
15f218
@@ -43,14 +43,14 @@ class GetStatusTextTest(TestCase):
15f218
         ])
15f218
 
15f218
     def test_error(self):
15f218
-        self.mock_runner.run.return_value = ("status error", 1)
15f218
+        self.mock_runner.run.return_value = ("some info", "status error", 1)
15f218
         assert_raise_library_error(
15f218
             lambda: lib.get_status_text(self.mock_runner),
15f218
             (
15f218
                 severity.ERROR,
15f218
                 report_codes.COROSYNC_QUORUM_GET_STATUS_ERROR,
15f218
                 {
15f218
-                    "reason": "status error",
15f218
+                    "reason": "status error\nsome info",
15f218
                 }
15f218
             )
15f218
         )
15f218
diff --git a/pcs/test/test_lib_corosync_qdevice_net.py b/pcs/test/test_lib_corosync_qdevice_net.py
15f218
index 340a8dc..21c526b 100644
15f218
--- a/pcs/test/test_lib_corosync_qdevice_net.py
15f218
+++ b/pcs/test/test_lib_corosync_qdevice_net.py
15f218
@@ -49,7 +49,7 @@ class QdeviceSetupTest(TestCase):
15f218
 
15f218
     def test_success(self, mock_is_dir_nonempty):
15f218
         mock_is_dir_nonempty.return_value = False
15f218
-        self.mock_runner.run.return_value = ("initialized", 0)
15f218
+        self.mock_runner.run.return_value = ("initialized", "", 0)
15f218
 
15f218
         lib.qdevice_setup(self.mock_runner)
15f218
 
15f218
@@ -73,7 +73,7 @@ class QdeviceSetupTest(TestCase):
15f218
 
15f218
     def test_init_tool_fail(self, mock_is_dir_nonempty):
15f218
         mock_is_dir_nonempty.return_value = False
15f218
-        self.mock_runner.run.return_value = ("test error", 1)
15f218
+        self.mock_runner.run.return_value = ("stdout", "test error", 1)
15f218
 
15f218
         assert_raise_library_error(
15f218
             lambda: lib.qdevice_setup(self.mock_runner),
15f218
@@ -82,7 +82,7 @@ class QdeviceSetupTest(TestCase):
15f218
                 report_codes.QDEVICE_INITIALIZATION_ERROR,
15f218
                 {
15f218
                     "model": "net",
15f218
-                    "reason": "test error",
15f218
+                    "reason": "test error\nstdout",
15f218
                 }
15f218
             )
15f218
         )
15f218
@@ -126,7 +126,7 @@ class QdeviceStatusGenericTest(TestCase):
15f218
         self.mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
 
15f218
     def test_success(self):
15f218
-        self.mock_runner.run.return_value = ("status info", 0)
15f218
+        self.mock_runner.run.return_value = ("status info", "", 0)
15f218
         self.assertEqual(
15f218
             "status info",
15f218
             lib.qdevice_status_generic_text(self.mock_runner)
15f218
@@ -134,7 +134,7 @@ class QdeviceStatusGenericTest(TestCase):
15f218
         self.mock_runner.run.assert_called_once_with([_qnetd_tool, "-s"])
15f218
 
15f218
     def test_success_verbose(self):
15f218
-        self.mock_runner.run.return_value = ("status info", 0)
15f218
+        self.mock_runner.run.return_value = ("status info", "", 0)
15f218
         self.assertEqual(
15f218
             "status info",
15f218
             lib.qdevice_status_generic_text(self.mock_runner, True)
15f218
@@ -142,7 +142,7 @@ class QdeviceStatusGenericTest(TestCase):
15f218
         self.mock_runner.run.assert_called_once_with([_qnetd_tool, "-s", "-v"])
15f218
 
15f218
     def test_error(self):
15f218
-        self.mock_runner.run.return_value = ("status error", 1)
15f218
+        self.mock_runner.run.return_value = ("some info", "status error", 1)
15f218
         assert_raise_library_error(
15f218
             lambda: lib.qdevice_status_generic_text(self.mock_runner),
15f218
             (
15f218
@@ -150,7 +150,7 @@ class QdeviceStatusGenericTest(TestCase):
15f218
                 report_codes.QDEVICE_GET_STATUS_ERROR,
15f218
                 {
15f218
                     "model": "net",
15f218
-                    "reason": "status error",
15f218
+                    "reason": "status error\nsome info",
15f218
                 }
15f218
             )
15f218
         )
15f218
@@ -162,7 +162,7 @@ class QdeviceStatusClusterTest(TestCase):
15f218
         self.mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
 
15f218
     def test_success(self):
15f218
-        self.mock_runner.run.return_value = ("status info", 0)
15f218
+        self.mock_runner.run.return_value = ("status info", "", 0)
15f218
         self.assertEqual(
15f218
             "status info",
15f218
             lib.qdevice_status_cluster_text(self.mock_runner)
15f218
@@ -170,7 +170,7 @@ class QdeviceStatusClusterTest(TestCase):
15f218
         self.mock_runner.run.assert_called_once_with([_qnetd_tool, "-l"])
15f218
 
15f218
     def test_success_verbose(self):
15f218
-        self.mock_runner.run.return_value = ("status info", 0)
15f218
+        self.mock_runner.run.return_value = ("status info", "", 0)
15f218
         self.assertEqual(
15f218
             "status info",
15f218
             lib.qdevice_status_cluster_text(self.mock_runner, verbose=True)
15f218
@@ -178,7 +178,7 @@ class QdeviceStatusClusterTest(TestCase):
15f218
         self.mock_runner.run.assert_called_once_with([_qnetd_tool, "-l", "-v"])
15f218
 
15f218
     def test_success_cluster(self):
15f218
-        self.mock_runner.run.return_value = ("status info", 0)
15f218
+        self.mock_runner.run.return_value = ("status info", "", 0)
15f218
         self.assertEqual(
15f218
             "status info",
15f218
             lib.qdevice_status_cluster_text(self.mock_runner, "cluster")
15f218
@@ -188,7 +188,7 @@ class QdeviceStatusClusterTest(TestCase):
15f218
         ])
15f218
 
15f218
     def test_success_cluster_verbose(self):
15f218
-        self.mock_runner.run.return_value = ("status info", 0)
15f218
+        self.mock_runner.run.return_value = ("status info", "", 0)
15f218
         self.assertEqual(
15f218
             "status info",
15f218
             lib.qdevice_status_cluster_text(self.mock_runner, "cluster", True)
15f218
@@ -198,7 +198,7 @@ class QdeviceStatusClusterTest(TestCase):
15f218
         ])
15f218
 
15f218
     def test_error(self):
15f218
-        self.mock_runner.run.return_value = ("status error", 1)
15f218
+        self.mock_runner.run.return_value = ("some info", "status error", 1)
15f218
         assert_raise_library_error(
15f218
             lambda: lib.qdevice_status_cluster_text(self.mock_runner),
15f218
             (
15f218
@@ -206,13 +206,63 @@ class QdeviceStatusClusterTest(TestCase):
15f218
                 report_codes.QDEVICE_GET_STATUS_ERROR,
15f218
                 {
15f218
                     "model": "net",
15f218
-                    "reason": "status error",
15f218
+                    "reason": "status error\nsome info",
15f218
                 }
15f218
             )
15f218
         )
15f218
         self.mock_runner.run.assert_called_once_with([_qnetd_tool, "-l"])
15f218
 
15f218
 
15f218
+class QdeviceConnectedClustersTest(TestCase):
15f218
+    def test_empty_status(self):
15f218
+        status = ""
15f218
+        self.assertEqual(
15f218
+            [],
15f218
+            lib.qdevice_connected_clusters(status)
15f218
+        )
15f218
+
15f218
+    def test_one_cluster(self):
15f218
+        status = """\
15f218
+Cluster "rhel72":
15f218
+    Algorithm:          LMS
15f218
+    Tie-breaker:        Node with lowest node ID
15f218
+    Node ID 2:
15f218
+        Client address:         ::ffff:192.168.122.122:59738
15f218
+        Configured node list:   1, 2
15f218
+        Membership node list:   1, 2
15f218
+        Vote:                   ACK (ACK)
15f218
+    Node ID 1:
15f218
+        Client address:         ::ffff:192.168.122.121:43420
15f218
+        Configured node list:   1, 2
15f218
+        Membership node list:   1, 2
15f218
+        Vote:                   ACK (ACK)
15f218
+"""
15f218
+        self.assertEqual(
15f218
+            ["rhel72"],
15f218
+            lib.qdevice_connected_clusters(status)
15f218
+        )
15f218
+
15f218
+    def test_more_clusters(self):
15f218
+        status = """\
15f218
+Cluster "rhel72":
15f218
+Cluster "rhel73":
15f218
+"""
15f218
+        self.assertEqual(
15f218
+            ["rhel72", "rhel73"],
15f218
+            lib.qdevice_connected_clusters(status)
15f218
+        )
15f218
+
15f218
+    def test_invalid_status(self):
15f218
+        status = """\
15f218
+Cluster:
15f218
+    Cluster "rhel72":
15f218
+"""
15f218
+        self.assertEqual(
15f218
+            [],
15f218
+            lib.qdevice_connected_clusters(status)
15f218
+        )
15f218
+
15f218
+
15f218
 @mock.patch("pcs.lib.corosync.qdevice_net._get_output_certificate")
15f218
 @mock.patch("pcs.lib.corosync.qdevice_net._store_to_tmpfile")
15f218
 class QdeviceSignCertificateRequestTest(CertificateTestCase):
15f218
@@ -222,7 +272,7 @@ class QdeviceSignCertificateRequestTest(CertificateTestCase):
15f218
     )
15f218
     def test_success(self, mock_tmp_store, mock_get_cert):
15f218
         mock_tmp_store.return_value = self.mock_tmpfile
15f218
-        self.mock_runner.run.return_value = ("tool output", 0)
15f218
+        self.mock_runner.run.return_value = ("tool output", "", 0)
15f218
         mock_get_cert.return_value = "new certificate".encode("utf-8")
15f218
 
15f218
         result = lib.qdevice_sign_certificate_request(
15f218
@@ -293,7 +343,7 @@ class QdeviceSignCertificateRequestTest(CertificateTestCase):
15f218
     )
15f218
     def test_sign_error(self, mock_tmp_store, mock_get_cert):
15f218
         mock_tmp_store.return_value = self.mock_tmpfile
15f218
-        self.mock_runner.run.return_value = ("tool output error", 1)
15f218
+        self.mock_runner.run.return_value = ("stdout", "tool output error", 1)
15f218
 
15f218
         assert_raise_library_error(
15f218
             lambda: lib.qdevice_sign_certificate_request(
15f218
@@ -305,7 +355,7 @@ class QdeviceSignCertificateRequestTest(CertificateTestCase):
15f218
                 severity.ERROR,
15f218
                 report_codes.QDEVICE_CERTIFICATE_SIGN_ERROR,
15f218
                 {
15f218
-                    "reason": "tool output error",
15f218
+                    "reason": "tool output error\nstdout",
15f218
                 }
15f218
             )
15f218
         )
15f218
@@ -326,7 +376,7 @@ class QdeviceSignCertificateRequestTest(CertificateTestCase):
15f218
     )
15f218
     def test_output_read_error(self, mock_tmp_store, mock_get_cert):
15f218
         mock_tmp_store.return_value = self.mock_tmpfile
15f218
-        self.mock_runner.run.return_value = ("tool output", 0)
15f218
+        self.mock_runner.run.return_value = ("tool output", "", 0)
15f218
         mock_get_cert.side_effect = LibraryError
15f218
 
15f218
         self.assertRaises(
15f218
@@ -399,7 +449,7 @@ class ClientSetupTest(TestCase):
15f218
 
15f218
     @mock.patch("pcs.lib.corosync.qdevice_net.client_destroy")
15f218
     def test_success(self, mock_destroy):
15f218
-        self.mock_runner.run.return_value = ("tool output", 0)
15f218
+        self.mock_runner.run.return_value = ("tool output", "", 0)
15f218
 
15f218
         lib.client_setup(self.mock_runner, "certificate data".encode("utf-8"))
15f218
 
15f218
@@ -414,7 +464,7 @@ class ClientSetupTest(TestCase):
15f218
 
15f218
     @mock.patch("pcs.lib.corosync.qdevice_net.client_destroy")
15f218
     def test_init_error(self, mock_destroy):
15f218
-        self.mock_runner.run.return_value = ("tool output error", 1)
15f218
+        self.mock_runner.run.return_value = ("stdout", "tool output error", 1)
15f218
 
15f218
         assert_raise_library_error(
15f218
             lambda: lib.client_setup(
15f218
@@ -426,7 +476,7 @@ class ClientSetupTest(TestCase):
15f218
                 report_codes.QDEVICE_INITIALIZATION_ERROR,
15f218
                 {
15f218
                     "model": "net",
15f218
-                    "reason": "tool output error",
15f218
+                    "reason": "tool output error\nstdout",
15f218
                 }
15f218
             )
15f218
         )
15f218
@@ -448,7 +498,7 @@ class ClientGenerateCertificateRequestTest(CertificateTestCase):
15f218
         lambda: True
15f218
     )
15f218
     def test_success(self, mock_get_cert):
15f218
-        self.mock_runner.run.return_value = ("tool output", 0)
15f218
+        self.mock_runner.run.return_value = ("tool output", "", 0)
15f218
         mock_get_cert.return_value = "new certificate".encode("utf-8")
15f218
 
15f218
         result = lib.client_generate_certificate_request(
15f218
@@ -492,7 +542,7 @@ class ClientGenerateCertificateRequestTest(CertificateTestCase):
15f218
         lambda: True
15f218
     )
15f218
     def test_tool_error(self, mock_get_cert):
15f218
-        self.mock_runner.run.return_value = ("tool output error", 1)
15f218
+        self.mock_runner.run.return_value = ("stdout", "tool output error", 1)
15f218
 
15f218
         assert_raise_library_error(
15f218
             lambda: lib.client_generate_certificate_request(
15f218
@@ -504,7 +554,7 @@ class ClientGenerateCertificateRequestTest(CertificateTestCase):
15f218
                 report_codes.QDEVICE_INITIALIZATION_ERROR,
15f218
                 {
15f218
                     "model": "net",
15f218
-                    "reason": "tool output error",
15f218
+                    "reason": "tool output error\nstdout",
15f218
                 }
15f218
             )
15f218
         )
15f218
@@ -523,7 +573,7 @@ class ClientCertRequestToPk12Test(CertificateTestCase):
15f218
     )
15f218
     def test_success(self, mock_tmp_store, mock_get_cert):
15f218
         mock_tmp_store.return_value = self.mock_tmpfile
15f218
-        self.mock_runner.run.return_value = ("tool output", 0)
15f218
+        self.mock_runner.run.return_value = ("tool output", "", 0)
15f218
         mock_get_cert.return_value = "new certificate".encode("utf-8")
15f218
 
15f218
         result = lib.client_cert_request_to_pk12(
15f218
@@ -594,7 +644,7 @@ class ClientCertRequestToPk12Test(CertificateTestCase):
15f218
     )
15f218
     def test_transform_error(self, mock_tmp_store, mock_get_cert):
15f218
         mock_tmp_store.return_value = self.mock_tmpfile
15f218
-        self.mock_runner.run.return_value = ("tool output error", 1)
15f218
+        self.mock_runner.run.return_value = ("stdout", "tool output error", 1)
15f218
 
15f218
         assert_raise_library_error(
15f218
             lambda: lib.client_cert_request_to_pk12(
15f218
@@ -605,7 +655,7 @@ class ClientCertRequestToPk12Test(CertificateTestCase):
15f218
                 severity.ERROR,
15f218
                 report_codes.QDEVICE_CERTIFICATE_IMPORT_ERROR,
15f218
                 {
15f218
-                    "reason": "tool output error",
15f218
+                    "reason": "tool output error\nstdout",
15f218
                 }
15f218
             )
15f218
         )
15f218
@@ -625,7 +675,7 @@ class ClientCertRequestToPk12Test(CertificateTestCase):
15f218
     )
15f218
     def test_output_read_error(self, mock_tmp_store, mock_get_cert):
15f218
         mock_tmp_store.return_value = self.mock_tmpfile
15f218
-        self.mock_runner.run.return_value = ("tool output", 0)
15f218
+        self.mock_runner.run.return_value = ("tool output", "", 0)
15f218
         mock_get_cert.side_effect = LibraryError
15f218
 
15f218
         self.assertRaises(
15f218
@@ -657,7 +707,7 @@ class ClientImportCertificateAndKeyTest(CertificateTestCase):
15f218
     )
15f218
     def test_success(self, mock_tmp_store):
15f218
         mock_tmp_store.return_value = self.mock_tmpfile
15f218
-        self.mock_runner.run.return_value = ("tool output", 0)
15f218
+        self.mock_runner.run.return_value = ("tool output", "", 0)
15f218
 
15f218
         lib.client_import_certificate_and_key(
15f218
             self.mock_runner,
15f218
@@ -721,7 +771,7 @@ class ClientImportCertificateAndKeyTest(CertificateTestCase):
15f218
     )
15f218
     def test_import_error(self, mock_tmp_store):
15f218
         mock_tmp_store.return_value = self.mock_tmpfile
15f218
-        self.mock_runner.run.return_value = ("tool output error", 1)
15f218
+        self.mock_runner.run.return_value = ("stdout", "tool output error", 1)
15f218
 
15f218
         assert_raise_library_error(
15f218
             lambda: lib.client_import_certificate_and_key(
15f218
@@ -732,7 +782,7 @@ class ClientImportCertificateAndKeyTest(CertificateTestCase):
15f218
                 severity.ERROR,
15f218
                 report_codes.QDEVICE_CERTIFICATE_IMPORT_ERROR,
15f218
                 {
15f218
-                    "reason": "tool output error",
15f218
+                    "reason": "tool output error\nstdout",
15f218
                 }
15f218
             )
15f218
         )
15f218
diff --git a/pcs/test/test_lib_external.py b/pcs/test/test_lib_external.py
15f218
index aafbe85..d37747a 100644
15f218
--- a/pcs/test/test_lib_external.py
15f218
+++ b/pcs/test/test_lib_external.py
15f218
@@ -57,19 +57,23 @@ class CommandRunnerTest(TestCase):
15f218
         self.assertEqual(filtered_kwargs, kwargs)
15f218
 
15f218
     def test_basic(self, mock_popen):
15f218
-        expected_output = "expected output"
15f218
+        expected_stdout = "expected stdout"
15f218
+        expected_stderr = "expected stderr"
15f218
         expected_retval = 123
15f218
         command = ["a_command"]
15f218
         command_str = "a_command"
15f218
         mock_process = mock.MagicMock(spec_set=["communicate", "returncode"])
15f218
-        mock_process.communicate.return_value = (expected_output, "dummy")
15f218
+        mock_process.communicate.return_value = (
15f218
+            expected_stdout, expected_stderr
15f218
+        )
15f218
         mock_process.returncode = expected_retval
15f218
         mock_popen.return_value = mock_process
15f218
 
15f218
         runner = lib.CommandRunner(self.mock_logger, self.mock_reporter)
15f218
-        real_output, real_retval = runner.run(command)
15f218
+        real_stdout, real_stderr, real_retval = runner.run(command)
15f218
 
15f218
-        self.assertEqual(real_output, expected_output)
15f218
+        self.assertEqual(real_stdout, expected_stdout)
15f218
+        self.assertEqual(real_stderr, expected_stderr)
15f218
         self.assertEqual(real_retval, expected_retval)
15f218
         mock_process.communicate.assert_called_once_with(None)
15f218
         self.assert_popen_called_with(
15f218
@@ -82,9 +86,14 @@ class CommandRunnerTest(TestCase):
15f218
             mock.call("""\
15f218
 Finished running: {0}
15f218
 Return value: {1}
15f218
---Debug Output Start--
15f218
+--Debug Stdout Start--
15f218
 {2}
15f218
---Debug Output End--""".format(command_str, expected_retval, expected_output))
15f218
+--Debug Stdout End--
15f218
+--Debug Stderr Start--
15f218
+{3}
15f218
+--Debug Stderr End--""".format(
15f218
+                command_str, expected_retval, expected_stdout, expected_stderr
15f218
+            ))
15f218
         ]
15f218
         self.assertEqual(self.mock_logger.debug.call_count, len(logger_calls))
15f218
         self.mock_logger.debug.assert_has_calls(logger_calls)
15f218
@@ -105,19 +114,23 @@ Return value: {1}
15f218
                     {
15f218
                         "command": command_str,
15f218
                         "return_value": expected_retval,
15f218
-                        "stdout": expected_output,
15f218
+                        "stdout": expected_stdout,
15f218
+                        "stderr": expected_stderr,
15f218
                     }
15f218
                 )
15f218
             ]
15f218
         )
15f218
 
15f218
     def test_env(self, mock_popen):
15f218
-        expected_output = "expected output"
15f218
+        expected_stdout = "expected output"
15f218
+        expected_stderr = "expected stderr"
15f218
         expected_retval = 123
15f218
         command = ["a_command"]
15f218
         command_str = "a_command"
15f218
         mock_process = mock.MagicMock(spec_set=["communicate", "returncode"])
15f218
-        mock_process.communicate.return_value = (expected_output, "dummy")
15f218
+        mock_process.communicate.return_value = (
15f218
+            expected_stdout, expected_stderr
15f218
+        )
15f218
         mock_process.returncode = expected_retval
15f218
         mock_popen.return_value = mock_process
15f218
 
15f218
@@ -126,12 +139,13 @@ Return value: {1}
15f218
             self.mock_reporter,
15f218
             {"a": "a", "b": "b"}
15f218
         )
15f218
-        real_output, real_retval = runner.run(
15f218
+        real_stdout, real_stderr, real_retval = runner.run(
15f218
             command,
15f218
             env_extend={"b": "B", "c": "C"}
15f218
         )
15f218
 
15f218
-        self.assertEqual(real_output, expected_output)
15f218
+        self.assertEqual(real_stdout, expected_stdout)
15f218
+        self.assertEqual(real_stderr, expected_stderr)
15f218
         self.assertEqual(real_retval, expected_retval)
15f218
         mock_process.communicate.assert_called_once_with(None)
15f218
         self.assert_popen_called_with(
15f218
@@ -144,9 +158,14 @@ Return value: {1}
15f218
             mock.call("""\
15f218
 Finished running: {0}
15f218
 Return value: {1}
15f218
---Debug Output Start--
15f218
+--Debug Stdout Start--
15f218
 {2}
15f218
---Debug Output End--""".format(command_str, expected_retval, expected_output))
15f218
+--Debug Stdout End--
15f218
+--Debug Stderr Start--
15f218
+{3}
15f218
+--Debug Stderr End--""".format(
15f218
+                command_str, expected_retval, expected_stdout, expected_stderr
15f218
+            ))
15f218
         ]
15f218
         self.assertEqual(self.mock_logger.debug.call_count, len(logger_calls))
15f218
         self.mock_logger.debug.assert_has_calls(logger_calls)
15f218
@@ -167,27 +186,34 @@ Return value: {1}
15f218
                     {
15f218
                         "command": command_str,
15f218
                         "return_value": expected_retval,
15f218
-                        "stdout": expected_output,
15f218
+                        "stdout": expected_stdout,
15f218
+                        "stderr": expected_stderr,
15f218
                     }
15f218
                 )
15f218
             ]
15f218
         )
15f218
 
15f218
     def test_stdin(self, mock_popen):
15f218
-        expected_output = "expected output"
15f218
+        expected_stdout = "expected output"
15f218
+        expected_stderr = "expected stderr"
15f218
         expected_retval = 123
15f218
         command = ["a_command"]
15f218
         command_str = "a_command"
15f218
         stdin = "stdin string"
15f218
         mock_process = mock.MagicMock(spec_set=["communicate", "returncode"])
15f218
-        mock_process.communicate.return_value = (expected_output, "dummy")
15f218
+        mock_process.communicate.return_value = (
15f218
+            expected_stdout, expected_stderr
15f218
+        )
15f218
         mock_process.returncode = expected_retval
15f218
         mock_popen.return_value = mock_process
15f218
 
15f218
         runner = lib.CommandRunner(self.mock_logger, self.mock_reporter)
15f218
-        real_output, real_retval = runner.run(command, stdin_string=stdin)
15f218
+        real_stdout, real_stderr, real_retval = runner.run(
15f218
+            command, stdin_string=stdin
15f218
+        )
15f218
 
15f218
-        self.assertEqual(real_output, expected_output)
15f218
+        self.assertEqual(real_stdout, expected_stdout)
15f218
+        self.assertEqual(real_stderr, expected_stderr)
15f218
         self.assertEqual(real_retval, expected_retval)
15f218
         mock_process.communicate.assert_called_once_with(stdin)
15f218
         self.assert_popen_called_with(
15f218
@@ -204,9 +230,14 @@ Running: {0}
15f218
             mock.call("""\
15f218
 Finished running: {0}
15f218
 Return value: {1}
15f218
---Debug Output Start--
15f218
+--Debug Stdout Start--
15f218
 {2}
15f218
---Debug Output End--""".format(command_str, expected_retval, expected_output))
15f218
+--Debug Stdout End--
15f218
+--Debug Stderr Start--
15f218
+{3}
15f218
+--Debug Stderr End--""".format(
15f218
+                command_str, expected_retval, expected_stdout, expected_stderr
15f218
+            ))
15f218
         ]
15f218
         self.assertEqual(self.mock_logger.debug.call_count, len(logger_calls))
15f218
         self.mock_logger.debug.assert_has_calls(logger_calls)
15f218
@@ -227,7 +258,8 @@ Return value: {1}
15f218
                     {
15f218
                         "command": command_str,
15f218
                         "return_value": expected_retval,
15f218
-                        "stdout": expected_output,
15f218
+                        "stdout": expected_stdout,
15f218
+                        "stderr": expected_stderr,
15f218
                     }
15f218
                 )
15f218
             ]
15f218
@@ -957,7 +989,7 @@ class ParallelCommunicationHelperTest(TestCase):
15f218
 class IsCmanClusterTest(TestCase):
15f218
     def template_test(self, is_cman, corosync_output, corosync_retval=0):
15f218
         mock_runner = mock.MagicMock(spec_set=lib.CommandRunner)
15f218
-        mock_runner.run.return_value = (corosync_output, corosync_retval)
15f218
+        mock_runner.run.return_value = (corosync_output, "", corosync_retval)
15f218
         self.assertEqual(is_cman, lib.is_cman_cluster(mock_runner))
15f218
         mock_runner.run.assert_called_once_with([
15f218
             os.path.join(settings.corosync_binaries, "corosync"),
15f218
@@ -1021,7 +1053,7 @@ class DisableServiceTest(TestCase):
15f218
     def test_systemctl(self, mock_is_installed, mock_systemctl):
15f218
         mock_is_installed.return_value = True
15f218
         mock_systemctl.return_value = True
15f218
-        self.mock_runner.run.return_value = ("", 0)
15f218
+        self.mock_runner.run.return_value = ("", "Removed symlink", 0)
15f218
         lib.disable_service(self.mock_runner, self.service)
15f218
         self.mock_runner.run.assert_called_once_with(
15f218
             ["systemctl", "disable", self.service + ".service"]
15f218
@@ -1030,7 +1062,7 @@ class DisableServiceTest(TestCase):
15f218
     def test_systemctl_failed(self, mock_is_installed, mock_systemctl):
15f218
         mock_is_installed.return_value = True
15f218
         mock_systemctl.return_value = True
15f218
-        self.mock_runner.run.return_value = ("", 1)
15f218
+        self.mock_runner.run.return_value = ("", "Failed", 1)
15f218
         self.assertRaises(
15f218
             lib.DisableServiceError,
15f218
             lambda: lib.disable_service(self.mock_runner, self.service)
15f218
@@ -1042,7 +1074,7 @@ class DisableServiceTest(TestCase):
15f218
     def test_not_systemctl(self, mock_is_installed, mock_systemctl):
15f218
         mock_is_installed.return_value = True
15f218
         mock_systemctl.return_value = False
15f218
-        self.mock_runner.run.return_value = ("", 0)
15f218
+        self.mock_runner.run.return_value = ("", "", 0)
15f218
         lib.disable_service(self.mock_runner, self.service)
15f218
         self.mock_runner.run.assert_called_once_with(
15f218
             ["chkconfig", self.service, "off"]
15f218
@@ -1051,7 +1083,7 @@ class DisableServiceTest(TestCase):
15f218
     def test_not_systemctl_failed(self, mock_is_installed, mock_systemctl):
15f218
         mock_is_installed.return_value = True
15f218
         mock_systemctl.return_value = False
15f218
-        self.mock_runner.run.return_value = ("", 1)
15f218
+        self.mock_runner.run.return_value = ("", "error", 1)
15f218
         self.assertRaises(
15f218
             lib.DisableServiceError,
15f218
             lambda: lib.disable_service(self.mock_runner, self.service)
15f218
@@ -1079,7 +1111,7 @@ class DisableServiceTest(TestCase):
15f218
     def test_instance_systemctl(self, mock_is_installed, mock_systemctl):
15f218
         mock_is_installed.return_value = True
15f218
         mock_systemctl.return_value = True
15f218
-        self.mock_runner.run.return_value = ("", 0)
15f218
+        self.mock_runner.run.return_value = ("", "Removed symlink", 0)
15f218
         lib.disable_service(self.mock_runner, self.service, instance="test")
15f218
         self.mock_runner.run.assert_called_once_with([
15f218
             "systemctl",
15f218
@@ -1090,7 +1122,7 @@ class DisableServiceTest(TestCase):
15f218
     def test_instance_not_systemctl(self, mock_is_installed, mock_systemctl):
15f218
         mock_is_installed.return_value = True
15f218
         mock_systemctl.return_value = False
15f218
-        self.mock_runner.run.return_value = ("", 0)
15f218
+        self.mock_runner.run.return_value = ("", "", 0)
15f218
         lib.disable_service(self.mock_runner, self.service, instance="test")
15f218
         self.mock_runner.run.assert_called_once_with(
15f218
             ["chkconfig", self.service, "off"]
15f218
@@ -1104,7 +1136,7 @@ class EnableServiceTest(TestCase):
15f218
 
15f218
     def test_systemctl(self, mock_systemctl):
15f218
         mock_systemctl.return_value = True
15f218
-        self.mock_runner.run.return_value = ("", 0)
15f218
+        self.mock_runner.run.return_value = ("", "Created symlink", 0)
15f218
         lib.enable_service(self.mock_runner, self.service)
15f218
         self.mock_runner.run.assert_called_once_with(
15f218
             ["systemctl", "enable", self.service + ".service"]
15f218
@@ -1112,7 +1144,7 @@ class EnableServiceTest(TestCase):
15f218
 
15f218
     def test_systemctl_failed(self, mock_systemctl):
15f218
         mock_systemctl.return_value = True
15f218
-        self.mock_runner.run.return_value = ("", 1)
15f218
+        self.mock_runner.run.return_value = ("", "Failed", 1)
15f218
         self.assertRaises(
15f218
             lib.EnableServiceError,
15f218
             lambda: lib.enable_service(self.mock_runner, self.service)
15f218
@@ -1123,7 +1155,7 @@ class EnableServiceTest(TestCase):
15f218
 
15f218
     def test_not_systemctl(self, mock_systemctl):
15f218
         mock_systemctl.return_value = False
15f218
-        self.mock_runner.run.return_value = ("", 0)
15f218
+        self.mock_runner.run.return_value = ("", "", 0)
15f218
         lib.enable_service(self.mock_runner, self.service)
15f218
         self.mock_runner.run.assert_called_once_with(
15f218
             ["chkconfig", self.service, "on"]
15f218
@@ -1131,7 +1163,7 @@ class EnableServiceTest(TestCase):
15f218
 
15f218
     def test_not_systemctl_failed(self, mock_systemctl):
15f218
         mock_systemctl.return_value = False
15f218
-        self.mock_runner.run.return_value = ("", 1)
15f218
+        self.mock_runner.run.return_value = ("", "error", 1)
15f218
         self.assertRaises(
15f218
             lib.EnableServiceError,
15f218
             lambda: lib.enable_service(self.mock_runner, self.service)
15f218
@@ -1142,7 +1174,7 @@ class EnableServiceTest(TestCase):
15f218
 
15f218
     def test_instance_systemctl(self, mock_systemctl):
15f218
         mock_systemctl.return_value = True
15f218
-        self.mock_runner.run.return_value = ("", 0)
15f218
+        self.mock_runner.run.return_value = ("", "Created symlink", 0)
15f218
         lib.enable_service(self.mock_runner, self.service, instance="test")
15f218
         self.mock_runner.run.assert_called_once_with([
15f218
             "systemctl",
15f218
@@ -1152,7 +1184,7 @@ class EnableServiceTest(TestCase):
15f218
 
15f218
     def test_instance_not_systemctl(self, mock_systemctl):
15f218
         mock_systemctl.return_value = False
15f218
-        self.mock_runner.run.return_value = ("", 0)
15f218
+        self.mock_runner.run.return_value = ("", "", 0)
15f218
         lib.enable_service(self.mock_runner, self.service, instance="test")
15f218
         self.mock_runner.run.assert_called_once_with(
15f218
             ["chkconfig", self.service, "on"]
15f218
@@ -1167,7 +1199,7 @@ class StartServiceTest(TestCase):
15f218
 
15f218
     def test_systemctl(self, mock_systemctl):
15f218
         mock_systemctl.return_value = True
15f218
-        self.mock_runner.run.return_value = ("", 0)
15f218
+        self.mock_runner.run.return_value = ("", "", 0)
15f218
         lib.start_service(self.mock_runner, self.service)
15f218
         self.mock_runner.run.assert_called_once_with(
15f218
             ["systemctl", "start", self.service + ".service"]
15f218
@@ -1175,7 +1207,7 @@ class StartServiceTest(TestCase):
15f218
 
15f218
     def test_systemctl_failed(self, mock_systemctl):
15f218
         mock_systemctl.return_value = True
15f218
-        self.mock_runner.run.return_value = ("", 1)
15f218
+        self.mock_runner.run.return_value = ("", "Failed", 1)
15f218
         self.assertRaises(
15f218
             lib.StartServiceError,
15f218
             lambda: lib.start_service(self.mock_runner, self.service)
15f218
@@ -1186,7 +1218,7 @@ class StartServiceTest(TestCase):
15f218
 
15f218
     def test_not_systemctl(self, mock_systemctl):
15f218
         mock_systemctl.return_value = False
15f218
-        self.mock_runner.run.return_value = ("", 0)
15f218
+        self.mock_runner.run.return_value = ("Starting...", "", 0)
15f218
         lib.start_service(self.mock_runner, self.service)
15f218
         self.mock_runner.run.assert_called_once_with(
15f218
             ["service", self.service, "start"]
15f218
@@ -1194,7 +1226,7 @@ class StartServiceTest(TestCase):
15f218
 
15f218
     def test_not_systemctl_failed(self, mock_systemctl):
15f218
         mock_systemctl.return_value = False
15f218
-        self.mock_runner.run.return_value = ("", 1)
15f218
+        self.mock_runner.run.return_value = ("", "unrecognized", 1)
15f218
         self.assertRaises(
15f218
             lib.StartServiceError,
15f218
             lambda: lib.start_service(self.mock_runner, self.service)
15f218
@@ -1205,7 +1237,7 @@ class StartServiceTest(TestCase):
15f218
 
15f218
     def test_instance_systemctl(self, mock_systemctl):
15f218
         mock_systemctl.return_value = True
15f218
-        self.mock_runner.run.return_value = ("", 0)
15f218
+        self.mock_runner.run.return_value = ("", "", 0)
15f218
         lib.start_service(self.mock_runner, self.service, instance="test")
15f218
         self.mock_runner.run.assert_called_once_with([
15f218
             "systemctl", "start", "{0}@{1}.service".format(self.service, "test")
15f218
@@ -1213,7 +1245,7 @@ class StartServiceTest(TestCase):
15f218
 
15f218
     def test_instance_not_systemctl(self, mock_systemctl):
15f218
         mock_systemctl.return_value = False
15f218
-        self.mock_runner.run.return_value = ("", 0)
15f218
+        self.mock_runner.run.return_value = ("Starting...", "", 0)
15f218
         lib.start_service(self.mock_runner, self.service, instance="test")
15f218
         self.mock_runner.run.assert_called_once_with(
15f218
             ["service", self.service, "start"]
15f218
@@ -1228,7 +1260,7 @@ class StopServiceTest(TestCase):
15f218
 
15f218
     def test_systemctl(self, mock_systemctl):
15f218
         mock_systemctl.return_value = True
15f218
-        self.mock_runner.run.return_value = ("", 0)
15f218
+        self.mock_runner.run.return_value = ("", "", 0)
15f218
         lib.stop_service(self.mock_runner, self.service)
15f218
         self.mock_runner.run.assert_called_once_with(
15f218
             ["systemctl", "stop", self.service + ".service"]
15f218
@@ -1236,7 +1268,7 @@ class StopServiceTest(TestCase):
15f218
 
15f218
     def test_systemctl_failed(self, mock_systemctl):
15f218
         mock_systemctl.return_value = True
15f218
-        self.mock_runner.run.return_value = ("", 1)
15f218
+        self.mock_runner.run.return_value = ("", "Failed", 1)
15f218
         self.assertRaises(
15f218
             lib.StopServiceError,
15f218
             lambda: lib.stop_service(self.mock_runner, self.service)
15f218
@@ -1247,7 +1279,7 @@ class StopServiceTest(TestCase):
15f218
 
15f218
     def test_not_systemctl(self, mock_systemctl):
15f218
         mock_systemctl.return_value = False
15f218
-        self.mock_runner.run.return_value = ("", 0)
15f218
+        self.mock_runner.run.return_value = ("Stopping...", "", 0)
15f218
         lib.stop_service(self.mock_runner, self.service)
15f218
         self.mock_runner.run.assert_called_once_with(
15f218
             ["service", self.service, "stop"]
15f218
@@ -1255,7 +1287,7 @@ class StopServiceTest(TestCase):
15f218
 
15f218
     def test_not_systemctl_failed(self, mock_systemctl):
15f218
         mock_systemctl.return_value = False
15f218
-        self.mock_runner.run.return_value = ("", 1)
15f218
+        self.mock_runner.run.return_value = ("", "unrecognized", 1)
15f218
         self.assertRaises(
15f218
             lib.StopServiceError,
15f218
             lambda: lib.stop_service(self.mock_runner, self.service)
15f218
@@ -1266,7 +1298,7 @@ class StopServiceTest(TestCase):
15f218
 
15f218
     def test_instance_systemctl(self, mock_systemctl):
15f218
         mock_systemctl.return_value = True
15f218
-        self.mock_runner.run.return_value = ("", 0)
15f218
+        self.mock_runner.run.return_value = ("", "", 0)
15f218
         lib.stop_service(self.mock_runner, self.service, instance="test")
15f218
         self.mock_runner.run.assert_called_once_with([
15f218
             "systemctl", "stop", "{0}@{1}.service".format(self.service, "test")
15f218
@@ -1274,7 +1306,7 @@ class StopServiceTest(TestCase):
15f218
 
15f218
     def test_instance_not_systemctl(self, mock_systemctl):
15f218
         mock_systemctl.return_value = False
15f218
-        self.mock_runner.run.return_value = ("", 0)
15f218
+        self.mock_runner.run.return_value = ("Stopping...", "", 0)
15f218
         lib.stop_service(self.mock_runner, self.service, instance="test")
15f218
         self.mock_runner.run.assert_called_once_with(
15f218
             ["service", self.service, "stop"]
15f218
@@ -1287,14 +1319,14 @@ class KillServicesTest(TestCase):
15f218
         self.services = ["service1", "service2"]
15f218
 
15f218
     def test_success(self):
15f218
-        self.mock_runner.run.return_value = ("", 0)
15f218
+        self.mock_runner.run.return_value = ("", "", 0)
15f218
         lib.kill_services(self.mock_runner, self.services)
15f218
         self.mock_runner.run.assert_called_once_with(
15f218
             ["killall", "--quiet", "--signal", "9", "--"] + self.services
15f218
         )
15f218
 
15f218
     def test_failed(self):
15f218
-        self.mock_runner.run.return_value = ("error", 1)
15f218
+        self.mock_runner.run.return_value = ("", "error", 1)
15f218
         self.assertRaises(
15f218
             lib.KillServicesError,
15f218
             lambda: lib.kill_services(self.mock_runner, self.services)
15f218
@@ -1304,7 +1336,7 @@ class KillServicesTest(TestCase):
15f218
         )
15f218
 
15f218
     def test_service_not_running(self):
15f218
-        self.mock_runner.run.return_value = ("", 1)
15f218
+        self.mock_runner.run.return_value = ("", "", 1)
15f218
         lib.kill_services(self.mock_runner, self.services)
15f218
         self.mock_runner.run.assert_called_once_with(
15f218
             ["killall", "--quiet", "--signal", "9", "--"] + self.services
15f218
@@ -1348,7 +1380,7 @@ class IsServiceEnabledTest(TestCase):
15f218
 
15f218
     def test_systemctl_enabled(self, mock_systemctl):
15f218
         mock_systemctl.return_value = True
15f218
-        self.mock_runner.run.return_value = ("enabled\n", 0)
15f218
+        self.mock_runner.run.return_value = ("enabled\n", "", 0)
15f218
         self.assertTrue(lib.is_service_enabled(self.mock_runner, self.service))
15f218
         self.mock_runner.run.assert_called_once_with(
15f218
             ["systemctl", "is-enabled", self.service + ".service"]
15f218
@@ -1356,7 +1388,7 @@ class IsServiceEnabledTest(TestCase):
15f218
 
15f218
     def test_systemctl_disabled(self, mock_systemctl):
15f218
         mock_systemctl.return_value = True
15f218
-        self.mock_runner.run.return_value = ("disabled\n", 2)
15f218
+        self.mock_runner.run.return_value = ("disabled\n", "", 2)
15f218
         self.assertFalse(lib.is_service_enabled(self.mock_runner, self.service))
15f218
         self.mock_runner.run.assert_called_once_with(
15f218
             ["systemctl", "is-enabled", self.service + ".service"]
15f218
@@ -1364,7 +1396,7 @@ class IsServiceEnabledTest(TestCase):
15f218
 
15f218
     def test_not_systemctl_enabled(self, mock_systemctl):
15f218
         mock_systemctl.return_value = False
15f218
-        self.mock_runner.run.return_value = ("", 0)
15f218
+        self.mock_runner.run.return_value = ("", "", 0)
15f218
         self.assertTrue(lib.is_service_enabled(self.mock_runner, self.service))
15f218
         self.mock_runner.run.assert_called_once_with(
15f218
             ["chkconfig", self.service]
15f218
@@ -1372,7 +1404,7 @@ class IsServiceEnabledTest(TestCase):
15f218
 
15f218
     def test_not_systemctl_disabled(self, mock_systemctl):
15f218
         mock_systemctl.return_value = False
15f218
-        self.mock_runner.run.return_value = ("", 3)
15f218
+        self.mock_runner.run.return_value = ("", "", 3)
15f218
         self.assertFalse(lib.is_service_enabled(self.mock_runner, self.service))
15f218
         self.mock_runner.run.assert_called_once_with(
15f218
             ["chkconfig", self.service]
15f218
@@ -1387,7 +1419,7 @@ class IsServiceRunningTest(TestCase):
15f218
 
15f218
     def test_systemctl_running(self, mock_systemctl):
15f218
         mock_systemctl.return_value = True
15f218
-        self.mock_runner.run.return_value = ("", 0)
15f218
+        self.mock_runner.run.return_value = ("active", "", 0)
15f218
         self.assertTrue(lib.is_service_running(self.mock_runner, self.service))
15f218
         self.mock_runner.run.assert_called_once_with(
15f218
             ["systemctl", "is-active", self.service + ".service"]
15f218
@@ -1395,7 +1427,7 @@ class IsServiceRunningTest(TestCase):
15f218
 
15f218
     def test_systemctl_not_running(self, mock_systemctl):
15f218
         mock_systemctl.return_value = True
15f218
-        self.mock_runner.run.return_value = ("", 2)
15f218
+        self.mock_runner.run.return_value = ("inactive", "", 2)
15f218
         self.assertFalse(lib.is_service_running(self.mock_runner, self.service))
15f218
         self.mock_runner.run.assert_called_once_with(
15f218
             ["systemctl", "is-active", self.service + ".service"]
15f218
@@ -1403,7 +1435,7 @@ class IsServiceRunningTest(TestCase):
15f218
 
15f218
     def test_not_systemctl_running(self, mock_systemctl):
15f218
         mock_systemctl.return_value = False
15f218
-        self.mock_runner.run.return_value = ("", 0)
15f218
+        self.mock_runner.run.return_value = ("is running", "", 0)
15f218
         self.assertTrue(lib.is_service_running(self.mock_runner, self.service))
15f218
         self.mock_runner.run.assert_called_once_with(
15f218
             ["service", self.service, "status"]
15f218
@@ -1411,7 +1443,7 @@ class IsServiceRunningTest(TestCase):
15f218
 
15f218
     def test_not_systemctl_not_running(self, mock_systemctl):
15f218
         mock_systemctl.return_value = False
15f218
-        self.mock_runner.run.return_value = ("", 3)
15f218
+        self.mock_runner.run.return_value = ("is stopped", "", 3)
15f218
         self.assertFalse(lib.is_service_running(self.mock_runner, self.service))
15f218
         self.mock_runner.run.assert_called_once_with(
15f218
             ["service", self.service, "status"]
15f218
@@ -1484,7 +1516,7 @@ sbd.service                                 enabled
15f218
 pacemaker.service                           enabled
15f218
 
15f218
 3 unit files listed.
15f218
-""", 0)
15f218
+""", "", 0)
15f218
         self.assertEqual(
15f218
             lib.get_systemd_services(self.mock_runner),
15f218
             ["pcsd", "sbd", "pacemaker"]
15f218
@@ -1496,7 +1528,7 @@ pacemaker.service                           enabled
15f218
 
15f218
     def test_failed(self, mock_is_systemctl):
15f218
         mock_is_systemctl.return_value = True
15f218
-        self.mock_runner.run.return_value = ("failed", 1)
15f218
+        self.mock_runner.run.return_value = ("stdout", "failed", 1)
15f218
         self.assertEqual(lib.get_systemd_services(self.mock_runner), [])
15f218
         self.assertEqual(mock_is_systemctl.call_count, 1)
15f218
         self.mock_runner.run.assert_called_once_with(
15f218
@@ -1505,10 +1537,9 @@ pacemaker.service                           enabled
15f218
 
15f218
     def test_not_systemd(self, mock_is_systemctl):
15f218
         mock_is_systemctl.return_value = False
15f218
-        self.mock_runner.run.return_value = ("", 0)
15f218
         self.assertEqual(lib.get_systemd_services(self.mock_runner), [])
15f218
-        self.assertEqual(mock_is_systemctl.call_count, 1)
15f218
-        self.assertEqual(self.mock_runner.call_count, 0)
15f218
+        mock_is_systemctl.assert_called_once_with()
15f218
+        self.mock_runner.assert_not_called()
15f218
 
15f218
 
15f218
 @mock.patch("pcs.lib.external.is_systemctl")
15f218
@@ -1522,24 +1553,20 @@ class GetNonSystemdServicesTest(TestCase):
15f218
 pcsd           	0:off	1:off	2:on	3:on	4:on	5:on	6:off
15f218
 sbd            	0:off	1:on	2:on	3:on	4:on	5:on	6:off
15f218
 pacemaker      	0:off	1:off	2:off	3:off	4:off	5:off	6:off
15f218
-""", 0)
15f218
+""", "", 0)
15f218
         self.assertEqual(
15f218
             lib.get_non_systemd_services(self.mock_runner),
15f218
             ["pcsd", "sbd", "pacemaker"]
15f218
         )
15f218
         self.assertEqual(mock_is_systemctl.call_count, 1)
15f218
-        self.mock_runner.run.assert_called_once_with(
15f218
-            ["chkconfig"], ignore_stderr=True
15f218
-        )
15f218
+        self.mock_runner.run.assert_called_once_with(["chkconfig"])
15f218
 
15f218
     def test_failed(self, mock_is_systemctl):
15f218
         mock_is_systemctl.return_value = False
15f218
-        self.mock_runner.run.return_value = ("failed", 1)
15f218
+        self.mock_runner.run.return_value = ("stdout", "failed", 1)
15f218
         self.assertEqual(lib.get_non_systemd_services(self.mock_runner), [])
15f218
         self.assertEqual(mock_is_systemctl.call_count, 1)
15f218
-        self.mock_runner.run.assert_called_once_with(
15f218
-            ["chkconfig"], ignore_stderr=True
15f218
-        )
15f218
+        self.mock_runner.run.assert_called_once_with(["chkconfig"])
15f218
 
15f218
     def test_systemd(self, mock_is_systemctl):
15f218
         mock_is_systemctl.return_value = True
15f218
diff --git a/pcs/test/test_lib_pacemaker.py b/pcs/test/test_lib_pacemaker.py
15f218
index c475db6..7ca7b77 100644
15f218
--- a/pcs/test/test_lib_pacemaker.py
15f218
+++ b/pcs/test/test_lib_pacemaker.py
15f218
@@ -64,21 +64,31 @@ class LibraryPacemakerNodeStatusTest(LibraryPacemakerTest):
15f218
 
15f218
 class GetClusterStatusXmlTest(LibraryPacemakerTest):
15f218
     def test_success(self):
15f218
-        expected_xml = "<xml />"
15f218
+        expected_stdout = "<xml />"
15f218
+        expected_stderr = ""
15f218
         expected_retval = 0
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (expected_xml, expected_retval)
15f218
+        mock_runner.run.return_value = (
15f218
+            expected_stdout,
15f218
+            expected_stderr,
15f218
+            expected_retval
15f218
+        )
15f218
 
15f218
         real_xml = lib.get_cluster_status_xml(mock_runner)
15f218
 
15f218
         mock_runner.run.assert_called_once_with(self.crm_mon_cmd())
15f218
-        self.assertEqual(expected_xml, real_xml)
15f218
+        self.assertEqual(expected_stdout, real_xml)
15f218
 
15f218
     def test_error(self):
15f218
-        expected_error = "some error"
15f218
+        expected_stdout = "some info"
15f218
+        expected_stderr = "some error"
15f218
         expected_retval = 1
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (expected_error, expected_retval)
15f218
+        mock_runner.run.return_value = (
15f218
+            expected_stdout,
15f218
+            expected_stderr,
15f218
+            expected_retval
15f218
+        )
15f218
 
15f218
         assert_raise_library_error(
15f218
             lambda: lib.get_cluster_status_xml(mock_runner),
15f218
@@ -86,8 +96,7 @@ class GetClusterStatusXmlTest(LibraryPacemakerTest):
15f218
                 Severity.ERROR,
15f218
                 report_codes.CRM_MON_ERROR,
15f218
                 {
15f218
-                    "return_value": expected_retval,
15f218
-                    "stdout": expected_error,
15f218
+                    "reason": expected_stderr + "\n" + expected_stdout,
15f218
                 }
15f218
             )
15f218
         )
15f218
@@ -96,23 +105,33 @@ class GetClusterStatusXmlTest(LibraryPacemakerTest):
15f218
 
15f218
 class GetCibXmlTest(LibraryPacemakerTest):
15f218
     def test_success(self):
15f218
-        expected_xml = "<xml />"
15f218
+        expected_stdout = "<xml />"
15f218
+        expected_stderr = ""
15f218
         expected_retval = 0
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (expected_xml, expected_retval)
15f218
+        mock_runner.run.return_value = (
15f218
+            expected_stdout,
15f218
+            expected_stderr,
15f218
+            expected_retval
15f218
+        )
15f218
 
15f218
         real_xml = lib.get_cib_xml(mock_runner)
15f218
 
15f218
         mock_runner.run.assert_called_once_with(
15f218
             [self.path("cibadmin"), "--local", "--query"]
15f218
         )
15f218
-        self.assertEqual(expected_xml, real_xml)
15f218
+        self.assertEqual(expected_stdout, real_xml)
15f218
 
15f218
     def test_error(self):
15f218
-        expected_error = "some error"
15f218
+        expected_stdout = "some info"
15f218
+        expected_stderr = "some error"
15f218
         expected_retval = 1
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (expected_error, expected_retval)
15f218
+        mock_runner.run.return_value = (
15f218
+            expected_stdout,
15f218
+            expected_stderr,
15f218
+            expected_retval
15f218
+        )
15f218
 
15f218
         assert_raise_library_error(
15f218
             lambda: lib.get_cib_xml(mock_runner),
15f218
@@ -120,8 +139,7 @@ class GetCibXmlTest(LibraryPacemakerTest):
15f218
                 Severity.ERROR,
15f218
                 report_codes.CIB_LOAD_ERROR,
15f218
                 {
15f218
-                    "return_value": expected_retval,
15f218
-                    "stdout": expected_error,
15f218
+                    "reason": expected_stderr + "\n" + expected_stdout,
15f218
                 }
15f218
             )
15f218
         )
15f218
@@ -131,11 +149,16 @@ class GetCibXmlTest(LibraryPacemakerTest):
15f218
         )
15f218
 
15f218
     def test_success_scope(self):
15f218
-        expected_xml = "<xml />"
15f218
+        expected_stdout = "<xml />"
15f218
+        expected_stderr = ""
15f218
         expected_retval = 0
15f218
         scope = "test_scope"
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (expected_xml, expected_retval)
15f218
+        mock_runner.run.return_value = (
15f218
+            expected_stdout,
15f218
+            expected_stderr,
15f218
+            expected_retval
15f218
+        )
15f218
 
15f218
         real_xml = lib.get_cib_xml(mock_runner, scope)
15f218
 
15f218
@@ -145,14 +168,19 @@ class GetCibXmlTest(LibraryPacemakerTest):
15f218
                 "--local", "--query", "--scope={0}".format(scope)
15f218
             ]
15f218
         )
15f218
-        self.assertEqual(expected_xml, real_xml)
15f218
+        self.assertEqual(expected_stdout, real_xml)
15f218
 
15f218
     def test_scope_error(self):
15f218
-        expected_error = "some error"
15f218
+        expected_stdout = "some info"
15f218
+        expected_stderr = "some error"
15f218
         expected_retval = 6
15f218
         scope = "test_scope"
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (expected_error, expected_retval)
15f218
+        mock_runner.run.return_value = (
15f218
+            expected_stdout,
15f218
+            expected_stderr,
15f218
+            expected_retval
15f218
+        )
15f218
 
15f218
         assert_raise_library_error(
15f218
             lambda: lib.get_cib_xml(mock_runner, scope=scope),
15f218
@@ -161,8 +189,7 @@ class GetCibXmlTest(LibraryPacemakerTest):
15f218
                 report_codes.CIB_LOAD_ERROR_SCOPE_MISSING,
15f218
                 {
15f218
                     "scope": scope,
15f218
-                    "return_value": expected_retval,
15f218
-                    "stdout": expected_error,
15f218
+                    "reason": expected_stderr + "\n" + expected_stdout,
15f218
                 }
15f218
             )
15f218
         )
15f218
@@ -194,10 +221,15 @@ class GetCibTest(LibraryPacemakerTest):
15f218
 class ReplaceCibConfigurationTest(LibraryPacemakerTest):
15f218
     def test_success(self):
15f218
         xml = "<xml/>"
15f218
-        expected_output = "expected output"
15f218
+        expected_stdout = "expected output"
15f218
+        expected_stderr = ""
15f218
         expected_retval = 0
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (expected_output, expected_retval)
15f218
+        mock_runner.run.return_value = (
15f218
+            expected_stdout,
15f218
+            expected_stderr,
15f218
+            expected_retval
15f218
+        )
15f218
 
15f218
         lib.replace_cib_configuration(
15f218
             mock_runner,
15f218
@@ -214,10 +246,15 @@ class ReplaceCibConfigurationTest(LibraryPacemakerTest):
15f218
 
15f218
     def test_cib_upgraded(self):
15f218
         xml = "<xml/>"
15f218
-        expected_output = "expected output"
15f218
+        expected_stdout = "expected output"
15f218
+        expected_stderr = ""
15f218
         expected_retval = 0
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (expected_output, expected_retval)
15f218
+        mock_runner.run.return_value = (
15f218
+            expected_stdout,
15f218
+            expected_stderr,
15f218
+            expected_retval
15f218
+        )
15f218
 
15f218
         lib.replace_cib_configuration(
15f218
             mock_runner, XmlManipulation.from_str(xml).tree, True
15f218
@@ -230,10 +267,15 @@ class ReplaceCibConfigurationTest(LibraryPacemakerTest):
15f218
 
15f218
     def test_error(self):
15f218
         xml = "<xml/>"
15f218
-        expected_error = "expected error"
15f218
+        expected_stdout = "expected output"
15f218
+        expected_stderr = "expected stderr"
15f218
         expected_retval = 1
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (expected_error, expected_retval)
15f218
+        mock_runner.run.return_value = (
15f218
+            expected_stdout,
15f218
+            expected_stderr,
15f218
+            expected_retval
15f218
+        )
15f218
 
15f218
         assert_raise_library_error(
15f218
             lambda: lib.replace_cib_configuration(
15f218
@@ -245,8 +287,8 @@ class ReplaceCibConfigurationTest(LibraryPacemakerTest):
15f218
                 Severity.ERROR,
15f218
                 report_codes.CIB_PUSH_ERROR,
15f218
                 {
15f218
-                    "return_value": expected_retval,
15f218
-                    "stdout": expected_error,
15f218
+                    "reason": expected_stderr,
15f218
+                    "pushed_cib": expected_stdout,
15f218
                 }
15f218
             )
15f218
         )
15f218
@@ -261,10 +303,15 @@ class ReplaceCibConfigurationTest(LibraryPacemakerTest):
15f218
 
15f218
 class GetLocalNodeStatusTest(LibraryPacemakerNodeStatusTest):
15f218
     def test_offline(self):
15f218
-        expected_error = "some error"
15f218
+        expected_stdout = "some info"
15f218
+        expected_stderr = "some error"
15f218
         expected_retval = 1
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (expected_error, expected_retval)
15f218
+        mock_runner.run.return_value = (
15f218
+            expected_stdout,
15f218
+            expected_stderr,
15f218
+            expected_retval
15f218
+        )
15f218
 
15f218
         self.assertEqual(
15f218
             {"offline": True},
15f218
@@ -273,10 +320,15 @@ class GetLocalNodeStatusTest(LibraryPacemakerNodeStatusTest):
15f218
         mock_runner.run.assert_called_once_with(self.crm_mon_cmd())
15f218
 
15f218
     def test_invalid_status(self):
15f218
-        expected_xml = "some error"
15f218
+        expected_stdout = "invalid xml"
15f218
+        expected_stderr = ""
15f218
         expected_retval = 0
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (expected_xml, expected_retval)
15f218
+        mock_runner.run.return_value = (
15f218
+            expected_stdout,
15f218
+            expected_stderr,
15f218
+            expected_retval
15f218
+        )
15f218
 
15f218
         assert_raise_library_error(
15f218
             lambda: lib.get_local_node_status(mock_runner),
15f218
@@ -310,9 +362,9 @@ class GetLocalNodeStatusTest(LibraryPacemakerNodeStatusTest):
15f218
             ),
15f218
         ]
15f218
         return_value_list = [
15f218
-            (str(self.status), 0),
15f218
-            (node_id, 0),
15f218
-            (node_name, 0)
15f218
+            (str(self.status), "", 0),
15f218
+            (node_id, "", 0),
15f218
+            (node_name, "", 0)
15f218
         ]
15f218
         mock_runner.run.side_effect = return_value_list
15f218
 
15f218
@@ -339,9 +391,9 @@ class GetLocalNodeStatusTest(LibraryPacemakerNodeStatusTest):
15f218
             ),
15f218
         ]
15f218
         return_value_list = [
15f218
-            (str(self.status), 0),
15f218
-            (node_id, 0),
15f218
-            (node_name_bad, 0)
15f218
+            (str(self.status), "", 0),
15f218
+            (node_id, "", 0),
15f218
+            (node_name_bad, "", 0)
15f218
         ]
15f218
         mock_runner.run.side_effect = return_value_list
15f218
 
15f218
@@ -370,8 +422,8 @@ class GetLocalNodeStatusTest(LibraryPacemakerNodeStatusTest):
15f218
             mock.call([self.path("crm_node"), "--cluster-id"]),
15f218
         ]
15f218
         return_value_list = [
15f218
-            (str(self.status), 0),
15f218
-            ("some error", 1),
15f218
+            (str(self.status), "", 0),
15f218
+            ("", "some error", 1),
15f218
         ]
15f218
         mock_runner.run.side_effect = return_value_list
15f218
 
15f218
@@ -403,9 +455,9 @@ class GetLocalNodeStatusTest(LibraryPacemakerNodeStatusTest):
15f218
             ),
15f218
         ]
15f218
         return_value_list = [
15f218
-            (str(self.status), 0),
15f218
-            (node_id, 0),
15f218
-            ("some error", 1),
15f218
+            (str(self.status), "", 0),
15f218
+            (node_id, "", 0),
15f218
+            ("", "some error", 1),
15f218
         ]
15f218
         mock_runner.run.side_effect = return_value_list
15f218
 
15f218
@@ -437,9 +489,9 @@ class GetLocalNodeStatusTest(LibraryPacemakerNodeStatusTest):
15f218
             ),
15f218
         ]
15f218
         return_value_list = [
15f218
-            (str(self.status), 0),
15f218
-            (node_id, 0),
15f218
-            ("(null)", 0),
15f218
+            (str(self.status), "", 0),
15f218
+            (node_id, "", 0),
15f218
+            ("(null)", "", 0),
15f218
         ]
15f218
         mock_runner.run.side_effect = return_value_list
15f218
 
15f218
@@ -465,15 +517,16 @@ class ResourceCleanupTest(LibraryPacemakerTest):
15f218
         return str(XmlManipulation(doc))
15f218
 
15f218
     def test_basic(self):
15f218
-        expected_output = "expected output"
15f218
+        expected_stdout = "expected output"
15f218
+        expected_stderr = "expected stderr"
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
         call_list = [
15f218
             mock.call(self.crm_mon_cmd()),
15f218
             mock.call([self.path("crm_resource"), "--cleanup"]),
15f218
         ]
15f218
         return_value_list = [
15f218
-            (self.fixture_status_xml(1, 1), 0),
15f218
-            (expected_output, 0),
15f218
+            (self.fixture_status_xml(1, 1), "", 0),
15f218
+            (expected_stdout, expected_stderr, 0),
15f218
         ]
15f218
         mock_runner.run.side_effect = return_value_list
15f218
 
15f218
@@ -482,11 +535,18 @@ class ResourceCleanupTest(LibraryPacemakerTest):
15f218
         self.assertEqual(len(return_value_list), len(call_list))
15f218
         self.assertEqual(len(return_value_list), mock_runner.run.call_count)
15f218
         mock_runner.run.assert_has_calls(call_list)
15f218
-        self.assertEqual(expected_output, real_output)
15f218
+        self.assertEqual(
15f218
+            expected_stdout + "\n" + expected_stderr,
15f218
+            real_output
15f218
+        )
15f218
 
15f218
     def test_threshold_exceeded(self):
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (self.fixture_status_xml(1000, 1000), 0)
15f218
+        mock_runner.run.return_value = (
15f218
+            self.fixture_status_xml(1000, 1000),
15f218
+            "",
15f218
+            0
15f218
+        )
15f218
 
15f218
         assert_raise_library_error(
15f218
             lambda: lib.resource_cleanup(mock_runner),
15f218
@@ -501,49 +561,62 @@ class ResourceCleanupTest(LibraryPacemakerTest):
15f218
         mock_runner.run.assert_called_once_with(self.crm_mon_cmd())
15f218
 
15f218
     def test_forced(self):
15f218
-        expected_output = "expected output"
15f218
+        expected_stdout = "expected output"
15f218
+        expected_stderr = "expected stderr"
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (expected_output, 0)
15f218
+        mock_runner.run.return_value = (expected_stdout, expected_stderr, 0)
15f218
 
15f218
         real_output = lib.resource_cleanup(mock_runner, force=True)
15f218
 
15f218
         mock_runner.run.assert_called_once_with(
15f218
             [self.path("crm_resource"), "--cleanup"]
15f218
         )
15f218
-        self.assertEqual(expected_output, real_output)
15f218
+        self.assertEqual(
15f218
+            expected_stdout + "\n" + expected_stderr,
15f218
+            real_output
15f218
+        )
15f218
 
15f218
     def test_resource(self):
15f218
         resource = "test_resource"
15f218
-        expected_output = "expected output"
15f218
+        expected_stdout = "expected output"
15f218
+        expected_stderr = "expected stderr"
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (expected_output, 0)
15f218
+        mock_runner.run.return_value = (expected_stdout, expected_stderr, 0)
15f218
 
15f218
         real_output = lib.resource_cleanup(mock_runner, resource=resource)
15f218
 
15f218
         mock_runner.run.assert_called_once_with(
15f218
             [self.path("crm_resource"), "--cleanup", "--resource", resource]
15f218
         )
15f218
-        self.assertEqual(expected_output, real_output)
15f218
+        self.assertEqual(
15f218
+            expected_stdout + "\n" + expected_stderr,
15f218
+            real_output
15f218
+        )
15f218
 
15f218
     def test_node(self):
15f218
         node = "test_node"
15f218
-        expected_output = "expected output"
15f218
+        expected_stdout = "expected output"
15f218
+        expected_stderr = "expected stderr"
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (expected_output, 0)
15f218
+        mock_runner.run.return_value = (expected_stdout, expected_stderr, 0)
15f218
 
15f218
         real_output = lib.resource_cleanup(mock_runner, node=node)
15f218
 
15f218
         mock_runner.run.assert_called_once_with(
15f218
             [self.path("crm_resource"), "--cleanup", "--node", node]
15f218
         )
15f218
-        self.assertEqual(expected_output, real_output)
15f218
+        self.assertEqual(
15f218
+            expected_stdout + "\n" + expected_stderr,
15f218
+            real_output
15f218
+        )
15f218
 
15f218
     def test_node_and_resource(self):
15f218
         node = "test_node"
15f218
         resource = "test_resource"
15f218
-        expected_output = "expected output"
15f218
+        expected_stdout = "expected output"
15f218
+        expected_stderr = "expected stderr"
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (expected_output, 0)
15f218
+        mock_runner.run.return_value = (expected_stdout, expected_stderr, 0)
15f218
 
15f218
         real_output = lib.resource_cleanup(
15f218
             mock_runner, resource=resource, node=node
15f218
@@ -555,13 +628,21 @@ class ResourceCleanupTest(LibraryPacemakerTest):
15f218
                 "--cleanup", "--resource", resource, "--node", node
15f218
             ]
15f218
         )
15f218
-        self.assertEqual(expected_output, real_output)
15f218
+        self.assertEqual(
15f218
+            expected_stdout + "\n" + expected_stderr,
15f218
+            real_output
15f218
+        )
15f218
 
15f218
     def test_error_state(self):
15f218
-        expected_error = "some error"
15f218
+        expected_stdout = "some info"
15f218
+        expected_stderr = "some error"
15f218
         expected_retval = 1
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (expected_error, expected_retval)
15f218
+        mock_runner.run.return_value = (
15f218
+            expected_stdout,
15f218
+            expected_stderr,
15f218
+            expected_retval
15f218
+        )
15f218
 
15f218
         assert_raise_library_error(
15f218
             lambda: lib.resource_cleanup(mock_runner),
15f218
@@ -569,8 +650,7 @@ class ResourceCleanupTest(LibraryPacemakerTest):
15f218
                 Severity.ERROR,
15f218
                 report_codes.CRM_MON_ERROR,
15f218
                 {
15f218
-                    "return_value": expected_retval,
15f218
-                    "stdout": expected_error,
15f218
+                    "reason": expected_stderr + "\n" + expected_stdout,
15f218
                 }
15f218
             )
15f218
         )
15f218
@@ -578,7 +658,8 @@ class ResourceCleanupTest(LibraryPacemakerTest):
15f218
         mock_runner.run.assert_called_once_with(self.crm_mon_cmd())
15f218
 
15f218
     def test_error_cleanup(self):
15f218
-        expected_error = "expected error"
15f218
+        expected_stdout = "some info"
15f218
+        expected_stderr = "some error"
15f218
         expected_retval = 1
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
         call_list = [
15f218
@@ -586,8 +667,8 @@ class ResourceCleanupTest(LibraryPacemakerTest):
15f218
             mock.call([self.path("crm_resource"), "--cleanup"]),
15f218
         ]
15f218
         return_value_list = [
15f218
-            (self.fixture_status_xml(1, 1), 0),
15f218
-            (expected_error, expected_retval),
15f218
+            (self.fixture_status_xml(1, 1), "", 0),
15f218
+            (expected_stdout, expected_stderr, expected_retval),
15f218
         ]
15f218
         mock_runner.run.side_effect = return_value_list
15f218
 
15f218
@@ -597,8 +678,7 @@ class ResourceCleanupTest(LibraryPacemakerTest):
15f218
                 Severity.ERROR,
15f218
                 report_codes.RESOURCE_CLEANUP_ERROR,
15f218
                 {
15f218
-                    "return_value": expected_retval,
15f218
-                    "stdout": expected_error,
15f218
+                    "reason": expected_stderr + "\n" + expected_stdout,
15f218
                 }
15f218
             )
15f218
         )
15f218
@@ -609,10 +689,33 @@ class ResourceCleanupTest(LibraryPacemakerTest):
15f218
 
15f218
 class ResourcesWaitingTest(LibraryPacemakerTest):
15f218
     def test_has_support(self):
15f218
-        expected_output = "something --wait something else"
15f218
+        expected_stdout = ""
15f218
+        expected_stderr = "something --wait something else"
15f218
+        expected_retval = 1
15f218
+        mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
+        mock_runner.run.return_value = (
15f218
+            expected_stdout,
15f218
+            expected_stderr,
15f218
+            expected_retval
15f218
+        )
15f218
+
15f218
+        self.assertTrue(
15f218
+            lib.has_resource_wait_support(mock_runner)
15f218
+        )
15f218
+        mock_runner.run.assert_called_once_with(
15f218
+            [self.path("crm_resource"), "-?"]
15f218
+        )
15f218
+
15f218
+    def test_has_support_stdout(self):
15f218
+        expected_stdout = "something --wait something else"
15f218
+        expected_stderr = ""
15f218
         expected_retval = 1
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (expected_output, expected_retval)
15f218
+        mock_runner.run.return_value = (
15f218
+            expected_stdout,
15f218
+            expected_stderr,
15f218
+            expected_retval
15f218
+        )
15f218
 
15f218
         self.assertTrue(
15f218
             lib.has_resource_wait_support(mock_runner)
15f218
@@ -622,10 +725,15 @@ class ResourcesWaitingTest(LibraryPacemakerTest):
15f218
         )
15f218
 
15f218
     def test_doesnt_have_support(self):
15f218
-        expected_output = "something something else"
15f218
+        expected_stdout = "something something else"
15f218
+        expected_stderr = "something something else"
15f218
         expected_retval = 1
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (expected_output, expected_retval)
15f218
+        mock_runner.run.return_value = (
15f218
+            expected_stdout,
15f218
+            expected_stderr,
15f218
+            expected_retval
15f218
+        )
15f218
 
15f218
         self.assertFalse(
15f218
             lib.has_resource_wait_support(mock_runner)
15f218
@@ -652,10 +760,15 @@ class ResourcesWaitingTest(LibraryPacemakerTest):
15f218
         )
15f218
 
15f218
     def test_wait_success(self):
15f218
-        expected_output = "expected output"
15f218
+        expected_stdout = "expected output"
15f218
+        expected_stderr = "expected stderr"
15f218
         expected_retval = 0
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (expected_output, expected_retval)
15f218
+        mock_runner.run.return_value = (
15f218
+            expected_stdout,
15f218
+            expected_stderr,
15f218
+            expected_retval
15f218
+        )
15f218
 
15f218
         self.assertEqual(None, lib.wait_for_resources(mock_runner))
15f218
 
15f218
@@ -664,11 +777,16 @@ class ResourcesWaitingTest(LibraryPacemakerTest):
15f218
         )
15f218
 
15f218
     def test_wait_timeout_success(self):
15f218
-        expected_output = "expected output"
15f218
+        expected_stdout = "expected output"
15f218
+        expected_stderr = "expected stderr"
15f218
         expected_retval = 0
15f218
         timeout = 10
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (expected_output, expected_retval)
15f218
+        mock_runner.run.return_value = (
15f218
+            expected_stdout,
15f218
+            expected_stderr,
15f218
+            expected_retval
15f218
+        )
15f218
 
15f218
         self.assertEqual(None, lib.wait_for_resources(mock_runner, timeout))
15f218
 
15f218
@@ -680,10 +798,15 @@ class ResourcesWaitingTest(LibraryPacemakerTest):
15f218
         )
15f218
 
15f218
     def test_wait_error(self):
15f218
-        expected_error = "some error"
15f218
+        expected_stdout = "some info"
15f218
+        expected_stderr = "some error"
15f218
         expected_retval = 1
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (expected_error, expected_retval)
15f218
+        mock_runner.run.return_value = (
15f218
+            expected_stdout,
15f218
+            expected_stderr,
15f218
+            expected_retval
15f218
+        )
15f218
 
15f218
         assert_raise_library_error(
15f218
             lambda: lib.wait_for_resources(mock_runner),
15f218
@@ -691,8 +814,7 @@ class ResourcesWaitingTest(LibraryPacemakerTest):
15f218
                 Severity.ERROR,
15f218
                 report_codes.RESOURCE_WAIT_ERROR,
15f218
                 {
15f218
-                    "return_value": expected_retval,
15f218
-                    "stdout": expected_error,
15f218
+                    "reason": expected_stderr + "\n" + expected_stdout,
15f218
                 }
15f218
             )
15f218
         )
15f218
@@ -702,10 +824,15 @@ class ResourcesWaitingTest(LibraryPacemakerTest):
15f218
         )
15f218
 
15f218
     def test_wait_error_timeout(self):
15f218
-        expected_error = "some error"
15f218
+        expected_stdout = "some info"
15f218
+        expected_stderr = "some error"
15f218
         expected_retval = 62
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (expected_error, expected_retval)
15f218
+        mock_runner.run.return_value = (
15f218
+            expected_stdout,
15f218
+            expected_stderr,
15f218
+            expected_retval
15f218
+        )
15f218
 
15f218
         assert_raise_library_error(
15f218
             lambda: lib.wait_for_resources(mock_runner),
15f218
@@ -713,8 +840,7 @@ class ResourcesWaitingTest(LibraryPacemakerTest):
15f218
                 Severity.ERROR,
15f218
                 report_codes.RESOURCE_WAIT_TIMED_OUT,
15f218
                 {
15f218
-                    "return_value": expected_retval,
15f218
-                    "stdout": expected_error,
15f218
+                    "reason": expected_stderr + "\n" + expected_stdout,
15f218
                 }
15f218
             )
15f218
         )
15f218
@@ -727,7 +853,7 @@ class NodeStandbyTest(LibraryPacemakerNodeStatusTest):
15f218
     def test_standby_local(self):
15f218
         expected_retval = 0
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = ("dummy", expected_retval)
15f218
+        mock_runner.run.return_value = ("dummy", "", expected_retval)
15f218
 
15f218
         output = lib.nodes_standby(mock_runner)
15f218
 
15f218
@@ -739,7 +865,7 @@ class NodeStandbyTest(LibraryPacemakerNodeStatusTest):
15f218
     def test_unstandby_local(self):
15f218
         expected_retval = 0
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = ("dummy", expected_retval)
15f218
+        mock_runner.run.return_value = ("dummy", "", expected_retval)
15f218
 
15f218
         output = lib.nodes_unstandby(mock_runner)
15f218
 
15f218
@@ -760,8 +886,8 @@ class NodeStandbyTest(LibraryPacemakerNodeStatusTest):
15f218
             mock.call([self.path("crm_standby"), "-v", "on", "-N", n])
15f218
             for n in nodes
15f218
         ]
15f218
-        return_value_list = [(str(self.status), 0)]
15f218
-        return_value_list += [("dummy", 0) for n in nodes]
15f218
+        return_value_list = [(str(self.status), "", 0)]
15f218
+        return_value_list += [("dummy", "", 0) for n in nodes]
15f218
         mock_runner.run.side_effect = return_value_list
15f218
 
15f218
         output = lib.nodes_standby(mock_runner, all_nodes=True)
15f218
@@ -783,8 +909,8 @@ class NodeStandbyTest(LibraryPacemakerNodeStatusTest):
15f218
             mock.call([self.path("crm_standby"), "-D", "-N", n])
15f218
             for n in nodes
15f218
         ]
15f218
-        return_value_list = [(str(self.status), 0)]
15f218
-        return_value_list += [("dummy", 0) for n in nodes]
15f218
+        return_value_list = [(str(self.status), "", 0)]
15f218
+        return_value_list += [("dummy", "", 0) for n in nodes]
15f218
         mock_runner.run.side_effect = return_value_list
15f218
 
15f218
         output = lib.nodes_unstandby(mock_runner, all_nodes=True)
15f218
@@ -806,8 +932,8 @@ class NodeStandbyTest(LibraryPacemakerNodeStatusTest):
15f218
             mock.call([self.path("crm_standby"), "-v", "on", "-N", n])
15f218
             for n in nodes[1:]
15f218
         ]
15f218
-        return_value_list = [(str(self.status), 0)]
15f218
-        return_value_list += [("dummy", 0) for n in nodes[1:]]
15f218
+        return_value_list = [(str(self.status), "", 0)]
15f218
+        return_value_list += [("dummy", "", 0) for n in nodes[1:]]
15f218
         mock_runner.run.side_effect = return_value_list
15f218
 
15f218
         output = lib.nodes_standby(mock_runner, node_list=nodes[1:])
15f218
@@ -829,8 +955,8 @@ class NodeStandbyTest(LibraryPacemakerNodeStatusTest):
15f218
             mock.call([self.path("crm_standby"), "-D", "-N", n])
15f218
             for n in nodes[:2]
15f218
         ]
15f218
-        return_value_list = [(str(self.status), 0)]
15f218
-        return_value_list += [("dummy", 0) for n in nodes[:2]]
15f218
+        return_value_list = [(str(self.status), "", 0)]
15f218
+        return_value_list += [("dummy", "", 0) for n in nodes[:2]]
15f218
         mock_runner.run.side_effect = return_value_list
15f218
 
15f218
         output = lib.nodes_unstandby(mock_runner, node_list=nodes[:2])
15f218
@@ -845,7 +971,7 @@ class NodeStandbyTest(LibraryPacemakerNodeStatusTest):
15f218
             self.fixture_get_node_status("node_1", "id_1")
15f218
         )
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (str(self.status), 0)
15f218
+        mock_runner.run.return_value = (str(self.status), "", 0)
15f218
 
15f218
         assert_raise_library_error(
15f218
             lambda: lib.nodes_standby(mock_runner, ["node_2"]),
15f218
@@ -863,7 +989,7 @@ class NodeStandbyTest(LibraryPacemakerNodeStatusTest):
15f218
             self.fixture_get_node_status("node_1", "id_1")
15f218
         )
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (str(self.status), 0)
15f218
+        mock_runner.run.return_value = (str(self.status), "", 0)
15f218
 
15f218
         assert_raise_library_error(
15f218
             lambda: lib.nodes_unstandby(mock_runner, ["node_2", "node_3"]),
15f218
@@ -882,17 +1008,24 @@ class NodeStandbyTest(LibraryPacemakerNodeStatusTest):
15f218
         mock_runner.run.assert_called_once_with(self.crm_mon_cmd())
15f218
 
15f218
     def test_error_one_node(self):
15f218
-        expected_error = "some error"
15f218
+        expected_stdout = "some info"
15f218
+        expected_stderr = "some error"
15f218
         expected_retval = 1
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (expected_error, expected_retval)
15f218
+        mock_runner.run.return_value = (
15f218
+            expected_stdout,
15f218
+            expected_stderr,
15f218
+            expected_retval
15f218
+        )
15f218
 
15f218
         assert_raise_library_error(
15f218
             lambda: lib.nodes_unstandby(mock_runner),
15f218
             (
15f218
                 Severity.ERROR,
15f218
                 report_codes.COMMON_ERROR,
15f218
-                {}
15f218
+                {
15f218
+                    "text": expected_stderr + "\n" + expected_stdout,
15f218
+                }
15f218
             )
15f218
         )
15f218
 
15f218
@@ -913,11 +1046,11 @@ class NodeStandbyTest(LibraryPacemakerNodeStatusTest):
15f218
             for n in nodes
15f218
         ]
15f218
         return_value_list = [
15f218
-            (str(self.status), 0),
15f218
-            ("dummy1", 0),
15f218
-            ("dummy2", 1),
15f218
-            ("dummy3", 0),
15f218
-            ("dummy4", 1),
15f218
+            (str(self.status), "", 0),
15f218
+            ("dummy1", "", 0),
15f218
+            ("dummy2", "error2", 1),
15f218
+            ("dummy3", "", 0),
15f218
+            ("dummy4", "error4", 1),
15f218
         ]
15f218
         mock_runner.run.side_effect = return_value_list
15f218
 
15f218
@@ -926,12 +1059,16 @@ class NodeStandbyTest(LibraryPacemakerNodeStatusTest):
15f218
             (
15f218
                 Severity.ERROR,
15f218
                 report_codes.COMMON_ERROR,
15f218
-                {}
15f218
+                {
15f218
+                    "text": "error2\ndummy2",
15f218
+                }
15f218
             ),
15f218
             (
15f218
                 Severity.ERROR,
15f218
                 report_codes.COMMON_ERROR,
15f218
-                {}
15f218
+                {
15f218
+                    "text": "error4\ndummy4",
15f218
+                }
15f218
             )
15f218
         )
15f218
 
15f218
diff --git a/pcs/test/test_lib_resource_agent.py b/pcs/test/test_lib_resource_agent.py
15f218
index 08f9061..a569e66 100644
15f218
--- a/pcs/test/test_lib_resource_agent.py
15f218
+++ b/pcs/test/test_lib_resource_agent.py
15f218
@@ -199,7 +199,7 @@ class GetFenceAgentMetadataTest(LibraryResourceTest):
15f218
     def test_execution_failed(self, mock_is_runnable):
15f218
         mock_is_runnable.return_value = True
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = ("error", 1)
15f218
+        mock_runner.run.return_value = ("", "error", 1)
15f218
         agent_name = "fence_ipmi"
15f218
 
15f218
         self.assert_raises(
15f218
@@ -210,13 +210,13 @@ class GetFenceAgentMetadataTest(LibraryResourceTest):
15f218
 
15f218
         script_path = os.path.join(settings.fence_agent_binaries, agent_name)
15f218
         mock_runner.run.assert_called_once_with(
15f218
-            [script_path, "-o", "metadata"], ignore_stderr=True
15f218
+            [script_path, "-o", "metadata"]
15f218
         )
15f218
 
15f218
     @mock.patch("pcs.lib.resource_agent.is_path_runnable")
15f218
     def test_invalid_xml(self, mock_is_runnable):
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = ("not xml", 0)
15f218
+        mock_runner.run.return_value = ("not xml", "", 0)
15f218
         mock_is_runnable.return_value = True
15f218
         agent_name = "fence_ipmi"
15f218
         self.assert_raises(
15f218
@@ -227,7 +227,7 @@ class GetFenceAgentMetadataTest(LibraryResourceTest):
15f218
 
15f218
         script_path = os.path.join(settings.fence_agent_binaries, agent_name)
15f218
         mock_runner.run.assert_called_once_with(
15f218
-            [script_path, "-o", "metadata"], ignore_stderr=True
15f218
+            [script_path, "-o", "metadata"]
15f218
         )
15f218
 
15f218
     @mock.patch("pcs.lib.resource_agent.is_path_runnable")
15f218
@@ -235,14 +235,14 @@ class GetFenceAgentMetadataTest(LibraryResourceTest):
15f218
         agent_name = "fence_ipmi"
15f218
         xml = "<xml />"
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (xml, 0)
15f218
+        mock_runner.run.return_value = (xml, "", 0)
15f218
         mock_is_runnable.return_value = True
15f218
 
15f218
         out_dom = lib_ra.get_fence_agent_metadata(mock_runner, agent_name)
15f218
 
15f218
         script_path = os.path.join(settings.fence_agent_binaries, agent_name)
15f218
         mock_runner.run.assert_called_once_with(
15f218
-            [script_path, "-o", "metadata"], ignore_stderr=True
15f218
+            [script_path, "-o", "metadata"]
15f218
         )
15f218
         assert_xml_equal(xml, str(XmlMan(out_dom)))
15f218
 
15f218
@@ -304,7 +304,7 @@ class GetOcfResourceAgentMetadataTest(LibraryResourceTest):
15f218
         provider = "provider"
15f218
         agent = "agent"
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = ("error", 1)
15f218
+        mock_runner.run.return_value = ("", "error", 1)
15f218
         mock_is_runnable.return_value = True
15f218
 
15f218
         self.assert_raises(
15f218
@@ -318,8 +318,7 @@ class GetOcfResourceAgentMetadataTest(LibraryResourceTest):
15f218
         script_path = os.path.join(settings.ocf_resources, provider, agent)
15f218
         mock_runner.run.assert_called_once_with(
15f218
             [script_path, "meta-data"],
15f218
-            env_extend={"OCF_ROOT": settings.ocf_root},
15f218
-            ignore_stderr=True
15f218
+            env_extend={"OCF_ROOT": settings.ocf_root}
15f218
         )
15f218
 
15f218
     @mock.patch("pcs.lib.resource_agent.is_path_runnable")
15f218
@@ -327,7 +326,7 @@ class GetOcfResourceAgentMetadataTest(LibraryResourceTest):
15f218
         provider = "provider"
15f218
         agent = "agent"
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = ("not xml", 0)
15f218
+        mock_runner.run.return_value = ("not xml", "", 0)
15f218
         mock_is_runnable.return_value = True
15f218
 
15f218
         self.assert_raises(
15f218
@@ -341,8 +340,7 @@ class GetOcfResourceAgentMetadataTest(LibraryResourceTest):
15f218
         script_path = os.path.join(settings.ocf_resources, provider, agent)
15f218
         mock_runner.run.assert_called_once_with(
15f218
             [script_path, "meta-data"],
15f218
-            env_extend={"OCF_ROOT": settings.ocf_root},
15f218
-            ignore_stderr=True
15f218
+            env_extend={"OCF_ROOT": settings.ocf_root}
15f218
         )
15f218
 
15f218
     @mock.patch("pcs.lib.resource_agent.is_path_runnable")
15f218
@@ -351,7 +349,7 @@ class GetOcfResourceAgentMetadataTest(LibraryResourceTest):
15f218
         agent = "agent"
15f218
         xml = "<xml />"
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (xml, 0)
15f218
+        mock_runner.run.return_value = (xml, "", 0)
15f218
         mock_is_runnable.return_value = True
15f218
 
15f218
         out_dom = lib_ra._get_ocf_resource_agent_metadata(
15f218
@@ -361,8 +359,7 @@ class GetOcfResourceAgentMetadataTest(LibraryResourceTest):
15f218
         script_path = os.path.join(settings.ocf_resources, provider, agent)
15f218
         mock_runner.run.assert_called_once_with(
15f218
             [script_path, "meta-data"],
15f218
-            env_extend={"OCF_ROOT": settings.ocf_root},
15f218
-            ignore_stderr=True
15f218
+            env_extend={"OCF_ROOT": settings.ocf_root}
15f218
         )
15f218
         assert_xml_equal(xml, str(XmlMan(out_dom)))
15f218
 
15f218
@@ -596,7 +593,7 @@ class GetPcmkAdvancedStonithParametersTest(LibraryResourceTest):
15f218
             </resource-agent>
15f218
         """
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = (xml, 0)
15f218
+        mock_runner.run.return_value = (xml, "", 0)
15f218
         self.assertEqual(
15f218
             [
15f218
                 {
15f218
@@ -623,12 +620,12 @@ class GetPcmkAdvancedStonithParametersTest(LibraryResourceTest):
15f218
             lib_ra._get_pcmk_advanced_stonith_parameters(mock_runner)
15f218
         )
15f218
         mock_runner.run.assert_called_once_with(
15f218
-            [settings.stonithd_binary, "metadata"], ignore_stderr=True
15f218
+            [settings.stonithd_binary, "metadata"]
15f218
         )
15f218
 
15f218
     def test_failed_to_get_xml(self):
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = ("", 1)
15f218
+        mock_runner.run.return_value = ("", "some error", 1)
15f218
         self.assert_raises(
15f218
             lib_ra.UnableToGetAgentMetadata,
15f218
             lambda: lib_ra._get_pcmk_advanced_stonith_parameters(mock_runner),
15f218
@@ -636,19 +633,19 @@ class GetPcmkAdvancedStonithParametersTest(LibraryResourceTest):
15f218
         )
15f218
 
15f218
         mock_runner.run.assert_called_once_with(
15f218
-            [settings.stonithd_binary, "metadata"], ignore_stderr=True
15f218
+            [settings.stonithd_binary, "metadata"]
15f218
         )
15f218
 
15f218
     def test_invalid_xml(self):
15f218
         mock_runner = mock.MagicMock(spec_set=CommandRunner)
15f218
-        mock_runner.run.return_value = ("invalid XML", 0)
15f218
+        mock_runner.run.return_value = ("invalid XML", "", 0)
15f218
         self.assertRaises(
15f218
             lib_ra.InvalidMetadataFormat,
15f218
             lambda: lib_ra._get_pcmk_advanced_stonith_parameters(mock_runner)
15f218
         )
15f218
 
15f218
         mock_runner.run.assert_called_once_with(
15f218
-            [settings.stonithd_binary, "metadata"], ignore_stderr=True
15f218
+            [settings.stonithd_binary, "metadata"]
15f218
         )
15f218
 
15f218
 
15f218
diff --git a/pcs/test/test_lib_sbd.py b/pcs/test/test_lib_sbd.py
15f218
index 720d8b1..9b7b801 100644
15f218
--- a/pcs/test/test_lib_sbd.py
15f218
+++ b/pcs/test/test_lib_sbd.py
15f218
@@ -155,9 +155,8 @@ class AtbHasToBeEnabledTest(TestCase):
15f218
         self.assertFalse(lib_sbd.atb_has_to_be_enabled(
15f218
             self.mock_runner, self.mock_conf, 1
15f218
         ))
15f218
-        mock_is_needed.assert_called_once_with(
15f218
-            self.mock_runner, self.mock_conf, 1
15f218
-        )
15f218
+        self.mock_conf.is_enabled_auto_tie_breaker.assert_called_once_with()
15f218
+        mock_is_needed.assert_not_called()
15f218
 
15f218
     def test_atb_needed_is_disabled(self, mock_is_needed):
15f218
         mock_is_needed.return_value = True
15f218
@@ -165,6 +164,7 @@ class AtbHasToBeEnabledTest(TestCase):
15f218
         self.assertTrue(lib_sbd.atb_has_to_be_enabled(
15f218
             self.mock_runner, self.mock_conf, -1
15f218
         ))
15f218
+        self.mock_conf.is_enabled_auto_tie_breaker.assert_called_once_with()
15f218
         mock_is_needed.assert_called_once_with(
15f218
             self.mock_runner, self.mock_conf, -1
15f218
         )
15f218
@@ -175,9 +175,8 @@ class AtbHasToBeEnabledTest(TestCase):
15f218
         self.assertFalse(lib_sbd.atb_has_to_be_enabled(
15f218
             self.mock_runner, self.mock_conf, 2
15f218
         ))
15f218
-        mock_is_needed.assert_called_once_with(
15f218
-            self.mock_runner, self.mock_conf, 2
15f218
-        )
15f218
+        self.mock_conf.is_enabled_auto_tie_breaker.assert_called_once_with()
15f218
+        mock_is_needed.assert_not_called()
15f218
 
15f218
     def test_atb_not_needed_is_disabled(self, mock_is_needed):
15f218
         mock_is_needed.return_value = False
15f218
@@ -185,6 +184,7 @@ class AtbHasToBeEnabledTest(TestCase):
15f218
         self.assertFalse(lib_sbd.atb_has_to_be_enabled(
15f218
             self.mock_runner, self.mock_conf, -2
15f218
         ))
15f218
+        self.mock_conf.is_enabled_auto_tie_breaker.assert_called_once_with()
15f218
         mock_is_needed.assert_called_once_with(
15f218
             self.mock_runner, self.mock_conf, -2
15f218
         )
15f218
-- 
15f218
1.8.3.1
15f218