# -*- coding: utf-8 -*- # Copyright: (c) 2012, Michael DeHaan # Copyright: (c) 2017, Ansible Project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = r''' --- module: copy version_added: historical short_description: Copy files to remote locations description: - The C(copy) module copies a file from the local or remote machine to a location on the remote machine. - Use the M(ansible.builtin.fetch) module to copy files from remote locations to the local box. - If you need variable interpolation in copied files, use the M(ansible.builtin.template) module. Using a variable in the C(content) field will result in unpredictable output. - For Windows targets, use the M(ansible.windows.win_copy) module instead. options: src: description: - Local path to a file to copy to the remote server. - This can be absolute or relative. - If path is a directory, it is copied recursively. In this case, if path ends with "/", only inside contents of that directory are copied to destination. Otherwise, if it does not end with "/", the directory itself with all contents is copied. This behavior is similar to the C(rsync) command line tool. type: path content: description: - When used instead of C(src), sets the contents of a file directly to the specified value. - Works only when C(dest) is a file. Creates the file if it does not exist. - For advanced formatting or if C(content) contains a variable, use the M(ansible.builtin.template) module. type: str version_added: '1.1' dest: description: - Remote absolute path where the file should be copied to. - If C(src) is a directory, this must be a directory too. - If C(dest) is a non-existent path and if either C(dest) ends with "/" or C(src) is a directory, C(dest) is created. - If I(dest) is a relative path, the starting directory is determined by the remote host. - If C(src) and C(dest) are files, the parent directory of C(dest) is not created and the task fails if it does not already exist. type: path required: yes backup: description: - Create a backup file including the timestamp information so you can get the original file back if you somehow clobbered it incorrectly. type: bool default: no version_added: '0.7' force: description: - Influence whether the remote file must always be replaced. - If C(true), the remote file will be replaced when contents are different than the source. - If C(false), the file will only be transferred if the destination does not exist. type: bool default: yes version_added: '1.1' mode: description: - The permissions of the destination file or directory. - For those used to C(/usr/bin/chmod) remember that modes are actually octal numbers. You must either add a leading zero so that Ansible's YAML parser knows it is an octal number (like C(0644) or C(01777)) or quote it (like C('644') or C('1777')) so Ansible receives a string and can do its own conversion from string into number. Giving Ansible a number without following one of these rules will end up with a decimal number which will have unexpected results. - As of Ansible 1.8, the mode may be specified as a symbolic mode (for example, C(u+rwx) or C(u=rw,g=r,o=r)). - As of Ansible 2.3, the mode may also be the special string C(preserve). - C(preserve) means that the file will be given the same permissions as the source file. - When doing a recursive copy, see also C(directory_mode). - If C(mode) is not specified and the destination file B(does not) exist, the default C(umask) on the system will be used when setting the mode for the newly created file. - If C(mode) is not specified and the destination file B(does) exist, the mode of the existing file will be used. - Specifying C(mode) is the best way to ensure files are created with the correct permissions. See CVE-2020-1736 for further details. directory_mode: description: - When doing a recursive copy set the mode for the directories. - If this is not set we will use the system defaults. - The mode is only set on directories which are newly created, and will not affect those that already existed. type: raw version_added: '1.5' remote_src: description: - Influence whether C(src) needs to be transferred or already is present remotely. - If C(false), it will search for C(src) on the controller node. - If C(true) it will search for C(src) on the managed (remote) node. - C(remote_src) supports recursive copying as of version 2.8. - C(remote_src) only works with C(mode=preserve) as of version 2.6. - Autodecryption of files does not work when C(remote_src=yes). type: bool default: no version_added: '2.0' follow: description: - This flag indicates that filesystem links in the destination, if they exist, should be followed. type: bool default: no version_added: '1.8' local_follow: description: - This flag indicates that filesystem links in the source tree, if they exist, should be followed. type: bool default: yes version_added: '2.4' checksum: description: - SHA1 checksum of the file being transferred. - Used to validate that the copy of the file was successful. - If this is not provided, ansible will use the local calculated checksum of the src file. type: str version_added: '2.5' extends_documentation_fragment: - decrypt - files - validate - action_common_attributes - action_common_attributes.files - action_common_attributes.flow notes: - The M(ansible.builtin.copy) module recursively copy facility does not scale to lots (>hundreds) of files. seealso: - module: ansible.builtin.assemble - module: ansible.builtin.fetch - module: ansible.builtin.file - module: ansible.builtin.template - module: ansible.posix.synchronize - module: ansible.windows.win_copy author: - Ansible Core Team - Michael DeHaan attributes: action: support: full async: support: none bypass_host_loop: support: none check_mode: support: full diff_mode: support: full platform: platforms: posix safe_file_operations: support: full vault: support: full version_added: '2.2' ''' EXAMPLES = r''' - name: Copy file with owner and permissions ansible.builtin.copy: src: /srv/myfiles/foo.conf dest: /etc/foo.conf owner: foo group: foo mode: '0644' - name: Copy file with owner and permission, using symbolic representation ansible.builtin.copy: src: /srv/myfiles/foo.conf dest: /etc/foo.conf owner: foo group: foo mode: u=rw,g=r,o=r - name: Another symbolic mode example, adding some permissions and removing others ansible.builtin.copy: src: /srv/myfiles/foo.conf dest: /etc/foo.conf owner: foo group: foo mode: u+rw,g-wx,o-rwx - name: Copy a new "ntp.conf" file into place, backing up the original if it differs from the copied version ansible.builtin.copy: src: /mine/ntp.conf dest: /etc/ntp.conf owner: root group: root mode: '0644' backup: yes - name: Copy a new "sudoers" file into place, after passing validation with visudo ansible.builtin.copy: src: /mine/sudoers dest: /etc/sudoers validate: /usr/sbin/visudo -csf %s - name: Copy a "sudoers" file on the remote machine for editing ansible.builtin.copy: src: /etc/sudoers dest: /etc/sudoers.edit remote_src: yes validate: /usr/sbin/visudo -csf %s - name: Copy using inline content ansible.builtin.copy: content: '# This file was moved to /etc/other.conf' dest: /etc/mine.conf - name: If follow=yes, /path/to/file will be overwritten by contents of foo.conf ansible.builtin.copy: src: /etc/foo.conf dest: /path/to/link # link to /path/to/file follow: yes - name: If follow=no, /path/to/link will become a file and be overwritten by contents of foo.conf ansible.builtin.copy: src: /etc/foo.conf dest: /path/to/link # link to /path/to/file follow: no ''' RETURN = r''' dest: description: Destination file/path. returned: success type: str sample: /path/to/file.txt src: description: Source file used for the copy on the target machine. returned: changed type: str sample: /home/httpd/.ansible/tmp/ansible-tmp-1423796390.97-147729857856000/source md5sum: description: MD5 checksum of the file after running copy. returned: when supported type: str sample: 2a5aeecc61dc98c4d780b14b330e3282 checksum: description: SHA1 checksum of the file after running copy. returned: success type: str sample: 6e642bb8dd5c2e027bf21dd923337cbb4214f827 backup_file: description: Name of backup file created. returned: changed and if backup=yes type: str sample: /path/to/file.txt.2015-02-12@22:09~ gid: description: Group id of the file, after execution. returned: success type: int sample: 100 group: description: Group of the file, after execution. returned: success type: str sample: httpd owner: description: Owner of the file, after execution. returned: success type: str sample: httpd uid: description: Owner id of the file, after execution. returned: success type: int sample: 100 mode: description: Permissions of the target, after execution. returned: success type: str sample: "0644" size: description: Size of the target, after execution. returned: success type: int sample: 1220 state: description: State of the target, after execution. returned: success type: str sample: file ''' import errno import filecmp import grp import os import os.path import platform import pwd import shutil import stat import tempfile import traceback from ansible.module_utils._text import to_bytes, to_native from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.common.process import get_bin_path from ansible.module_utils.common.locale import get_best_parsable_locale from ansible.module_utils.six import PY3 # The AnsibleModule object module = None class AnsibleModuleError(Exception): def __init__(self, results): self.results = results # Once we get run_command moved into common, we can move this into a common/files module. We can't # until then because of the module.run_command() method. We may need to move it into # basic::AnsibleModule() until then but if so, make it a private function so that we don't have to # keep it for backwards compatibility later. def clear_facls(path): setfacl = get_bin_path('setfacl') # FIXME "setfacl -b" is available on Linux and FreeBSD. There is "setfacl -D e" on z/OS. Others? acl_command = [setfacl, '-b', path] b_acl_command = [to_bytes(x) for x in acl_command] locale = get_best_parsable_locale(module) rc, out, err = module.run_command(b_acl_command, environ_update=dict(LANG=locale, LC_ALL=locale, LC_MESSAGES=locale)) if rc != 0: raise RuntimeError('Error running "{0}": stdout: "{1}"; stderr: "{2}"'.format(' '.join(b_acl_command), out, err)) def split_pre_existing_dir(dirname): ''' Return the first pre-existing directory and a list of the new directories that will be created. ''' head, tail = os.path.split(dirname) b_head = to_bytes(head, errors='surrogate_or_strict') if head == '': return ('.', [tail]) if not os.path.exists(b_head): if head == '/': raise AnsibleModuleError(results={'msg': "The '/' directory doesn't exist on this machine."}) (pre_existing_dir, new_directory_list) = split_pre_existing_dir(head) else: return (head, [tail]) new_directory_list.append(tail) return (pre_existing_dir, new_directory_list) def adjust_recursive_directory_permissions(pre_existing_dir, new_directory_list, module, directory_args, changed): ''' Walk the new directories list and make sure that permissions are as we would expect ''' if new_directory_list: working_dir = os.path.join(pre_existing_dir, new_directory_list.pop(0)) directory_args['path'] = working_dir changed = module.set_fs_attributes_if_different(directory_args, changed) changed = adjust_recursive_directory_permissions(working_dir, new_directory_list, module, directory_args, changed) return changed def chown_recursive(path, module): changed = False owner = module.params['owner'] group = module.params['group'] if owner is not None: if not module.check_mode: for dirpath, dirnames, filenames in os.walk(path): owner_changed = module.set_owner_if_different(dirpath, owner, False) if owner_changed is True: changed = owner_changed for dir in [os.path.join(dirpath, d) for d in dirnames]: owner_changed = module.set_owner_if_different(dir, owner, False) if owner_changed is True: changed = owner_changed for file in [os.path.join(dirpath, f) for f in filenames]: owner_changed = module.set_owner_if_different(file, owner, False) if owner_changed is True: changed = owner_changed else: uid = pwd.getpwnam(owner).pw_uid for dirpath, dirnames, filenames in os.walk(path): owner_changed = (os.stat(dirpath).st_uid != uid) if owner_changed is True: changed = owner_changed for dir in [os.path.join(dirpath, d) for d in dirnames]: owner_changed = (os.stat(dir).st_uid != uid) if owner_changed is True: changed = owner_changed for file in [os.path.join(dirpath, f) for f in filenames]: owner_changed = (os.stat(file).st_uid != uid) if owner_changed is True: changed = owner_changed if group is not None: if not module.check_mode: for dirpath, dirnames, filenames in os.walk(path): group_changed = module.set_group_if_different(dirpath, group, False) if group_changed is True: changed = group_changed for dir in [os.path.join(dirpath, d) for d in dirnames]: group_changed = module.set_group_if_different(dir, group, False) if group_changed is True: changed = group_changed for file in [os.path.join(dirpath, f) for f in filenames]: group_changed = module.set_group_if_different(file, group, False) if group_changed is True: changed = group_changed else: gid = grp.getgrnam(group).gr_gid for dirpath, dirnames, filenames in os.walk(path): group_changed = (os.stat(dirpath).st_gid != gid) if group_changed is True: changed = group_changed for dir in [os.path.join(dirpath, d) for d in dirnames]: group_changed = (os.stat(dir).st_gid != gid) if group_changed is True: changed = group_changed for file in [os.path.join(dirpath, f) for f in filenames]: group_changed = (os.stat(file).st_gid != gid) if group_changed is True: changed = group_changed return changed def copy_diff_files(src, dest, module): """Copy files that are different between `src` directory and `dest` directory.""" changed = False owner = module.params['owner'] group = module.params['group'] local_follow = module.params['local_follow'] diff_files = filecmp.dircmp(src, dest).diff_files if len(diff_files): changed = True if not module.check_mode: for item in diff_files: src_item_path = os.path.join(src, item) dest_item_path = os.path.join(dest, item) b_src_item_path = to_bytes(src_item_path, errors='surrogate_or_strict') b_dest_item_path = to_bytes(dest_item_path, errors='surrogate_or_strict') if os.path.islink(b_src_item_path) and local_follow is False: linkto = os.readlink(b_src_item_path) os.symlink(linkto, b_dest_item_path) else: shutil.copyfile(b_src_item_path, b_dest_item_path) shutil.copymode(b_src_item_path, b_dest_item_path) if owner is not None: module.set_owner_if_different(b_dest_item_path, owner, False) if group is not None: module.set_group_if_different(b_dest_item_path, group, False) changed = True return changed def copy_left_only(src, dest, module): """Copy files that exist in `src` directory only to the `dest` directory.""" changed = False owner = module.params['owner'] group = module.params['group'] local_follow = module.params['local_follow'] left_only = filecmp.dircmp(src, dest).left_only if len(left_only): changed = True if not module.check_mode: for item in left_only: src_item_path = os.path.join(src, item) dest_item_path = os.path.join(dest, item) b_src_item_path = to_bytes(src_item_path, errors='surrogate_or_strict') b_dest_item_path = to_bytes(dest_item_path, errors='surrogate_or_strict') if os.path.islink(b_src_item_path) and os.path.isdir(b_src_item_path) and local_follow is True: shutil.copytree(b_src_item_path, b_dest_item_path, symlinks=not local_follow) chown_recursive(b_dest_item_path, module) if os.path.islink(b_src_item_path) and os.path.isdir(b_src_item_path) and local_follow is False: linkto = os.readlink(b_src_item_path) os.symlink(linkto, b_dest_item_path) if os.path.islink(b_src_item_path) and os.path.isfile(b_src_item_path) and local_follow is True: shutil.copyfile(b_src_item_path, b_dest_item_path) if owner is not None: module.set_owner_if_different(b_dest_item_path, owner, False) if group is not None: module.set_group_if_different(b_dest_item_path, group, False) if os.path.islink(b_src_item_path) and os.path.isfile(b_src_item_path) and local_follow is False: linkto = os.readlink(b_src_item_path) os.symlink(linkto, b_dest_item_path) if not os.path.islink(b_src_item_path) and os.path.isfile(b_src_item_path): shutil.copyfile(b_src_item_path, b_dest_item_path) shutil.copymode(b_src_item_path, b_dest_item_path) if owner is not None: module.set_owner_if_different(b_dest_item_path, owner, False) if group is not None: module.set_group_if_different(b_dest_item_path, group, False) if not os.path.islink(b_src_item_path) and os.path.isdir(b_src_item_path): shutil.copytree(b_src_item_path, b_dest_item_path, symlinks=not local_follow) chown_recursive(b_dest_item_path, module) changed = True return changed def copy_common_dirs(src, dest, module): changed = False common_dirs = filecmp.dircmp(src, dest).common_dirs for item in common_dirs: src_item_path = os.path.join(src, item) dest_item_path = os.path.join(dest, item) b_src_item_path = to_bytes(src_item_path, errors='surrogate_or_strict') b_dest_item_path = to_bytes(dest_item_path, errors='surrogate_or_strict') diff_files_changed = copy_diff_files(b_src_item_path, b_dest_item_path, module) left_only_changed = copy_left_only(b_src_item_path, b_dest_item_path, module) if diff_files_changed or left_only_changed: changed = True # recurse into subdirectory changed = changed or copy_common_dirs(os.path.join(src, item), os.path.join(dest, item), module) return changed def main(): global module module = AnsibleModule( # not checking because of daisy chain to file module argument_spec=dict( src=dict(type='path'), _original_basename=dict(type='str'), # used to handle 'dest is a directory' via template, a slight hack content=dict(type='str', no_log=True), dest=dict(type='path', required=True), backup=dict(type='bool', default=False), force=dict(type='bool', default=True), validate=dict(type='str'), directory_mode=dict(type='raw'), remote_src=dict(type='bool'), local_follow=dict(type='bool'), checksum=dict(type='str'), follow=dict(type='bool', default=False), ), add_file_common_args=True, supports_check_mode=True, ) src = module.params['src'] b_src = to_bytes(src, errors='surrogate_or_strict') dest = module.params['dest'] # Make sure we always have a directory component for later processing if os.path.sep not in dest: dest = '.{0}{1}'.format(os.path.sep, dest) b_dest = to_bytes(dest, errors='surrogate_or_strict') backup = module.params['backup'] force = module.params['force'] _original_basename = module.params.get('_original_basename', None) validate = module.params.get('validate', None) follow = module.params['follow'] local_follow = module.params['local_follow'] mode = module.params['mode'] owner = module.params['owner'] group = module.params['group'] remote_src = module.params['remote_src'] checksum = module.params['checksum'] if not os.path.exists(b_src): module.fail_json(msg="Source %s not found" % (src)) if not os.access(b_src, os.R_OK): module.fail_json(msg="Source %s not readable" % (src)) # Preserve is usually handled in the action plugin but mode + remote_src has to be done on the # remote host if module.params['mode'] == 'preserve': module.params['mode'] = '0%03o' % stat.S_IMODE(os.stat(b_src).st_mode) mode = module.params['mode'] changed = False checksum_dest = None checksum_src = None md5sum_src = None if os.path.isfile(src): try: checksum_src = module.sha1(src) except (OSError, IOError) as e: module.warn("Unable to calculate src checksum, assuming change: %s" % to_native(e)) try: # Backwards compat only. This will be None in FIPS mode md5sum_src = module.md5(src) except ValueError: pass elif remote_src and not os.path.isdir(src): module.fail_json("Cannot copy invalid source '%s': not a file" % to_native(src)) if checksum and checksum_src != checksum: module.fail_json( msg='Copied file does not match the expected checksum. Transfer failed.', checksum=checksum_src, expected_checksum=checksum ) # Special handling for recursive copy - create intermediate dirs if dest.endswith(os.sep): if _original_basename: dest = os.path.join(dest, _original_basename) b_dest = to_bytes(dest, errors='surrogate_or_strict') dirname = os.path.dirname(dest) b_dirname = to_bytes(dirname, errors='surrogate_or_strict') if not os.path.exists(b_dirname): try: (pre_existing_dir, new_directory_list) = split_pre_existing_dir(dirname) except AnsibleModuleError as e: e.result['msg'] += ' Could not copy to {0}'.format(dest) module.fail_json(**e.results) if module.check_mode: module.exit_json(msg='dest directory %s would be created' % dirname, changed=True, src=src) os.makedirs(b_dirname) directory_args = module.load_file_common_arguments(module.params) directory_mode = module.params["directory_mode"] if directory_mode is not None: directory_args['mode'] = directory_mode else: directory_args['mode'] = None adjust_recursive_directory_permissions(pre_existing_dir, new_directory_list, module, directory_args, changed) if os.path.isdir(b_dest): basename = os.path.basename(src) if _original_basename: basename = _original_basename dest = os.path.join(dest, basename) b_dest = to_bytes(dest, errors='surrogate_or_strict') if os.path.exists(b_dest): if os.path.islink(b_dest) and follow: b_dest = os.path.realpath(b_dest) dest = to_native(b_dest, errors='surrogate_or_strict') if not force: module.exit_json(msg="file already exists", src=src, dest=dest, changed=False) if os.access(b_dest, os.R_OK) and os.path.isfile(b_dest): checksum_dest = module.sha1(dest) else: if not os.path.exists(os.path.dirname(b_dest)): try: # os.path.exists() can return false in some # circumstances where the directory does not have # the execute bit for the current user set, in # which case the stat() call will raise an OSError os.stat(os.path.dirname(b_dest)) except OSError as e: if "permission denied" in to_native(e).lower(): module.fail_json(msg="Destination directory %s is not accessible" % (os.path.dirname(dest))) module.fail_json(msg="Destination directory %s does not exist" % (os.path.dirname(dest))) if not os.access(os.path.dirname(b_dest), os.W_OK) and not module.params['unsafe_writes']: module.fail_json(msg="Destination %s not writable" % (os.path.dirname(dest))) backup_file = None if checksum_src != checksum_dest or os.path.islink(b_dest): if not module.check_mode: try: if backup: if os.path.exists(b_dest): backup_file = module.backup_local(dest) # allow for conversion from symlink. if os.path.islink(b_dest): os.unlink(b_dest) open(b_dest, 'w').close() if validate: # if we have a mode, make sure we set it on the temporary # file source as some validations may require it if mode is not None: module.set_mode_if_different(src, mode, False) if owner is not None: module.set_owner_if_different(src, owner, False) if group is not None: module.set_group_if_different(src, group, False) if "%s" not in validate: module.fail_json(msg="validate must contain %%s: %s" % (validate)) (rc, out, err) = module.run_command(validate % src) if rc != 0: module.fail_json(msg="failed to validate", exit_status=rc, stdout=out, stderr=err) b_mysrc = b_src if remote_src and os.path.isfile(b_src): _, b_mysrc = tempfile.mkstemp(dir=os.path.dirname(b_dest)) shutil.copyfile(b_src, b_mysrc) try: shutil.copystat(b_src, b_mysrc) except OSError as err: if err.errno == errno.ENOSYS and mode == "preserve": module.warn("Unable to copy stats {0}".format(to_native(b_src))) else: raise # might be needed below if PY3 and hasattr(os, 'listxattr'): try: src_has_acls = 'system.posix_acl_access' in os.listxattr(src) except Exception as e: # assume unwanted ACLs by default src_has_acls = True # at this point we should always have tmp file module.atomic_move(b_mysrc, dest, unsafe_writes=module.params['unsafe_writes']) if PY3 and hasattr(os, 'listxattr') and platform.system() == 'Linux' and not remote_src: # atomic_move used above to copy src into dest might, in some cases, # use shutil.copy2 which in turn uses shutil.copystat. # Since Python 3.3, shutil.copystat copies file extended attributes: # https://docs.python.org/3/library/shutil.html#shutil.copystat # os.listxattr (along with others) was added to handle the operation. # This means that on Python 3 we are copying the extended attributes which includes # the ACLs on some systems - further limited to Linux as the documentation above claims # that the extended attributes are copied only on Linux. Also, os.listxattr is only # available on Linux. # If not remote_src, then the file was copied from the controller. In that # case, any filesystem ACLs are artifacts of the copy rather than preservation # of existing attributes. Get rid of them: if src_has_acls: # FIXME If dest has any default ACLs, there are not applied to src now because # they were overridden by copystat. Should/can we do anything about this? # 'system.posix_acl_default' in os.listxattr(os.path.dirname(b_dest)) try: clear_facls(dest) except ValueError as e: if 'setfacl' in to_native(e): # No setfacl so we're okay. The controller couldn't have set a facl # without the setfacl command pass else: raise except RuntimeError as e: # setfacl failed. if 'Operation not supported' in to_native(e): # The file system does not support ACLs. pass else: raise except (IOError, OSError): module.fail_json(msg="failed to copy: %s to %s" % (src, dest), traceback=traceback.format_exc()) changed = True else: changed = False # If neither have checksums, both src and dest are directories. if checksum_src is None and checksum_dest is None: if remote_src and os.path.isdir(module.params['src']): b_src = to_bytes(module.params['src'], errors='surrogate_or_strict') b_dest = to_bytes(module.params['dest'], errors='surrogate_or_strict') if src.endswith(os.path.sep) and os.path.isdir(module.params['dest']): diff_files_changed = copy_diff_files(b_src, b_dest, module) left_only_changed = copy_left_only(b_src, b_dest, module) common_dirs_changed = copy_common_dirs(b_src, b_dest, module) owner_group_changed = chown_recursive(b_dest, module) if diff_files_changed or left_only_changed or common_dirs_changed or owner_group_changed: changed = True if src.endswith(os.path.sep) and not os.path.exists(module.params['dest']): b_basename = to_bytes(os.path.basename(src), errors='surrogate_or_strict') b_dest = to_bytes(os.path.join(b_dest, b_basename), errors='surrogate_or_strict') b_src = to_bytes(os.path.join(module.params['src'], ""), errors='surrogate_or_strict') if not module.check_mode: shutil.copytree(b_src, b_dest, symlinks=not local_follow) chown_recursive(dest, module) changed = True if not src.endswith(os.path.sep) and os.path.isdir(module.params['dest']): b_basename = to_bytes(os.path.basename(src), errors='surrogate_or_strict') b_dest = to_bytes(os.path.join(b_dest, b_basename), errors='surrogate_or_strict') b_src = to_bytes(os.path.join(module.params['src'], ""), errors='surrogate_or_strict') if not module.check_mode and not os.path.exists(b_dest): shutil.copytree(b_src, b_dest, symlinks=not local_follow) changed = True chown_recursive(dest, module) if module.check_mode and not os.path.exists(b_dest): changed = True if os.path.exists(b_dest): diff_files_changed = copy_diff_files(b_src, b_dest, module) left_only_changed = copy_left_only(b_src, b_dest, module) common_dirs_changed = copy_common_dirs(b_src, b_dest, module) owner_group_changed = chown_recursive(b_dest, module) if diff_files_changed or left_only_changed or common_dirs_changed or owner_group_changed: changed = True if not src.endswith(os.path.sep) and not os.path.exists(module.params['dest']): b_basename = to_bytes(os.path.basename(module.params['src']), errors='surrogate_or_strict') b_dest = to_bytes(os.path.join(b_dest, b_basename), errors='surrogate_or_strict') if not module.check_mode and not os.path.exists(b_dest): os.makedirs(b_dest) b_src = to_bytes(os.path.join(module.params['src'], ""), errors='surrogate_or_strict') diff_files_changed = copy_diff_files(b_src, b_dest, module) left_only_changed = copy_left_only(b_src, b_dest, module) common_dirs_changed = copy_common_dirs(b_src, b_dest, module) owner_group_changed = chown_recursive(b_dest, module) if diff_files_changed or left_only_changed or common_dirs_changed or owner_group_changed: changed = True if module.check_mode and not os.path.exists(b_dest): changed = True res_args = dict( dest=dest, src=src, md5sum=md5sum_src, checksum=checksum_src, changed=changed ) if backup_file: res_args['backup_file'] = backup_file file_args = module.load_file_common_arguments(module.params, path=dest) res_args['changed'] = module.set_fs_attributes_if_different(file_args, res_args['changed']) module.exit_json(**res_args) if __name__ == '__main__': main()