diff options
Diffstat (limited to 'test/unittests/test_bootstrap.py')
-rw-r--r-- | test/unittests/test_bootstrap.py | 1905 |
1 files changed, 1905 insertions, 0 deletions
diff --git a/test/unittests/test_bootstrap.py b/test/unittests/test_bootstrap.py new file mode 100644 index 0000000..45bf03d --- /dev/null +++ b/test/unittests/test_bootstrap.py @@ -0,0 +1,1905 @@ +""" +Unitary tests for crmsh/bootstrap.py + +:author: xinliang +:organization: SUSE Linux GmbH +:contact: XLiang@suse.de + +:since: 2019-10-21 +""" + +# pylint:disable=C0103,C0111,W0212,W0611 + +import subprocess +import unittest +import yaml +import socket + +import crmsh.sh +import crmsh.ssh_key +import crmsh.user_of_host +import crmsh.utils +from crmsh.ui_node import NodeMgmt + +try: + from unittest import mock +except ImportError: + import mock + +from crmsh import bootstrap +from crmsh import constants +from crmsh import qdevice + + +class TestContext(unittest.TestCase): + """ + Unitary tests for crmsh.bootstrap.Context + """ + + @classmethod + def setUpClass(cls): + """ + Global setUp. + """ + + def setUp(self): + """ + Test setUp. + """ + self.ctx_inst = bootstrap.Context() + + def tearDown(self): + """ + Test tearDown. + """ + + @classmethod + def tearDownClass(cls): + """ + Global tearDown. + """ + + @mock.patch('crmsh.bootstrap.Context.initialize_user') + def test_set_context(self, mock_initialize_user: mock.MagicMock): + options = mock.Mock(yes_to_all=True, ipv6=False) + ctx = self.ctx_inst.set_context(options) + self.assertEqual(ctx.yes_to_all, True) + self.assertEqual(ctx.ipv6, False) + mock_initialize_user.assert_called_once() + + @mock.patch('crmsh.qdevice.QDevice') + def test_initialize_qdevice_return(self, mock_qdevice): + self.ctx_inst.initialize_qdevice() + mock_qdevice.assert_not_called() + + @mock.patch('crmsh.qdevice.QDevice') + def test_initialize_qdevice(self, mock_qdevice): + ctx = crmsh.bootstrap.Context() + ctx.qnetd_addr = "node3" + ctx.qdevice_port = 123 + ctx.stage = "" + ctx.initialize_qdevice() + mock_qdevice.assert_called_once_with('node3', port=123, ssh_user=None, algo=None, tie_breaker=None, tls=None, cmds=None, mode=None, is_stage=False) + + @mock.patch('crmsh.qdevice.QDevice') + def test_initialize_qdevice_with_user(self, mock_qdevice): + ctx = crmsh.bootstrap.Context() + ctx.qnetd_addr = "alice@node3" + ctx.qdevice_port = 123 + ctx.stage = "" + ctx.initialize_qdevice() + mock_qdevice.assert_called_once_with('node3', port=123, ssh_user='alice', algo=None, tie_breaker=None, tls=None, cmds=None, mode=None, is_stage=False) + + @mock.patch('crmsh.utils.fatal') + def test_validate_sbd_option_error_together(self, mock_error): + mock_error.side_effect = SystemExit + ctx = crmsh.bootstrap.Context() + ctx.sbd_devices = ["/dev/sda1"] + ctx.diskless_sbd = True + with self.assertRaises(SystemExit): + ctx._validate_sbd_option() + mock_error.assert_called_once_with("Can't use -s and -S options together") + + @mock.patch('crmsh.utils.fatal') + def test_validate_sbd_option_error_sbd_stage_no_option(self, mock_error): + mock_error.side_effect = SystemExit + ctx = crmsh.bootstrap.Context() + ctx.stage = "sbd" + ctx.yes_to_all = True + with self.assertRaises(SystemExit): + ctx._validate_sbd_option() + mock_error.assert_called_once_with("Stage sbd should specify sbd device by -s or diskless sbd by -S option") + + @mock.patch('crmsh.utils.fatal') + @mock.patch('crmsh.service_manager.ServiceManager.service_is_active') + def test_validate_sbd_option_error_sbd_stage_service(self, mock_active, mock_error): + mock_error.side_effect = SystemExit + ctx = crmsh.bootstrap.Context() + ctx.stage = "sbd" + ctx.diskless_sbd = True + mock_active.return_value = True + with self.assertRaises(SystemExit): + ctx._validate_sbd_option() + mock_error.assert_called_once_with("Can't configure stage sbd: sbd.service already running! Please use crm option '-F' if need to redeploy") + mock_active.assert_called_once_with("sbd.service") + + @mock.patch('crmsh.utils.check_all_nodes_reachable') + @mock.patch('crmsh.service_manager.ServiceManager.service_is_active') + def test_validate_sbd_option_error_sbd_stage(self, mock_active, mock_check_all): + options = mock.Mock(stage="sbd", diskless_sbd=True, cluster_is_running=True) + ctx = crmsh.bootstrap.Context() + ctx.stage = "sbd" + ctx.diskless_sbd = True + ctx.cluster_is_running = True + mock_active.return_value = False + ctx._validate_sbd_option() + mock_active.assert_called_once_with("sbd.service") + mock_check_all.assert_called_once_with() + + @mock.patch('crmsh.utils.fatal') + def test_validate_option_error_nic_number(self, mock_error): + mock_error.side_effect = SystemExit + ctx = crmsh.bootstrap.Context() + ctx.nic_list = ["eth1", "eth2", "eth3"] + with self.assertRaises(SystemExit): + ctx.validate_option() + mock_error.assert_called_once_with("Maximum number of interface is 2") + + @mock.patch('crmsh.utils.fatal') + @mock.patch('socket.gethostbyname') + @mock.patch('crmsh.utils.InterfacesInfo.ip_in_local') + def test_validate_cluster_node_same_name(self, mock_ip_in_local, mock_gethost, mock_fatal): + options = mock.Mock(cluster_node="me", type="join") + ctx = crmsh.bootstrap.Context() + ctx.cluster_node = "me" + ctx.type = "join" + mock_fatal.side_effect = SystemExit + mock_gethost.return_value = ("10.10.10.41", None) + mock_ip_in_local.return_value = True + with self.assertRaises(SystemExit): + ctx._validate_cluster_node() + mock_fatal.assert_called_once_with("Please specify peer node's hostname or IP address") + + @mock.patch('crmsh.utils.fatal') + @mock.patch('socket.gethostbyname') + def test_validate_cluster_node_unknown_name(self, mock_gethost, mock_fatal): + ctx = crmsh.bootstrap.Context() + ctx.cluster_node = "xxxx" + ctx.type = "join" + mock_fatal.side_effect = SystemExit + mock_gethost.side_effect = socket.gaierror("gethostbyname error") + with self.assertRaises(SystemExit): + ctx._validate_cluster_node() + mock_fatal.assert_called_once_with('"xxxx": gethostbyname error') + + @mock.patch('logging.Logger.warning') + @mock.patch('crmsh.bootstrap.Validation.valid_admin_ip') + def test_validate_option(self, mock_admin_ip, mock_warn): + ctx = crmsh.bootstrap.Context() + ctx.admin_ip = "10.10.10.123" + ctx.qdevice_inst = mock.Mock() + ctx._validate_sbd_option = mock.Mock() + ctx._validate_nodes_option = mock.Mock() + ctx.validate_option() + mock_admin_ip.assert_called_once_with("10.10.10.123") + ctx.qdevice_inst.valid_qdevice_options.assert_called_once_with() + ctx._validate_sbd_option.assert_called_once_with() + + @mock.patch('logging.Logger.info') + def test_load_specific_profile_return(self, mock_status): + res = self.ctx_inst.load_specific_profile(None) + assert res == {} + mock_status.assert_not_called() + + @mock.patch('logging.Logger.info') + def test_load_specific_profile_not_exist(self, mock_status): + self.ctx_inst.profiles_data = {"name": "test"} + res = self.ctx_inst.load_specific_profile("newname") + assert res == {} + mock_status.assert_called_once_with("\"newname\" profile does not exist in {}".format(bootstrap.PROFILES_FILE)) + + @mock.patch('logging.Logger.info') + def test_load_specific_profile(self, mock_status): + self.ctx_inst.profiles_data = {"name": "test"} + res = self.ctx_inst.load_specific_profile("name") + assert res == "test" + mock_status.assert_called_once_with("Loading \"name\" profile from {}".format(bootstrap.PROFILES_FILE)) + + @mock.patch('logging.Logger.info') + @mock.patch('crmsh.utils.detect_cloud') + @mock.patch('os.uname') + def test_detect_platform_s390(self, mock_uname, mock_cloud, mock_status): + mock_uname.return_value = mock.Mock(machine="s390") + res = self.ctx_inst.detect_platform() + self.assertEqual(res, bootstrap.Context.S390_PROFILE_NAME) + mock_uname.assert_called_once_with() + mock_cloud.assert_not_called() + mock_status.assert_called_once_with("Detected \"{}\" platform".format(res)) + + @mock.patch('logging.Logger.info') + @mock.patch('crmsh.utils.detect_cloud') + @mock.patch('os.uname') + def test_detect_platform(self, mock_uname, mock_cloud, mock_status): + mock_uname.return_value = mock.Mock(machine="xxx") + mock_cloud.return_value = "azure" + res = self.ctx_inst.detect_platform() + self.assertEqual(res, "azure") + mock_uname.assert_called_once_with() + mock_cloud.assert_called_once_with() + mock_status.assert_called_once_with("Detected \"{}\" platform".format(res)) + + @mock.patch('os.path.exists') + @mock.patch('crmsh.bootstrap.Context.detect_platform') + def test_load_profiles_file_not_exist(self, mock_platform, mock_exists): + mock_platform.return_value = "s390" + mock_exists.return_value = False + self.ctx_inst.load_profiles() + mock_platform.assert_called_once_with() + mock_exists.assert_called_once_with(bootstrap.PROFILES_FILE) + + @mock.patch('yaml.load') + @mock.patch('builtins.open', new_callable=mock.mock_open, read_data="") + @mock.patch('os.path.exists') + @mock.patch('crmsh.bootstrap.Context.detect_platform') + def test_load_profiles_file_empty(self, mock_platform, mock_exists, mock_open_file, mock_load): + mock_platform.return_value = "s390" + mock_exists.return_value = True + mock_load.return_value = "" + self.ctx_inst.load_profiles() + mock_platform.assert_called_once_with() + mock_exists.assert_called_once_with(bootstrap.PROFILES_FILE) + mock_open_file.assert_called_once_with(bootstrap.PROFILES_FILE) + mock_load.assert_called_once_with(mock_open_file.return_value, Loader=yaml.SafeLoader) + + @mock.patch('crmsh.bootstrap.Context.load_specific_profile') + @mock.patch('yaml.load') + @mock.patch('builtins.open', new_callable=mock.mock_open, read_data="") + @mock.patch('os.path.exists') + @mock.patch('crmsh.bootstrap.Context.detect_platform') + def test_load_profiles_file(self, mock_platform, mock_exists, mock_open_file, mock_load, mock_load_specific): + mock_platform.return_value = "s390" + mock_exists.return_value = True + mock_load.return_value = "data" + mock_load_specific.side_effect = [ + {"name": "xin", "age": 18}, + {"name": "wang"} + ] + + self.ctx_inst.load_profiles() + assert self.ctx_inst.profiles_dict == {"name": "wang", "age": 18} + + mock_platform.assert_called_once_with() + mock_exists.assert_called_once_with(bootstrap.PROFILES_FILE) + mock_open_file.assert_called_once_with(bootstrap.PROFILES_FILE) + mock_load.assert_called_once_with(mock_open_file.return_value, Loader=yaml.SafeLoader) + mock_load_specific.assert_has_calls([ + mock.call(bootstrap.Context.DEFAULT_PROFILE_NAME), + mock.call("s390") + ]) + + @mock.patch('crmsh.userdir.get_sudoer') + @mock.patch('crmsh.userdir.getuser') + def test_initialize_user_without_args_without_sudoer(self, mock_getuser: mock.MagicMock, mock_get_sudoer: mock.MagicMock): + mock_getuser.return_value = 'root' + mock_get_sudoer.return_value = None + context = bootstrap.Context() + context.cluster_node = None + context.user_at_node_list = None + context.initialize_user() + self.assertEqual('root', context.current_user) + + @mock.patch('crmsh.userdir.get_sudoer') + @mock.patch('crmsh.userdir.getuser') + def test_initialize_user_without_args_with_sudoer(self, mock_getuser: mock.MagicMock, mock_get_sudoer: mock.MagicMock): + mock_getuser.return_value = 'root' + mock_get_sudoer.return_value = 'alice' + context = bootstrap.Context() + context.cluster_node = None + context.user_at_node_list = None + context.initialize_user() + self.assertEqual('root', context.current_user) + + @mock.patch('crmsh.userdir.get_sudoer') + @mock.patch('crmsh.userdir.getuser') + def test_initialize_user_cluster_node_without_user_without_sudoer(self, mock_getuser: mock.MagicMock, mock_get_sudoer: mock.MagicMock): + mock_getuser.return_value = 'root' + mock_get_sudoer.return_value = None + context = bootstrap.Context() + context.cluster_node = 'node1' + context.user_at_node_list = None + context.initialize_user() + self.assertEqual('root', context.current_user) + + @mock.patch('crmsh.userdir.get_sudoer') + @mock.patch('crmsh.userdir.getuser') + def test_initialize_user_cluster_node_with_user_without_sudoer(self, mock_getuser: mock.MagicMock, mock_get_sudoer: mock.MagicMock): + mock_getuser.return_value = 'root' + mock_get_sudoer.return_value = None + context = bootstrap.Context() + context.cluster_node = 'alice@node1' + context.user_at_node_list = None + with self.assertRaises(ValueError): + context.initialize_user() + + @mock.patch('crmsh.userdir.get_sudoer') + @mock.patch('crmsh.userdir.getuser') + def test_initialize_user_cluster_node_without_user_with_sudoer(self, mock_getuser: mock.MagicMock, mock_get_sudoer: mock.MagicMock): + mock_getuser.return_value = 'root' + mock_get_sudoer.return_value = 'bob' + context = bootstrap.Context() + context.cluster_node = 'node1' + context.user_at_node_list = None + context.initialize_user() + self.assertEqual('root', context.current_user) + + @mock.patch('crmsh.userdir.get_sudoer') + @mock.patch('crmsh.userdir.getuser') + def test_initialize_user_cluster_node_with_user_with_sudoer(self, mock_getuser: mock.MagicMock, mock_get_sudoer: mock.MagicMock): + mock_getuser.return_value = 'root' + mock_get_sudoer.return_value = 'bob' + context = bootstrap.Context() + context.cluster_node = 'alice@node1' + context.user_at_node_list = None + context.initialize_user() + self.assertEqual('bob', context.current_user) + + @mock.patch('crmsh.userdir.get_sudoer') + @mock.patch('crmsh.userdir.getuser') + def test_initialize_user_node_list_without_user_without_sudoer(self, mock_getuser: mock.MagicMock, mock_get_sudoer: mock.MagicMock): + mock_getuser.return_value = 'root' + mock_get_sudoer.return_value = None + context = bootstrap.Context() + context.user_at_node_list = ['node1', 'node2'] + context.cluster_node = None + context.initialize_user() + self.assertEqual('root', context.current_user) + + @mock.patch('crmsh.userdir.get_sudoer') + @mock.patch('crmsh.userdir.getuser') + def test_initialize_user_node_list_with_user_without_sudoer(self, mock_getuser: mock.MagicMock, mock_get_sudoer: mock.MagicMock): + mock_getuser.return_value = 'root' + mock_get_sudoer.return_value = None + context = bootstrap.Context() + context.user_at_node_list = ['alice@node1', 'alice@node2'] + context.cluster_node = None + with self.assertRaises(ValueError): + context.initialize_user() + + @mock.patch('crmsh.userdir.get_sudoer') + @mock.patch('crmsh.userdir.getuser') + def test_initialize_user_node_list_without_user_with_sudoer(self, mock_getuser: mock.MagicMock, mock_get_sudoer: mock.MagicMock): + mock_getuser.return_value = 'root' + mock_get_sudoer.return_value = 'bob' + context = bootstrap.Context() + context.user_at_node_list = ['node1', 'node2'] + context.cluster_node = None + context.initialize_user() + self.assertEqual('root', context.current_user) + + @mock.patch('crmsh.userdir.get_sudoer') + @mock.patch('crmsh.userdir.getuser') + def test_initialize_user_node_list_with_user_with_sudoer(self, mock_getuser: mock.MagicMock, mock_get_sudoer: mock.MagicMock): + mock_getuser.return_value = 'root' + mock_get_sudoer.return_value = 'bob' + context = bootstrap.Context() + context.user_at_node_list = ['alice@node1', 'alice@node2'] + context.cluster_node = None + context.initialize_user() + self.assertEqual('bob', context.current_user) + + +class TestBootstrap(unittest.TestCase): + """ + Unitary tests for crmsh/bootstrap.py + """ + + @classmethod + def setUpClass(cls): + """ + Global setUp. + """ + + def setUp(self): + """ + Test setUp. + """ + self.qdevice_with_ip = qdevice.QDevice("10.10.10.123") + + def tearDown(self): + """ + Test tearDown. + """ + + @classmethod + def tearDownClass(cls): + """ + Global tearDown. + """ + + @mock.patch('crmsh.parallax.parallax_call') + @mock.patch('crmsh.service_manager.ServiceManager.start_service') + @mock.patch('crmsh.sbd.SBDTimeout.is_sbd_delay_start') + @mock.patch('crmsh.service_manager.ServiceManager.service_is_enabled') + @mock.patch('crmsh.utils.package_is_installed') + def test_start_pacemaker(self, mock_installed, mock_enabled, mock_delay_start, mock_start, mock_parallax_call): + bootstrap._context = None + mock_installed.return_value = True + mock_enabled.return_value = True + mock_delay_start.return_value = True + node_list = ["node1", "node2", "node3", "node4", "node5", "node6"] + bootstrap.start_pacemaker(node_list) + mock_start.assert_has_calls([ + mock.call("corosync.service", remote_addr="node1"), + mock.call("corosync.service", remote_addr="node2"), + mock.call("corosync.service", remote_addr="node3"), + mock.call("corosync.service", remote_addr="node4"), + mock.call("corosync.service", remote_addr="node5"), + mock.call("corosync.service", remote_addr="node6"), + mock.call("pacemaker.service", enable=False, node_list=node_list) + ]) + mock_parallax_call.assert_has_calls([ + mock.call(node_list, 'mkdir -p /run/systemd/system/sbd.service.d/'), + mock.call(node_list, "echo -e '[Service]\nUnsetEnvironment=SBD_DELAY_START' > /run/systemd/system/sbd.service.d/sbd_delay_start_disabled.conf"), + mock.call(node_list, "systemctl daemon-reload"), + ]) + + @mock.patch('crmsh.bootstrap.configure_ssh_key') + @mock.patch('crmsh.service_manager.ServiceManager.start_service') + def test_init_ssh(self, mock_start_service, mock_config_ssh): + bootstrap._context = mock.Mock(current_user="alice", user_at_node_list=[], use_ssh_agent=False) + bootstrap.init_ssh() + mock_start_service.assert_called_once_with("sshd.service", enable=True) + mock_config_ssh.assert_has_calls([ + mock.call("alice") + ]) + + @mock.patch('crmsh.userdir.gethomedir') + def test_key_files(self, mock_gethome): + mock_gethome.return_value = "/root" + expected_res = {"private": "/root/.ssh/id_rsa", "public": "/root/.ssh/id_rsa.pub", "authorized": "/root/.ssh/authorized_keys"} + self.assertEqual(bootstrap.key_files("root"), expected_res) + mock_gethome.assert_called_once_with("root") + + @mock.patch('builtins.open') + def test_is_nologin(self, mock_open_file): + data = "hacluster:x:90:90:heartbeat processes:/var/lib/heartbeat/cores/hacluster:/sbin/nologin" + mock_open_file.return_value = mock.mock_open(read_data=data).return_value + assert bootstrap.is_nologin("hacluster") is not None + mock_open_file.assert_called_once_with("/etc/passwd") + + @mock.patch('crmsh.bootstrap.confirm') + @mock.patch('logging.Logger.info') + @mock.patch('crmsh.bootstrap.is_nologin') + def test_change_user_shell_return(self, mock_nologin, mock_status, mock_confirm): + bootstrap._context = mock.Mock(yes_to_all=False) + mock_nologin.return_value = True + mock_confirm.return_value = False + + bootstrap.change_user_shell("hacluster") + + mock_nologin.assert_called_once_with("hacluster", None) + mock_confirm.assert_called_once_with("Continue?") + + @mock.patch('crmsh.sh.ClusterShell.get_stdout_or_raise_error') + @mock.patch('crmsh.bootstrap.is_nologin') + def test_change_user_shell(self, mock_nologin, mock_invoke): + bootstrap._context = mock.Mock(yes_to_all=True) + mock_nologin.return_value = True + + bootstrap.change_user_shell("hacluster") + + mock_nologin.assert_called_once_with("hacluster", None) + mock_invoke.assert_called_once_with("usermod -s /bin/bash hacluster", None) + + @mock.patch('crmsh.sh.LocalShell.su_subprocess_run') + def test_generate_ssh_key_pair_on_remote(self, mock_su: mock.MagicMock): + mock_su.return_value = mock.Mock(returncode=0, stdout=b'') + bootstrap.generate_ssh_key_pair_on_remote('local_sudoer', 'remote_host', 'remote_sudoer', 'remote_user') + mock_su.assert_has_calls([ + mock.call( + 'local_sudoer', + 'ssh -o StrictHostKeyChecking=no remote_sudoer@remote_host sudo -H -u remote_user /bin/sh', + input=''' +[ -f ~/.ssh/id_rsa ] || ssh-keygen -q -t rsa -f ~/.ssh/id_rsa -C "Cluster internal on $(hostname)" -N '' +[ -f ~/.ssh/id_rsa.pub ] || ssh-keygen -y -f ~/.ssh/id_rsa > ~/.ssh/id_rsa.pub +'''.encode('utf-8'), + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ), + mock.call( + 'local_sudoer', + 'ssh -o StrictHostKeyChecking=no remote_sudoer@remote_host sudo -H -u remote_user /bin/sh', + input='cat ~/.ssh/id_rsa.pub'.encode('utf-8'), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ), + ]) + + @mock.patch('crmsh.bootstrap.append_unique') + @mock.patch('crmsh.sh.LocalShell.get_stdout_or_raise_error') + @mock.patch('crmsh.utils.detect_file') + @mock.patch('crmsh.bootstrap.key_files') + @mock.patch('crmsh.bootstrap.change_user_shell') + def _test_configure_ssh_key(self, mock_change_shell, mock_key_files, mock_detect, mock_su, mock_append_unique): + mock_key_files.return_value = {"private": "/test/.ssh/id_rsa", "public": "/test/.ssh/id_rsa.pub", "authorized": "/test/.ssh/authorized_keys"} + mock_detect.side_effect = [True, True, False] + + bootstrap.configure_ssh_key("test") + + mock_change_shell.assert_called_once_with("test") + mock_key_files.assert_called_once_with("test") + mock_detect.assert_has_calls([ + mock.call("/test/.ssh/id_rsa"), + mock.call("/test/.ssh/id_rsa.pub"), + mock.call("/test/.ssh/authorized_keys") + ]) + mock_append_unique.assert_called_once_with("/test/.ssh/id_rsa.pub", "/test/.ssh/authorized_keys", "test") + mock_su.assert_called_once_with('test', 'touch /test/.ssh/authorized_keys') + + @mock.patch('crmsh.ssh_key.AuthorizedKeyManager.add') + @mock.patch('crmsh.ssh_key.KeyFileManager.ensure_key_pair_exists_for_user') + def test_configure_ssh_key(self, mock_ensure_key_pair, mock_add): + public_key = crmsh.ssh_key.InMemoryPublicKey('foo') + mock_ensure_key_pair.return_value = (True, [public_key]) + bootstrap.configure_ssh_key('alice') + mock_ensure_key_pair.assert_called_once_with(None, 'alice') + mock_add.assert_called_once_with(None, 'alice', public_key) + + @mock.patch('crmsh.bootstrap.append_to_remote_file') + @mock.patch('crmsh.utils.check_file_content_included') + def test_append_unique_remote(self, mock_check, mock_append): + mock_check.return_value = False + bootstrap.append_unique("fromfile", "tofile", user="root", remote="node1", from_local=True) + mock_check.assert_called_once_with("fromfile", "tofile", remote="node1", source_local=True) + mock_append.assert_called_once_with("fromfile", "root", "node1", "tofile") + + @mock.patch('crmsh.bootstrap.append') + @mock.patch('crmsh.utils.check_file_content_included') + def test_append_unique(self, mock_check, mock_append): + mock_check.return_value = False + bootstrap.append_unique("fromfile", "tofile") + mock_check.assert_called_once_with("fromfile", "tofile", remote=None, source_local=False) + mock_append.assert_called_once_with("fromfile", "tofile", remote=None) + + @mock.patch('crmsh.sh.ClusterShell.get_stdout_or_raise_error') + def test_append_to_remote_file(self, mock_run): + bootstrap.append_to_remote_file("fromfile", "root", "node1", "tofile") + cmd = "cat fromfile | ssh {} root@node1 'cat >> tofile'".format(constants.SSH_OPTION) + mock_run.assert_called_once_with(cmd) + + @mock.patch('crmsh.utils.fatal') + def test_join_ssh_no_seed_host(self, mock_error): + mock_error.side_effect = ValueError + with self.assertRaises(ValueError): + bootstrap.join_ssh(None, None) + mock_error.assert_called_once_with("No existing IP/hostname specified (use -c option)") + + @mock.patch('crmsh.bootstrap.swap_public_ssh_key_for_secondary_user') + @mock.patch('crmsh.bootstrap.change_user_shell') + @mock.patch('crmsh.sh.LocalShell.get_stdout_or_raise_error') + @mock.patch('crmsh.bootstrap.swap_public_ssh_key') + @mock.patch('crmsh.utils.ssh_copy_id_no_raise') + @mock.patch('crmsh.bootstrap.configure_ssh_key') + @mock.patch('crmsh.service_manager.ServiceManager.start_service') + def test_join_ssh( + self, + mock_start_service, mock_config_ssh, mock_ssh_copy_id, mock_swap, mock_invoke, mock_change, mock_swap_2, + ): + bootstrap._context = mock.Mock(current_user="bob", default_nic_list=["eth1"], use_ssh_agent=False) + mock_invoke.return_value = '' + mock_swap.return_value = None + mock_ssh_copy_id.return_value = 0 + + bootstrap.join_ssh("node1", "alice") + + mock_start_service.assert_called_once_with("sshd.service", enable=True) + mock_config_ssh.assert_has_calls([ + mock.call("bob"), + mock.call("hacluster"), + ]) + mock_ssh_copy_id.assert_called_once_with("bob", "alice", "node1") + mock_swap.assert_called_once_with("node1", "bob", "alice", "bob", "alice", add=True) + mock_invoke.assert_called_once_with( + "bob", + "ssh {} alice@node1 sudo crm cluster init -i eth1 ssh_remote".format(constants.SSH_OPTION), + ) + mock_swap_2.assert_called_once() + args, kwargs = mock_swap_2.call_args + self.assertEqual(3, len(args)) + self.assertEqual('node1', args[1]) + self.assertEqual('hacluster', args[2]) + + @mock.patch('crmsh.ssh_key.AuthorizedKeyManager.add') + @mock.patch('crmsh.ssh_key.KeyFile.public_key') + @mock.patch('crmsh.ssh_key.KeyFileManager.ensure_key_pair_exists_for_user') + @mock.patch('crmsh.ssh_key.KeyFileManager.list_public_key_for_user') + @mock.patch('logging.Logger.info') + def test_swap_public_ssh_key_for_secondary_user( + self, + mock_log_info, + mock_list_public_key_for_user, + mock_ensure_key_pair_exists_for_user, + mock_public_key, + mock_authorized_key_manager_add, + ): + mock_shell = mock.Mock( + crmsh.sh.ClusterShell, + local_shell=mock.Mock(crmsh.sh.LocalShell), + user_of_host=mock.Mock(crmsh.user_of_host.UserOfHost), + ) + mock_list_public_key_for_user.return_value = ['~/.ssh/id_rsa', '~/.ssh/id_ed25519'] + mock_ensure_key_pair_exists_for_user.return_value = (True, [ + crmsh.ssh_key.InMemoryPublicKey('foo'), + crmsh.ssh_key.InMemoryPublicKey('bar'), + ]) + mock_public_key.return_value = 'public_key' + crmsh.bootstrap.swap_public_ssh_key_for_secondary_user(mock_shell, 'node1', 'alice') + mock_list_public_key_for_user.assert_called_once_with(None, 'alice') + mock_ensure_key_pair_exists_for_user.assert_called_once_with('node1', 'alice') + mock_authorized_key_manager_add.assert_has_calls([ + mock.call(None, 'alice', crmsh.ssh_key.InMemoryPublicKey('foo')), + mock.call('node1', 'alice', crmsh.ssh_key.KeyFile('~/.ssh/id_rsa')), + ]) + mock_log_info.assert_called_with("A new ssh keypair is generated for user %s@%s.", 'alice', 'node1') + + @mock.patch('crmsh.bootstrap.change_user_shell') + @mock.patch('crmsh.sh.LocalShell.get_stdout_or_raise_error') + @mock.patch('crmsh.bootstrap.swap_public_ssh_key') + @mock.patch('crmsh.utils.ssh_copy_id_no_raise') + @mock.patch('crmsh.bootstrap.configure_ssh_key') + @mock.patch('crmsh.service_manager.ServiceManager.start_service') + def test_join_ssh_bad_credential(self, mock_start_service, mock_config_ssh, mock_ssh_copy_id, mock_swap, mock_invoke, mock_change): + bootstrap._context = mock.Mock(current_user="bob", default_nic_list=["eth1"], use_ssh_agent=False) + mock_invoke.return_value = '' + mock_swap.return_value = None + mock_ssh_copy_id.return_value = 255 + + with self.assertRaises(ValueError): + bootstrap.join_ssh("node1", "alice") + + mock_start_service.assert_called_once_with("sshd.service", enable=True) + mock_config_ssh.assert_has_calls([ + mock.call("bob"), + ]) + mock_ssh_copy_id.assert_called_once_with("bob", "alice", "node1") + mock_swap.assert_not_called() + mock_invoke.assert_not_called() + + + @mock.patch('crmsh.bootstrap.import_ssh_key') + @mock.patch('crmsh.bootstrap.export_ssh_key_non_interactive') + @mock.patch('logging.Logger.warning') + @mock.patch('crmsh.utils.check_ssh_passwd_need') + def test_swap_public_ssh_key_exception(self, mock_check_passwd, mock_warn, mock_export_ssh_key, mock_import_ssh): + mock_check_passwd.return_value = False + mock_import_ssh.side_effect = ValueError("Can't get the remote id_rsa.pub from {}: {}") + + bootstrap.swap_public_ssh_key("node1", "bob", "bob", "alice", "alice") + + mock_check_passwd.assert_called_once_with("bob", "bob", "node1") + mock_import_ssh.assert_called_once_with("bob", "bob", "alice", "node1", "alice") + mock_warn.assert_called_once_with(mock_import_ssh.side_effect) + + @mock.patch('crmsh.bootstrap.import_ssh_key') + @mock.patch('crmsh.bootstrap.export_ssh_key_non_interactive') + @mock.patch('crmsh.utils.check_ssh_passwd_need') + def test_swap_public_ssh_key(self, mock_check_passwd, mock_export_ssh, mock_import_ssh): + mock_check_passwd.return_value = True + + bootstrap.swap_public_ssh_key("node1", "bob", "bob", "alice", "alice") + + mock_check_passwd.assert_called_once_with("bob", "bob", "node1") + mock_export_ssh.assert_called_once_with("bob", "bob", "node1", "alice", "alice") + mock_import_ssh.assert_called_once_with("bob", "bob", "alice", "node1", "alice") + + @mock.patch('crmsh.utils.this_node') + def test_bootstrap_add_return(self, mock_this_node): + ctx = mock.Mock(user_at_node_list=[], use_ssh_agent=False) + bootstrap.bootstrap_add(ctx) + mock_this_node.assert_not_called() + + @mock.patch('crmsh.sh.ClusterShell.get_stdout_or_raise_error') + @mock.patch('logging.Logger.info') + @mock.patch('crmsh.utils.this_node') + def test_bootstrap_add(self, mock_this_node, mock_info, mock_run): + ctx = mock.Mock(current_user="alice", user_at_node_list=["bob@node2", "carol@node3"], nic_list=["eth1"], use_ssh_agent=False) + mock_this_node.return_value = "node1" + bootstrap.bootstrap_add(ctx) + mock_info.assert_has_calls([ + mock.call("Adding node node2 to cluster"), + mock.call("Running command on node2: crm cluster join -y -i eth1 -c alice@node1"), + mock.call("Adding node node3 to cluster"), + mock.call("Running command on node3: crm cluster join -y -i eth1 -c alice@node1") + ]) + + @mock.patch('crmsh.utils.fatal') + @mock.patch('crmsh.sh.ClusterShell.get_rc_stdout_stderr_without_input') + def test_setup_passwordless_with_other_nodes_failed_fetch_nodelist(self, mock_run, mock_error): + bootstrap._context = mock.Mock(current_user="carol", use_ssh_agent=False) + mock_run.return_value = (1, None, None) + mock_error.side_effect = SystemExit + + with self.assertRaises(SystemExit): + bootstrap.setup_passwordless_with_other_nodes("node1", "alice") + + mock_run.assert_called_once_with('node1', 'crm_node -l') + mock_error.assert_called_once_with("Can't fetch cluster nodes list from node1: None") + + @mock.patch('crmsh.utils.fatal') + @mock.patch('crmsh.utils.HostUserConfig') + @mock.patch('crmsh.bootstrap._fetch_core_hosts') + @mock.patch('crmsh.sh.ClusterShell.get_rc_stdout_stderr_without_input') + def test_setup_passwordless_with_other_nodes_failed_fetch_hostname( + self, + mock_run, + mock_fetch_core_hosts, + mock_host_user_config_class, + mock_error, + ): + bootstrap._context = mock.Mock(current_user="carol", use_ssh_agent=False) + out_node_list = """1 node1 member + 2 node2 member""" + mock_run.side_effect = [ + (0, out_node_list, None), + (1, None, None) + ] + mock_fetch_core_hosts.return_value = (["alice", "bob"], ["node1", "node2"]) + mock_error.side_effect = SystemExit + + with self.assertRaises(SystemExit): + bootstrap.setup_passwordless_with_other_nodes("node1", "alice") + + mock_run.assert_has_calls([ + mock.call('node1', 'crm_node -l'), + mock.call('node1', 'hostname'), + ]) + mock_error.assert_called_once_with("Can't fetch hostname of node1: None") + + @mock.patch('crmsh.bootstrap.swap_key_for_hacluster') + @mock.patch('crmsh.bootstrap.change_user_shell') + @mock.patch('crmsh.utils.HostUserConfig') + @mock.patch('crmsh.bootstrap._fetch_core_hosts') + @mock.patch('crmsh.utils.ssh_copy_id') + @mock.patch('crmsh.utils.user_of') + @mock.patch('crmsh.bootstrap.swap_public_ssh_key') + @mock.patch('crmsh.sh.ClusterShell.get_rc_stdout_stderr_without_input') + def test_setup_passwordless_with_other_nodes( + self, + mock_run, + mock_swap, + mock_userof, + mock_ssh_copy_id: mock.MagicMock, + mock_fetch_core_hosts, + mock_host_user_config_class, + mock_change_shell, + mock_swap_hacluster + ): + bootstrap._context = mock.Mock(current_user="carol", use_ssh_agent=False) + mock_fetch_core_hosts.return_value = (["alice", "bob"], ["node1", "node2"]) + mock_userof.return_value = "bob" + out_node_list = """1 node1 member + 2 node2 member""" + mock_run.side_effect = [ + (0, out_node_list, None), + (0, "node1", None) + ] + + bootstrap.setup_passwordless_with_other_nodes("node1", "alice") + + mock_run.assert_has_calls([ + mock.call('node1', 'crm_node -l'), + mock.call('node1', 'hostname'), + ]) + mock_userof.assert_called_once_with("node2") + mock_ssh_copy_id.assert_has_calls([ + mock.call('carol', 'bob', 'node2') + ]) + mock_swap.assert_has_calls([ + mock.call('node2', "carol", "bob", "carol", "bob"), + mock.call('node2', 'hacluster', 'hacluster', 'carol', 'bob', add=True) + ]) + + @mock.patch('crmsh.userdir.getuser') + @mock.patch('crmsh.bootstrap.key_files') + @mock.patch('builtins.open') + @mock.patch('crmsh.bootstrap.append') + @mock.patch('os.path.join') + @mock.patch('os.path.exists') + def test_init_ssh_remote_no_sshkey(self, mock_exists, mock_join, mock_append, mock_open_file, mock_key_files, mock_getuser): + mock_getuser.return_value = "alice" + mock_key_files.return_value = {"private": "/home/alice/.ssh/id_rsa", "public": "/home/alice/.ssh/id_rsa.pub", "authorized": "/home/alice/.ssh/authorized_keys"} + mock_exists.side_effect = [False, True, False, False, False] + mock_join.side_effect = ["/home/alice/.ssh/id_rsa", + "/home/alice/.ssh/id_dsa", + "/home/alice/.ssh/id_ecdsa", + "/home/alice/.ssh/id_ed25519"] + mock_open_file.side_effect = [ + mock.mock_open().return_value, + mock.mock_open(read_data="data1 data2").return_value, + mock.mock_open(read_data="data1111").return_value + ] + + bootstrap.init_ssh_remote() + + mock_getuser.assert_called_once_with() + mock_key_files.assert_called_once_with("alice") + + mock_open_file.assert_has_calls([ + mock.call("/home/alice/.ssh/authorized_keys", 'w'), + mock.call("/home/alice/.ssh/authorized_keys", "r+"), + mock.call("/home/alice/.ssh/id_rsa.pub") + ]) + mock_exists.assert_has_calls([ + mock.call("/home/alice/.ssh/authorized_keys"), + mock.call("/home/alice/.ssh/id_rsa"), + mock.call("/home/alice/.ssh/id_dsa"), + mock.call("/home/alice/.ssh/id_ecdsa"), + mock.call("/home/alice/.ssh/id_ed25519"), + ]) + mock_append.assert_called_once_with("/home/alice/.ssh/id_rsa.pub", "/home/alice/.ssh/authorized_keys") + + @mock.patch('crmsh.sh.ClusterShell.get_rc_stdout_stderr_without_input') + def test_get_node_canonical_hostname(self, mock_run): + mock_run.return_value = (0, "Node1", None) + + peer_node = bootstrap.get_node_canonical_hostname('node1') + self.assertEqual('Node1', peer_node) + mock_run.assert_called_once_with('node1', 'crm_node --name') + + @mock.patch('crmsh.utils.fatal') + @mock.patch('crmsh.sh.ClusterShell.get_rc_stdout_stderr_without_input') + def test_get_node_canonical_hostname_error(self, mock_run, mock_error): + mock_run.return_value = (1, None, "error") + mock_error.side_effect = SystemExit + + with self.assertRaises(SystemExit): + bootstrap.get_node_canonical_hostname('node1') + + mock_run.assert_called_once_with("node1", "crm_node --name") + mock_error.assert_called_once_with("error") + + @mock.patch('crmsh.utils.this_node') + @mock.patch('crmsh.bootstrap.get_node_canonical_hostname') + @mock.patch('crmsh.xmlutil.CrmMonXmlParser.is_node_online') + def test_is_online_local_offline(self, mock_is_online, mock_get_hostname, mock_this_node): + bootstrap._context = mock.Mock(cluster_node='node2') + mock_this_node.return_value = "node1" + mock_is_online.return_value = False + + assert bootstrap.is_online() is False + + mock_this_node.assert_called_once_with() + mock_get_hostname.assert_not_called() + mock_is_online.assert_called_once_with("node1") + + @mock.patch('crmsh.utils.this_node') + @mock.patch('crmsh.bootstrap.get_node_canonical_hostname') + @mock.patch('crmsh.xmlutil.CrmMonXmlParser.is_node_online') + def test_is_online_on_init_node(self, mock_is_online, mock_get_hostname, mock_this_node): + bootstrap._context = mock.Mock(cluster_node=None) + mock_this_node.return_value = "node1" + mock_is_online.return_value = True + + assert bootstrap.is_online() is True + + mock_this_node.assert_called_once_with() + mock_get_hostname.assert_not_called() + mock_is_online.assert_called_once_with("node1") + + @mock.patch('crmsh.utils.fatal') + @mock.patch('crmsh.service_manager.ServiceManager.stop_service') + @mock.patch('crmsh.bootstrap.sync_file') + @mock.patch('crmsh.corosync.conf') + @mock.patch('shutil.copy') + @mock.patch('crmsh.utils.this_node') + @mock.patch('crmsh.bootstrap.get_node_canonical_hostname') + @mock.patch('crmsh.xmlutil.CrmMonXmlParser.is_node_online') + def test_is_online_peer_offline(self, mock_is_online, mock_get_hostname, mock_this_node, + mock_copy, mock_corosync_conf, mock_csync2, mock_stop_service, mock_error): + bootstrap._context = mock.Mock(cluster_node='node1') + mock_is_online.side_effect = [True, False] + bootstrap.COROSYNC_CONF_ORIG = "/tmp/crmsh_tmpfile" + mock_this_node.return_value = "node2" + mock_get_hostname.return_value = "node1" + mock_corosync_conf.side_effect = [ "/etc/corosync/corosync.conf", + "/etc/corosync/corosync.conf"] + + bootstrap.is_online() + + mock_this_node.assert_called_once_with() + mock_get_hostname.assert_called_once_with('node1') + mock_corosync_conf.assert_has_calls([ + mock.call(), + mock.call() + ]) + mock_copy.assert_called_once_with(bootstrap.COROSYNC_CONF_ORIG, "/etc/corosync/corosync.conf") + mock_csync2.assert_called_once_with("/etc/corosync/corosync.conf") + mock_stop_service.assert_called_once_with("corosync") + mock_error.assert_called_once_with("Cannot see peer node \"node1\", please check the communication IP") + + @mock.patch('crmsh.utils.fatal') + @mock.patch('crmsh.service_manager.ServiceManager.stop_service') + @mock.patch('crmsh.bootstrap.sync_file') + @mock.patch('crmsh.corosync.conf') + @mock.patch('shutil.copy') + @mock.patch('crmsh.utils.this_node') + @mock.patch('crmsh.bootstrap.get_node_canonical_hostname') + @mock.patch('crmsh.xmlutil.CrmMonXmlParser.is_node_online') + def test_is_online_both_online(self, mock_is_online, mock_get_hostname, mock_this_node, + mock_copy, mock_corosync_conf, mock_csync2, mock_stop_service, mock_error): + bootstrap._context = mock.Mock(cluster_node='node2') + mock_is_online.side_effect = [True, True] + mock_this_node.return_value = "node2" + mock_get_hostname.return_value = "node2" + + assert bootstrap.is_online() is True + + mock_this_node.assert_called_once_with() + mock_get_hostname.assert_called_once_with('node2') + mock_corosync_conf.assert_not_called() + mock_copy.assert_not_called() + mock_csync2.assert_not_called() + mock_stop_service.assert_not_called() + mock_error.assert_not_called() + + @mock.patch('crmsh.bootstrap.invokerc') + @mock.patch('crmsh.bootstrap.invoke') + def test_csync2_update_no_conflicts(self, mock_invoke, mock_invokerc): + mock_invokerc.return_value = True + bootstrap.csync2_update("/etc/corosync.conf") + mock_invoke.assert_called_once_with("csync2 -rm /etc/corosync.conf") + mock_invokerc.assert_called_once_with("csync2 -rxv /etc/corosync.conf") + + @mock.patch('logging.Logger.warning') + @mock.patch('crmsh.bootstrap.invokerc') + @mock.patch('crmsh.bootstrap.invoke') + def test_csync2_update(self, mock_invoke, mock_invokerc, mock_warn): + mock_invokerc.side_effect = [False, False] + bootstrap.csync2_update("/etc/corosync.conf") + mock_invoke.assert_has_calls([ + mock.call("csync2 -rm /etc/corosync.conf"), + mock.call("csync2 -rf /etc/corosync.conf") + ]) + mock_invokerc.assert_has_calls([ + mock.call("csync2 -rxv /etc/corosync.conf"), + mock.call("csync2 -rxv /etc/corosync.conf") + ]) + mock_warn.assert_called_once_with("/etc/corosync.conf was not synced") + + @mock.patch('crmsh.utils.InterfacesInfo') + def test_init_network(self, mock_interfaces): + mock_interfaces_inst = mock.Mock() + mock_interfaces.return_value = mock_interfaces_inst + mock_interfaces_inst.get_default_nic_list_from_route.return_value = ["eth0", "eth1"] + bootstrap._context = mock.Mock(ipv6=False, second_heartbeat=False, nic_list=["eth0", "eth1"], default_nic_list=["eth0", "eth1"]) + + bootstrap.init_network() + + mock_interfaces.assert_called_once_with(False, False, bootstrap._context.nic_list) + mock_interfaces_inst.get_interfaces_info.assert_called_once_with() + mock_interfaces_inst.get_default_nic_list_from_route.assert_called_once_with() + mock_interfaces_inst.get_default_ip_list.assert_called_once_with() + + @mock.patch('crmsh.service_manager.ServiceManager.disable_service') + @mock.patch('logging.Logger.info') + def test_init_qdevice_no_config(self, mock_status, mock_disable): + bootstrap._context = mock.Mock(qdevice_inst=None) + bootstrap.init_qdevice() + mock_status.assert_not_called() + mock_disable.assert_called_once_with("corosync-qdevice.service") + + @mock.patch('crmsh.utils.HostUserConfig') + @mock.patch('crmsh.user_of_host.UserOfHost.instance') + @mock.patch('crmsh.utils.list_cluster_nodes') + @mock.patch('crmsh.utils.ssh_copy_id_no_raise') + @mock.patch('crmsh.bootstrap.configure_ssh_key') + @mock.patch('crmsh.utils.check_ssh_passwd_need') + @mock.patch('logging.Logger.info') + def test_init_qdevice_copy_ssh_key_failed( + self, + mock_status, mock_check_ssh_passwd_need, + mock_configure_ssh_key, mock_ssh_copy_id, mock_list_nodes, mock_user_of_host, + mock_host_user_config_class, + ): + mock_list_nodes.return_value = [] + bootstrap._context = mock.Mock(qdevice_inst=self.qdevice_with_ip, current_user="bob") + mock_check_ssh_passwd_need.return_value = True + mock_ssh_copy_id.return_value = 255 + mock_user_of_host.return_value = mock.MagicMock(crmsh.user_of_host.UserOfHost) + mock_user_of_host.return_value.user_pair_for_ssh.return_value = "bob", "bob" + mock_user_of_host.return_value.use_ssh_agent.return_value = False + + with self.assertRaises(ValueError): + bootstrap.init_qdevice() + + mock_status.assert_has_calls([ + mock.call("Configure Qdevice/Qnetd:"), + ]) + mock_check_ssh_passwd_need.assert_called_once_with("bob", "bob", "10.10.10.123") + mock_configure_ssh_key.assert_called_once_with('bob') + mock_ssh_copy_id.assert_called_once_with('bob', 'bob', '10.10.10.123') + + @mock.patch('crmsh.utils.HostUserConfig') + @mock.patch('crmsh.user_of_host.UserOfHost.instance') + @mock.patch('crmsh.utils.list_cluster_nodes') + @mock.patch('crmsh.bootstrap.confirm') + @mock.patch('crmsh.utils.is_qdevice_configured') + @mock.patch('crmsh.bootstrap.configure_ssh_key') + @mock.patch('crmsh.utils.check_ssh_passwd_need') + @mock.patch('logging.Logger.info') + def test_init_qdevice_already_configured( + self, + mock_status, mock_ssh, mock_configure_ssh_key, + mock_qdevice_configured, mock_confirm, mock_list_nodes, mock_user_of_host, + mock_host_user_config_class, + ): + mock_list_nodes.return_value = [] + bootstrap._context = mock.Mock(qdevice_inst=self.qdevice_with_ip, current_user="bob") + mock_ssh.return_value = False + mock_user_of_host.return_value = mock.MagicMock(crmsh.user_of_host.UserOfHost) + mock_user_of_host.return_value.user_pair_for_ssh.return_value = "bob", "bob" + mock_user_of_host.return_value.use_ssh_agent.return_value = False + mock_qdevice_configured.return_value = True + mock_confirm.return_value = False + self.qdevice_with_ip.start_qdevice_service = mock.Mock() + + bootstrap.init_qdevice() + + mock_status.assert_called_once_with("Configure Qdevice/Qnetd:") + mock_ssh.assert_called_once_with("bob", "bob", "10.10.10.123") + mock_configure_ssh_key.assert_not_called() + mock_host_user_config_class.return_value.save_remote.assert_called_once_with(mock_list_nodes.return_value) + mock_qdevice_configured.assert_called_once_with() + mock_confirm.assert_called_once_with("Qdevice is already configured - overwrite?") + self.qdevice_with_ip.start_qdevice_service.assert_called_once_with() + + @mock.patch('crmsh.utils.HostUserConfig') + @mock.patch('crmsh.user_of_host.UserOfHost.instance') + @mock.patch('crmsh.bootstrap.adjust_priority_fencing_delay') + @mock.patch('crmsh.bootstrap.adjust_priority_in_rsc_defaults') + @mock.patch('crmsh.utils.list_cluster_nodes') + @mock.patch('crmsh.utils.this_node') + @mock.patch('crmsh.utils.is_qdevice_configured') + @mock.patch('crmsh.bootstrap.configure_ssh_key') + @mock.patch('crmsh.utils.check_ssh_passwd_need') + @mock.patch('logging.Logger.info') + def test_init_qdevice(self, mock_info, mock_ssh, mock_configure_ssh_key, mock_qdevice_configured, + mock_this_node, mock_list_nodes, mock_adjust_priority, mock_adjust_fence_delay, + mock_user_of_host, mock_host_user_config_class): + bootstrap._context = mock.Mock(qdevice_inst=self.qdevice_with_ip, current_user="bob") + mock_this_node.return_value = "192.0.2.100" + mock_list_nodes.return_value = [] + mock_ssh.return_value = False + mock_user_of_host.return_value = mock.MagicMock(crmsh.user_of_host.UserOfHost) + mock_user_of_host.return_value.user_pair_for_ssh.return_value = "bob", "bob" + mock_user_of_host.return_value.use_ssh_agent.return_value = False + mock_qdevice_configured.return_value = False + self.qdevice_with_ip.set_cluster_name = mock.Mock() + self.qdevice_with_ip.valid_qnetd = mock.Mock() + self.qdevice_with_ip.config_and_start_qdevice = mock.Mock() + + bootstrap.init_qdevice() + + mock_info.assert_called_once_with("Configure Qdevice/Qnetd:") + mock_ssh.assert_called_once_with("bob", "bob", "10.10.10.123") + mock_host_user_config_class.return_value.add.assert_has_calls([ + mock.call('bob', '192.0.2.100'), + mock.call('bob', '10.10.10.123'), + ]) + mock_host_user_config_class.return_value.save_remote.assert_called_once_with(mock_list_nodes.return_value) + mock_qdevice_configured.assert_called_once_with() + self.qdevice_with_ip.set_cluster_name.assert_called_once_with() + self.qdevice_with_ip.valid_qnetd.assert_called_once_with() + self.qdevice_with_ip.config_and_start_qdevice.assert_called_once_with() + + @mock.patch('crmsh.utils.fatal') + @mock.patch('crmsh.utils.HostUserConfig') + @mock.patch('crmsh.service_manager.ServiceManager.service_is_available') + @mock.patch('crmsh.utils.list_cluster_nodes') + @mock.patch('logging.Logger.info') + def test_init_qdevice_service_not_available( + self, + mock_info, mock_list_nodes, mock_available, + mock_host_user_config_class, + mock_fatal, + ): + bootstrap._context = mock.Mock(qdevice_inst=self.qdevice_with_ip) + mock_list_nodes.return_value = ["node1"] + mock_available.return_value = False + mock_fatal.side_effect = SystemExit + + with self.assertRaises(SystemExit): + bootstrap.init_qdevice() + + mock_host_user_config_class.return_value.save_local.assert_not_called() + mock_host_user_config_class.return_value.save_remote.assert_not_called() + mock_fatal.assert_called_once_with("corosync-qdevice.service is not available on node1") + mock_available.assert_called_once_with("corosync-qdevice.service", "node1") + mock_info.assert_called_once_with("Configure Qdevice/Qnetd:") + + @mock.patch('crmsh.bootstrap.prompt_for_string') + def test_configure_qdevice_interactive_return(self, mock_prompt): + bootstrap._context = mock.Mock(yes_to_all=True) + bootstrap.configure_qdevice_interactive() + mock_prompt.assert_not_called() + + @mock.patch('logging.Logger.info') + @mock.patch('crmsh.bootstrap.confirm') + def test_configure_qdevice_interactive_not_confirm(self, mock_confirm, mock_info): + bootstrap._context = mock.Mock(yes_to_all=False) + mock_confirm.return_value = False + bootstrap.configure_qdevice_interactive() + mock_confirm.assert_called_once_with("Do you want to configure QDevice?") + + @mock.patch('logging.Logger.error') + @mock.patch('crmsh.qdevice.QDevice.check_package_installed') + @mock.patch('logging.Logger.info') + @mock.patch('crmsh.bootstrap.confirm') + def test_configure_qdevice_interactive_not_installed(self, mock_confirm, mock_info, mock_installed, mock_error): + bootstrap._context = mock.Mock(yes_to_all=False) + mock_confirm.side_effect = [True, False] + mock_installed.side_effect = ValueError("corosync-qdevice not installed") + bootstrap.configure_qdevice_interactive() + mock_confirm.assert_has_calls([ + mock.call("Do you want to configure QDevice?"), + mock.call("Please install the package manually and press 'y' to continue") + ]) + + @mock.patch('crmsh.qdevice.QDevice') + @mock.patch('crmsh.bootstrap.prompt_for_string') + @mock.patch('crmsh.qdevice.QDevice.check_package_installed') + @mock.patch('logging.Logger.info') + @mock.patch('crmsh.bootstrap.confirm') + def test_configure_qdevice_interactive(self, mock_confirm, mock_info, mock_installed, mock_prompt, mock_qdevice): + bootstrap._context = mock.Mock(yes_to_all=False) + mock_confirm.return_value = True + mock_prompt.side_effect = ["alice@qnetd-node", 5403, "ffsplit", "lowest", "on", None] + mock_qdevice_inst = mock.Mock() + mock_qdevice.return_value = mock_qdevice_inst + + bootstrap.configure_qdevice_interactive() + mock_confirm.assert_called_once_with("Do you want to configure QDevice?") + mock_prompt.assert_has_calls([ + mock.call("HOST or IP of the QNetd server to be used", + valid_func=qdevice.QDevice.check_qnetd_addr), + mock.call("TCP PORT of QNetd server", default=5403, + valid_func=qdevice.QDevice.check_qdevice_port), + mock.call("QNetd decision ALGORITHM (ffsplit/lms)", default="ffsplit", + valid_func=qdevice.QDevice.check_qdevice_algo), + mock.call("QNetd TIE_BREAKER (lowest/highest/valid node id)", default="lowest", + valid_func=qdevice.QDevice.check_qdevice_tie_breaker), + mock.call("Whether using TLS on QDevice/QNetd (on/off/required)", default="on", + valid_func=qdevice.QDevice.check_qdevice_tls), + mock.call("Heuristics COMMAND to run with absolute path; For multiple commands, use \";\" to separate", + valid_func=qdevice.QDevice.check_qdevice_heuristics, + allow_empty=True) + ]) + mock_qdevice.assert_called_once_with('qnetd-node', port=5403, ssh_user='alice', algo='ffsplit', tie_breaker='lowest', tls='on', cmds=None, mode=None, is_stage=False) + + @mock.patch('crmsh.utils.fatal') + @mock.patch('crmsh.utils.is_qdevice_configured') + def test_remove_qdevice_no_configured(self, mock_qdevice_configured, mock_error): + mock_qdevice_configured.return_value = False + mock_error.side_effect = SystemExit + + with self.assertRaises(SystemExit): + bootstrap.remove_qdevice() + + mock_qdevice_configured.assert_called_once_with() + mock_error.assert_called_once_with("No QDevice configuration in this cluster") + + @mock.patch('crmsh.bootstrap.confirm') + @mock.patch('crmsh.utils.is_qdevice_configured') + def test_remove_qdevice_not_confirmed(self, mock_qdevice_configured, mock_confirm): + mock_qdevice_configured.return_value = True + mock_confirm.return_value = False + + bootstrap.remove_qdevice() + + mock_qdevice_configured.assert_called_once_with() + mock_confirm.assert_called_once_with("Removing QDevice service and configuration from cluster: Are you sure?") + + @mock.patch('crmsh.service_manager.ServiceManager.service_is_active') + @mock.patch('crmsh.bootstrap.adjust_priority_fencing_delay') + @mock.patch('crmsh.bootstrap.adjust_priority_in_rsc_defaults') + @mock.patch('crmsh.qdevice.QDevice.remove_certification_files_on_qnetd') + @mock.patch('crmsh.qdevice.QDevice.remove_qdevice_db') + @mock.patch('crmsh.qdevice.QDevice.remove_qdevice_config') + @mock.patch('crmsh.bootstrap.update_expected_votes') + @mock.patch('crmsh.log.LoggerUtils.status_long') + @mock.patch('crmsh.bootstrap.invoke') + @mock.patch('logging.Logger.info') + @mock.patch('crmsh.qdevice.evaluate_qdevice_quorum_effect') + @mock.patch('crmsh.utils.check_all_nodes_reachable') + @mock.patch('crmsh.bootstrap.confirm') + @mock.patch('crmsh.utils.is_qdevice_configured') + def test_remove_qdevice_reload(self, mock_qdevice_configured, mock_confirm, mock_reachable, mock_evaluate, + mock_status, mock_invoke, mock_status_long, mock_update_votes, mock_remove_config, mock_remove_db, + mock_remove_files, mock_adjust_priority, mock_adjust_fence_delay, mock_service_is_active): + mock_qdevice_configured.return_value = True + mock_confirm.return_value = True + mock_evaluate.return_value = qdevice.QdevicePolicy.QDEVICE_RELOAD + mock_service_is_active.return_value = False + + bootstrap.remove_qdevice() + + mock_qdevice_configured.assert_called_once_with() + mock_confirm.assert_called_once_with("Removing QDevice service and configuration from cluster: Are you sure?") + mock_reachable.assert_called_once_with() + mock_evaluate.assert_called_once_with(qdevice.QDEVICE_REMOVE) + mock_status.assert_has_calls([ + mock.call("Disable corosync-qdevice.service"), + mock.call("Stopping corosync-qdevice.service") + ]) + mock_invoke.assert_has_calls([ + mock.call("crm cluster run 'systemctl disable corosync-qdevice'"), + mock.call("crm cluster run 'systemctl stop corosync-qdevice'"), + mock.call("crm cluster run 'crm corosync reload'") + ] ) + mock_status_long.assert_called_once_with("Removing QDevice configuration from cluster") + mock_update_votes.assert_called_once_with() + mock_remove_config.assert_called_once_with() + mock_remove_db.assert_called_once_with() + + @mock.patch('crmsh.service_manager.ServiceManager.start_service') + @mock.patch('crmsh.qdevice.QDevice') + @mock.patch('crmsh.corosync.get_value') + @mock.patch('crmsh.utils.is_qdevice_tls_on') + @mock.patch('crmsh.bootstrap.invoke') + @mock.patch('crmsh.bootstrap.sync_file') + @mock.patch('crmsh.corosync.conf') + @mock.patch('crmsh.corosync.add_nodelist_from_cmaptool') + @mock.patch('crmsh.corosync.is_unicast') + @mock.patch('crmsh.log.LoggerUtils.status_long') + def test_start_qdevice_on_join_node(self, mock_status_long, mock_is_unicast, mock_add_nodelist, + mock_conf, mock_csync2_update, mock_invoke, mock_qdevice_tls, + mock_get_value, mock_qdevice, mock_start_service): + mock_is_unicast.return_value = False + mock_qdevice_tls.return_value = True + mock_conf.return_value = "corosync.conf" + mock_get_value.return_value = "10.10.10.123" + mock_qdevice_inst = mock.Mock() + mock_qdevice.return_value = mock_qdevice_inst + mock_qdevice_inst.certificate_process_on_join = mock.Mock() + + bootstrap.start_qdevice_on_join_node("node2") + + mock_status_long.assert_called_once_with("Starting corosync-qdevice.service") + mock_is_unicast.assert_called_once_with() + mock_add_nodelist.assert_called_once_with() + mock_conf.assert_called_once_with() + mock_csync2_update.assert_called_once_with("corosync.conf") + mock_invoke.assert_called_once_with("crm corosync reload") + mock_qdevice_tls.assert_called_once_with() + mock_get_value.assert_called_once_with("quorum.device.net.host") + mock_qdevice.assert_called_once_with("10.10.10.123", cluster_node="node2") + mock_qdevice_inst.certificate_process_on_join.assert_called_once_with() + mock_start_service.assert_called_once_with("corosync-qdevice.service", enable=True) + + @mock.patch('crmsh.sh.ShellUtils.get_stdout_stderr') + @mock.patch('crmsh.log.LoggerUtils.log_only_to_file') + def test_invoke(self, mock_log, mock_run): + mock_run.return_value = (0, "output", "error") + res = bootstrap.invoke("cmd --option") + self.assertEqual(res, (True, "output", "error")) + mock_log.assert_has_calls([ + mock.call('invoke: cmd --option'), + mock.call('stdout: output'), + mock.call('stderr: error') + ]) + + @mock.patch('crmsh.bootstrap.invoke') + def test_invokerc(self, mock_invoke): + mock_invoke.return_value = (True, None, None) + res = bootstrap.invokerc("cmd") + self.assertEqual(res, True) + mock_invoke.assert_called_once_with("cmd") + + @mock.patch('crmsh.utils.cluster_run_cmd') + @mock.patch('os.path.isfile') + def test_sync_files_to_disk(self, mock_isfile, mock_cluster_cmd): + bootstrap.FILES_TO_SYNC = ("file1", "file2") + mock_isfile.side_effect = [True, True] + bootstrap.sync_files_to_disk() + mock_isfile.assert_has_calls([mock.call("file1"), mock.call("file2")]) + mock_cluster_cmd.assert_called_once_with("sync file1 file2") + + @mock.patch('logging.Logger.debug') + @mock.patch('crmsh.sh.ClusterShell.get_stdout_or_raise_error') + @mock.patch('crmsh.bootstrap.cib_factory') + def test_adjust_pcmk_delay_2node(self, mock_cib_factory, mock_run, mock_debug): + mock_cib_factory.refresh = mock.Mock() + mock_cib_factory.fence_id_list_without_pcmk_delay = mock.Mock() + mock_cib_factory.fence_id_list_without_pcmk_delay.return_value = ["res_1"] + bootstrap.adjust_pcmk_delay_max(True) + mock_run.assert_called_once_with("crm resource param res_1 set pcmk_delay_max {}s".format(constants.PCMK_DELAY_MAX)) + + @mock.patch('logging.Logger.debug') + @mock.patch('crmsh.sh.ClusterShell.get_stdout_or_raise_error') + @mock.patch('crmsh.bootstrap.cib_factory') + def test_adjust_pcmk_delay(self, mock_cib_factory, mock_run, mock_debug): + mock_cib_factory.refresh = mock.Mock() + mock_cib_factory.fence_id_list_with_pcmk_delay = mock.Mock() + mock_cib_factory.fence_id_list_with_pcmk_delay.return_value = ["res_1"] + bootstrap.adjust_pcmk_delay_max(False) + mock_run.assert_called_once_with("crm resource param res_1 delete pcmk_delay_max") + + @mock.patch('crmsh.sbd.SBDTimeout') + @mock.patch('crmsh.service_manager.ServiceManager.service_is_active') + def test_adjust_stonith_timeout_sbd(self, mock_is_active, mock_sbd_timeout): + mock_is_active.return_value = True + mock_sbd_timeout.adjust_sbd_timeout_related_cluster_configuration = mock.Mock() + bootstrap.adjust_stonith_timeout() + mock_sbd_timeout.adjust_sbd_timeout_related_cluster_configuration.assert_called_once_with() + + @mock.patch('crmsh.utils.set_property') + @mock.patch('crmsh.bootstrap.get_stonith_timeout_generally_expected') + @mock.patch('crmsh.service_manager.ServiceManager.service_is_active') + def test_adjust_stonith_timeout(self, mock_is_active, mock_get_timeout, mock_set): + mock_is_active.return_value = False + mock_get_timeout.return_value = 30 + bootstrap.adjust_stonith_timeout() + mock_set.assert_called_once_with("stonith-timeout", 30, conditional=True) + + @mock.patch('crmsh.utils.set_property') + def test_adjust_priority_in_rsc_defaults_2node(self, mock_set): + bootstrap.adjust_priority_in_rsc_defaults(True) + mock_set.assert_called_once_with('priority', 1, property_type='rsc_defaults', conditional=True) + + @mock.patch('crmsh.utils.set_property') + def test_adjust_priority_in_rsc_defaults(self, mock_set): + bootstrap.adjust_priority_in_rsc_defaults(False) + mock_set.assert_called_once_with('priority', 0, property_type='rsc_defaults') + + @mock.patch('crmsh.sh.ClusterShell.get_stdout_or_raise_error') + def test_adjust_priority_fencing_delay_no_fence_agent(self, mock_run): + mock_run.return_value = None + bootstrap.adjust_priority_fencing_delay(False) + mock_run.assert_called_once_with("crm configure show related:stonith") + + @mock.patch('crmsh.utils.set_property') + @mock.patch('crmsh.sh.ClusterShell.get_stdout_or_raise_error') + def test_adjust_priority_fencing_delay_no_pcmk_delay(self, mock_run, mock_set): + mock_run.return_value = "data" + bootstrap.adjust_priority_fencing_delay(False) + mock_run.assert_called_once_with("crm configure show related:stonith") + mock_set.assert_called_once_with("priority-fencing-delay", 0) + + @mock.patch('crmsh.service_manager.ServiceManager.service_is_active') + def test_adjust_properties_no_service(self, mock_is_active): + mock_is_active.return_value = False + bootstrap.adjust_properties() + mock_is_active.assert_called_once_with("pacemaker.service") + + @mock.patch('crmsh.bootstrap.adjust_priority_fencing_delay') + @mock.patch('crmsh.bootstrap.adjust_priority_in_rsc_defaults') + @mock.patch('crmsh.bootstrap.adjust_stonith_timeout') + @mock.patch('crmsh.bootstrap.adjust_pcmk_delay_max') + @mock.patch('crmsh.utils.is_2node_cluster_without_qdevice') + @mock.patch('crmsh.service_manager.ServiceManager.service_is_active') + def test_adjust_properties(self, mock_is_active, mock_2node_qdevice, mock_adj_pcmk, mock_adj_stonith, mock_adj_priority, mock_adj_fence): + mock_is_active.return_value = True + mock_2node_qdevice.return_value = True + bootstrap.adjust_properties() + mock_is_active.assert_called_once_with("pacemaker.service") + mock_adj_pcmk.assert_called_once_with(True) + mock_adj_stonith.assert_called_once_with() + mock_adj_priority.assert_called_once_with(True) + mock_adj_fence.assert_called_once_with(True) + + @mock.patch('crmsh.utils.cluster_copy_file') + def test_sync_file_skip_csync2(self, mock_copy): + bootstrap._context = mock.Mock(skip_csync2=True, node_list_in_cluster=["node1", "node2"]) + bootstrap.sync_file("/file1") + mock_copy.assert_called_once_with("/file1", nodes=["node1", "node2"], output=False) + + @mock.patch('crmsh.bootstrap.csync2_update') + def test_sync_file(self, mock_csync2_update): + bootstrap._context = mock.Mock(skip_csync2=False) + bootstrap.sync_file("/file1") + mock_csync2_update.assert_called_once_with("/file1") + + +class TestValidation(unittest.TestCase): + """ + Unitary tests for class bootstrap.Validation + """ + @classmethod + def setUpClass(cls): + """ + Global setUp. + """ + + def setUp(self): + """ + Test setUp. + """ + self.validate_inst = bootstrap.Validation("10.10.10.1") + self.validate_port_inst_in_use = bootstrap.Validation("4567", ["4568"]) + self.validate_port_inst_out_of_range = bootstrap.Validation("456766") + + def tearDown(self): + """ + Test tearDown. + """ + + @classmethod + def tearDownClass(cls): + """ + Global tearDown. + """ + + @mock.patch('crmsh.utils.IP.is_mcast') + def test_is_mcast_addr(self, mock_mcast): + mock_mcast.return_value = False + with self.assertRaises(ValueError) as err: + self.validate_inst._is_mcast_addr() + self.assertEqual("10.10.10.1 is not multicast address", str(err.exception)) + mock_mcast.assert_called_once_with("10.10.10.1") + + def test_is_local_addr(self): + with self.assertRaises(ValueError) as err: + self.validate_inst._is_local_addr(["20.20.20.1", "20.20.20.2"]) + self.assertEqual("Address must be a local address (one of ['20.20.20.1', '20.20.20.2'])", str(err.exception)) + + def test_is_valid_port_in_use(self): + with self.assertRaises(ValueError) as err: + self.validate_port_inst_in_use._is_valid_port() + self.assertEqual("Port 4567 is already in use by corosync. Leave a gap between multiple rings.", str(err.exception)) + + def test_is_valid_port_out_of_range(self): + with self.assertRaises(ValueError) as err: + self.validate_port_inst_out_of_range._is_valid_port() + self.assertEqual("Valid port range should be 1025-65535", str(err.exception)) + + @mock.patch('crmsh.bootstrap.Validation._is_mcast_addr') + def test_valid_mcast_address(self, mock_mcast): + bootstrap.Validation.valid_mcast_address("10.10.10.1") + mock_mcast.assert_called_once_with() + + @mock.patch('crmsh.bootstrap.Validation._is_local_addr') + def test_valid_ucast_ip(self, mock_local_addr): + bootstrap._context = mock.Mock(local_ip_list=["10.10.10.2", "10.10.10.3"]) + bootstrap.Validation.valid_ucast_ip("10.10.10.1") + mock_local_addr.assert_called_once_with(["10.10.10.2", "10.10.10.3"]) + + @mock.patch('crmsh.bootstrap.Validation._is_local_addr') + def test_valid_mcast_ip(self, mock_local_addr): + bootstrap._context = mock.Mock(local_ip_list=["10.10.10.2", "10.10.10.3"], + local_network_list=["10.10.10.0"]) + bootstrap.Validation.valid_mcast_ip("10.10.10.1") + mock_local_addr.assert_called_once_with(["10.10.10.2", "10.10.10.3", "10.10.10.0"]) + + @mock.patch('crmsh.bootstrap.Validation._is_valid_port') + def test_valid_port(self, mock_port): + bootstrap.Validation.valid_port("10.10.10.1") + mock_port.assert_called_once_with() + + @mock.patch('crmsh.bootstrap.invokerc') + @mock.patch('crmsh.utils.IP.is_ipv6') + def test_valid_admin_ip_in_use(self, mock_ipv6, mock_invoke): + mock_ipv6.return_value = False + mock_invoke.return_value = True + + with self.assertRaises(ValueError) as err: + self.validate_inst.valid_admin_ip("10.10.10.1") + self.assertEqual("Address already in use: 10.10.10.1", str(err.exception)) + + mock_ipv6.assert_called_once_with("10.10.10.1") + mock_invoke.assert_called_once_with("ping -c 1 10.10.10.1") + + @mock.patch('crmsh.utils.fatal') + @mock.patch('crmsh.service_manager.ServiceManager.service_is_active') + @mock.patch('crmsh.bootstrap.init') + @mock.patch('crmsh.bootstrap.Context') + def test_bootstrap_remove_cluster_is_active(self, mock_context, mock_init, mock_active, + mock_error): + mock_context_inst = mock.Mock() + mock_context.return_value = mock_context_inst + mock_active.return_value = False + mock_error.side_effect = SystemExit + + with self.assertRaises(SystemExit): + bootstrap.bootstrap_remove(mock_context_inst) + + mock_init.assert_called_once_with() + mock_active.assert_called_once_with("corosync.service") + mock_error.assert_called_once_with("Cluster is not active - can't execute removing action") + + @mock.patch('crmsh.bootstrap.remove_qdevice') + @mock.patch('crmsh.utils.fatal') + @mock.patch('crmsh.service_manager.ServiceManager.service_is_active') + @mock.patch('crmsh.bootstrap.init') + @mock.patch('crmsh.bootstrap.Context') + def test_bootstrap_remove_qdevice(self, mock_context, mock_init, mock_active, + mock_error, mock_qdevice): + mock_context_inst = mock.Mock(qdevice=True, cluster_node=None) + mock_context.return_value = mock_context_inst + mock_active.return_value = [True, True] + + bootstrap.bootstrap_remove(mock_context_inst) + + mock_init.assert_called_once_with() + mock_active.assert_has_calls([ + mock.call("corosync.service"), + mock.call("csync2.socket") + ]) + mock_error.assert_not_called() + mock_qdevice.assert_called_once_with() + + @mock.patch('crmsh.utils.fatal') + @mock.patch('crmsh.service_manager.ServiceManager.service_is_active') + @mock.patch('crmsh.bootstrap.init') + @mock.patch('crmsh.bootstrap.Context') + def test_bootstrap_remove_qdevice_cluster_node(self, mock_context, mock_init, mock_active, mock_error): + mock_context_inst = mock.Mock(qdevice=True, cluster_node="node1") + mock_context.return_value = mock_context_inst + mock_active.return_value = True + mock_error.side_effect = SystemExit + + with self.assertRaises(SystemExit): + bootstrap.bootstrap_remove(mock_context_inst) + + mock_init.assert_called_once_with() + mock_active.assert_called_once_with("corosync.service") + mock_error.assert_called_once_with("Either remove node or qdevice") + + @mock.patch('crmsh.bootstrap.prompt_for_string') + @mock.patch('logging.Logger.info') + @mock.patch('crmsh.bootstrap.remove_qdevice') + @mock.patch('crmsh.utils.fatal') + @mock.patch('crmsh.service_manager.ServiceManager.service_is_active') + @mock.patch('crmsh.bootstrap.init') + @mock.patch('crmsh.bootstrap.Context') + def test_bootstrap_remove_no_cluster_node(self, mock_context, mock_init, mock_active, + mock_error, mock_qdevice, mock_status, mock_prompt): + mock_context_inst = mock.Mock(yes_to_all=False, cluster_node=None, qdevice_rm_flag=None) + mock_context.return_value = mock_context_inst + mock_active.return_value = [True, True] + mock_prompt.return_value = None + mock_error.side_effect = SystemExit + + with self.assertRaises(SystemExit): + bootstrap.bootstrap_remove(mock_context_inst) + + mock_init.assert_called_once_with() + mock_active.assert_has_calls([ + mock.call("corosync.service"), + mock.call("csync2.socket") + ]) + mock_qdevice.assert_not_called() + mock_status.assert_called_once_with('Remove This Node from Cluster:\n You will be asked for the IP address or name of an existing node,\n which will be removed from the cluster. This command must be\n executed from a different node in the cluster.\n') + mock_prompt.assert_called_once_with("IP address or hostname of cluster node (e.g.: 192.168.1.1)", ".+") + mock_error.assert_called_once_with("No existing IP/hostname specified (use -c option)") + + @mock.patch('crmsh.bootstrap.confirm') + @mock.patch('crmsh.bootstrap.get_node_canonical_hostname') + @mock.patch('crmsh.bootstrap.remove_qdevice') + @mock.patch('crmsh.utils.fatal') + @mock.patch('crmsh.service_manager.ServiceManager.service_is_active') + @mock.patch('crmsh.bootstrap.init') + @mock.patch('crmsh.bootstrap.Context') + def test_bootstrap_remove_no_confirm(self, mock_context, mock_init, mock_active, + mock_error, mock_qdevice, mock_hostname, mock_confirm): + mock_context_inst = mock.Mock(cluster_node="node1", force=False, qdevice_rm_flag=None) + mock_context.return_value = mock_context_inst + mock_active.return_value = [True, True] + mock_hostname.return_value = "node1" + mock_confirm.return_value = False + + bootstrap.bootstrap_remove(mock_context_inst) + + mock_init.assert_called_once_with() + mock_active.assert_has_calls([ + mock.call("corosync.service"), + mock.call("csync2.socket") + ]) + mock_qdevice.assert_not_called() + mock_error.assert_not_called() + mock_hostname.assert_called_once_with('node1') + mock_confirm.assert_called_once_with('Removing node "node1" from the cluster: Are you sure?') + + @mock.patch('crmsh.utils.this_node') + @mock.patch('crmsh.bootstrap.confirm') + @mock.patch('crmsh.bootstrap.get_node_canonical_hostname') + @mock.patch('crmsh.bootstrap.remove_qdevice') + @mock.patch('crmsh.utils.fatal') + @mock.patch('crmsh.service_manager.ServiceManager.service_is_active') + @mock.patch('crmsh.bootstrap.init') + @mock.patch('crmsh.bootstrap.Context') + def test_bootstrap_remove_self_need_force(self, mock_context, mock_init, mock_active, + mock_error, mock_qdevice, mock_hostname, mock_confirm, mock_this_node): + mock_context_inst = mock.Mock(cluster_node="node1", force=False, qdevice_rm_flag=None) + mock_context.return_value = mock_context_inst + mock_active.return_value = [True, True] + mock_hostname.return_value = "node1" + mock_confirm.return_value = True + mock_this_node.return_value = "node1" + mock_error.side_effect = SystemExit + + with self.assertRaises(SystemExit): + bootstrap.bootstrap_remove(mock_context_inst) + + mock_init.assert_called_once_with() + mock_active.assert_has_calls([ + mock.call("corosync.service"), + mock.call("csync2.socket") + ]) + mock_qdevice.assert_not_called() + mock_hostname.assert_called_once_with('node1') + mock_confirm.assert_called_once_with('Removing node "node1" from the cluster: Are you sure?') + mock_this_node.assert_called_once_with() + mock_error.assert_called_once_with("Removing self requires --force") + + @mock.patch('crmsh.sh.ClusterShell.get_stdout_or_raise_error') + @mock.patch('crmsh.bootstrap.remove_self') + @mock.patch('crmsh.utils.this_node') + @mock.patch('crmsh.bootstrap.confirm') + @mock.patch('crmsh.bootstrap.get_node_canonical_hostname') + @mock.patch('crmsh.bootstrap.remove_qdevice') + @mock.patch('crmsh.utils.fatal') + @mock.patch('crmsh.service_manager.ServiceManager.service_is_active') + @mock.patch('crmsh.bootstrap.init') + @mock.patch('crmsh.bootstrap.Context') + def test_bootstrap_remove_self(self, mock_context, mock_init, mock_active, + mock_error, mock_qdevice, mock_hostname, mock_confirm, mock_this_node, mock_self, mock_run): + mock_context_inst = mock.Mock(cluster_node="node1", force=True, qdevice_rm_flag=None) + mock_context.return_value = mock_context_inst + mock_active.return_value = [True, True] + mock_hostname.return_value = "node1" + mock_this_node.return_value = "node1" + + bootstrap.bootstrap_remove(mock_context_inst) + + mock_init.assert_called_once_with() + mock_active.assert_has_calls([ + mock.call("corosync.service"), + mock.call("csync2.socket") + ]) + mock_qdevice.assert_not_called() + mock_hostname.assert_called_once_with('node1') + mock_confirm.assert_not_called() + mock_this_node.assert_called_once_with() + mock_error.assert_not_called() + mock_self.assert_called_once_with(True) + mock_run.assert_called_once_with('rm -rf /var/lib/crmsh', 'node1') + + @mock.patch('crmsh.xmlutil.listnodes') + @mock.patch('crmsh.utils.this_node') + @mock.patch('crmsh.bootstrap.confirm') + @mock.patch('crmsh.bootstrap.get_node_canonical_hostname') + @mock.patch('crmsh.bootstrap.remove_qdevice') + @mock.patch('crmsh.utils.fatal') + @mock.patch('crmsh.service_manager.ServiceManager.service_is_active') + @mock.patch('crmsh.bootstrap.init') + @mock.patch('crmsh.bootstrap.Context') + def test_bootstrap_remove_not_in_cluster(self, mock_context, mock_init, mock_active, + mock_error, mock_qdevice, mock_hostname, mock_confirm, mock_this_node, mock_list): + mock_context_inst = mock.Mock(cluster_node="node2", force=True, qdevice_rm_flag=None) + mock_context.return_value = mock_context_inst + mock_active.return_value = [True, True] + mock_hostname.return_value = "node2" + mock_this_node.return_value = "node1" + mock_list.return_value = ["node1", "node3"] + mock_error.side_effect = SystemExit + + with self.assertRaises(SystemExit): + bootstrap.bootstrap_remove(mock_context_inst) + + mock_init.assert_called_once_with() + mock_active.assert_has_calls([ + mock.call("corosync.service"), + mock.call("csync2.socket") + ]) + mock_qdevice.assert_not_called() + mock_hostname.assert_called_once_with('node2') + mock_confirm.assert_not_called() + mock_this_node.assert_called_once_with() + mock_error.assert_called_once_with("Specified node node2 is not configured in cluster! Unable to remove.") + + @mock.patch('crmsh.sh.ClusterShell.get_stdout_or_raise_error') + @mock.patch('crmsh.utils.fetch_cluster_node_list_from_node') + @mock.patch('crmsh.bootstrap.remove_node_from_cluster') + @mock.patch('crmsh.xmlutil.listnodes') + @mock.patch('crmsh.utils.this_node') + @mock.patch('crmsh.bootstrap.confirm') + @mock.patch('crmsh.bootstrap.get_node_canonical_hostname') + @mock.patch('crmsh.bootstrap.remove_qdevice') + @mock.patch('crmsh.utils.fatal') + @mock.patch('crmsh.service_manager.ServiceManager.service_is_active') + @mock.patch('crmsh.bootstrap.init') + @mock.patch('crmsh.bootstrap.Context') + def test_bootstrap_remove(self, mock_context, mock_init, mock_active, + mock_error, mock_qdevice, mock_hostname, mock_confirm, mock_this_node, + mock_list, mock_remove, mock_fetch, mock_run): + mock_context_inst = mock.Mock(cluster_node="node2", qdevice_rm_flag=None, force=True) + mock_context.return_value = mock_context_inst + mock_active.side_effect = [True, False] + mock_hostname.return_value = "node2" + mock_this_node.return_value = "node1" + mock_list.return_value = ["node1", "node2"] + mock_fetch.return_value = ["node1", "node2"] + + bootstrap.bootstrap_remove(mock_context_inst) + + mock_init.assert_called_once_with() + mock_active.assert_has_calls([ + mock.call("corosync.service"), + mock.call("csync2.socket") + ]) + mock_qdevice.assert_not_called() + mock_hostname.assert_called_once_with('node2') + mock_confirm.assert_not_called() + mock_error.assert_not_called() + mock_remove.assert_called_once_with('node2') + mock_run.assert_called_once_with('rm -rf /var/lib/crmsh', 'node2') + + @mock.patch('crmsh.utils.fatal') + @mock.patch('crmsh.sh.ClusterShell.get_rc_stdout_stderr_without_input') + @mock.patch('crmsh.xmlutil.listnodes') + @mock.patch('crmsh.utils.this_node') + def test_remove_self_other_nodes(self, mock_this_node, mock_list, mock_run, mock_error): + mock_this_node.return_value = 'node1' + mock_list.return_value = ["node1", "node2"] + mock_run.return_value = (1, '', 'err') + mock_error.side_effect = SystemExit + + with self.assertRaises(SystemExit): + bootstrap._context = mock.Mock(cluster_node="node1", yes_to_all=True) + bootstrap.remove_self() + + mock_list.assert_called_once_with(include_remote_nodes=False) + mock_run.assert_called_once_with("node2", "crm cluster remove -y -c node1") + mock_error.assert_called_once_with("Failed to remove this node from node2") + + @mock.patch('crmsh.utils.package_is_installed') + @mock.patch('crmsh.sh.ClusterShell.get_stdout_or_raise_error') + def test_rm_configuration_files(self, mock_run, mock_installed): + bootstrap._context = mock.Mock(rm_list=["file1", "file2"]) + mock_installed.return_value = True + bootstrap.rm_configuration_files() + mock_run.assert_has_calls([ + mock.call('rm -f file1 file2', None), + mock.call('cp /usr/share/fillup-templates/sysconfig.sbd /etc/sysconfig/sbd', None) + ]) + + @mock.patch('crmsh.utils.get_iplist_from_name') + @mock.patch('crmsh.corosync.get_values') + def test_get_cluster_node_ip_host(self, mock_get_values, mock_get_iplist): + mock_get_values.return_value = ["node1", "node2"] + self.assertIsNone(bootstrap.get_cluster_node_ip('node1')) + mock_get_values.assert_called_once_with("nodelist.node.ring0_addr") + mock_get_iplist.assert_not_called() + + @mock.patch('crmsh.utils.get_iplist_from_name') + @mock.patch('crmsh.corosync.get_values') + def test_get_cluster_node_ip(self, mock_get_values, mock_get_iplist): + mock_get_values.return_value = ["10.10.10.1", "10.10.10.2"] + mock_get_iplist.return_value = ["10.10.10.1"] + self.assertEqual("10.10.10.1", bootstrap.get_cluster_node_ip('node1')) + mock_get_values.assert_called_once_with("nodelist.node.ring0_addr") + mock_get_iplist.assert_called_once_with('node1') + + @mock.patch('crmsh.service_manager.ServiceManager.stop_service') + @mock.patch('logging.Logger.info') + @mock.patch('crmsh.service_manager.ServiceManager.service_is_active') + def test_stop_services(self, mock_active, mock_status, mock_stop): + mock_active.side_effect = [True, True, True, True] + bootstrap.stop_services(bootstrap.SERVICES_STOP_LIST) + mock_active.assert_has_calls([ + mock.call("corosync-qdevice.service", remote_addr=None), + mock.call("corosync.service", remote_addr=None), + mock.call("hawk.service", remote_addr=None), + mock.call("csync2.socket", remote_addr=None) + ]) + mock_status.assert_has_calls([ + mock.call('Stopping the %s%s', 'corosync-qdevice.service', ''), + mock.call('Stopping the %s%s', 'corosync.service', ''), + mock.call('Stopping the %s%s', 'hawk.service', ''), + mock.call('Stopping the %s%s', 'csync2.socket', '') + ]) + mock_stop.assert_has_calls([ + mock.call("corosync-qdevice.service", disable=True, remote_addr=None), + mock.call("corosync.service", disable=True, remote_addr=None), + mock.call("hawk.service", disable=True, remote_addr=None), + mock.call("csync2.socket", disable=True, remote_addr=None) + ]) + + @mock.patch.object(NodeMgmt, 'call_delnode') + @mock.patch('crmsh.bootstrap.rm_configuration_files') + @mock.patch('crmsh.utils.fatal') + @mock.patch('crmsh.bootstrap.invoke') + @mock.patch('logging.Logger.info') + @mock.patch('crmsh.bootstrap.stop_services') + @mock.patch('crmsh.bootstrap.get_cluster_node_ip') + def test_remove_node_from_cluster_rm_node_failed(self, mock_get_ip, mock_stop, mock_status, mock_invoke, mock_error, mock_rm_conf_files, mock_call_delnode): + mock_get_ip.return_value = '192.0.2.100' + mock_call_delnode.return_value = False + mock_error.side_effect = SystemExit + + with self.assertRaises(SystemExit): + bootstrap._context = mock.Mock(rm_list=["file1", "file2"]) + bootstrap.remove_node_from_cluster('node1') + + mock_get_ip.assert_called_once_with('node1') + mock_status.assert_called_once_with("Removing the node node1") + mock_stop.assert_called_once_with(bootstrap.SERVICES_STOP_LIST, remote_addr="node1") + mock_invoke.assert_not_called() + mock_call_delnode.assert_called_once_with("node1") + mock_error.assert_called_once_with("Failed to remove node1.") + + @mock.patch.object(NodeMgmt, 'call_delnode') + @mock.patch('crmsh.bootstrap.rm_configuration_files') + @mock.patch('crmsh.utils.fatal') + @mock.patch('crmsh.bootstrap.invokerc') + @mock.patch('crmsh.bootstrap.invoke') + @mock.patch('logging.Logger.info') + @mock.patch('crmsh.bootstrap.stop_services') + @mock.patch('crmsh.bootstrap.get_cluster_node_ip') + def test_remove_node_from_cluster_rm_csync_failed(self, mock_get_ip, mock_stop, mock_status, mock_invoke, mock_invokerc, mock_error, mock_rm_conf_files, mock_call_delnode): + mock_get_ip.return_value = '192.0.2.100' + mock_call_delnode.return_value = True + mock_invokerc.return_value = False + mock_error.side_effect = SystemExit + + with self.assertRaises(SystemExit): + bootstrap._context = mock.Mock(rm_list=["file1", "file2"]) + bootstrap.remove_node_from_cluster('node1') + + mock_get_ip.assert_called_once_with('node1') + mock_status.assert_called_once_with("Removing the node node1") + mock_stop.assert_called_once_with(bootstrap.SERVICES_STOP_LIST, remote_addr="node1") + mock_invoke.assert_not_called() + mock_call_delnode.assert_called_once_with("node1") + mock_invokerc.assert_has_calls([ + mock.call("sed -i /node1/d {}".format(bootstrap.CSYNC2_CFG)) + ]) + mock_error.assert_called_once_with("Removing the node node1 from {} failed".format(bootstrap.CSYNC2_CFG)) + + @mock.patch.object(NodeMgmt, 'call_delnode') + @mock.patch('crmsh.service_manager.ServiceManager.service_is_active') + @mock.patch('crmsh.bootstrap.rm_configuration_files') + @mock.patch('crmsh.bootstrap.adjust_priority_fencing_delay') + @mock.patch('crmsh.bootstrap.adjust_priority_in_rsc_defaults') + @mock.patch('crmsh.bootstrap.sync_file') + @mock.patch('crmsh.bootstrap.decrease_expected_votes') + @mock.patch('crmsh.corosync.del_node') + @mock.patch('crmsh.corosync.get_values') + @mock.patch('crmsh.utils.fatal') + @mock.patch('crmsh.bootstrap.invokerc') + @mock.patch('crmsh.bootstrap.invoke') + @mock.patch('logging.Logger.info') + @mock.patch('crmsh.bootstrap.stop_services') + @mock.patch('crmsh.bootstrap.get_cluster_node_ip') + def test_remove_node_from_cluster_hostname(self, mock_get_ip, mock_stop, mock_status, + mock_invoke, mock_invokerc, mock_error, mock_get_values, mock_del, mock_decrease, mock_csync2, + mock_adjust_priority, mock_adjust_fence_delay, mock_rm_conf_files, mock_is_active, mock_cal_delnode): + mock_get_ip.return_value = "10.10.10.1" + mock_cal_delnode.return_value = True + mock_invoke.side_effect = [(True, None, None)] + mock_invokerc.return_value = True + mock_get_values.return_value = ["10.10.10.1"] + mock_is_active.return_value = False + + bootstrap._context = mock.Mock(cluster_node="node1", rm_list=["file1", "file2"]) + bootstrap.remove_node_from_cluster('node1') + + mock_get_ip.assert_called_once_with('node1') + mock_status.assert_has_calls([ + mock.call("Removing the node node1"), + mock.call("Propagating configuration changes across the remaining nodes") + ]) + mock_stop.assert_called_once_with(bootstrap.SERVICES_STOP_LIST, remote_addr="node1") + mock_cal_delnode.assert_called_once_with("node1") + mock_invoke.assert_has_calls([ + mock.call("corosync-cfgtool -R") + ]) + mock_invokerc.assert_called_once_with("sed -i /node1/d {}".format(bootstrap.CSYNC2_CFG)) + mock_error.assert_not_called() + mock_get_values.assert_called_once_with("nodelist.node.ring0_addr") + mock_del.assert_called_once_with("10.10.10.1") + mock_decrease.assert_called_once_with() + mock_csync2.assert_has_calls([ + mock.call(bootstrap.CSYNC2_CFG), + mock.call("/etc/corosync/corosync.conf") + ]) |