Blob Blame History Raw
From bd852905ad905b83daa1a7240e7a79c3357db5b8 Mon Sep 17 00:00:00 2001
From: Tomas Jelinek <tojeline@redhat.com>
Date: Thu, 30 Jun 2016 15:09:31 +0200
Subject: [PATCH] bz1158805-01 add "pcs quorum expected-votes" command

---
 pcs/cli/common/lib_wrapper.py        |  1 +
 pcs/common/report_codes.py           |  1 +
 pcs/lib/commands/quorum.py           | 21 ++++++++++++++++++
 pcs/lib/corosync/live.py             | 15 +++++++++++++
 pcs/lib/reports.py                   | 13 +++++++++++
 pcs/pcs.8                            |  3 +++
 pcs/quorum.py                        |  7 ++++++
 pcs/test/suite.py                    |  6 +++--
 pcs/test/test_lib_commands_quorum.py | 43 ++++++++++++++++++++++++++++++++++++
 pcs/test/test_lib_corosync_live.py   | 42 +++++++++++++++++++++++++++++++++++
 pcs/usage.py                         |  4 ++++
 11 files changed, 154 insertions(+), 2 deletions(-)

diff --git a/pcs/cli/common/lib_wrapper.py b/pcs/cli/common/lib_wrapper.py
index 2dd5810..c4b8342 100644
--- a/pcs/cli/common/lib_wrapper.py
+++ b/pcs/cli/common/lib_wrapper.py
@@ -116,6 +116,7 @@ def load_module(env, middleware_factory, name):
                 "add_device": quorum.add_device,
                 "get_config": quorum.get_config,
                 "remove_device": quorum.remove_device,
+                "set_expected_votes_live": quorum.set_expected_votes_live,
                 "set_options": quorum.set_options,
                 "status": quorum.status_text,
                 "status_device": quorum.status_device_text,
diff --git a/pcs/common/report_codes.py b/pcs/common/report_codes.py
index afe0554..2b39938 100644
--- a/pcs/common/report_codes.py
+++ b/pcs/common/report_codes.py
@@ -47,6 +47,7 @@ COROSYNC_NOT_RUNNING_CHECK_NODE_ERROR = "COROSYNC_NOT_RUNNING_CHECK_NODE_ERROR"
 COROSYNC_NOT_RUNNING_ON_NODE = "COROSYNC_NOT_RUNNING_ON_NODE"
 COROSYNC_OPTIONS_INCOMPATIBLE_WITH_QDEVICE = "COROSYNC_OPTIONS_INCOMPATIBLE_WITH_QDEVICE"
 COROSYNC_QUORUM_GET_STATUS_ERROR = "COROSYNC_QUORUM_GET_STATUS_ERROR"
+COROSYNC_QUORUM_SET_EXPECTED_VOTES_ERROR = "COROSYNC_QUORUM_SET_EXPECTED_VOTES_ERROR"
 COROSYNC_RUNNING_ON_NODE = "COROSYNC_RUNNING_ON_NODE"
 CRM_MON_ERROR = "CRM_MON_ERROR"
 DUPLICATE_CONSTRAINTS_EXIST = "DUPLICATE_CONSTRAINTS_EXIST"
diff --git a/pcs/lib/commands/quorum.py b/pcs/lib/commands/quorum.py
index aa00bbd..7425e78 100644
--- a/pcs/lib/commands/quorum.py
+++ b/pcs/lib/commands/quorum.py
@@ -314,6 +314,27 @@ def _remove_device_model_net(lib_env, cluster_nodes, skip_offline_nodes):
         skip_offline_nodes
     )
 
+def set_expected_votes_live(lib_env, expected_votes):
+    """
+    set expected votes in live cluster to specified value
+    numeric expected_votes desired value of expected votes
+    """
+    if lib_env.is_cman_cluster:
+        raise LibraryError(reports.cman_unsupported_command())
+
+    try:
+        votes_int = int(expected_votes)
+        if votes_int < 1:
+            raise ValueError()
+    except ValueError:
+        raise LibraryError(reports.invalid_option_value(
+            "expected votes",
+            expected_votes,
+            "positive integer"
+        ))
+
+    corosync_live.set_expected_votes(lib_env.cmd_runner(), votes_int)
+
 def __ensure_not_cman(lib_env):
     if lib_env.is_corosync_conf_live and lib_env.is_cman_cluster:
         raise LibraryError(reports.cman_unsupported_command())
