Blame SOURCES/bz1164402-02-sbd-fixes.patch

15f218
From e43d7324b9fc6933d8fa431e66c6236721724b98 Mon Sep 17 00:00:00 2001
15f218
From: Ivan Devat <idevat@redhat.com>
15f218
Date: Fri, 19 Aug 2016 02:57:38 +0200
15f218
Subject: [PATCH] squash bz1164402 Support for sbd configuration is
15f218
15f218
f2da8ad476c3 fix disable_service
15f218
15f218
9367e7162b7b fix code formating
15f218
15f218
57b618777d14 test: fix tests with parallel operations in SBD
15f218
15f218
f7b9fc15072c sbd: change error message
15f218
15f218
0dbdff4628d5 sbd: fix setting watchdog in config
15f218
15f218
1c5ccd3be588 sbd: ban changing sbd option SBD_PACEMAKER
15f218
15f218
733e28337589 sbd: add validation for SBD_WATCHDOG_TIMEOUT option
15f218
15f218
9951f3262ef1 docs: add watchdog option to node add command
15f218
15f218
d79592e05158 lib: fix disabling service on systemd systems
15f218
15f218
17e4c5838842 sbd: set auto_tie_breaker whenever it is needed for SBD to work
15f218
15f218
1ed4c2e3bc38 lib: fix enabled ATB in corosync.conf detection
15f218
---
15f218
 pcs/cluster.py                              |  54 +++++-
15f218
 pcs/common/report_codes.py                  |   4 +-
15f218
 pcs/lib/commands/quorum.py                  |  41 ++++-
15f218
 pcs/lib/commands/sbd.py                     |  30 ++-
15f218
 pcs/lib/corosync/config_facade.py           |  15 +-
15f218
 pcs/lib/external.py                         |   4 +-
15f218
 pcs/lib/reports.py                          |  27 +++
15f218
 pcs/lib/sbd.py                              |  78 +++++++-
15f218
 pcs/pcs.8                                   |   4 +-
15f218
 pcs/quorum.py                               |   3 +-
15f218
 pcs/stonith.py                              |   4 +-
15f218
 pcs/test/test_lib_commands_quorum.py        | 205 ++++++++++++++++++++-
15f218
 pcs/test/test_lib_commands_sbd.py           | 134 +++++++++++++-
15f218
 pcs/test/test_lib_corosync_config_facade.py |  28 +++
15f218
 pcs/test/test_lib_external.py               |  22 ++-
15f218
 pcs/test/test_lib_sbd.py                    | 272 +++++++++++++++++++++++++++-
15f218
 pcs/usage.py                                |   3 +
15f218
 pcs/utils.py                                |  25 ++-
15f218
 pcsd/pcs.rb                                 |  10 +-
15f218
 19 files changed, 908 insertions(+), 55 deletions(-)
15f218
15f218
diff --git a/pcs/cluster.py b/pcs/cluster.py
15f218
index 90fec63..577e08e 100644
15f218
--- a/pcs/cluster.py
15f218
+++ b/pcs/cluster.py
15f218
@@ -43,6 +43,7 @@ from pcs.lib import (
15f218
     reports as lib_reports,
15f218
 )
15f218
 from pcs.lib.booth import sync as booth_sync
15f218
+from pcs.lib.nodes_task import check_corosync_offline_on_nodes
15f218
 from pcs.lib.commands.quorum import _add_device_model_net
