From 28c7fd323c8fe20c1773384448d3fd9daeaf2f33 Mon Sep 17 00:00:00 2001 From: xin liang Date: Mon, 8 Sep 2025 17:31:38 +0800 Subject: [PATCH 1/5] Dev: utils: Improve cluster_copy_file with absolute path, dir check, mkdir on remote - Resolve local_path to absolute if necessary. - Check existence of local_path before operation. - Determine whether to copy recursively based on path type. - Use Pathlib for safe path operations. - Pre-create target directory on remote nodes using 'test -d ... || mkdir -p ...'. --- crmsh/utils.py | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/crmsh/utils.py b/crmsh/utils.py index ed6e8bffd..527dac42b 100644 --- a/crmsh/utils.py +++ b/crmsh/utils.py @@ -1990,17 +1990,40 @@ def cluster_copy_file(local_path, nodes=None, output=True): """ if not nodes: nodes = list_cluster_nodes_except_me() - rc = True if not nodes: - return rc - results = prun.pcopy_to_remote(local_path, nodes, local_path) + return True + + p = Path(local_path) + if not p.exists(): + logger.error("%s does not exist", local_path) + return False + + if p.is_absolute(): + source_path = local_path + parent_path = p.parent + else: + absolute_path = p.resolve() + source_path = str(absolute_path) + parent_path = absolute_path.parent + mkdir_cmd = f"test -d {parent_path} || mkdir -p {parent_path}" + crmsh.parallax.parallax_call(nodes, mkdir_cmd) + + recursive = False + sync_type = "file" + target_path = source_path + if p.is_dir(): + recursive = True + sync_type = "directory" + target_path = parent_path + + rc = True + results = prun.pcopy_to_remote(source_path, nodes, target_path, recursive=recursive) for host, exc in results.items(): if exc is not None: - logger.error("Failed to copy %s to %s@%s: %s", local_path, exc.user, host, exc) + logger.error("Failed to copy %s to %s@%s: %s", source_path, exc.user, host, exc) rc = False else: - logger.info("%s", host) - logger.debug("Sync file %s to %s", local_path, host) + logger.info("Sync %s %s to %s", sync_type, source_path, host) return rc From 7c42789b5571cbacdbda441a74815a3ee05963fe Mon Sep 17 00:00:00 2001 From: xin liang Date: Mon, 8 Sep 2025 18:05:39 +0800 Subject: [PATCH 2/5] Dev: Rename function utils.cluster_copy_file to utils.cluster_copy_path --- crmsh/bootstrap.py | 2 +- crmsh/corosync.py | 2 +- crmsh/ui_cluster.py | 8 ++++---- crmsh/utils.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crmsh/bootstrap.py b/crmsh/bootstrap.py index ec0022f3d..a0a406277 100644 --- a/crmsh/bootstrap.py +++ b/crmsh/bootstrap.py @@ -3155,7 +3155,7 @@ def sync_file(path): Sync files between cluster nodes """ if _context.skip_csync2: - utils.cluster_copy_file(path, nodes=_context.node_list_in_cluster, output=False) + utils.cluster_copy_path(path, nodes=_context.node_list_in_cluster) else: csync2_update(path) diff --git a/crmsh/corosync.py b/crmsh/corosync.py index 581dc4d2f..dcd40b4ef 100644 --- a/crmsh/corosync.py +++ b/crmsh/corosync.py @@ -370,7 +370,7 @@ def push_configuration(nodes): ''' Push the local configuration to the list of remote nodes ''' - return utils.cluster_copy_file(conf(), nodes) + return utils.cluster_copy_path(conf(), nodes) def pull_configuration(from_node): diff --git a/crmsh/ui_cluster.py b/crmsh/ui_cluster.py index 47aa029eb..58f5ce8ba 100644 --- a/crmsh/ui_cluster.py +++ b/crmsh/ui_cluster.py @@ -963,13 +963,13 @@ def do_run(self, context, cmd, *nodes): else: logger.info("[%s]\n%s", host, utils.to_ascii(result.stdout)) - def do_copy(self, context, local_file, *nodes): + def do_copy(self, context, local_path, *nodes): ''' - usage: copy [nodes ...] - Copy file to other cluster nodes. + usage: copy [nodes ...] + Copy file/directory to other cluster nodes. If given no nodes as arguments, copy to all other cluster nodes. ''' - return utils.cluster_copy_file(local_file, nodes) + return utils.cluster_copy_path(local_path, nodes) def do_diff(self, context, filename, *nodes): "usage: diff [--checksum] [nodes...]. Diff file across cluster." diff --git a/crmsh/utils.py b/crmsh/utils.py index 527dac42b..add4a5f79 100644 --- a/crmsh/utils.py +++ b/crmsh/utils.py @@ -1984,9 +1984,9 @@ def remote_checksum(local_path, nodes, this_node): print("%-16s: %s" % (host, hashlib.sha1(f.read()).hexdigest())) -def cluster_copy_file(local_path, nodes=None, output=True): +def cluster_copy_path(local_path, nodes=None): """ - Copies given file to all other cluster nodes. + Copies given file/directory to all other cluster nodes. """ if not nodes: nodes = list_cluster_nodes_except_me() From f410851b37a8797d360c32391f115d0c0ca9edbf Mon Sep 17 00:00:00 2001 From: xin liang Date: Mon, 8 Sep 2025 18:33:20 +0800 Subject: [PATCH 3/5] Dev: doc: Update help text for crm cluster copy subcommand --- doc/crm.8.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/crm.8.adoc b/doc/crm.8.adoc index 5ff6cee67..51eedee65 100644 --- a/doc/crm.8.adoc +++ b/doc/crm.8.adoc @@ -1014,17 +1014,17 @@ These commands enable easy installation and maintenance of a HA cluster, by providing support for package installation, configuration of the cluster messaging layer, file system setup and more. -[[cmdhelp_cluster_copy,Copy file to other cluster nodes]] +[[cmdhelp_cluster_copy,Copy file or directory to other cluster nodes]] ==== `copy` -Copy file to other cluster nodes. +Copy file or directory to other cluster nodes. -Copies the given file to all other nodes unless given a +Copies the given file or directory to all other nodes unless given a list of nodes to copy to as argument. Usage: ............... -copy [nodes ...] +copy [nodes ...] ............... Example: From 5862937ca29f66798787bdf1d40e186ce870c0c3 Mon Sep 17 00:00:00 2001 From: xin liang Date: Wed, 10 Sep 2025 21:57:42 +0800 Subject: [PATCH 4/5] Dev: behave: Adjust functional test for previous commit --- test/features/cluster_api.feature | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/features/cluster_api.feature b/test/features/cluster_api.feature index c9ce7e0ff..ecfad736d 100644 --- a/test/features/cluster_api.feature +++ b/test/features/cluster_api.feature @@ -21,6 +21,23 @@ Feature: Functional test to cover SAP clusterAPI When Run "echo 'export PATH=$PATH:/usr/sbin/' > ~hacluster/.bashrc" on "hanode1" When Run "echo 'export PATH=$PATH:/usr/sbin/' > ~hacluster/.bashrc" on "hanode2" + @clean + Scenario: Test cluster copy + When Try "crm cluster copy /tmp/none" + Then Expected "/tmp/none does not exist" in stderr + When Run "touch /tmp/file1" on "hanode1" + When Run "crm cluster copy /tmp/file1" on "hanode1" + When Run "ls /tmp/file1" on "hanode2" + Then Expected return code is "0" + When Run "touch /tmp/file2" on "hanode1" + When Run "cd /tmp; crm cluster copy file2" on "hanode1" + When Run "ls /tmp/file2" on "hanode2" + Then Expected return code is "0" + When Run "mkdir -p /tmp/dir1/dir2" on "hanode1" + When Run "crm cluster copy /tmp/dir1/dir2" on "hanode1" + When Run "test -d /tmp/dir1/dir2" on "hanode2" + Then Expected return code is "0" + @clean Scenario: Start and stop resource by hacluster When Run "su - hacluster -c 'crm resource stop d'" on "hanode1" From 40fb85278548997d667377ab5ddc31493c9ada7d Mon Sep 17 00:00:00 2001 From: xin liang Date: Wed, 10 Sep 2025 22:03:27 +0800 Subject: [PATCH 5/5] Dev: unittests: Adjust unit test for previous commit --- test/unittests/test_bootstrap.py | 4 ++-- test/unittests/test_utils.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/unittests/test_bootstrap.py b/test/unittests/test_bootstrap.py index 9a7cffb65..fc6621fd6 100644 --- a/test/unittests/test_bootstrap.py +++ b/test/unittests/test_bootstrap.py @@ -1404,11 +1404,11 @@ def test_adjust_properties(self, mock_is_active, mock_2node_qdevice, mock_adj_pc mock_adj_priority.assert_called_once_with(True) mock_adj_fence.assert_called_once_with(True) - @mock.patch('crmsh.utils.cluster_copy_file') + @mock.patch('crmsh.utils.cluster_copy_path') 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_copy.assert_called_once_with("/file1", nodes=["node1", "node2"]) @mock.patch('crmsh.bootstrap.csync2_update') def test_sync_file(self, mock_csync2_update): diff --git a/test/unittests/test_utils.py b/test/unittests/test_utils.py index d56c798a8..14a66d6ba 100644 --- a/test/unittests/test_utils.py +++ b/test/unittests/test_utils.py @@ -1391,9 +1391,9 @@ def test_fetch_cluster_node_list_from_node(mock_run, mock_warn): @mock.patch('crmsh.utils.list_cluster_nodes_except_me') -def test_cluster_copy_file_return(mock_list_nodes): +def test_cluster_copy_path_return(mock_list_nodes): mock_list_nodes.return_value = [] - assert utils.cluster_copy_file("/file1") == True + assert utils.cluster_copy_path("/file1") == True @mock.patch('crmsh.sh.ShellUtils.get_stdout_stderr')