From f9dc2eac2928723cfaca292eeef7fc98bbe115d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B8=CC=86=20=D0=9E?= =?UTF-8?q?=D0=B7=D0=BE=D0=BB=D0=B8=D0=BD?= Date: Thu, 12 Feb 2026 00:13:45 +0300 Subject: [PATCH] add multi path src --- plugins/action/synchronize.py | 10 ++- plugins/modules/synchronize.py | 18 +++-- .../targets/synchronize/tasks/main.yml | 72 +++++++++++++++++++ 3 files changed, 92 insertions(+), 8 deletions(-) diff --git a/plugins/action/synchronize.py b/plugins/action/synchronize.py index a171a2b..792f26b 100644 --- a/plugins/action/synchronize.py +++ b/plugins/action/synchronize.py @@ -337,6 +337,10 @@ class ActionModule(ActionBase): # MUNGE SRC AND DEST PER REMOTE_HOST INFO src = _tmp_args.get('src', None) dest = _tmp_args.get('dest', None) + + if isinstance(src, str): + src = [src] + if src is None or dest is None: return dict(failed=True, msg="synchronize requires both src and dest parameters are set") @@ -365,11 +369,11 @@ class ActionModule(ActionBase): # use the mode to define src and dest's url if _tmp_args.get('mode', 'push') == 'pull': # src is a remote path: @, dest is a local path - src = self._process_remote(_tmp_args, src_host, src, user, inv_port in localhost_ports) + src = [self._process_remote(_tmp_args, src_host, p, user, inv_port in localhost_ports) for p in src] dest = self._process_origin(dest_host, dest, user) else: # src is a local path, dest is a remote path: @ - src = self._process_origin(src_host, src, user) + src = [self._process_origin(src_host, p, user) for p in src] dest = self._process_remote(_tmp_args, dest_host, dest, user, inv_port in localhost_ports) password = dest_host_inventory_vars.get('ansible_ssh_pass', None) or dest_host_inventory_vars.get('ansible_password', None) @@ -378,7 +382,7 @@ class ActionModule(ActionBase): else: # Still need to munge paths (to account for roles) even if we aren't # copying files between hosts - src = self._get_absolute_path(path=src) + src = [self._get_absolute_path(path=p) for p in src] dest = self._get_absolute_path(path=dest) _tmp_args['_local_rsync_password'] = password diff --git a/plugins/modules/synchronize.py b/plugins/modules/synchronize.py index d65e08f..df4d4ad 100644 --- a/plugins/modules/synchronize.py +++ b/plugins/modules/synchronize.py @@ -350,6 +350,14 @@ EXAMPLES = r''' dest: /tmp/path_b/foo.txt link_dest: /tmp/path_a/ +# Save hardlink src moved +- name: Use hardlinks when synchronizing filesystems src + ansible.posix.synchronize: + src: + - /tmp/path_a + - /tmp/path_b + dest: /tmp + # Specify the rsync binary to use on remote host and on local host - hosts: groupofhosts vars: @@ -396,9 +404,9 @@ def substitute_controller(path): def is_rsh_needed(source, dest): - if source.startswith('rsync://') or dest.startswith('rsync://'): + if all(src.startswith('rsync://') for src in source) or dest.startswith('rsync://'): return False - if ':' in source or ':' in dest: + if any(':' in src for src in source) or ':' in dest: return True return False @@ -406,7 +414,7 @@ def is_rsh_needed(source, dest): def main(): module = AnsibleModule( argument_spec=dict( - src=dict(type='path', required=True), + src=dict(type='list', required=True), dest=dict(type='path', required=True), dest_port=dict(type='int'), delete=dict(type='bool', default=False), @@ -540,7 +548,7 @@ def main(): if dirs: cmd.append('--dirs') - if source.startswith('rsync://') and dest.startswith('rsync://'): + if all(src.startswith('rsync://') for src in source) and dest.startswith('rsync://'): module.fail_json(msg='either src or dest must be a localhost', rc=1) if is_rsh_needed(source, dest): @@ -600,7 +608,7 @@ def main(): changed_marker = '<>' cmd.append('--out-format=%s' % shlex_quote(changed_marker + '%i %n%L')) - cmd.append(shlex_quote(source)) + [cmd.append(shlex_quote(src)) for src in source] cmd.append(shlex_quote(dest)) cmdstr = ' '.join(cmd) diff --git a/tests/integration/targets/synchronize/tasks/main.yml b/tests/integration/targets/synchronize/tasks/main.yml index d6dcdad..71c7694 100644 --- a/tests/integration/targets/synchronize/tasks/main.yml +++ b/tests/integration/targets/synchronize/tasks/main.yml @@ -348,3 +348,75 @@ - directory a/foo.txt - directory a - directory b + +- name: Setup - test for multipath the moved hardlink + ansible.builtin.file: + state: directory + path: "{{ output_dir }}/{{ item }}" + mode: "0755" + recurse: true + loop: + - directory_a/data + - directory_a/data_tmp + - directory_b/data + - directory_b/data_tmp + +- name: Setup - create test new files + ansible.builtin.copy: + dest: "{{ output_dir }}/directory_a/data/foo.txt" + mode: "0644" + content: hello world + +- name: Setup - moved test file for save attr + ansible.posix.synchronize: + src: "{{ output_dir }}/directory_a/data/foo.txt" + dest: "{{ output_dir }}/directory_b/data/foo.txt" + delegate_to: "{{ inventory_hostname }}" + register: sync_result + ignore_errors: true + +- name: Setup - create hardlink for directory_a + ansible.builtin.file: + src: "{{ output_dir }}/directory_a/data/foo.txt" + dest: "{{ output_dir }}/directory_a/data_tmp/foo.txt" + state: hard + +- name: Setup - get stat hardlink + ansible.builtin.stat: + path: "{{ output_dir }}/directory_a/data/foo.txt" + register: stat_result_a + +- name: Copy multipath src the hardlink + ansible.posix.synchronize: + src: + - "{{ output_dir }}/directory_a/data" + - "{{ output_dir }}/directory_a/data_tmp" + dest: "{{ output_dir }}/directory_b" + times: false + rsync_opts: + - "--hard-links" + delegate_to: "{{ inventory_hostname }}" + register: sync_result + +- name: Get stat information for directory_b + ansible.builtin.stat: + path: "{{ output_dir }}/directory_b/data_tmp/foo.txt" + register: stat_result_b + +- name: Ensure file exists and checksum matches and hardlink moved + ansible.builtin.assert: + that: + - "'changed' in sync_result" + - sync_result.changed == true + - stat_result_b.stat.exists == True + - stat_result_a.stat.checksum == stat_result_b.stat.checksum + - stat_result_a.stat.nlink == stat_result_b.stat.nlink + - "'hf+++++++++ data_tmp/foo.txt => data/foo.txt' in sync_result.stdout_lines" + +- name: Cleanup + ansible.builtin.file: + state: absent + path: "{{ output_dir }}/{{ item }}" + loop: + - directory_a + - directory_b \ No newline at end of file