15f218
 from pcs.lib.corosync import (
15f218
     config_parser as corosync_conf_utils,
15f218
@@ -1328,6 +1329,36 @@ def get_cib(argv):
15f218
         except IOError as e:
15f218
             utils.err("Unable to write to file '%s', %s" % (filename, e.strerror))
15f218
 
15f218
+
15f218
+def _ensure_cluster_is_offline_if_atb_should_be_enabled(
15f218
+    lib_env, node_num_modifier, skip_offline_nodes=False
15f218
+):
15f218
+    """
15f218
+    Check if cluster is offline if auto tie breaker should be enabled.
15f218
+    Raises LibraryError if ATB needs to be enabled cluster is not offline.
15f218
+
15f218
+    lib_env -- LibraryEnvironment
15f218
+    node_num_modifier -- number which wil be added to the number of nodes in
15f218
+        cluster when determining whenever ATB is needed.
15f218
+    skip_offline_nodes -- if True offline nodes will be skipped
15f218
+    """
15f218
+    corosync_conf = lib_env.get_corosync_conf()
15f218
+    if lib_sbd.atb_has_to_be_enabled(
15f218
+        lib_env.cmd_runner(), corosync_conf, node_num_modifier
15f218
+    ):
15f218
+        print(
15f218
+            "Warning: auto_tie_breaker quorum option will be enabled to make "
15f218
+            "SBD fencing effecive after this change. Cluster has to be offline "
15f218
+            "to be able to make this change."
15f218
+        )
15f218
+        check_corosync_offline_on_nodes(
15f218
+            lib_env.node_communicator(),
15f218
+            lib_env.report_processor,
15f218
+            corosync_conf.get_nodes(),
15f218
+            skip_offline_nodes
15f218
+        )
15f218
+
15f218
+
15f218
 def cluster_node(argv):
15f218
     if len(argv) != 2:
15f218
         usage.cluster()
15f218
@@ -1363,6 +1394,9 @@ def cluster_node(argv):
15f218
                 msg += ", use --force to override"
15f218
             utils.err(msg)
15f218
 
15f218
+    lib_env = utils.get_lib_env()
15f218
+    modifiers = utils.get_modificators()
15f218
+
15f218
     if add_node == True:
15f218
         wait = False
15f218
         wait_timeout = None
15f218
@@ -1385,11 +1419,9 @@ def cluster_node(argv):
15f218
         if not canAdd:
15f218
             utils.err("Unable to add '%s' to cluster: %s" % (node0, error))
15f218
 
15f218
-        lib_env = utils.get_lib_env()
15f218
         report_processor = lib_env.report_processor
15f218
         node_communicator = lib_env.node_communicator()
15f218
         node_addr = NodeAddresses(node0, node1)
15f218
-        modifiers = utils.get_modificators()
15f218
         try:
15f218
             if lib_sbd.is_sbd_enabled(utils.cmd_runner()):
15f218
                 if "--watchdog" not in utils.pcs_options:
15f218
@@ -1400,6 +1432,10 @@ def cluster_node(argv):
15f218
                 else:
15f218
                     watchdog = utils.pcs_options["--watchdog"][0]
15f218
 
15f218
+                _ensure_cluster_is_offline_if_atb_should_be_enabled(
15f218
+                    lib_env, 1, modifiers["skip_offline_nodes"]
15f218
+                )
15f218
+
15f218
                 report_processor.process(lib_reports.sbd_check_started())
15f218
                 lib_sbd.check_sbd_on_node(
15f218
                     report_processor, node_communicator, node_addr, watchdog
15f218
@@ -1407,12 +1443,15 @@ def cluster_node(argv):
15f218
                 sbd_cfg = environment_file_to_dict(
15f218
                     lib_sbd.get_local_sbd_config()
15f218
                 )
15f218
-                sbd_cfg["SBD_WATCHDOG_DEV"] = watchdog
15f218
                 report_processor.process(
15f218
                     lib_reports.sbd_config_distribution_started()
15f218
                 )
15f218
                 lib_sbd.set_sbd_config_on_node(
15f218
-                    report_processor, node_communicator, node_addr, sbd_cfg
15f218
+                    report_processor,
15f218
+                    node_communicator,
15f218
+                    node_addr,
15f218
+                    sbd_cfg,
15f218
+                    watchdog
15f218
                 )
15f218
                 report_processor.process(lib_reports.sbd_enabling_started())
15f218
                 lib_sbd.enable_sbd_service_on_node(
15f218
@@ -1549,6 +1588,13 @@ def cluster_node(argv):
15f218
                 )
15f218
             # else the node seems to be stopped already, we're ok to proceed
15f218
 
15f218
+        try:
15f218
+            _ensure_cluster_is_offline_if_atb_should_be_enabled(
15f218
+                lib_env, -1, modifiers["skip_offline_nodes"]
15f218
+            )
15f218
+        except LibraryError as e:
15f218
+            utils.process_library_reports(e.args)
15f218
+
15f218
         nodesRemoved = False
15f218
         c_nodes = utils.getNodesFromCorosyncConf()
15f218
         destroy_cluster([node0], keep_going=("--force" in utils.pcs_options))
15f218
diff --git a/pcs/common/report_codes.py b/pcs/common/report_codes.py
15f218
index e71d418..672c2e3 100644
15f218
--- a/pcs/common/report_codes.py
15f218
+++ b/pcs/common/report_codes.py
15f218
@@ -10,9 +10,9 @@ 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_FILE_OVERWRITE = "FORCE_FILE_OVERWRITE"
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_OPTIONS = "OPTIONS"
15f218
 FORCE_QDEVICE_MODEL = "QDEVICE_MODEL"
15f218
@@ -81,6 +81,7 @@ COROSYNC_NOT_RUNNING_CHECK_STARTED = "COROSYNC_NOT_RUNNING_CHECK_STARTED"
15f218
 COROSYNC_NOT_RUNNING_CHECK_NODE_ERROR = "COROSYNC_NOT_RUNNING_CHECK_NODE_ERROR"
15f218
 COROSYNC_NOT_RUNNING_ON_NODE = "COROSYNC_NOT_RUNNING_ON_NODE"
15f218
 COROSYNC_OPTIONS_INCOMPATIBLE_WITH_QDEVICE = "COROSYNC_OPTIONS_INCOMPATIBLE_WITH_QDEVICE"
15f218
+COROSYNC_QUORUM_CANNOT_DISABLE_ATB_DUE_TO_SBD = "COROSYNC_QUORUM_CANNOT_DISABLE_ATB_DUE_TO_SBD"
15f218
 COROSYNC_QUORUM_GET_STATUS_ERROR = "COROSYNC_QUORUM_GET_STATUS_ERROR"
15f218
 COROSYNC_QUORUM_SET_EXPECTED_VOTES_ERROR = "COROSYNC_QUORUM_SET_EXPECTED_VOTES_ERROR"
15f218
 COROSYNC_RUNNING_ON_NODE = "COROSYNC_RUNNING_ON_NODE"
15f218
@@ -179,5 +180,6 @@ UNABLE_TO_GET_SBD_CONFIG = "UNABLE_TO_GET_SBD_CONFIG"
15f218
 UNABLE_TO_GET_SBD_STATUS = "UNABLE_TO_GET_SBD_STATUS"
15f218
 UNKNOWN_COMMAND = 'UNKNOWN_COMMAND'
15f218
 UNSUPPORTED_AGENT = 'UNSUPPORTED_AGENT'
15f218
+WATCHDOG_INVALID = "WATCHDOG_INVALID"
15f218
 UNSUPPORTED_OPERATION_ON_NON_SYSTEMD_SYSTEMS = "UNSUPPORTED_OPERATION_ON_NON_SYSTEMD_SYSTEMS"
15f218
 WATCHDOG_NOT_FOUND = "WATCHDOG_NOT_FOUND"
15f218
diff --git a/pcs/lib/commands/quorum.py b/pcs/lib/commands/quorum.py
15f218
index 7425e78..7fb7bb4 100644
15f218
--- a/pcs/lib/commands/quorum.py
15f218
+++ b/pcs/lib/commands/quorum.py
15f218
@@ -5,8 +5,9 @@ from __future__ import (
15f218
     unicode_literals,
15f218
 )
15f218
 
15f218
-from pcs.lib import reports
15f218
-from pcs.lib.errors import LibraryError
15f218
+from pcs.common import report_codes
15f218
+from pcs.lib import reports, sbd
15f218
+from pcs.lib.errors import LibraryError, ReportItemSeverity
15f218
 from pcs.lib.corosync import (
15f218
     live as corosync_live,
15f218
     qdevice_net,
15f218
@@ -39,16 +40,50 @@ def get_config(lib_env):
15f218
         "device": device,
15f218
     }
15f218
 
15f218
-def set_options(lib_env, options, skip_offline_nodes=False):
15f218
+
15f218
+def _check_if_atb_can_be_disabled(
15f218
+    runner, report_processor, corosync_conf, was_enabled, force=False
15f218
+):
15f218
+    """
15f218
+    Check whenever auto_tie_breaker can be changed without affecting SBD.
15f218
+    Raises LibraryError if change of ATB will affect SBD functionality.
15f218
+
15f218
+    runner -- CommandRunner
15f218
+    report_processor -- report processor
15f218
+    corosync_conf -- corosync conf facade
15f218
+    was_enabled -- True if ATB was enabled, False otherwise
15f218
+    force -- force change
15f218
+    """
15f218
+    if (
15f218
+        was_enabled
15f218
+        and
15f218
+        not corosync_conf.is_enabled_auto_tie_breaker()
15f218
+        and
15f218
+        sbd.is_auto_tie_breaker_needed(runner, corosync_conf)
15f218
+    ):
15f218
+        report_processor.process(reports.quorum_cannot_disable_atb_due_to_sbd(
15f218
+            ReportItemSeverity.WARNING if force else ReportItemSeverity.ERROR,
15f218
+            None if force else report_codes.FORCE_OPTIONS
15f218
+        ))
15f218
+
15f218
+
15f218
+def set_options(lib_env, options, skip_offline_nodes=False, force=False):
15f218
     """
15f218
     Set corosync quorum options, distribute and reload corosync.conf if live
15f218
     lib_env LibraryEnvironment
15f218
     options quorum options (dict)
15f218
     skip_offline_nodes continue even if not all nodes are accessible
15f218
+    bool force force changes
15f218
     """
15f218
     __ensure_not_cman(lib_env)
15f218
     cfg = lib_env.get_corosync_conf()
15f218
+    atb_enabled = cfg.is_enabled_auto_tie_breaker()
15f218
     cfg.set_quorum_options(lib_env.report_processor, options)
15f218
+    if lib_env.is_corosync_conf_live:
15f218
+        _check_if_atb_can_be_disabled(
15f218
+            lib_env.cmd_runner(), lib_env.report_processor,
15f218
+            cfg, atb_enabled, force
15f218
+        )
15f218
     lib_env.push_corosync_conf(cfg, skip_offline_nodes)
15f218
 
15f218
 def status_text(lib_env):
15f218
diff --git a/pcs/lib/commands/sbd.py b/pcs/lib/commands/sbd.py
15f218
index 875758f..265ebb5 100644
15f218
--- a/pcs/lib/commands/sbd.py
15f218
+++ b/pcs/lib/commands/sbd.py
15f218
@@ -5,6 +5,7 @@ from __future__ import (
15f218
     unicode_literals,
15f218
 )
15f218
 
15f218
+import os
15f218
 import json
15f218
 
15f218
 from pcs import settings
15f218
@@ -44,7 +45,9 @@ def _validate_sbd_options(sbd_config, allow_unknown_opts=False):
15f218
     """
15f218
 
15f218
     report_item_list = []
15f218
-    unsupported_sbd_option_list = ["SBD_WATCHDOG_DEV", "SBD_OPTS"]
15f218
+    unsupported_sbd_option_list = [
15f218
+        "SBD_WATCHDOG_DEV", "SBD_OPTS", "SBD_PACEMAKER"
15f218
+    ]
15f218
     allowed_sbd_options = [
15f218
         "SBD_DELAY_START", "SBD_STARTMODE", "SBD_WATCHDOG_TIMEOUT"
15f218
     ]
15f218
@@ -62,6 +65,17 @@ def _validate_sbd_options(sbd_config, allow_unknown_opts=False):
15f218
                 Severities.WARNING if allow_unknown_opts else Severities.ERROR,
15f218
                 None if allow_unknown_opts else report_codes.FORCE_OPTIONS
15f218
             ))
15f218
+    if "SBD_WATCHDOG_TIMEOUT" in sbd_config:
15f218
+        report_item = reports.invalid_option_value(
15f218
+            "SBD_WATCHDOG_TIMEOUT",
15f218
+            sbd_config["SBD_WATCHDOG_TIMEOUT"],
15f218
+            "nonnegative integer"
15f218
+        )
15f218
+        try:
15f218
+            if int(sbd_config["SBD_WATCHDOG_TIMEOUT"]) < 0:
15f218
+                report_item_list.append(report_item)
15f218
+        except (ValueError, TypeError):
15f218
+            report_item_list.append(report_item)
15f218
 
15f218
     return report_item_list
15f218
 
15f218
@@ -81,6 +95,9 @@ def _get_full_watchdog_list(node_list, default_watchdog, watchdog_dict):
15f218
     report_item_list = []
15f218
 
15f218
     for node_name, watchdog in watchdog_dict.items():
15f218
+        if not watchdog or not os.path.isabs(watchdog):
15f218
+            report_item_list.append(reports.invalid_watchdog_path(watchdog))
15f218
+            continue
15f218
         try:
15f218
             full_dict[node_list.find_by_label(node_name)] = watchdog
15f218
         except NodeNotFound:
15f218
@@ -140,6 +157,14 @@ def enable_sbd(
15f218
         full_watchdog_dict
15f218
     )
15f218
 
15f218
+    # enable ATB if needed
15f218
+    corosync_conf = lib_env.get_corosync_conf()
15f218
+    if sbd.atb_has_to_be_enabled(lib_env.cmd_runner(), corosync_conf):
15f218
+        corosync_conf.set_quorum_options(
15f218
+            lib_env.report_processor, {"auto_tie_breaker": "1"}
15f218
+        )
15f218
+        lib_env.push_corosync_conf(corosync_conf, ignore_offline_nodes)
15f218
+
15f218
     # distribute SBD configuration
15f218
     config = sbd.get_default_sbd_config()
15f218
     config.update(sbd_options)
15f218
@@ -147,7 +172,8 @@ def enable_sbd(
15f218
         lib_env.report_processor,
15f218
         lib_env.node_communicator(),
15f218
         online_nodes,
15f218
-        config
15f218
+        config,
15f218
+        full_watchdog_dict
15f218
     )
15f218
 
15f218
     # remove cluster prop 'stonith_watchdog_timeout'
15f218
diff --git a/pcs/lib/corosync/config_facade.py b/pcs/lib/corosync/config_facade.py
15f218
index 600a89b..be621c0 100644
15f218
--- a/pcs/lib/corosync/config_facade.py
15f218
+++ b/pcs/lib/corosync/config_facade.py
15f218
@@ -129,6 +129,16 @@ class ConfigFacade(object):
15f218
                     options[name] = value
15f218
         return options
15f218
 
15f218
+    def is_enabled_auto_tie_breaker(self):
15f218
+        """
15f218
+        Returns True if auto tie braker option is enabled, False otherwise.
15f218
+        """
15f218
+        auto_tie_breaker = "0"
15f218
+        for quorum in self.config.get_sections("quorum"):
15f218
+            for attr in quorum.get_attributes("auto_tie_breaker"):
15f218
+                auto_tie_breaker = attr[1]
15f218
+        return auto_tie_breaker == "1"
15f218
+
15f218
     def __validate_quorum_options(self, options):
15f218
         report_items = []
15f218
         has_qdevice = self.has_quorum_device()
15f218
@@ -488,10 +498,7 @@ class ConfigFacade(object):
15f218
         # get relevant status
15f218
         has_quorum_device = self.has_quorum_device()
15f218
         has_two_nodes = len(self.get_nodes()) == 2
15f218
-        auto_tie_breaker = False
15f218
-        for quorum in self.config.get_sections("quorum"):
15f218
-            for attr in quorum.get_attributes("auto_tie_breaker"):
15f218
-                auto_tie_breaker = attr[1] != "0"
15f218
+        auto_tie_breaker = self.is_enabled_auto_tie_breaker()
15f218
         # update two_node
15f218
         if has_two_nodes and not auto_tie_breaker and not has_quorum_device:
15f218
             quorum_section_list = self.__ensure_section(self.config, "quorum")
15f218
diff --git a/pcs/lib/external.py b/pcs/lib/external.py
15f218
index 25e071f..08bf2bb 100644
15f218
--- a/pcs/lib/external.py
15f218
+++ b/pcs/lib/external.py
15f218
@@ -135,13 +135,13 @@ def disable_service(runner, service, instance=None):
15f218
     instance -- instance name, it ha no effect on not systemd systems.
15f218
         If None no instance name will be used.
15f218
     """
15f218
+    if not is_service_installed(runner, service):
15f218
+        return
15f218
     if is_systemctl():
15f218
         output, retval = runner.run([
15f218
             "systemctl", "disable", _get_service_name(service, instance)
15f218
         ])
15f218
     else:
15f218
-        if not is_service_installed(runner, service):
15f218
-            return
15f218
         output, retval = runner.run(["chkconfig", service, "off"])
15f218
     if retval != 0:
15f218
         raise DisableServiceError(service, output.rstrip(), instance)
15f218
diff --git a/pcs/lib/reports.py b/pcs/lib/reports.py
15f218
index eac95c7..568bb7e 100644
15f218
--- a/pcs/lib/reports.py
15f218
+++ b/pcs/lib/reports.py
15f218
@@ -1701,6 +1701,19 @@ def watchdog_not_found(node, watchdog):
15f218
     )
15f218
 
15f218
 
15f218
+def invalid_watchdog_path(watchdog):
15f218
+    """
15f218
+    watchdog path is not absolut path
15f218
+
15f218
+    watchdog -- watchdog device path
15f218
+    """
15f218
+    return ReportItem.error(
15f218
+        report_codes.WATCHDOG_INVALID,
15f218
+        "Watchdog path '{watchdog}' is invalid.",
15f218
+        info={"watchdog": watchdog}
15f218
+    )
15f218
+
15f218
+
15f218
 def unable_to_get_sbd_status(node, reason):
15f218
     """
15f218
     there was (communication or parsing) failure during obtaining status of SBD
15f218
@@ -1901,3 +1914,17 @@ def live_environment_required(forbidden_options):
15f218
             "options_string": ", ".join(forbidden_options),
15f218
         }
15f218
     )
15f218
+
15f218
+
15f218
+def quorum_cannot_disable_atb_due_to_sbd(
15f218
+    severity=ReportItemSeverity.ERROR, forceable=None
15f218
+):
15f218
+    """
15f218
+    Quorum option auto_tie_breaker cannot be disbled due to SBD.
15f218
+    """
15f218
+    return ReportItem(
15f218
+        report_codes.COROSYNC_QUORUM_CANNOT_DISABLE_ATB_DUE_TO_SBD,
15f218
+        severity,
15f218
+        "unable to disable auto_tie_breaker: SBD fencing will have no effect",
15f218
+        forceable=forceable
15f218
+    )
15f218
diff --git a/pcs/lib/sbd.py b/pcs/lib/sbd.py
15f218
index 4488a73..c9f013b 100644
15f218
--- a/pcs/lib/sbd.py
15f218
+++ b/pcs/lib/sbd.py
15f218
@@ -46,6 +46,50 @@ def _run_parallel_and_raise_lib_error_on_failure(func, param_list):
15f218
         raise LibraryError(*report_list)
15f218
 
15f218
 
15f218
+def is_auto_tie_breaker_needed(
15f218
+    runner, corosync_conf_facade, node_number_modifier=0
15f218
+):
15f218
+    """
15f218
+    Returns True whenever quorum option auto tie breaker is needed to be enabled
15f218
+    for proper working of SBD fencing. False if it is not needed.
15f218
+
15f218
+    runner -- command runner
15f218
+    corosync_conf_facade --
15f218
+    node_number_modifier -- this value vill be added to current number of nodes.
15f218
+        This can be useful to test whenever is ATB needed when adding/removeing
15f218
+        node.
15f218
+    """
15f218
+    return (
15f218
+        not corosync_conf_facade.has_quorum_device()
15f218
+        and
15f218
+        (len(corosync_conf_facade.get_nodes()) + node_number_modifier) % 2 == 0
15f218
+        and
15f218
+        is_sbd_installed(runner)
15f218
+        and
15f218
+        is_sbd_enabled(runner)
15f218
+    )
15f218
+
15f218
+def atb_has_to_be_enabled(runner, corosync_conf_facade, node_number_modifier=0):
15f218
+    """
15f218
+    Return True whenever quorum option auto tie breaker has to be enabled for
15f218
+    proper working of SBD fencing. False if it's not needed or it is already
15f218
+    enabled.
15f218
+
15f218
+    runner -- command runner
15f218
+    corosync_conf_facade --
15f218
+    node_number_modifier -- this value vill be added to current number of nodes.
15f218
+        This can be useful to test whenever is ATB needed when adding/removeing
15f218
+        node.
15f218
+    """
15f218
+    return (
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
 def check_sbd(communicator, node, watchdog):
15f218
     """
15f218
     Check SBD on specified 'node' and existence of specified watchdog.
15f218
@@ -123,18 +167,23 @@ def set_sbd_config(communicator, node, config):
15f218
     )
