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..7e77b8f 100644 --- a/plugins/modules/synchronize.py +++ b/plugins/modules/synchronize.py @@ -27,8 +27,10 @@ options: description: - Path on the source host that will be synchronized to the destination. - The path can be absolute or relative. - type: path + - Support multi path + type: list required: true + elements: path dest: description: - Path on the destination host that will be synchronized from the source. @@ -350,6 +352,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 +406,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 +416,7 @@ def is_rsh_needed(source, dest): def main(): module = AnsibleModule( argument_spec=dict( - src=dict(type='path', required=True), + src=dict(type='list', elements='path', required=True), dest=dict(type='path', required=True), dest_port=dict(type='int'), delete=dict(type='bool', default=False), @@ -540,7 +550,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 +610,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..ecbe596 100644 --- a/tests/integration/targets/synchronize/tasks/main.yml +++ b/tests/integration/targets/synchronize/tasks/main.yml @@ -348,3 +348,77 @@ - 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 + mode: "0644" + +- 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' in sync_result.msg" + - "'data_tmp/foo.txt => data/foo.txt' in sync_result.msg" + +- name: Cleanup + ansible.builtin.file: + state: absent + path: "{{ output_dir }}/{{ item }}" + loop: + - directory_a + - directory_b diff --git a/tests/unit/plugins/action/fixtures/synchronize/basic/meta.yaml b/tests/unit/plugins/action/fixtures/synchronize/basic/meta.yaml index 28b7045..090595c 100644 --- a/tests/unit/plugins/action/fixtures/synchronize/basic/meta.yaml +++ b/tests/unit/plugins/action/fixtures/synchronize/basic/meta.yaml @@ -14,5 +14,5 @@ asserts: - self._play_context.shell == 'sh' - self.execute_called - self.final_module_args['_local_rsync_path'] == 'rsync' - - self.final_module_args['src'] == '/tmp/deleteme' + - self.final_module_args['src'] == ['/tmp/deleteme'] - self.final_module_args['dest'] == 'root@el6host:/tmp/deleteme' diff --git a/tests/unit/plugins/action/fixtures/synchronize/basic_become/meta.yaml b/tests/unit/plugins/action/fixtures/synchronize/basic_become/meta.yaml index 1eb0b92..6fe68d6 100644 --- a/tests/unit/plugins/action/fixtures/synchronize/basic_become/meta.yaml +++ b/tests/unit/plugins/action/fixtures/synchronize/basic_become/meta.yaml @@ -28,7 +28,7 @@ asserts: # this is a crucial aspect of this scenario ... # note: become_user None -> root - self.final_module_args['rsync_path'] == 'sudo -u root rsync' - - self.final_module_args['src'] == '/tmp/deleteme' + - self.final_module_args['src'] == ['/tmp/deleteme'] - self.final_module_args['dest'] == 'root@el6host:/tmp/deleteme' - self.task.become == True - self.task.become_user == None diff --git a/tests/unit/plugins/action/fixtures/synchronize/basic_become_cli/meta.yaml b/tests/unit/plugins/action/fixtures/synchronize/basic_become_cli/meta.yaml index 1bec3b4..04ad811 100644 --- a/tests/unit/plugins/action/fixtures/synchronize/basic_become_cli/meta.yaml +++ b/tests/unit/plugins/action/fixtures/synchronize/basic_become_cli/meta.yaml @@ -28,7 +28,7 @@ asserts: # this is a crucial aspect of this scenario ... # note: become_user None -> root - self.final_module_args['rsync_path'] == 'sudo -u root rsync' - - self.final_module_args['src'] == '/tmp/deleteme' + - self.final_module_args['src'] == ['/tmp/deleteme'] - self.final_module_args['dest'] == 'root@el6host:/tmp/deleteme' - self.task.become == None - self.task.become_user == None diff --git a/tests/unit/plugins/action/fixtures/synchronize/basic_vagrant/meta.yaml b/tests/unit/plugins/action/fixtures/synchronize/basic_vagrant/meta.yaml index 574ee6a..c265b95 100644 --- a/tests/unit/plugins/action/fixtures/synchronize/basic_vagrant/meta.yaml +++ b/tests/unit/plugins/action/fixtures/synchronize/basic_vagrant/meta.yaml @@ -20,7 +20,7 @@ asserts: - self.execute_called - self.final_module_args['_local_rsync_path'] == 'rsync' - self.final_module_args['dest_port'] == 2202 - - self.final_module_args['src'] == '/tmp/deleteme' + - self.final_module_args['src'] == ['/tmp/deleteme'] - self.final_module_args['dest'] == 'vagrant@127.0.0.1:/tmp/deleteme' - self._play_context.shell == 'sh' - self._play_context.remote_addr == '127.0.0.1' diff --git a/tests/unit/plugins/action/fixtures/synchronize/basic_vagrant_become_cli/meta.yaml b/tests/unit/plugins/action/fixtures/synchronize/basic_vagrant_become_cli/meta.yaml index eb0d5b1..1ac3b52 100644 --- a/tests/unit/plugins/action/fixtures/synchronize/basic_vagrant_become_cli/meta.yaml +++ b/tests/unit/plugins/action/fixtures/synchronize/basic_vagrant_become_cli/meta.yaml @@ -23,7 +23,7 @@ asserts: - self.execute_called - self.final_module_args['_local_rsync_path'] == 'rsync' - self.final_module_args['dest_port'] == 2202 - - self.final_module_args['src'] == '/tmp/deleteme' + - self.final_module_args['src'] == ['/tmp/deleteme'] - self.final_module_args['dest'] == 'vagrant@127.0.0.1:/tmp/deleteme' - self._play_context.shell == 'sh' - self._play_context.remote_addr == '127.0.0.1' diff --git a/tests/unit/plugins/action/fixtures/synchronize/basic_vagrant_sudo/meta.yaml b/tests/unit/plugins/action/fixtures/synchronize/basic_vagrant_sudo/meta.yaml index 574ee6a..c265b95 100644 --- a/tests/unit/plugins/action/fixtures/synchronize/basic_vagrant_sudo/meta.yaml +++ b/tests/unit/plugins/action/fixtures/synchronize/basic_vagrant_sudo/meta.yaml @@ -20,7 +20,7 @@ asserts: - self.execute_called - self.final_module_args['_local_rsync_path'] == 'rsync' - self.final_module_args['dest_port'] == 2202 - - self.final_module_args['src'] == '/tmp/deleteme' + - self.final_module_args['src'] == ['/tmp/deleteme'] - self.final_module_args['dest'] == 'vagrant@127.0.0.1:/tmp/deleteme' - self._play_context.shell == 'sh' - self._play_context.remote_addr == '127.0.0.1' diff --git a/tests/unit/plugins/action/fixtures/synchronize/basic_with_private_key/meta.yaml b/tests/unit/plugins/action/fixtures/synchronize/basic_with_private_key/meta.yaml index 7405cb6..7f5546f 100644 --- a/tests/unit/plugins/action/fixtures/synchronize/basic_with_private_key/meta.yaml +++ b/tests/unit/plugins/action/fixtures/synchronize/basic_with_private_key/meta.yaml @@ -21,6 +21,6 @@ asserts: - self._play_context.shell == 'sh' - self.execute_called - self.final_module_args['_local_rsync_path'] == 'rsync' - - self.final_module_args['src'] == '/tmp/deleteme' + - self.final_module_args['src'] == ['/tmp/deleteme'] - self.final_module_args['dest'] == 'root@el6host:/tmp/deleteme' - self.final_module_args['private_key'] == '~/.ssh/id_rsa' diff --git a/tests/unit/plugins/action/fixtures/synchronize/delegate_remote/meta.yaml b/tests/unit/plugins/action/fixtures/synchronize/delegate_remote/meta.yaml index e943b79..3fbac10 100644 --- a/tests/unit/plugins/action/fixtures/synchronize/delegate_remote/meta.yaml +++ b/tests/unit/plugins/action/fixtures/synchronize/delegate_remote/meta.yaml @@ -23,5 +23,5 @@ asserts: - self._play_context.shell == None - self.execute_called - self.final_module_args['_local_rsync_path'] == 'rsync' - - self.final_module_args['src'] == '/tmp/deleteme' + - self.final_module_args['src'] == ['/tmp/deleteme'] - self.final_module_args['dest'] == 'root@el6host:/tmp/deleteme' diff --git a/tests/unit/plugins/action/fixtures/synchronize/delegate_remote_play_context_private_key/meta.yaml b/tests/unit/plugins/action/fixtures/synchronize/delegate_remote_play_context_private_key/meta.yaml index 2d88e63..1d6d4cc 100644 --- a/tests/unit/plugins/action/fixtures/synchronize/delegate_remote_play_context_private_key/meta.yaml +++ b/tests/unit/plugins/action/fixtures/synchronize/delegate_remote_play_context_private_key/meta.yaml @@ -24,6 +24,6 @@ asserts: - self._play_context.shell == None - self.execute_called - self.final_module_args['_local_rsync_path'] == 'rsync' - - self.final_module_args['src'] == '/tmp/deleteme' + - self.final_module_args['src'] == ['/tmp/deleteme'] - self.final_module_args['dest'] == 'root@el6host:/tmp/deleteme' - self.final_module_args['private_key'] == '~/test.pem' diff --git a/tests/unit/plugins/action/fixtures/synchronize/delegate_remote_su/meta.yaml b/tests/unit/plugins/action/fixtures/synchronize/delegate_remote_su/meta.yaml index 34df19d..3fe6432 100644 --- a/tests/unit/plugins/action/fixtures/synchronize/delegate_remote_su/meta.yaml +++ b/tests/unit/plugins/action/fixtures/synchronize/delegate_remote_su/meta.yaml @@ -30,5 +30,5 @@ asserts: - self._play_context.become_method == 'su' - self.execute_called - self.final_module_args['_local_rsync_path'] == 'rsync' - - self.final_module_args['src'] == '/tmp/deleteme' + - self.final_module_args['src'] == ['/tmp/deleteme'] - self.final_module_args['dest'] == 'root@el6host:/tmp/deleteme' diff --git a/tests/unit/plugins/action/fixtures/synchronize/delegate_remote_with_private_key/meta.yaml b/tests/unit/plugins/action/fixtures/synchronize/delegate_remote_with_private_key/meta.yaml index 603a4cb..0cc99e0 100644 --- a/tests/unit/plugins/action/fixtures/synchronize/delegate_remote_with_private_key/meta.yaml +++ b/tests/unit/plugins/action/fixtures/synchronize/delegate_remote_with_private_key/meta.yaml @@ -25,6 +25,6 @@ asserts: - self._play_context.shell == None - self.execute_called - self.final_module_args['_local_rsync_path'] == 'rsync' - - self.final_module_args['src'] == '/tmp/deleteme' + - self.final_module_args['src'] == ['/tmp/deleteme'] - self.final_module_args['dest'] == 'root@el6host:/tmp/deleteme' - self.final_module_args['private_key'] == '~/.ssh/id_rsa'