diff --git a/pcs/lib/corosync/live.py b/pcs/lib/corosync/live.py
index 4129aeb..b49b9f6 100644
--- a/pcs/lib/corosync/live.py
+++ b/pcs/lib/corosync/live.py
@@ -62,3 +62,18 @@ def get_quorum_status_text(runner):
             reports.corosync_quorum_get_status_error(output)
         )
     return output
+
+def set_expected_votes(runner, votes):
+    """
+    set expected votes in live cluster to specified value
+    """
+    output, retval = runner.run([
+        os.path.join(settings.corosync_binaries, "corosync-quorumtool"),
+        # format votes to handle the case where they are int
+        "-e", "{0}".format(votes)
+    ])
+    if retval != 0:
+        raise LibraryError(
+            reports.corosync_quorum_set_expected_votes_error(output)
+        )
+    return output
diff --git a/pcs/lib/reports.py b/pcs/lib/reports.py
index d8f88cd..9ececf9 100644
--- a/pcs/lib/reports.py
+++ b/pcs/lib/reports.py
@@ -565,6 +565,19 @@ def corosync_quorum_get_status_error(reason):
         }
     )
 
+def corosync_quorum_set_expected_votes_error(reason):
+    """
+    unable to set expcted votes in a live cluster
+    string reason an error message
+    """
+    return ReportItem.error(
+        report_codes.COROSYNC_QUORUM_SET_EXPECTED_VOTES_ERROR,
+        "Unable to set expected votes: {reason}",
+        info={
+            "reason": reason,
+        }
+    )
+
 def corosync_config_reloaded():
     """
     corosync configuration has been reloaded
diff --git a/pcs/pcs.8 b/pcs/pcs.8
index 949d918..a436b4c 100644
--- a/pcs/pcs.8
+++ b/pcs/pcs.8
@@ -563,6 +563,9 @@ Add/Change quorum device options.  Generic options and model options are all doc
 
 WARNING: If you want to change "host" option of qdevice model net, use "pcs quorum device remove" and "pcs quorum device add" commands to set up configuration properly unless old and new host is the same machine.
 .TP
+expected\-votes <votes>
+Set expected votes in the live cluster to specified value.  This only affects the live cluster, not changes any configuration files.
+.TP
 unblock [\fB\-\-force\fR]
 Cancel waiting for all nodes when establishing quorum.  Useful in situations where you know the cluster is inquorate, but you are confident that the cluster should proceed with resource management regardless.  This command should ONLY be used when nodes which the cluster is waiting for have been confirmed to be powered off and to have no access to shared resources.
 
diff --git a/pcs/quorum.py b/pcs/quorum.py
index 27085ac..2d54ed7 100644
--- a/pcs/quorum.py
+++ b/pcs/quorum.py
@@ -28,6 +28,8 @@ def quorum_cmd(lib, argv, modificators):
             usage.quorum(argv)
         elif sub_cmd == "config":
             quorum_config_cmd(lib, argv_next, modificators)
+        elif sub_cmd == "expected-votes":
+            quorum_expected_votes_cmd(lib, argv_next, modificators)
         elif sub_cmd == "status":
             quorum_status_cmd(lib, argv_next, modificators)
         elif sub_cmd == "device":
@@ -101,6 +103,11 @@ def quorum_config_to_str(config):
 
     return lines
 
+def quorum_expected_votes_cmd(lib, argv, modificators):
+    if len(argv) != 1:
+        raise CmdLineInputError()
+    lib.quorum.set_expected_votes_live(argv[0])
+
 def quorum_status_cmd(lib, argv, modificators):
     if argv:
         raise CmdLineInputError()
diff --git a/pcs/test/suite.py b/pcs/test/suite.py
index 85dd20c..5b29918 100755
--- a/pcs/test/suite.py
+++ b/pcs/test/suite.py
@@ -74,7 +74,7 @@ def run_tests(tests, verbose=False, color=False):
         verbosity=2 if verbose else 1,
         resultclass=resultclass
     )
-    testRunner.run(tests)
+    return testRunner.run(tests)
 
 put_package_to_path()
 explicitly_enumerated_tests = [
@@ -85,7 +85,7 @@ explicitly_enumerated_tests = [
         "--all-but",
     )
 ]
-run_tests(
+test_result = run_tests(
     discover_tests(explicitly_enumerated_tests, "--all-but" in sys.argv),
     verbose="-v" in sys.argv,
     color=(
@@ -99,6 +99,8 @@ run_tests(
         )
     ),
 )
+if not test_result.wasSuccessful():
+    sys.exit(1)
 
 # assume that we are in pcs root dir
 #
diff --git a/pcs/test/test_lib_commands_quorum.py b/pcs/test/test_lib_commands_quorum.py
index e824f37..c12ab66 100644
--- a/pcs/test/test_lib_commands_quorum.py
+++ b/pcs/test/test_lib_commands_quorum.py
@@ -1750,3 +1750,46 @@ class UpdateDeviceTest(TestCase, CmanMixin):
                 "model: net\n        bad_option: bad_value"
             )
         )
+
+
+@mock.patch("pcs.lib.commands.quorum.corosync_live.set_expected_votes")
+@mock.patch.object(
+    LibraryEnvironment,
+    "cmd_runner",
+    lambda self: "mock_runner"
+)
+class SetExpectedVotesLiveTest(TestCase, CmanMixin):
+    def setUp(self):
+        self.mock_logger = mock.MagicMock(logging.Logger)
+        self.mock_reporter = MockLibraryReportProcessor()
+
+    @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: True)
+    def test_disabled_on_cman(self, mock_set_votes):
+        lib_env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
+        self.assert_disabled_on_cman(
+            lambda: lib.set_expected_votes_live(lib_env, "5")
+        )
+        mock_set_votes.assert_not_called()
+
+    @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
+    def test_success(self, mock_set_votes):
+        lib_env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
+        lib.set_expected_votes_live(lib_env, "5")
+        mock_set_votes.assert_called_once_with("mock_runner", 5)
+
+    @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
+    def test_invalid_votes(self, mock_set_votes):
+        lib_env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
+        assert_raise_library_error(
+            lambda: lib.set_expected_votes_live(lib_env, "-5"),
+            (
+                severity.ERROR,
+                report_codes.INVALID_OPTION_VALUE,
+                {
+                    "option_name": "expected votes",
+                    "option_value": "-5",
+                    "allowed_values": "positive integer",
+                }
+            )
+        )
+        mock_set_votes.assert_not_called()
diff --git a/pcs/test/test_lib_corosync_live.py b/pcs/test/test_lib_corosync_live.py
index 96fe235..0fc5eb2 100644
--- a/pcs/test/test_lib_corosync_live.py
+++ b/pcs/test/test_lib_corosync_live.py
@@ -141,3 +141,45 @@ class GetQuorumStatusTextTest(TestCase):
         self.mock_runner.run.assert_called_once_with([
             self.quorum_tool, "-p"
         ])
+
+
+class SetExpectedVotesTest(TestCase):
+    def setUp(self):
+        self.mock_runner = mock.MagicMock(spec_set=CommandRunner)
+
+    def path(self, name):
+        return os.path.join(settings.corosync_binaries, name)
+
+    def test_success(self):
+        cmd_retval = 0
+        cmd_output = "cmd output"
+        mock_runner = mock.MagicMock(spec_set=CommandRunner)
+        mock_runner.run.return_value = (cmd_output, cmd_retval)
+
+        lib.set_expected_votes(mock_runner, 3)
+
+        mock_runner.run.assert_called_once_with([
+            self.path("corosync-quorumtool"), "-e", "3"
+        ])
+
+    def test_error(self):
+        cmd_retval = 1
+        cmd_output = "cmd output"
+        mock_runner = mock.MagicMock(spec_set=CommandRunner)
+        mock_runner.run.return_value = (cmd_output, cmd_retval)
+
+        assert_raise_library_error(
+            lambda: lib.set_expected_votes(mock_runner, 3),
+            (
+                severity.ERROR,
+                report_codes.COROSYNC_QUORUM_SET_EXPECTED_VOTES_ERROR,
+                {
+                    "reason": cmd_output,
+                }
+            )
+        )
+
+        mock_runner.run.assert_called_once_with([
+            self.path("corosync-quorumtool"), "-e", "3"
+        ])
+
diff --git a/pcs/usage.py b/pcs/usage.py
index 542f806..ee53a2f 100644
--- a/pcs/usage.py
+++ b/pcs/usage.py
@@ -1352,6 +1352,10 @@ Commands:
         to set up configuration properly unless old and new host is the same
         machine.
 
+    expected-votes <votes>
+        Set expected votes in the live cluster to specified value.  This only
+        affects the live cluster, not changes any configuration files.
+
     unblock [--force]
         Cancel waiting for all nodes when establishing quorum.  Useful in
         situations where you know the cluster is inquorate, but you are
-- 
1.8.3.1