15f218
 
15f218
 
15f218
-def set_sbd_config_on_node(report_processor, node_communicator, node, config):
15f218
+def set_sbd_config_on_node(
15f218
+    report_processor, node_communicator, node, config, watchdog
15f218
+):
15f218
     """
15f218
-    Send SBD configuration to 'node'. Also puts correct node name into
15f218
-        SBD_OPTS option (SBD_OPTS="-n <node_name>").
15f218
+    Send SBD configuration to 'node' with specified watchdog set. Also puts
15f218
+    correct node name into SBD_OPTS option (SBD_OPTS="-n <node_name>").
15f218
 
15f218
     report_processor --
15f218
     node_communicator -- NodeCommunicator
15f218
     node -- NodeAddresses
15f218
     config -- dictionary in format: <SBD config option>: <value>
15f218
+    watchdog -- path to watchdog device
15f218
     """
15f218
     config = dict(config)
15f218
     config["SBD_OPTS"] = '"-n {node_name}"'.format(node_name=node.label)
15f218
+    if watchdog:
15f218
+        config["SBD_WATCHDOG_DEV"] = watchdog
15f218
     set_sbd_config(node_communicator, node, dict_to_environment_file(config))
15f218
     report_processor.process(
15f218
         reports.sbd_config_accepted_by_node(node.label)
15f218
@@ -142,7 +191,7 @@ def set_sbd_config_on_node(report_processor, node_communicator, node, config):
15f218
 
15f218
 
15f218
 def set_sbd_config_on_all_nodes(
15f218
-        report_processor, node_communicator, node_list, config
15f218
+    report_processor, node_communicator, node_list, config, watchdog_dict
15f218
 ):
15f218
     """
15f218
     Send SBD configuration 'config' to all nodes in 'node_list'. Option
15f218
@@ -153,12 +202,20 @@ def set_sbd_config_on_all_nodes(
15f218
     node_communicator -- NodeCommunicator
15f218
     node_list -- NodeAddressesList
15f218
     config -- dictionary in format: <SBD config option>: <value>
15f218
+    watchdog_dict -- dictionary of watchdogs where key is NodeAdresses object
15f218
+        and value is path to watchdog
15f218
     """
15f218
     report_processor.process(reports.sbd_config_distribution_started())
15f218
     _run_parallel_and_raise_lib_error_on_failure(
15f218
         set_sbd_config_on_node,
15f218
         [
15f218
-            ([report_processor, node_communicator, node, config], {})
15f218
+            (
15f218
+                [
15f218
+                    report_processor, node_communicator, node, config,
15f218
+                    watchdog_dict.get(node)
15f218
+                ],
15f218
+                {}
15f218
+            )
15f218
             for node in node_list
15f218
         ]
15f218
     )
15f218
@@ -362,3 +419,14 @@ def is_sbd_enabled(runner):
15f218
     runner -- CommandRunner
15f218
     """
15f218
     return external.is_service_enabled(runner, "sbd")
15f218
+
15f218
+
15f218
+def is_sbd_installed(runner):
15f218
+    """
15f218
+    Check if SBD service is installed in local system.
15f218
+    Reurns True id SBD service is installed. False otherwise.
15f218
+
15f218
+    runner -- CommandRunner
15f218
+    """
15f218
+    return external.is_service_installed(runner, "sbd")
15f218
+
15f218
diff --git a/pcs/pcs.8 b/pcs/pcs.8
15f218
index 9064054..7a054ca 100644
15f218
--- a/pcs/pcs.8
15f218
+++ b/pcs/pcs.8
15f218
@@ -271,8 +271,8 @@ Upgrade the CIB to conform to the latest version of the document schema.
15f218
 edit [scope=<scope> | \fB\-\-config\fR]
15f218
 Edit the cib in the editor specified by the $EDITOR environment variable and push out any changes upon saving.  Specify scope to edit a specific section of the CIB.  Valid values of the scope are: configuration, nodes, resources, constraints, crm_config, rsc_defaults, op_defaults.  \fB\-\-config\fR is the same as scope=configuration.  Use of \fB\-\-config\fR is recommended.  Do not specify a scope if you need to edit the whole CIB or be warned in the case of outdated CIB.
15f218
 .TP
15f218
-node add <node[,node\-altaddr]> [\fB\-\-start\fR [\fB\-\-wait\fR[=<n>]]] [\fB\-\-enable\fR]
15f218
-Add the node to corosync.conf and corosync on all nodes in the cluster and sync the new corosync.conf to the new node.  If \fB\-\-start\fR is specified also start corosync/pacemaker on the new node, if \fB\-\-wait\fR is sepcified wait up to 'n' seconds for the new node to start.  If \fB\-\-enable\fR is specified enable corosync/pacemaker on new node.  When using Redundant Ring Protocol (RRP) with udpu transport, specify the ring 0 address first followed by a ',' and then the ring 1 address.
15f218
+node add <node[,node\-altaddr]> [\fB\-\-start\fR [\fB\-\-wait\fR[=<n>]]] [\fB\-\-enable\fR] [\fB\-\-watchdog\fR=<watchdog\-path>]
15f218
+Add the node to corosync.conf and corosync on all nodes in the cluster and sync the new corosync.conf to the new node.  If \fB\-\-start\fR is specified also start corosync/pacemaker on the new node, if \fB\-\-wait\fR is sepcified wait up to 'n' seconds for the new node to start.  If \fB\-\-enable\fR is specified enable corosync/pacemaker on new node.  When using Redundant Ring Protocol (RRP) with udpu transport, specify the ring 0 address first followed by a ',' and then the ring 1 address. Use \fB\-\-watchdog\fR to specify path to watchdog on newly added node, when SBD is enabled in cluster.
15f218
 .TP
15f218
 node remove <node>
15f218
 Shutdown specified node and remove it from pacemaker and corosync on all other nodes in the cluster.
15f218
diff --git a/pcs/quorum.py b/pcs/quorum.py
15f218
index 1c2d41d..6cd06ca 100644
15f218
--- a/pcs/quorum.py
15f218
+++ b/pcs/quorum.py
15f218
@@ -121,7 +121,8 @@ def quorum_update_cmd(lib, argv, modificators):
15f218
 
15f218
     lib.quorum.set_options(
15f218
         options,
15f218
-        skip_offline_nodes=modificators["skip_offline_nodes"]
15f218
+        skip_offline_nodes=modificators["skip_offline_nodes"],
15f218
+        force=modificators["force"]
15f218
     )
15f218
 
15f218
 def quorum_device_add_cmd(lib, argv, modificators):
15f218
diff --git a/pcs/stonith.py b/pcs/stonith.py
15f218
index 93332ef..23f3800 100644
15f218
--- a/pcs/stonith.py
15f218
+++ b/pcs/stonith.py
15f218
@@ -495,7 +495,7 @@ def _sbd_parse_watchdogs(watchdog_list):
15f218
     for watchdog_node in watchdog_list:
15f218
         if "@" not in watchdog_node:
15f218
             if default_watchdog:
15f218
-                raise CmdLineInputError("Multiple default watchdogs.")
15f218
+                raise CmdLineInputError("Multiple watchdog definitions.")
15f218
             default_watchdog = watchdog_node
15f218
         else:
15f218
             watchdog, node_name = watchdog_node.rsplit("@", 1)
15f218
@@ -553,7 +553,7 @@ def sbd_config(lib, argv, modifiers):
15f218
 
15f218
     config = config_list[0]["config"]
15f218
 
15f218
-    filtered_options = ["SBD_WATCHDOG_DEV", "SBD_OPTS"]
15f218
+    filtered_options = ["SBD_WATCHDOG_DEV", "SBD_OPTS", "SBD_PACEMAKER"]
15f218
     for key, val in config.items():
15f218
         if key in filtered_options:
15f218
             continue
15f218
diff --git a/pcs/test/test_lib_commands_quorum.py b/pcs/test/test_lib_commands_quorum.py
15f218
index 826251a..d286a8f 100644
15f218
--- a/pcs/test/test_lib_commands_quorum.py
15f218
+++ b/pcs/test/test_lib_commands_quorum.py
15f218
@@ -25,6 +25,7 @@ from pcs.lib.errors import (
15f218
     LibraryError,
15f218
     ReportItemSeverity as severity,
15f218
 )
15f218
+from pcs.lib.corosync.config_facade import ConfigFacade
15f218
 from pcs.lib.external import NodeCommunicationException
15f218
 from pcs.lib.node import NodeAddresses, NodeAddressesList
15f218
 
15f218
@@ -146,23 +147,201 @@ class GetQuorumConfigTest(TestCase, CmanMixin):
15f218
         self.assertEqual([], self.mock_reporter.report_item_list)
15f218
 
15f218
 
15f218
+@mock.patch("pcs.lib.sbd.is_auto_tie_breaker_needed")
15f218
+class CheckIfAtbCanBeDisabledTest(TestCase):
15f218
+    def setUp(self):
15f218
+        self.mock_reporter = MockLibraryReportProcessor()
15f218
+        self.mock_runner = "cmd_runner"
15f218
+        self.mock_corosync_conf = mock.MagicMock(spec_set=ConfigFacade)
15f218
+
15f218
+    def test_atb_no_need_was_disabled_atb_disabled(self, mock_atb_needed):
15f218
+        mock_atb_needed.return_value = False
15f218
+        self.mock_corosync_conf.is_enabled_auto_tie_breaker.return_value = False
15f218
+        lib._check_if_atb_can_be_disabled(
15f218
+            self.mock_runner, self.mock_reporter, self.mock_corosync_conf, False
15f218
+        )
15f218
+        self.assertEqual([], self.mock_reporter.report_item_list)
15f218
+
15f218
+    def test_atb_no_need_was_disabled_atb_enabled(self, mock_atb_needed):
15f218
+        mock_atb_needed.return_value = False
15f218
+        self.mock_corosync_conf.is_enabled_auto_tie_breaker.return_value = True
15f218
+        lib._check_if_atb_can_be_disabled(
15f218
+            self.mock_runner, self.mock_reporter, self.mock_corosync_conf, False
15f218
+        )
15f218
+        self.assertEqual([], self.mock_reporter.report_item_list)
15f218
+
15f218
+    def test_atb_no_need_was_enable_atb_disabled(self, mock_atb_needed):
15f218
+        mock_atb_needed.return_value = False
15f218
+        self.mock_corosync_conf.is_enabled_auto_tie_breaker.return_value = False
15f218
+        lib._check_if_atb_can_be_disabled(
15f218
+            self.mock_runner, self.mock_reporter, self.mock_corosync_conf, True
15f218
+        )
15f218
+        self.assertEqual([], self.mock_reporter.report_item_list)
15f218
+
15f218
+    def test_atb_no_need_was_enabled_atb_enabled(self, mock_atb_needed):
15f218
+        mock_atb_needed.return_value = False
15f218
+        self.mock_corosync_conf.is_enabled_auto_tie_breaker.return_value = True
15f218
+        lib._check_if_atb_can_be_disabled(
15f218
+            self.mock_runner, self.mock_reporter, self.mock_corosync_conf, True
15f218
+        )
15f218
+        self.assertEqual([], self.mock_reporter.report_item_list)
15f218
+
15f218
+    def test_atb_needed_was_disabled_atb_disabled(self, mock_atb_needed):
15f218
+        mock_atb_needed.return_value = True
15f218
+        self.mock_corosync_conf.is_enabled_auto_tie_breaker.return_value = False
15f218
+        lib._check_if_atb_can_be_disabled(
15f218
+            self.mock_runner, self.mock_reporter, self.mock_corosync_conf, False
15f218
+        )
15f218
+        self.assertEqual([], self.mock_reporter.report_item_list)
15f218
+
15f218
+    def test_atb_needed_was_disabled_atb_enabled(self, mock_atb_needed):
15f218
+        mock_atb_needed.return_value = True
15f218
+        self.mock_corosync_conf.is_enabled_auto_tie_breaker.return_value = True
15f218
+        lib._check_if_atb_can_be_disabled(
15f218
+            self.mock_runner, self.mock_reporter, self.mock_corosync_conf, False
15f218
+        )
15f218
+        self.assertEqual([], self.mock_reporter.report_item_list)
15f218
+
15f218
+    def test_atb_needed_was_enable_atb_disabled(self, mock_atb_needed):
15f218
+        mock_atb_needed.return_value = True
15f218
+        self.mock_corosync_conf.is_enabled_auto_tie_breaker.return_value = False
15f218
+        report_item = (
15f218
+            severity.ERROR,
15f218
+            report_codes.COROSYNC_QUORUM_CANNOT_DISABLE_ATB_DUE_TO_SBD,
15f218
+            {},
15f218
+            report_codes.FORCE_OPTIONS
15f218
+        )
15f218
+        assert_raise_library_error(
15f218
+            lambda: lib._check_if_atb_can_be_disabled(
15f218
+                self.mock_runner,
15f218
+                self.mock_reporter,
15f218
+                self.mock_corosync_conf,
15f218
+                True
15f218
+            ),
15f218
+            report_item
15f218
+        )
15f218
+        assert_report_item_list_equal(
15f218
+            self.mock_reporter.report_item_list, [report_item]
15f218
+        )
15f218
+
15f218
+    def test_atb_needed_was_enabled_atb_enabled(self, mock_atb_needed):
15f218
+        mock_atb_needed.return_value = True
15f218
+        self.mock_corosync_conf.is_enabled_auto_tie_breaker.return_value = True
15f218
+        lib._check_if_atb_can_be_disabled(
15f218
+            self.mock_runner, self.mock_reporter, self.mock_corosync_conf, True
15f218
+        )
15f218
+        self.assertEqual([], self.mock_reporter.report_item_list)
15f218
+
15f218
+
15f218
+    def test_atb_no_need_was_disabled_atb_disabled_force(
15f218
+        self, mock_atb_needed
15f218
+    ):
15f218
+        mock_atb_needed.return_value = False
15f218
+        self.mock_corosync_conf.is_enabled_auto_tie_breaker.return_value = False
15f218
+        lib._check_if_atb_can_be_disabled(
15f218
+            self.mock_runner, self.mock_reporter, self.mock_corosync_conf,
15f218
+            False, force=True
15f218
+        )
15f218
+        self.assertEqual([], self.mock_reporter.report_item_list)
15f218
+
15f218
+    def test_atb_no_need_was_disabled_atb_enabled_force(
15f218
+        self, mock_atb_needed
15f218
+    ):
15f218
+        mock_atb_needed.return_value = False
15f218
+        self.mock_corosync_conf.is_enabled_auto_tie_breaker.return_value = True
15f218
+        lib._check_if_atb_can_be_disabled(
15f218
+            self.mock_runner, self.mock_reporter, self.mock_corosync_conf,
15f218
+            False, force=True
15f218
+        )
15f218
+        self.assertEqual([], self.mock_reporter.report_item_list)
15f218
+
15f218
+    def test_atb_no_need_was_enable_atb_disabled_force(self, mock_atb_needed):
15f218
+        mock_atb_needed.return_value = False
15f218
+        self.mock_corosync_conf.is_enabled_auto_tie_breaker.return_value = False
15f218
+        lib._check_if_atb_can_be_disabled(
15f218
+            self.mock_runner, self.mock_reporter, self.mock_corosync_conf, True,
15f218
+            force=True
15f218
+        )
15f218
+        self.assertEqual([], self.mock_reporter.report_item_list)
15f218
+
15f218
+    def test_atb_no_need_was_enabled_atb_enabled_force(self, mock_atb_needed):
15f218
+        mock_atb_needed.return_value = False
15f218
+        self.mock_corosync_conf.is_enabled_auto_tie_breaker.return_value = True
15f218
+        lib._check_if_atb_can_be_disabled(
15f218
+            self.mock_runner, self.mock_reporter, self.mock_corosync_conf, True,
15f218
+            force=True
15f218
+        )
15f218
+        self.assertEqual([], self.mock_reporter.report_item_list)
15f218
+
15f218
+    def test_atb_needed_was_disabled_atb_disabled_force(
15f218
+        self, mock_atb_needed
15f218
+    ):
15f218
+        mock_atb_needed.return_value = True
15f218
+        self.mock_corosync_conf.is_enabled_auto_tie_breaker.return_value = False
15f218
+        lib._check_if_atb_can_be_disabled(
15f218
+            self.mock_runner, self.mock_reporter, self.mock_corosync_conf,
15f218
+            False, force=True
15f218
+        )
15f218
+        self.assertEqual([], self.mock_reporter.report_item_list)
15f218
+
15f218
+    def test_atb_needed_was_disabled_atb_enabled_force(self, mock_atb_needed):
15f218
+        mock_atb_needed.return_value = True
15f218
+        self.mock_corosync_conf.is_enabled_auto_tie_breaker.return_value = True
15f218
+        lib._check_if_atb_can_be_disabled(
15f218
+            self.mock_runner, self.mock_reporter, self.mock_corosync_conf,
15f218
+            False, force=True
15f218
+        )
15f218
+        self.assertEqual([], self.mock_reporter.report_item_list)
15f218
+
15f218
+    def test_atb_needed_was_enable_atb_disabled_force(self, mock_atb_needed):
15f218
+        mock_atb_needed.return_value = True
15f218
+        self.mock_corosync_conf.is_enabled_auto_tie_breaker.return_value = False
15f218
+        lib._check_if_atb_can_be_disabled(
15f218
+            self.mock_runner, self.mock_reporter, self.mock_corosync_conf, True,
15f218
+            force=True
15f218
+        )
15f218
+        assert_report_item_list_equal(
15f218
+            self.mock_reporter.report_item_list,
15f218
+            [(
15f218
+                severity.WARNING,
15f218
+                report_codes.COROSYNC_QUORUM_CANNOT_DISABLE_ATB_DUE_TO_SBD,
15f218
+                {},
15f218
+                None
15f218
+            )]
15f218
+        )
15f218
+
15f218
+    def test_atb_needed_was_enabled_atb_enabled_force(self, mock_atb_needed):
15f218
+        mock_atb_needed.return_value = True
15f218
+        self.mock_corosync_conf.is_enabled_auto_tie_breaker.return_value = True
15f218
+        lib._check_if_atb_can_be_disabled(
15f218
+            self.mock_runner, self.mock_reporter, self.mock_corosync_conf, True,
15f218
+            force=True
15f218
+        )
15f218
+        self.assertEqual([], self.mock_reporter.report_item_list)
15f218
+
15f218
+
15f218
+@mock.patch("pcs.lib.commands.quorum._check_if_atb_can_be_disabled")
15f218
 @mock.patch.object(LibraryEnvironment, "push_corosync_conf")
15f218
 @mock.patch.object(LibraryEnvironment, "get_corosync_conf_data")
15f218
+@mock.patch.object(LibraryEnvironment, "cmd_runner")
15f218
 class SetQuorumOptionsTest(TestCase, CmanMixin):
15f218
     def setUp(self):
15f218
         self.mock_logger = mock.MagicMock(logging.Logger)
15f218
         self.mock_reporter = MockLibraryReportProcessor()
15f218
 
15f218
     @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: True)
15f218
-    def test_disabled_on_cman(self, mock_get_corosync, mock_push_corosync):
15f218
+    def test_disabled_on_cman(
15f218
+        self, mock_runner, mock_get_corosync, mock_push_corosync, mock_check
15f218
+    ):
15f218
         lib_env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
15f218
         self.assert_disabled_on_cman(lambda: lib.set_options(lib_env, {}))
15f218
         mock_get_corosync.assert_not_called()
15f218
         mock_push_corosync.assert_not_called()
15f218
+        mock_check.assert_not_called()
15f218
 
15f218
     @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: True)
15f218
     def test_enabled_on_cman_if_not_live(
15f218
-        self, mock_get_corosync, mock_push_corosync
15f218
+        self, mock_runner, mock_get_corosync, mock_push_corosync, mock_check
15f218
     ):
15f218
         original_conf = "invalid {\nconfig: stop after cman test"
15f218
         mock_get_corosync.return_value = original_conf
15f218
@@ -182,11 +361,16 @@ class SetQuorumOptionsTest(TestCase, CmanMixin):
15f218
         )
15f218
 
15f218
         mock_push_corosync.assert_not_called()
15f218
+        mock_check.assert_not_called()
15f218
+        mock_runner.assert_not_called()
15f218
 
15f218
     @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
15f218
-    def test_success(self, mock_get_corosync, mock_push_corosync):
15f218
+    def test_success(
15f218
+        self, mock_runner, mock_get_corosync, mock_push_corosync, mock_check
15f218
+    ):
15f218
         original_conf = open(rc("corosync-3nodes.conf")).read()
15f218
         mock_get_corosync.return_value = original_conf
15f218
+        mock_runner.return_value = "cmd_runner"
15f218
         lib_env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
15f218
 
15f218
         new_options = {"wait_for_all": "1"}
15f218
@@ -201,9 +385,16 @@ class SetQuorumOptionsTest(TestCase, CmanMixin):
15f218
             )
15f218
         )
15f218
         self.assertEqual([], self.mock_reporter.report_item_list)
15f218
+        self.assertEqual(1, mock_check.call_count)
15f218
+        self.assertEqual("cmd_runner", mock_check.call_args[0][0])
15f218
+        self.assertEqual(self.mock_reporter, mock_check.call_args[0][1])
15f218
+        self.assertFalse(mock_check.call_args[0][3])
15f218
+        self.assertFalse(mock_check.call_args[0][4])
15f218
 
15f218
     @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
15f218
-    def test_bad_options(self, mock_get_corosync, mock_push_corosync):
15f218
+    def test_bad_options(
15f218
+        self, mock_runner, mock_get_corosync, mock_push_corosync, mock_check
15f218
+    ):
15f218
         original_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
@@ -228,9 +419,12 @@ class SetQuorumOptionsTest(TestCase, CmanMixin):
15f218
         )
15f218
 
15f218
         mock_push_corosync.assert_not_called()
15f218
+        mock_check.assert_not_called()
15f218
 
15f218
     @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
15f218
-    def test_bad_config(self, mock_get_corosync, mock_push_corosync):
15f218
+    def test_bad_config(
15f218
+        self, mock_runner, mock_get_corosync, mock_push_corosync, mock_check
15f218
+    ):
15f218
         original_conf = "invalid {\nconfig: this is"
15f218
         mock_get_corosync.return_value = original_conf
15f218
         lib_env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
15f218
@@ -246,6 +440,7 @@ class SetQuorumOptionsTest(TestCase, CmanMixin):
15f218
         )
15f218
 
15f218
         mock_push_corosync.assert_not_called()
15f218
+        mock_check.assert_not_called()
15f218
 
15f218
 
15f218
 @mock.patch("pcs.lib.commands.quorum.corosync_live.get_quorum_status_text")
15f218
diff --git a/pcs/test/test_lib_commands_sbd.py b/pcs/test/test_lib_commands_sbd.py
15f218
index 9a96757..0663082 100644
15f218
--- a/pcs/test/test_lib_commands_sbd.py
15f218
+++ b/pcs/test/test_lib_commands_sbd.py
15f218
@@ -35,6 +35,15 @@ from pcs.lib.external import (
15f218
 import pcs.lib.commands.sbd as cmd_sbd
15f218
 
15f218
 
15f218
+def _assert_equal_list_of_dictionaries_without_order(expected, actual):
15f218
+    for item in actual:
15f218
+        if item not in expected:
15f218
+            raise AssertionError("Given but not expected: {0}".format(item))
15f218
+    for item in expected:
15f218
+        if item not in actual:
15f218
+            raise AssertionError("Expected but not given: {0}".format(item))
15f218
+
15f218
+
15f218
 class CommandSbdTest(TestCase):
15f218
     def setUp(self):
15f218
         self.mock_env = mock.MagicMock(spec_set=LibraryEnvironment)
15f218
@@ -234,7 +243,8 @@ class ValidateSbdOptionsTest(TestCase):
15f218
             "SBD_STARTMODE": "clean",
15f218
             "SBD_WATCHDOG_DEV": "/dev/watchdog",
15f218
             "SBD_UNKNOWN": "",
15f218
-            "SBD_OPTS": "  "
15f218
+            "SBD_OPTS": "  ",
15f218
+            "SBD_PACEMAKER": "false",
15f218
         }
15f218
 
15f218
         assert_report_item_list_equal(
15f218
@@ -272,6 +282,90 @@ class ValidateSbdOptionsTest(TestCase):
15f218
                         "allowed_str": self.allowed_sbd_options_str
15f218
                     },
15f218
                     None
15f218
+                ),
15f218
+                (
15f218
+                    Severities.ERROR,
15f218
+                    report_codes.INVALID_OPTION,
15f218
+                    {
15f218
+                        "option_name": "SBD_PACEMAKER",
15f218
+                        "option_type": None,
15f218
+                        "allowed": self.allowed_sbd_options,
15f218
+                        "allowed_str": self.allowed_sbd_options_str
15f218
+                    },
15f218
+                    None
15f218
+                )
15f218
+            ]
15f218
+        )
15f218
+
15f218
+    def test_watchdog_timeout_is_not_present(self):
15f218
+        config = {
15f218
+            "SBD_DELAY_START": "yes",
15f218
+            "SBD_STARTMODE": "clean"
15f218
+        }
15f218
+        self.assertEqual([], cmd_sbd._validate_sbd_options(config))
15f218
+
15f218
+    def test_watchdog_timeout_is_nonnegative_int(self):
15f218
+        config = {
15f218
+            "SBD_WATCHDOG_TIMEOUT": "-1",
15f218
+        }
15f218
+
15f218
+        assert_report_item_list_equal(
15f218
+            cmd_sbd._validate_sbd_options(config),
15f218
+            [
15f218
+                (
15f218
+                    Severities.ERROR,
15f218
+                    report_codes.INVALID_OPTION_VALUE,
15f218
+                    {
15f218
+                        "option_name": "SBD_WATCHDOG_TIMEOUT",
15f218
+                        "option_value": "-1",
15f218
+                        "allowed_values": "nonnegative integer",
15f218
+                        "allowed_values_str": "nonnegative integer",
15f218
+                    },
15f218
+                    None
15f218
+                )
15f218
+            ]
15f218
+        )
15f218
+
15f218
+    def test_watchdog_timeout_is_not_int(self):
15f218
+        config = {
15f218
+            "SBD_WATCHDOG_TIMEOUT": "not int",
15f218
+        }
15f218
+
15f218
+        assert_report_item_list_equal(
15f218
+            cmd_sbd._validate_sbd_options(config),
15f218
+            [
15f218
+                (
15f218
+                    Severities.ERROR,
15f218
+                    report_codes.INVALID_OPTION_VALUE,
15f218
+                    {
15f218
+                        "option_name": "SBD_WATCHDOG_TIMEOUT",
15f218
+                        "option_value": "not int",
15f218
+                        "allowed_values": "nonnegative integer",
15f218
+                        "allowed_values_str": "nonnegative integer",
15f218
+                    },
15f218
+                    None
15f218
+                )
15f218
+            ]
15f218
+        )
15f218
+
15f218
+    def test_watchdog_timeout_is_none(self):
15f218
+        config = {
15f218
+            "SBD_WATCHDOG_TIMEOUT": None,
15f218
+        }
15f218
+
15f218
+        assert_report_item_list_equal(
15f218
+            cmd_sbd._validate_sbd_options(config),
15f218
+            [
15f218
+                (
15f218
+                    Severities.ERROR,
15f218
+                    report_codes.INVALID_OPTION_VALUE,
15f218
+                    {
15f218
+                        "option_name": "SBD_WATCHDOG_TIMEOUT",
15f218
+                        "option_value": None,
15f218
+                        "allowed_values": "nonnegative integer",
15f218
+                        "allowed_values_str": "nonnegative integer",
15f218
+                    },
15f218
+                    None
15f218
                 )
15f218
             ]
15f218
         )
15f218
@@ -325,6 +419,35 @@ class GetFullWatchdogListTest(TestCase):
15f218
             )
15f218
         )
15f218
 
15f218
+    def test_invalid_watchdogs(self):
15f218
+        watchdog_dict = {
15f218
+            self.node_list[1].label: "",
15f218
+            self.node_list[2].label: None,
15f218
+            self.node_list[3].label: "not/abs/path",
15f218
+            self.node_list[4].label: "/dev/watchdog"
15f218
+
15f218
+        }
15f218
+        assert_raise_library_error(
15f218
+            lambda: cmd_sbd._get_full_watchdog_list(
15f218
+                self.node_list, "/dev/dog", watchdog_dict
15f218
+            ),
15f218
+            (
15f218
+                Severities.ERROR,
15f218
+                report_codes.WATCHDOG_INVALID,
15f218
+                {"watchdog": ""}
15f218
+            ),
15f218
+            (
15f218
+                Severities.ERROR,
15f218
+                report_codes.WATCHDOG_INVALID,
15f218
+                {"watchdog": None}
15f218
+            ),
15f218
+            (
15f218
+                Severities.ERROR,
15f218
+                report_codes.WATCHDOG_INVALID,
15f218
+                {"watchdog": "not/abs/path"}
15f218
+            )
15f218
+        )
15f218
+
15f218
 
15f218
 @mock.patch("pcs.lib.commands.sbd._get_cluster_nodes")
15f218
 @mock.patch("pcs.lib.sbd.check_sbd")
15f218
@@ -393,8 +516,7 @@ class GetClusterSbdStatusTest(CommandSbdTest):
15f218
                 }
15f218
             }
15f218
         ]
15f218
-
15f218
-        self.assertEqual(
15f218
+        _assert_equal_list_of_dictionaries_without_order(
15f218
             expected, cmd_sbd.get_cluster_sbd_status(self.mock_env)
15f218
         )
15f218
         mock_get_nodes.assert_called_once_with(self.mock_env)
15f218
@@ -447,7 +569,7 @@ class GetClusterSbdStatusTest(CommandSbdTest):
15f218
             }
15f218
         ]
15f218
 
15f218
-        self.assertEqual(
15f218
+        _assert_equal_list_of_dictionaries_without_order(
15f218
             expected, cmd_sbd.get_cluster_sbd_status(self.mock_env)
15f218
         )
15f218
         mock_get_nodes.assert_called_once_with(self.mock_env)
15f218
@@ -538,7 +660,7 @@ OPTION=   value
15f218
             }
15f218
         ]
15f218
 
15f218
-        self.assertEqual(
15f218
+        _assert_equal_list_of_dictionaries_without_order(
15f218
             expected, cmd_sbd.get_cluster_sbd_config(self.mock_env)
15f218
         )
15f218
         mock_get_nodes.assert_called_once_with(self.mock_env)
15f218
@@ -589,7 +711,7 @@ invalid value
15f218
             }
15f218
         ]
15f218
 
15f218
-        self.assertEqual(
15f218
+        _assert_equal_list_of_dictionaries_without_order(
15f218
             expected, cmd_sbd.get_cluster_sbd_config(self.mock_env)
15f218
         )
15f218
         mock_get_nodes.assert_called_once_with(self.mock_env)
15f218
diff --git a/pcs/test/test_lib_corosync_config_facade.py b/pcs/test/test_lib_corosync_config_facade.py
15f218
index 4a35fd9..91f7b40 100644
15f218
--- a/pcs/test/test_lib_corosync_config_facade.py
15f218
+++ b/pcs/test/test_lib_corosync_config_facade.py
15f218
@@ -281,6 +281,34 @@ quorum {
15f218
         self.assertFalse(facade.need_qdevice_reload)
15f218
 
15f218
 
15f218
+class IsEnabledAutoTieBreaker(TestCase):
15f218
+    def test_enabled(self):
15f218
+        config = """\
15f218
+quorum {
15f218
+    auto_tie_breaker: 1
15f218
+}
15f218
+"""
15f218
+        facade = lib.ConfigFacade.from_string(config)
15f218
+        self.assertTrue(facade.is_enabled_auto_tie_breaker())
15f218
+
15f218
+    def test_disabled(self):
15f218
+        config = """\
15f218
+quorum {
15f218
+    auto_tie_breaker: 0
15f218
+}
15f218
+"""
15f218
+        facade = lib.ConfigFacade.from_string(config)
15f218
+        self.assertFalse(facade.is_enabled_auto_tie_breaker())
15f218
+
15f218
+    def test_no_value(self):
15f218
+        config = """\
15f218
+quorum {
15f218
+}
15f218
+"""
15f218
+        facade = lib.ConfigFacade.from_string(config)
15f218
+        self.assertFalse(facade.is_enabled_auto_tie_breaker())
15f218
+
15f218
+
15f218
 class SetQuorumOptionsTest(TestCase):
15f218
     def get_two_node(self, facade):
15f218
         two_node = None
15f218
diff --git a/pcs/test/test_lib_external.py b/pcs/test/test_lib_external.py
15f218
index a4ec0f9..b0ffdbb 100644
15f218
--- a/pcs/test/test_lib_external.py
15f218
+++ b/pcs/test/test_lib_external.py
15f218
@@ -1012,12 +1012,14 @@ Copyright (c) 2006-2009 Red Hat, Inc.
15f218
 
15f218
 
15f218
 @mock.patch("pcs.lib.external.is_systemctl")
15f218
+@mock.patch("pcs.lib.external.is_service_installed")
15f218
 class DisableServiceTest(TestCase):
15f218
     def setUp(self):
15f218
         self.mock_runner = mock.MagicMock(spec_set=lib.CommandRunner)
15f218
         self.service = "service_name"
15f218
 
15f218
-    def test_systemctl(self, mock_systemctl):
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
         lib.disable_service(self.mock_runner, self.service)
15f218
@@ -1025,7 +1027,8 @@ class DisableServiceTest(TestCase):
15f218
             ["systemctl", "disable", self.service + ".service"]
15f218
         )
15f218
 
15f218
-    def test_systemctl_failed(self, mock_systemctl):
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.assertRaises(
15f218
@@ -1036,7 +1039,6 @@ class DisableServiceTest(TestCase):
15f218
             ["systemctl", "disable", self.service + ".service"]
15f218
         )
15f218
 
15f218
-    @mock.patch("pcs.lib.external.is_service_installed")
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
@@ -1046,7 +1048,6 @@ class DisableServiceTest(TestCase):
15f218
             ["chkconfig", self.service, "off"]
15f218
         )
15f218
 
15f218
-    @mock.patch("pcs.lib.external.is_service_installed")
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
@@ -1059,7 +1060,14 @@ class DisableServiceTest(TestCase):
15f218
             ["chkconfig", self.service, "off"]
15f218
         )
15f218
 
15f218
-    @mock.patch("pcs.lib.external.is_service_installed")
15f218
+    def test_systemctl_not_installed(
15f218
+            self, mock_is_installed, mock_systemctl
15f218
+    ):
15f218
+        mock_is_installed.return_value = False
15f218
+        mock_systemctl.return_value = True
15f218
+        lib.disable_service(self.mock_runner, self.service)
15f218
+        self.assertEqual(self.mock_runner.run.call_count, 0)
15f218
+
15f218
     def test_not_systemctl_not_installed(
15f218
             self, mock_is_installed, mock_systemctl
15f218
     ):
15f218
@@ -1068,7 +1076,8 @@ class DisableServiceTest(TestCase):
15f218
         lib.disable_service(self.mock_runner, self.service)
15f218
         self.assertEqual(self.mock_runner.run.call_count, 0)
15f218
 
15f218
-    def test_instance_systemctl(self, mock_systemctl):
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
         lib.disable_service(self.mock_runner, self.service, instance="test")
15f218
@@ -1078,7 +1087,6 @@ class DisableServiceTest(TestCase):
15f218
             "{0}@{1}.service".format(self.service, "test")
15f218
         ])
15f218
 
15f218
-    @mock.patch("pcs.lib.external.is_service_installed")
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
diff --git a/pcs/test/test_lib_sbd.py b/pcs/test/test_lib_sbd.py
15f218
index e3c1401..fd29484 100644
15f218
--- a/pcs/test/test_lib_sbd.py
15f218
+++ b/pcs/test/test_lib_sbd.py
15f218
@@ -28,6 +28,7 @@ from pcs.lib.external import (
15f218
     NodeConnectionException,
15f218
 )
15f218
 import pcs.lib.sbd as lib_sbd
15f218
+from pcs.lib.corosync.config_facade import ConfigFacade as CorosyncConfigFacade
15f218
 
15f218
 
15f218
 class TestException(Exception):
15f218
@@ -85,6 +86,246 @@ class RunParallelAndRaiseLibErrorOnFailureTest(TestCase):
15f218
         )
15f218
 
15f218
 
15f218
+@mock.patch("pcs.lib.sbd.is_sbd_installed")
15f218
+@mock.patch("pcs.lib.sbd.is_sbd_enabled")
15f218
+class IsAutoTieBreakerNeededTest(TestCase):
15f218
+    def setUp(self):
15f218
+        self.runner = "runner"
15f218
+        self.mock_corosync_conf = mock.MagicMock(spec_set=CorosyncConfigFacade)
15f218
+
15f218
+    def _set_ret_vals(self, nodes, qdevice):
15f218
+        self.mock_corosync_conf.get_nodes.return_value = nodes
15f218
+        self.mock_corosync_conf.has_quorum_device.return_value = qdevice
15f218
+
15f218
+    def test_sbd_enabled_even_nodes_has_qdevice(
15f218
+        self, mock_enabled, mock_installed
15f218
+    ):
15f218
+        mock_enabled.return_value = True
15f218
+        mock_installed.return_value = True
15f218
+        self._set_ret_vals([1, 2], True)
15f218
+        self.assertFalse(lib_sbd.is_auto_tie_breaker_needed(
15f218
+            self.runner, self.mock_corosync_conf
15f218
+        ))
15f218
+
15f218
+    def test_sbd_enabled_even_nodes_no_qdevice(
15f218
+        self, mock_enabled, mock_installed
15f218
+    ):
15f218
+        mock_enabled.return_value = True
15f218
+        mock_installed.return_value = True
15f218
+        self._set_ret_vals([1, 2], False)
15f218
+        self.assertTrue(lib_sbd.is_auto_tie_breaker_needed(
15f218
+            self.runner, self.mock_corosync_conf
15f218
+        ))
15f218
+
15f218
+    def test_sbd_not_installed_even_nodes_no_qdevice(
15f218
+        self, mock_enabled, mock_installed
15f218
+    ):
15f218
+        mock_enabled.return_value = False
15f218
+        mock_installed.return_value = False
15f218
+        self._set_ret_vals([1, 2], False)
15f218
+        self.assertFalse(lib_sbd.is_auto_tie_breaker_needed(
15f218
+            self.runner, self.mock_corosync_conf
15f218
+        ))
15f218
+
15f218
+    def test_sbd_enabled_odd_nodes_has_qdevice(
15f218
+        self, mock_enabled, mock_installed
15f218
+    ):
15f218
+        mock_enabled.return_value = True
15f218
+        mock_installed.return_value = True
15f218
+        self._set_ret_vals([1, 2, 3], True)
15f218
+        self.assertFalse(lib_sbd.is_auto_tie_breaker_needed(
15f218
+            self.runner, self.mock_corosync_conf
15f218
+        ))
15f218
+
15f218
+    def test_sbd_enabled_odd_nodes_no_qdevice(
15f218
+        self, mock_enabled, mock_installed
15f218
+    ):
15f218
+        mock_enabled.return_value = True
15f218
+        mock_installed.return_value = True
15f218
+        self._set_ret_vals([1, 2, 3], False)
15f218
+        self.assertFalse(lib_sbd.is_auto_tie_breaker_needed(
15f218
+            self.runner, self.mock_corosync_conf
15f218
+        ))
15f218
+
15f218
+    def test_sbd_disabled_even_nodes_has_qdevice(
15f218
+        self, mock_enabled, mock_installed
15f218
+    ):
15f218
+        mock_enabled.return_value = False
15f218
+        mock_installed.return_value = True
15f218
+        self._set_ret_vals([1, 2], True)
15f218
+        self.assertFalse(lib_sbd.is_auto_tie_breaker_needed(
15f218
+            self.runner, self.mock_corosync_conf
15f218
+        ))
15f218
+
15f218
+    def test_sbd_disabled_even_nodes_no_qdevice(
15f218
+        self, mock_enabled, mock_installed
15f218
+    ):
15f218
+        mock_enabled.return_value = False
15f218
+        mock_installed.return_value = True
15f218
+        self._set_ret_vals([1, 2], False)
15f218
+        self.assertFalse(lib_sbd.is_auto_tie_breaker_needed(
15f218
+            self.runner, self.mock_corosync_conf
15f218
+        ))
15f218
+
15f218
+    def test_sbd_disabled_odd_nodes_has_qdevice(
15f218
+        self, mock_enabled, mock_installed
15f218
+    ):
15f218
+        mock_enabled.return_value = False
15f218
+        mock_installed.return_value = True
15f218
+        self._set_ret_vals([1, 2, 3], True)
15f218
+        self.assertFalse(lib_sbd.is_auto_tie_breaker_needed(
15f218
+            self.runner, self.mock_corosync_conf
15f218
+        ))
15f218
+
15f218
+    def test_sbd_disabled_odd_nodes_no_qdevice(
15f218
+        self, mock_enabled, mock_installed
15f218
+    ):
15f218
+        mock_enabled.return_value = False
15f218
+        mock_installed.return_value = True
15f218
+        self._set_ret_vals([1, 2, 3], False)
15f218
+        self.assertFalse(lib_sbd.is_auto_tie_breaker_needed(
15f218
+            self.runner, self.mock_corosync_conf
15f218
+        ))
15f218
+
15f218
+    def test_sbd_enabled_odd_nodes_no_qdevice_plus_node(
15f218
+        self, mock_enabled, mock_installed
15f218
+    ):
15f218
+        mock_enabled.return_value = True
15f218
+        mock_installed.return_value = True
15f218
+        self._set_ret_vals([1, 2, 3], False)
15f218
+        self.assertTrue(lib_sbd.is_auto_tie_breaker_needed(
15f218
+            self.runner, self.mock_corosync_conf, 1
15f218
+        ))
15f218
+
15f218
+    def test_sbd_not_installed_odd_nodes_no_qdevice_plus_node(
15f218
+        self, mock_enabled, mock_installed
15f218
+    ):
15f218
+        mock_enabled.return_value = False
15f218
+        mock_installed.return_value = False
15f218
+        self._set_ret_vals([1, 2, 3], False)
15f218
+        self.assertFalse(lib_sbd.is_auto_tie_breaker_needed(
15f218
+            self.runner, self.mock_corosync_conf, 1
15f218
+        ))
15f218
+
15f218
+    def test_sbd_enabled_odd_nodes_no_qdevice_minus_node(
15f218
+        self, mock_enabled, mock_installed
15f218
+    ):
15f218
+        mock_enabled.return_value = True
15f218
+        mock_installed.return_value = True
15f218
+        self._set_ret_vals([1, 2, 3], False)
15f218
+        self.assertTrue(lib_sbd.is_auto_tie_breaker_needed(
15f218
+            self.runner, self.mock_corosync_conf, -1
15f218
+        ))
15f218
+
15f218
+    def test_sbd_enabled_odd_nodes_no_qdevice_plus_2_nodes(
15f218
+        self, mock_enabled, mock_installed
15f218
+    ):
15f218
+        mock_enabled.return_value = True
15f218
+        mock_installed.return_value = True
15f218
+        self._set_ret_vals([1, 2, 3], False)
15f218
+        self.assertFalse(lib_sbd.is_auto_tie_breaker_needed(
15f218
+            self.runner, self.mock_corosync_conf, 2
15f218
+        ))
15f218
+
15f218
+    def test_sbd_enabled_odd_nodes_no_qdevice_minus_2_nodes(
15f218
+        self, mock_enabled, mock_installed
15f218
+    ):
15f218
+        mock_enabled.return_value = True
15f218
+        mock_installed.return_value = True
15f218
+        self._set_ret_vals([1, 2, 3], False)
15f218
+        self.assertFalse(lib_sbd.is_auto_tie_breaker_needed(
15f218
+            self.runner, self.mock_corosync_conf, -2
15f218
+        ))
15f218
+
15f218
+    def test_sbd_enabled_even_nodes_no_qdevice_plus_node(
15f218
+        self, mock_enabled, mock_installed
15f218
+    ):
15f218
+        mock_enabled.return_value = True
15f218
+        mock_installed.return_value = True
15f218
+        self._set_ret_vals([1, 2], False)
15f218
+        self.assertFalse(lib_sbd.is_auto_tie_breaker_needed(
15f218
+            self.runner, self.mock_corosync_conf, 1
15f218
+        ))
15f218
+
15f218
+    def test_sbd_enabled_even_nodes_no_qdevice_minus_node(
15f218
+        self, mock_enabled, mock_installed
15f218
+    ):
15f218
+        mock_enabled.return_value = True
15f218
+        mock_installed.return_value = True
15f218
+        self._set_ret_vals([1, 2], False)
15f218
+        self.assertFalse(lib_sbd.is_auto_tie_breaker_needed(
15f218
+            self.runner, self.mock_corosync_conf, -1
15f218
+        ))
15f218
+
15f218
+    def test_sbd_enabled_even_nodes_no_qdevice_plus_2_nodes(
15f218
+        self, mock_enabled, mock_installed
15f218
+    ):
15f218
+        mock_enabled.return_value = True
15f218
+        mock_installed.return_value = True
15f218
+        self._set_ret_vals([1, 2], False)
15f218
+        self.assertTrue(lib_sbd.is_auto_tie_breaker_needed(
15f218
+            self.runner, self.mock_corosync_conf, 2
15f218
+        ))
15f218
+
15f218
+    def test_sbd_enabled_even_nodes_no_qdevice_minus_2_nodes(
15f218
+        self, mock_enabled, mock_installed
15f218
+    ):
15f218
+        mock_enabled.return_value = True
15f218
+        mock_installed.return_value = True
15f218
+        self._set_ret_vals([1, 2, 3, 4], False)
15f218
+        self.assertTrue(lib_sbd.is_auto_tie_breaker_needed(
15f218
+            self.runner, self.mock_corosync_conf, -2
15f218
+        ))
15f218
+
15f218
+
15f218
+@mock.patch("pcs.lib.sbd.is_auto_tie_breaker_needed")
15f218
+class AtbHasToBeEnabledTest(TestCase):
15f218
+    def setUp(self):
15f218
+        self.mock_runner = "runner"
15f218
+        self.mock_conf = mock.MagicMock(spec_set=CorosyncConfigFacade)
15f218
+
15f218
+    def test_atb_needed_is_enabled(self, mock_is_needed):
15f218
+        mock_is_needed.return_value = True
15f218
+        self.mock_conf.is_enabled_auto_tie_breaker.return_value = True
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
+
15f218
+    def test_atb_needed_is_disabled(self, mock_is_needed):
15f218
+        mock_is_needed.return_value = True
15f218
+        self.mock_conf.is_enabled_auto_tie_breaker.return_value = False
15f218
+        self.assertTrue(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
+
15f218
+    def test_atb_not_needed_is_enabled(self, mock_is_needed):
15f218
+        mock_is_needed.return_value = False
15f218
+        self.mock_conf.is_enabled_auto_tie_breaker.return_value = True
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
+
15f218
+    def test_atb_not_needed_is_disabled(self, mock_is_needed):
15f218
+        mock_is_needed.return_value = False
15f218
+        self.mock_conf.is_enabled_auto_tie_breaker.return_value = False
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
+
15f218
+
15f218
+
15f218
 class CheckSbdTest(TestCase):
15f218
     def test_success(self):
15f218
         mock_communicator = mock.MagicMock(spec_set=NodeCommunicator)
15f218
@@ -316,11 +557,11 @@ class SetSbdConfigOnNodeTest(TestCase):
15f218
         }
15f218
         cfg_out = """# This file has been generated by pcs.
15f218
 SBD_OPTS="-n node1"
15f218
-SBD_WATCHDOG_DEV=/dev/watchdog
15f218
+SBD_WATCHDOG_DEV=/my/watchdog
15f218
 SBD_WATCHDOG_TIMEOUT=0
15f218
 """
15f218
         lib_sbd.set_sbd_config_on_node(
15f218
-            self.mock_rep, self.mock_com, self.node, cfg_in
15f218
+            self.mock_rep, self.mock_com, self.node, cfg_in, "/my/watchdog"
15f218
         )
15f218
         mock_set_sbd_cfg.assert_called_once_with(
15f218
             self.mock_com, self.node, cfg_out
15f218
@@ -340,17 +581,24 @@ class SetSbdConfigOnAllNodesTest(TestCase):
15f218
     def test_success(self, mock_func):
15f218
         mock_com = mock.MagicMock(spec_set=NodeCommunicator)
15f218
         mock_rep = MockLibraryReportProcessor()
15f218
-        node_list = [NodeAddresses("node" + str(i)) for i in range(5)]
15f218
+        watchdog_dict = dict([
15f218
+            (NodeAddresses("node" + str(i)), "/dev/watchdog" + str(i))
15f218
+            for i in range(5)
15f218
+        ])
15f218
+        node_list = list(watchdog_dict.keys())
15f218
         config = {
15f218
             "opt1": "val1",
15f218
             "opt2": "val2"
15f218
         }
15f218
         lib_sbd.set_sbd_config_on_all_nodes(
15f218
-            mock_rep, mock_com, node_list, config
15f218
+            mock_rep, mock_com, node_list, config, watchdog_dict
15f218
         )
15f218
         mock_func.assert_called_once_with(
15f218
             lib_sbd.set_sbd_config_on_node,
15f218
-            [([mock_rep, mock_com, node, config], {}) for node in node_list]
15f218
+            [
15f218
+                ([mock_rep, mock_com, node, config, watchdog_dict[node]], {})
15f218
+                for node in node_list
15f218
+            ]
15f218
         )
15f218
 
15f218
 
15f218
@@ -594,3 +842,17 @@ class IsSbdEnabledTest(TestCase):
15f218
         mock_obj = mock.MagicMock()
15f218
         mock_is_service_enabled.return_value = True
15f218
         self.assertTrue(lib_sbd.is_sbd_enabled(mock_obj))
15f218
+
15f218
+
15f218
+@mock.patch("pcs.lib.external.is_service_installed")
15f218
+class IsSbdInstalledTest(TestCase):
15f218
+    def test_installed(self, mock_is_service_installed):
15f218
+        mock_obj = mock.MagicMock()
15f218
+        mock_is_service_installed.return_value = True
15f218
+        self.assertTrue(lib_sbd.is_sbd_installed(mock_obj))
15f218
+
15f218
+    def test_not_installed(self, mock_is_service_installed):
15f218
+        mock_obj = mock.MagicMock()
15f218
+        mock_is_service_installed.return_value = False
15f218
+        self.assertFalse(lib_sbd.is_sbd_installed(mock_obj))
15f218
+
15f218
diff --git a/pcs/usage.py b/pcs/usage.py
15f218
index b11a5fa..9ebbca9 100644
15f218
--- a/pcs/usage.py
15f218
+++ b/pcs/usage.py
15f218
@@ -683,6 +683,7 @@ Commands:
15f218
         the whole CIB or be warned in the case of outdated CIB.
15f218
 
15f218
     node add <node[,node-altaddr]> [--start [--wait[=<n>]]] [--enable]
15f218
+            [--watchdog=<watchdog-path>]
15f218
         Add the node to corosync.conf and corosync on all nodes in the cluster
15f218
         and sync the new corosync.conf to the new node.  If --start is
15f218
         specified also start corosync/pacemaker on the new node, if --wait is
15f218
@@ -690,6 +691,8 @@ Commands:
15f218
         is specified enable corosync/pacemaker on new node.
15f218
         When using Redundant Ring Protocol (RRP) with udpu transport, specify
15f218
         the ring 0 address first followed by a ',' and then the ring 1 address.
15f218
+        Use --watchdog to specify path to watchdog on newly added node, when SBD
15f218
+        is enabled in cluster.
15f218
 
15f218
     node remove <node>
15f218
         Shutdown specified node and remove it from pacemaker and corosync on
15f218
diff --git a/pcs/utils.py b/pcs/utils.py
15f218
index 53cc0b0..a7ff7ca 100644
15f218
--- a/pcs/utils.py
15f218
+++ b/pcs/utils.py
15f218
@@ -32,7 +32,7 @@ from pcs.cli.common.reports import (
15f218
     LibraryReportProcessorToConsole as LibraryReportProcessorToConsole,
15f218
 )
15f218
 from pcs.common.tools import simple_cache
15f218
-from pcs.lib import reports
15f218
+from pcs.lib import reports, sbd
15f218
 from pcs.lib.env import LibraryEnvironment
15f218
 from pcs.lib.errors import LibraryError
15f218
 from pcs.lib.external import (
15f218
@@ -574,6 +574,23 @@ def getCorosyncActiveNodes():
15f218
 
15f218
     return nodes_active
15f218
 
15f218
+
15f218
+def _enable_auto_tie_breaker_for_sbd(corosync_conf):
15f218
+    """
15f218
+    Enable auto tie breaker in specified corosync conf if it is needed by SBD.
15f218
+
15f218
+    corosync_conf -- parsed corosync conf
15f218
+    """
15f218
+    try:
15f218
+        corosync_facade = corosync_conf_facade(corosync_conf)
15f218
+        if sbd.atb_has_to_be_enabled(cmd_runner(), corosync_facade):
15f218
+            corosync_facade.set_quorum_options(
15f218
+                get_report_processor(), {"auto_tie_breaker": "1"}
15f218
+            )
15f218
+    except LibraryError as e:
15f218
+        process_library_reports(e.args)
15f218
+
15f218
+
15f218
 # Add node specified to corosync.conf and reload corosync.conf (if running)
15f218
 def addNodeToCorosync(node):
15f218
 # Before adding, make sure node isn't already in corosync.conf
15f218
@@ -600,6 +617,9 @@ def addNodeToCorosync(node):
15f218
         new_node.add_attribute("ring1_addr", node1)
15f218
     new_node.add_attribute("nodeid", new_nodeid)
15f218
 
15f218
+    # enable ATB if it's needed
15f218
+    _enable_auto_tie_breaker_for_sbd(corosync_conf)
15f218
+
15f218
     corosync_conf = autoset_2node_corosync(corosync_conf)
15f218
     setCorosyncConf(str(corosync_conf))
15f218
     return True
15f218
@@ -667,6 +687,9 @@ def removeNodeFromCorosync(node):
15f218
                     removed_node = True
15f218
 
15f218
     if removed_node:
15f218
+        # enable ATB if it's needed
15f218
+        _enable_auto_tie_breaker_for_sbd(corosync_conf)
15f218
+
15f218
         corosync_conf = autoset_2node_corosync(corosync_conf)
15f218
         setCorosyncConf(str(corosync_conf))
15f218
 
15f218
diff --git a/pcsd/pcs.rb b/pcsd/pcs.rb
15f218
index d46cd62..137bb3d 100644
15f218
--- a/pcsd/pcs.rb
15f218
+++ b/pcsd/pcs.rb
15f218
@@ -1995,14 +1995,14 @@ def enable_service(service)
15f218
 end
15f218
 
15f218
 def disable_service(service)
15f218
+  # fails when the service is not installed, so we need to check it beforehand
15f218
+  if not is_service_installed?(service)
15f218
+    return true
15f218
+  end
15f218
+
15f218
   if ISSYSTEMCTL
15f218
-    # returns success even if the service is not installed
15f218
     cmd = ['systemctl', 'disable', "#{service}.service"]
15f218
   else
15f218
-    if not is_service_installed?(service)
15f218
-      return true
15f218
-    end
15f218
-    # fails when the service is not installed, so we need to check it beforehand
15f218
     cmd = ['chkconfig', service, 'off']
15f218
   end
15f218
   _, _, retcode = run_cmd(PCSAuth.getSuperuserAuth(), *cmd)
15f218
-- 
15f218
1.8.3.1
15f218