diff --git a/crmsh/bootstrap.py b/crmsh/bootstrap.py index 001e1b8401..bfd848444b 100644 --- a/crmsh/bootstrap.py +++ b/crmsh/bootstrap.py @@ -2425,6 +2425,8 @@ def remove_qdevice() -> None: utils.check_all_nodes_reachable("removing QDevice from the cluster") qdevice_reload_policy = qdevice.evaluate_qdevice_quorum_effect(qdevice.QDEVICE_REMOVE) + if qdevice_reload_policy == qdevice.QdevicePolicy.QDEVICE_REMOVE_REJECT: + utils.fatal("Can't remove QDevice since the quorum will be lost for diskless SBD cluster") if qdevice_reload_policy == qdevice.QdevicePolicy.QDEVICE_RESTART_LATER: with utils.leverage_maintenance_mode() as enabled: if not utils.able_to_restart_cluster(enabled): diff --git a/crmsh/qdevice.py b/crmsh/qdevice.py index 11fe630b65..48b6c3a8a1 100644 --- a/crmsh/qdevice.py +++ b/crmsh/qdevice.py @@ -30,6 +30,7 @@ class QdevicePolicy(Enum): QDEVICE_RELOAD = 0 QDEVICE_RESTART = 1 QDEVICE_RESTART_LATER = 2 + QDEVICE_REMOVE_REJECT = 3 def evaluate_qdevice_quorum_effect(mode): @@ -44,10 +45,16 @@ def evaluate_qdevice_quorum_effect(mode): if mode == QDEVICE_ADD: expected_votes += 1 elif mode == QDEVICE_REMOVE: - actual_votes -= 1 + vote = corosync.get_value("quorum.device.votes") or 1 + actual_votes -= int(vote) diskless_sbd = sbd.SBDUtils.is_using_diskless_sbd() - if utils.calculate_quorate_status(expected_votes, actual_votes) and not diskless_sbd: + quorate = utils.calculate_quorate_status(expected_votes, actual_votes) + if not quorate and diskless_sbd and mode == QDEVICE_REMOVE: + # Reject to remove qdevice, as it will lose quorum + # then diskless SBD will self-fence the node + return QdevicePolicy.QDEVICE_REMOVE_REJECT + elif quorate and not diskless_sbd: # safe to use reload return QdevicePolicy.QDEVICE_RELOAD elif xmlutil.CrmMonXmlParser().is_non_stonith_resource_running() and not utils.is_cluster_in_maintenance_mode(): @@ -60,7 +67,6 @@ def evaluate_qdevice_quorum_effect(mode): # safe to restart cluster service return QdevicePolicy.QDEVICE_RESTART - def qnetd_lock_for_same_cluster_name(func): """ Decorator to claim lock on qnetd, to avoid the same cluster name added in qnetd diff --git a/test/unittests/test_qdevice.py b/test/unittests/test_qdevice.py index 36c1d06e57..6137aec4b9 100644 --- a/test/unittests/test_qdevice.py +++ b/test/unittests/test_qdevice.py @@ -22,12 +22,14 @@ def test_evaluate_qdevice_quorum_effect_reload(mock_get_dict, mock_quorate): mock_quorate.assert_called_once_with(3, 2) +@mock.patch('crmsh.corosync.get_value') @mock.patch('crmsh.xmlutil.CrmMonXmlParser') @mock.patch('crmsh.utils.calculate_quorate_status') @mock.patch('crmsh.utils.get_quorum_votes_dict') -def test_evaluate_qdevice_quorum_effect_later(mock_get_dict, mock_quorate, mock_parser): +def test_evaluate_qdevice_quorum_effect_later(mock_get_dict, mock_quorate, mock_parser, mock_get_value): mock_get_dict.return_value = {'Expected': '2', 'Total': '2'} mock_quorate.return_value = False + mock_get_value.return_value = '1' mock_parser().is_non_stonith_resource_running.return_value = True res = qdevice.evaluate_qdevice_quorum_effect(qdevice.QDEVICE_REMOVE) assert res == qdevice.QdevicePolicy.QDEVICE_RESTART_LATER @@ -35,12 +37,14 @@ def test_evaluate_qdevice_quorum_effect_later(mock_get_dict, mock_quorate, mock_ mock_quorate.assert_called_once_with(2, 1) +@mock.patch('crmsh.corosync.get_value') @mock.patch('crmsh.xmlutil.CrmMonXmlParser') @mock.patch('crmsh.utils.calculate_quorate_status') @mock.patch('crmsh.utils.get_quorum_votes_dict') -def test_evaluate_qdevice_quorum_effect(mock_get_dict, mock_quorate, mock_parser): +def test_evaluate_qdevice_quorum_effect(mock_get_dict, mock_quorate, mock_parser, mock_get_value): mock_get_dict.return_value = {'Expected': '2', 'Total': '2'} mock_quorate.return_value = False + mock_get_value.return_value = '1' mock_parser().is_non_stonith_resource_running.return_value = False res = qdevice.evaluate_qdevice_quorum_effect(qdevice.QDEVICE_REMOVE) assert res == qdevice.QdevicePolicy.QDEVICE_RESTART