1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4# Copyright: (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
5# Copyright: (c) 2017, Ansible Project
6# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
7
8from __future__ import absolute_import, division, print_function
9__metaclass__ = type
10
11
12DOCUMENTATION = r'''
13---
14module: copy
15version_added: historical
16short_description: Copy files to remote locations
17description:
18    - The C(copy) module copies a file from the local or remote machine to a location on the remote machine.
19    - Use the M(ansible.builtin.fetch) module to copy files from remote locations to the local box.
20    - If you need variable interpolation in copied files, use the M(ansible.builtin.template) module.
21      Using a variable in the C(content) field will result in unpredictable output.
22    - For Windows targets, use the M(ansible.windows.win_copy) module instead.
23options:
24  src:
25    description:
26    - Local path to a file to copy to the remote server.
27    - This can be absolute or relative.
28    - If path is a directory, it is copied recursively. In this case, if path ends
29      with "/", only inside contents of that directory are copied to destination.
30      Otherwise, if it does not end with "/", the directory itself with all contents
31      is copied. This behavior is similar to the C(rsync) command line tool.
32    type: path
33  content:
34    description:
35    - When used instead of C(src), sets the contents of a file directly to the specified value.
36    - Works only when C(dest) is a file. Creates the file if it does not exist.
37    - For advanced formatting or if C(content) contains a variable, use the
38      M(ansible.builtin.template) module.
39    type: str
40    version_added: '1.1'
41  dest:
42    description:
43    - Remote absolute path where the file should be copied to.
44    - If C(src) is a directory, this must be a directory too.
45    - 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.
46    - If I(dest) is a relative path, the starting directory is determined by the remote host.
47    - 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.
48    type: path
49    required: yes
50  backup:
51    description:
52    - Create a backup file including the timestamp information so you can get the original file back if you somehow clobbered it incorrectly.
53    type: bool
54    default: no
55    version_added: '0.7'
56  force:
57    description:
58    - Influence whether the remote file must always be replaced.
59    - If C(yes), the remote file will be replaced when contents are different than the source.
60    - If C(no), the file will only be transferred if the destination does not exist.
61    - Alias C(thirsty) has been deprecated and will be removed in 2.13.
62    type: bool
63    default: yes
64    aliases: [ thirsty ]
65    version_added: '1.1'
66  mode:
67    description:
68    - The permissions of the destination file or directory.
69    - For those used to C(/usr/bin/chmod) remember that modes are actually octal numbers.
70      You must either add a leading zero so that Ansible's YAML parser knows it is an octal number
71      (like C(0644) or C(01777))or quote it (like C('644') or C('1777')) so Ansible receives a string
72      and can do its own conversion from string into number. Giving Ansible a number without following
73      one of these rules will end up with a decimal number which will have unexpected results.
74    - 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)).
75    - As of Ansible 2.3, the mode may also be the special string C(preserve).
76    - C(preserve) means that the file will be given the same permissions as the source file.
77    - When doing a recursive copy, see also C(directory_mode).
78    type: path
79  directory_mode:
80    description:
81    - When doing a recursive copy set the mode for the directories.
82    - If this is not set we will use the system defaults.
83    - The mode is only set on directories which are newly created, and will not affect those that already existed.
84    type: raw
85    version_added: '1.5'
86  remote_src:
87    description:
88    - Influence whether C(src) needs to be transferred or already is present remotely.
89    - If C(no), it will search for C(src) at originating/master machine.
90    - If C(yes) it will go to the remote/target machine for the C(src).
91    - C(remote_src) supports recursive copying as of version 2.8.
92    - C(remote_src) only works with C(mode=preserve) as of version 2.6.
93    type: bool
94    default: no
95    version_added: '2.0'
96  follow:
97    description:
98    - This flag indicates that filesystem links in the destination, if they exist, should be followed.
99    type: bool
100    default: no
101    version_added: '1.8'
102  local_follow:
103    description:
104    - This flag indicates that filesystem links in the source tree, if they exist, should be followed.
105    type: bool
106    default: yes
107    version_added: '2.4'
108  checksum:
109    description:
110    - SHA1 checksum of the file being transferred.
111    - Used to validate that the copy of the file was successful.
112    - If this is not provided, ansible will use the local calculated checksum of the src file.
113    type: str
114    version_added: '2.5'
115extends_documentation_fragment:
116- decrypt
117- files
118- validate
119notes:
120- The M(ansible.builtin.copy) module recursively copy facility does not scale to lots (>hundreds) of files.
121- Supports C(check_mode).
122seealso:
123- module: ansible.builtin.assemble
124- module: ansible.builtin.fetch
125- module: ansible.builtin.file
126- module: ansible.builtin.template
127- module: ansible.posix.synchronize
128- module: ansible.windows.win_copy
129author:
130- Ansible Core Team
131- Michael DeHaan
132'''
133
134EXAMPLES = r'''
135- name: Copy file with owner and permissions
136  ansible.builtin.copy:
137    src: /srv/myfiles/foo.conf
138    dest: /etc/foo.conf
139    owner: foo
140    group: foo
141    mode: '0644'
142
143- name: Copy file with owner and permission, using symbolic representation
144  ansible.builtin.copy:
145    src: /srv/myfiles/foo.conf
146    dest: /etc/foo.conf
147    owner: foo
148    group: foo
149    mode: u=rw,g=r,o=r
150
151- name: Another symbolic mode example, adding some permissions and removing others
152  ansible.builtin.copy:
153    src: /srv/myfiles/foo.conf
154    dest: /etc/foo.conf
155    owner: foo
156    group: foo
157    mode: u+rw,g-wx,o-rwx
158
159- name: Copy a new "ntp.conf" file into place, backing up the original if it differs from the copied version
160  ansible.builtin.copy:
161    src: /mine/ntp.conf
162    dest: /etc/ntp.conf
163    owner: root
164    group: root
165    mode: '0644'
166    backup: yes
167
168- name: Copy a new "sudoers" file into place, after passing validation with visudo
169  ansible.builtin.copy:
170    src: /mine/sudoers
171    dest: /etc/sudoers
172    validate: /usr/sbin/visudo -csf %s
173
174- name: Copy a "sudoers" file on the remote machine for editing
175  ansible.builtin.copy:
176    src: /etc/sudoers
177    dest: /etc/sudoers.edit
178    remote_src: yes
179    validate: /usr/sbin/visudo -csf %s
180
181- name: Copy using inline content
182  ansible.builtin.copy:
183    content: '# This file was moved to /etc/other.conf'
184    dest: /etc/mine.conf
185
186- name: If follow=yes, /path/to/file will be overwritten by contents of foo.conf
187  ansible.builtin.copy:
188    src: /etc/foo.conf
189    dest: /path/to/link  # link to /path/to/file
190    follow: yes
191
192- name: If follow=no, /path/to/link will become a file and be overwritten by contents of foo.conf
193  ansible.builtin.copy:
194    src: /etc/foo.conf
195    dest: /path/to/link  # link to /path/to/file
196    follow: no
197'''
198
199RETURN = r'''
200dest:
201    description: Destination file/path.
202    returned: success
203    type: str
204    sample: /path/to/file.txt
205src:
206    description: Source file used for the copy on the target machine.
207    returned: changed
208    type: str
209    sample: /home/httpd/.ansible/tmp/ansible-tmp-1423796390.97-147729857856000/source
210md5sum:
211    description: MD5 checksum of the file after running copy.
212    returned: when supported
213    type: str
214    sample: 2a5aeecc61dc98c4d780b14b330e3282
215checksum:
216    description: SHA1 checksum of the file after running copy.
217    returned: success
218    type: str
219    sample: 6e642bb8dd5c2e027bf21dd923337cbb4214f827
220backup_file:
221    description: Name of backup file created.
222    returned: changed and if backup=yes
223    type: str
224    sample: /path/to/file.txt.2015-02-12@22:09~
225gid:
226    description: Group id of the file, after execution.
227    returned: success
228    type: int
229    sample: 100
230group:
231    description: Group of the file, after execution.
232    returned: success
233    type: str
234    sample: httpd
235owner:
236    description: Owner of the file, after execution.
237    returned: success
238    type: str
239    sample: httpd
240uid:
241    description: Owner id of the file, after execution.
242    returned: success
243    type: int
244    sample: 100
245mode:
246    description: Permissions of the target, after execution.
247    returned: success
248    type: str
249    sample: 0644
250size:
251    description: Size of the target, after execution.
252    returned: success
253    type: int
254    sample: 1220
255state:
256    description: State of the target, after execution.
257    returned: success
258    type: str
259    sample: file
260'''
261
262import errno
263import filecmp
264import grp
265import os
266import os.path
267import platform
268import pwd
269import shutil
270import stat
271import tempfile
272import traceback
273
274from ansible.module_utils.basic import AnsibleModule
275from ansible.module_utils.common.process import get_bin_path
276from ansible.module_utils._text import to_bytes, to_native
277from ansible.module_utils.six import PY3
278
279
280# The AnsibleModule object
281module = None
282
283
284class AnsibleModuleError(Exception):
285    def __init__(self, results):
286        self.results = results
287
288
289# Once we get run_command moved into common, we can move this into a common/files module.  We can't
290# until then because of the module.run_command() method.  We may need to move it into
291# basic::AnsibleModule() until then but if so, make it a private function so that we don't have to
292# keep it for backwards compatibility later.
293def clear_facls(path):
294    setfacl = get_bin_path('setfacl')
295    # FIXME "setfacl -b" is available on Linux and FreeBSD. There is "setfacl -D e" on z/OS. Others?
296    acl_command = [setfacl, '-b', path]
297    b_acl_command = [to_bytes(x) for x in acl_command]
298    rc, out, err = module.run_command(b_acl_command, environ_update=dict(LANG='C', LC_ALL='C', LC_MESSAGES='C'))
299    if rc != 0:
300        raise RuntimeError('Error running "{0}": stdout: "{1}"; stderr: "{2}"'.format(' '.join(b_acl_command), out, err))
301
302
303def split_pre_existing_dir(dirname):
304    '''
305    Return the first pre-existing directory and a list of the new directories that will be created.
306    '''
307    head, tail = os.path.split(dirname)
308    b_head = to_bytes(head, errors='surrogate_or_strict')
309    if head == '':
310        return ('.', [tail])
311    if not os.path.exists(b_head):
312        if head == '/':
313            raise AnsibleModuleError(results={'msg': "The '/' directory doesn't exist on this machine."})
314        (pre_existing_dir, new_directory_list) = split_pre_existing_dir(head)
315    else:
316        return (head, [tail])
317    new_directory_list.append(tail)
318    return (pre_existing_dir, new_directory_list)
319
320
321def adjust_recursive_directory_permissions(pre_existing_dir, new_directory_list, module, directory_args, changed):
322    '''
323    Walk the new directories list and make sure that permissions are as we would expect
324    '''
325
326    if new_directory_list:
327        working_dir = os.path.join(pre_existing_dir, new_directory_list.pop(0))
328        directory_args['path'] = working_dir
329        changed = module.set_fs_attributes_if_different(directory_args, changed)
330        changed = adjust_recursive_directory_permissions(working_dir, new_directory_list, module, directory_args, changed)
331    return changed
332
333
334def chown_recursive(path, module):
335    changed = False
336    owner = module.params['owner']
337    group = module.params['group']
338
339    if owner is not None:
340        if not module.check_mode:
341            for dirpath, dirnames, filenames in os.walk(path):
342                owner_changed = module.set_owner_if_different(dirpath, owner, False)
343                if owner_changed is True:
344                    changed = owner_changed
345                for dir in [os.path.join(dirpath, d) for d in dirnames]:
346                    owner_changed = module.set_owner_if_different(dir, owner, False)
347                    if owner_changed is True:
348                        changed = owner_changed
349                for file in [os.path.join(dirpath, f) for f in filenames]:
350                    owner_changed = module.set_owner_if_different(file, owner, False)
351                    if owner_changed is True:
352                        changed = owner_changed
353        else:
354            uid = pwd.getpwnam(owner).pw_uid
355            for dirpath, dirnames, filenames in os.walk(path):
356                owner_changed = (os.stat(dirpath).st_uid != uid)
357                if owner_changed is True:
358                    changed = owner_changed
359                for dir in [os.path.join(dirpath, d) for d in dirnames]:
360                    owner_changed = (os.stat(dir).st_uid != uid)
361                    if owner_changed is True:
362                        changed = owner_changed
363                for file in [os.path.join(dirpath, f) for f in filenames]:
364                    owner_changed = (os.stat(file).st_uid != uid)
365                    if owner_changed is True:
366                        changed = owner_changed
367    if group is not None:
368        if not module.check_mode:
369            for dirpath, dirnames, filenames in os.walk(path):
370                group_changed = module.set_group_if_different(dirpath, group, False)
371                if group_changed is True:
372                    changed = group_changed
373                for dir in [os.path.join(dirpath, d) for d in dirnames]:
374                    group_changed = module.set_group_if_different(dir, group, False)
375                    if group_changed is True:
376                        changed = group_changed
377                for file in [os.path.join(dirpath, f) for f in filenames]:
378                    group_changed = module.set_group_if_different(file, group, False)
379                    if group_changed is True:
380                        changed = group_changed
381        else:
382            gid = grp.getgrnam(group).gr_gid
383            for dirpath, dirnames, filenames in os.walk(path):
384                group_changed = (os.stat(dirpath).st_gid != gid)
385                if group_changed is True:
386                    changed = group_changed
387                for dir in [os.path.join(dirpath, d) for d in dirnames]:
388                    group_changed = (os.stat(dir).st_gid != gid)
389                    if group_changed is True:
390                        changed = group_changed
391                for file in [os.path.join(dirpath, f) for f in filenames]:
392                    group_changed = (os.stat(file).st_gid != gid)
393                    if group_changed is True:
394                        changed = group_changed
395
396    return changed
397
398
399def copy_diff_files(src, dest, module):
400    """Copy files that are different between `src` directory and `dest` directory."""
401
402    changed = False
403    owner = module.params['owner']
404    group = module.params['group']
405    local_follow = module.params['local_follow']
406    diff_files = filecmp.dircmp(src, dest).diff_files
407    if len(diff_files):
408        changed = True
409    if not module.check_mode:
410        for item in diff_files:
411            src_item_path = os.path.join(src, item)
412            dest_item_path = os.path.join(dest, item)
413            b_src_item_path = to_bytes(src_item_path, errors='surrogate_or_strict')
414            b_dest_item_path = to_bytes(dest_item_path, errors='surrogate_or_strict')
415            if os.path.islink(b_src_item_path) and local_follow is False:
416                linkto = os.readlink(b_src_item_path)
417                os.symlink(linkto, b_dest_item_path)
418            else:
419                shutil.copyfile(b_src_item_path, b_dest_item_path)
420                shutil.copymode(b_src_item_path, b_dest_item_path)
421
422            if owner is not None:
423                module.set_owner_if_different(b_dest_item_path, owner, False)
424            if group is not None:
425                module.set_group_if_different(b_dest_item_path, group, False)
426            changed = True
427    return changed
428
429
430def copy_left_only(src, dest, module):
431    """Copy files that exist in `src` directory only to the `dest` directory."""
432
433    changed = False
434    owner = module.params['owner']
435    group = module.params['group']
436    local_follow = module.params['local_follow']
437    left_only = filecmp.dircmp(src, dest).left_only
438    if len(left_only):
439        changed = True
440    if not module.check_mode:
441        for item in left_only:
442            src_item_path = os.path.join(src, item)
443            dest_item_path = os.path.join(dest, item)
444            b_src_item_path = to_bytes(src_item_path, errors='surrogate_or_strict')
445            b_dest_item_path = to_bytes(dest_item_path, errors='surrogate_or_strict')
446
447            if os.path.islink(b_src_item_path) and os.path.isdir(b_src_item_path) and local_follow is True:
448                shutil.copytree(b_src_item_path, b_dest_item_path, symlinks=not(local_follow))
449                chown_recursive(b_dest_item_path, module)
450
451            if os.path.islink(b_src_item_path) and os.path.isdir(b_src_item_path) and local_follow is False:
452                linkto = os.readlink(b_src_item_path)
453                os.symlink(linkto, b_dest_item_path)
454
455            if os.path.islink(b_src_item_path) and os.path.isfile(b_src_item_path) and local_follow is True:
456                shutil.copyfile(b_src_item_path, b_dest_item_path)
457                if owner is not None:
458                    module.set_owner_if_different(b_dest_item_path, owner, False)
459                if group is not None:
460                    module.set_group_if_different(b_dest_item_path, group, False)
461
462            if os.path.islink(b_src_item_path) and os.path.isfile(b_src_item_path) and local_follow is False:
463                linkto = os.readlink(b_src_item_path)
464                os.symlink(linkto, b_dest_item_path)
465
466            if not os.path.islink(b_src_item_path) and os.path.isfile(b_src_item_path):
467                shutil.copyfile(b_src_item_path, b_dest_item_path)
468                shutil.copymode(b_src_item_path, b_dest_item_path)
469
470                if owner is not None:
471                    module.set_owner_if_different(b_dest_item_path, owner, False)
472                if group is not None:
473                    module.set_group_if_different(b_dest_item_path, group, False)
474
475            if not os.path.islink(b_src_item_path) and os.path.isdir(b_src_item_path):
476                shutil.copytree(b_src_item_path, b_dest_item_path, symlinks=not(local_follow))
477                chown_recursive(b_dest_item_path, module)
478
479            changed = True
480    return changed
481
482
483def copy_common_dirs(src, dest, module):
484    changed = False
485    common_dirs = filecmp.dircmp(src, dest).common_dirs
486    for item in common_dirs:
487        src_item_path = os.path.join(src, item)
488        dest_item_path = os.path.join(dest, item)
489        b_src_item_path = to_bytes(src_item_path, errors='surrogate_or_strict')
490        b_dest_item_path = to_bytes(dest_item_path, errors='surrogate_or_strict')
491        diff_files_changed = copy_diff_files(b_src_item_path, b_dest_item_path, module)
492        left_only_changed = copy_left_only(b_src_item_path, b_dest_item_path, module)
493        if diff_files_changed or left_only_changed:
494            changed = True
495
496        # recurse into subdirectory
497        changed = changed or copy_common_dirs(os.path.join(src, item), os.path.join(dest, item), module)
498    return changed
499
500
501def main():
502
503    global module
504
505    module = AnsibleModule(
506        # not checking because of daisy chain to file module
507        argument_spec=dict(
508            src=dict(type='path'),
509            _original_basename=dict(type='str'),  # used to handle 'dest is a directory' via template, a slight hack
510            content=dict(type='str', no_log=True),
511            dest=dict(type='path', required=True),
512            backup=dict(type='bool', default=False),
513            force=dict(type='bool', default=True, aliases=['thirsty']),
514            validate=dict(type='str'),
515            directory_mode=dict(type='raw'),
516            remote_src=dict(type='bool'),
517            local_follow=dict(type='bool'),
518            checksum=dict(type='str'),
519            follow=dict(type='bool', default=False),
520        ),
521        add_file_common_args=True,
522        supports_check_mode=True,
523    )
524
525    if module.params.get('thirsty'):
526        module.deprecate('The alias "thirsty" has been deprecated and will be removed, use "force" instead',
527                         version='2.13', collection_name='ansible.builtin')
528
529    src = module.params['src']
530    b_src = to_bytes(src, errors='surrogate_or_strict')
531    dest = module.params['dest']
532    # Make sure we always have a directory component for later processing
533    if os.path.sep not in dest:
534        dest = '.{0}{1}'.format(os.path.sep, dest)
535    b_dest = to_bytes(dest, errors='surrogate_or_strict')
536    backup = module.params['backup']
537    force = module.params['force']
538    _original_basename = module.params.get('_original_basename', None)
539    validate = module.params.get('validate', None)
540    follow = module.params['follow']
541    local_follow = module.params['local_follow']
542    mode = module.params['mode']
543    owner = module.params['owner']
544    group = module.params['group']
545    remote_src = module.params['remote_src']
546    checksum = module.params['checksum']
547
548    if not os.path.exists(b_src):
549        module.fail_json(msg="Source %s not found" % (src))
550    if not os.access(b_src, os.R_OK):
551        module.fail_json(msg="Source %s not readable" % (src))
552
553    # Preserve is usually handled in the action plugin but mode + remote_src has to be done on the
554    # remote host
555    if module.params['mode'] == 'preserve':
556        module.params['mode'] = '0%03o' % stat.S_IMODE(os.stat(b_src).st_mode)
557    mode = module.params['mode']
558
559    checksum_dest = None
560
561    if os.path.isfile(src):
562        checksum_src = module.sha1(src)
563    else:
564        checksum_src = None
565
566    # Backwards compat only.  This will be None in FIPS mode
567    try:
568        if os.path.isfile(src):
569            md5sum_src = module.md5(src)
570        else:
571            md5sum_src = None
572    except ValueError:
573        md5sum_src = None
574
575    changed = False
576
577    if checksum and checksum_src != checksum:
578        module.fail_json(
579            msg='Copied file does not match the expected checksum. Transfer failed.',
580            checksum=checksum_src,
581            expected_checksum=checksum
582        )
583
584    # Special handling for recursive copy - create intermediate dirs
585    if dest.endswith(os.sep):
586        if _original_basename:
587            dest = os.path.join(dest, _original_basename)
588        b_dest = to_bytes(dest, errors='surrogate_or_strict')
589        dirname = os.path.dirname(dest)
590        b_dirname = to_bytes(dirname, errors='surrogate_or_strict')
591        if not os.path.exists(b_dirname):
592            try:
593                (pre_existing_dir, new_directory_list) = split_pre_existing_dir(dirname)
594            except AnsibleModuleError as e:
595                e.result['msg'] += ' Could not copy to {0}'.format(dest)
596                module.fail_json(**e.results)
597
598            os.makedirs(b_dirname)
599            directory_args = module.load_file_common_arguments(module.params)
600            directory_mode = module.params["directory_mode"]
601            if directory_mode is not None:
602                directory_args['mode'] = directory_mode
603            else:
604                directory_args['mode'] = None
605            adjust_recursive_directory_permissions(pre_existing_dir, new_directory_list, module, directory_args, changed)
606
607    if os.path.isdir(b_dest):
608        basename = os.path.basename(src)
609        if _original_basename:
610            basename = _original_basename
611        dest = os.path.join(dest, basename)
612        b_dest = to_bytes(dest, errors='surrogate_or_strict')
613
614    if os.path.exists(b_dest):
615        if os.path.islink(b_dest) and follow:
616            b_dest = os.path.realpath(b_dest)
617            dest = to_native(b_dest, errors='surrogate_or_strict')
618        if not force:
619            module.exit_json(msg="file already exists", src=src, dest=dest, changed=False)
620        if os.access(b_dest, os.R_OK) and os.path.isfile(b_dest):
621            checksum_dest = module.sha1(dest)
622    else:
623        if not os.path.exists(os.path.dirname(b_dest)):
624            try:
625                # os.path.exists() can return false in some
626                # circumstances where the directory does not have
627                # the execute bit for the current user set, in
628                # which case the stat() call will raise an OSError
629                os.stat(os.path.dirname(b_dest))
630            except OSError as e:
631                if "permission denied" in to_native(e).lower():
632                    module.fail_json(msg="Destination directory %s is not accessible" % (os.path.dirname(dest)))
633            module.fail_json(msg="Destination directory %s does not exist" % (os.path.dirname(dest)))
634
635    if not os.access(os.path.dirname(b_dest), os.W_OK) and not module.params['unsafe_writes']:
636        module.fail_json(msg="Destination %s not writable" % (os.path.dirname(dest)))
637
638    backup_file = None
639    if checksum_src != checksum_dest or os.path.islink(b_dest):
640        if not module.check_mode:
641            try:
642                if backup:
643                    if os.path.exists(b_dest):
644                        backup_file = module.backup_local(dest)
645                # allow for conversion from symlink.
646                if os.path.islink(b_dest):
647                    os.unlink(b_dest)
648                    open(b_dest, 'w').close()
649                if validate:
650                    # if we have a mode, make sure we set it on the temporary
651                    # file source as some validations may require it
652                    if mode is not None:
653                        module.set_mode_if_different(src, mode, False)
654                    if owner is not None:
655                        module.set_owner_if_different(src, owner, False)
656                    if group is not None:
657                        module.set_group_if_different(src, group, False)
658                    if "%s" not in validate:
659                        module.fail_json(msg="validate must contain %%s: %s" % (validate))
660                    (rc, out, err) = module.run_command(validate % src)
661                    if rc != 0:
662                        module.fail_json(msg="failed to validate", exit_status=rc, stdout=out, stderr=err)
663                b_mysrc = b_src
664                if remote_src and os.path.isfile(b_src):
665                    _, b_mysrc = tempfile.mkstemp(dir=os.path.dirname(b_dest))
666
667                    shutil.copyfile(b_src, b_mysrc)
668                    try:
669                        shutil.copystat(b_src, b_mysrc)
670                    except OSError as err:
671                        if err.errno == errno.ENOSYS and mode == "preserve":
672                            module.warn("Unable to copy stats {0}".format(to_native(b_src)))
673                        else:
674                            raise
675
676                # might be needed below
677                if PY3 and hasattr(os, 'listxattr'):
678                    try:
679                        src_has_acls = 'system.posix_acl_access' in os.listxattr(src)
680                    except Exception as e:
681                        # assume unwanted ACLs by default
682                        src_has_acls = True
683
684                module.atomic_move(b_mysrc, dest, unsafe_writes=module.params['unsafe_writes'])
685
686                if PY3 and hasattr(os, 'listxattr') and platform.system() == 'Linux' and not remote_src:
687                    # atomic_move used above to copy src into dest might, in some cases,
688                    # use shutil.copy2 which in turn uses shutil.copystat.
689                    # Since Python 3.3, shutil.copystat copies file extended attributes:
690                    # https://docs.python.org/3/library/shutil.html#shutil.copystat
691                    # os.listxattr (along with others) was added to handle the operation.
692
693                    # This means that on Python 3 we are copying the extended attributes which includes
694                    # the ACLs on some systems - further limited to Linux as the documentation above claims
695                    # that the extended attributes are copied only on Linux. Also, os.listxattr is only
696                    # available on Linux.
697
698                    # If not remote_src, then the file was copied from the controller. In that
699                    # case, any filesystem ACLs are artifacts of the copy rather than preservation
700                    # of existing attributes. Get rid of them:
701
702                    if src_has_acls:
703                        # FIXME If dest has any default ACLs, there are not applied to src now because
704                        # they were overridden by copystat. Should/can we do anything about this?
705                        # 'system.posix_acl_default' in os.listxattr(os.path.dirname(b_dest))
706
707                        try:
708                            clear_facls(dest)
709                        except ValueError as e:
710                            if 'setfacl' in to_native(e):
711                                # No setfacl so we're okay.  The controller couldn't have set a facl
712                                # without the setfacl command
713                                pass
714                            else:
715                                raise
716                        except RuntimeError as e:
717                            # setfacl failed.
718                            if 'Operation not supported' in to_native(e):
719                                # The file system does not support ACLs.
720                                pass
721                            else:
722                                raise
723
724            except (IOError, OSError):
725                module.fail_json(msg="failed to copy: %s to %s" % (src, dest), traceback=traceback.format_exc())
726        changed = True
727    else:
728        changed = False
729
730    # If neither have checksums, both src and dest are directories.
731    if checksum_src is None and checksum_dest is None:
732        if remote_src and os.path.isdir(module.params['src']):
733            b_src = to_bytes(module.params['src'], errors='surrogate_or_strict')
734            b_dest = to_bytes(module.params['dest'], errors='surrogate_or_strict')
735
736            if src.endswith(os.path.sep) and os.path.isdir(module.params['dest']):
737                diff_files_changed = copy_diff_files(b_src, b_dest, module)
738                left_only_changed = copy_left_only(b_src, b_dest, module)
739                common_dirs_changed = copy_common_dirs(b_src, b_dest, module)
740                owner_group_changed = chown_recursive(b_dest, module)
741                if diff_files_changed or left_only_changed or common_dirs_changed or owner_group_changed:
742                    changed = True
743
744            if src.endswith(os.path.sep) and not os.path.exists(module.params['dest']):
745                b_basename = to_bytes(os.path.basename(src), errors='surrogate_or_strict')
746                b_dest = to_bytes(os.path.join(b_dest, b_basename), errors='surrogate_or_strict')
747                b_src = to_bytes(os.path.join(module.params['src'], ""), errors='surrogate_or_strict')
748                if not module.check_mode:
749                    shutil.copytree(b_src, b_dest, symlinks=not(local_follow))
750                chown_recursive(dest, module)
751                changed = True
752
753            if not src.endswith(os.path.sep) and os.path.isdir(module.params['dest']):
754                b_basename = to_bytes(os.path.basename(src), errors='surrogate_or_strict')
755                b_dest = to_bytes(os.path.join(b_dest, b_basename), errors='surrogate_or_strict')
756                b_src = to_bytes(os.path.join(module.params['src'], ""), errors='surrogate_or_strict')
757                if not module.check_mode and not os.path.exists(b_dest):
758                    shutil.copytree(b_src, b_dest, symlinks=not(local_follow))
759                    changed = True
760                    chown_recursive(dest, module)
761                if module.check_mode and not os.path.exists(b_dest):
762                    changed = True
763                if os.path.exists(b_dest):
764                    diff_files_changed = copy_diff_files(b_src, b_dest, module)
765                    left_only_changed = copy_left_only(b_src, b_dest, module)
766                    common_dirs_changed = copy_common_dirs(b_src, b_dest, module)
767                    owner_group_changed = chown_recursive(b_dest, module)
768                    if diff_files_changed or left_only_changed or common_dirs_changed or owner_group_changed:
769                        changed = True
770
771            if not src.endswith(os.path.sep) and not os.path.exists(module.params['dest']):
772                b_basename = to_bytes(os.path.basename(module.params['src']), errors='surrogate_or_strict')
773                b_dest = to_bytes(os.path.join(b_dest, b_basename), errors='surrogate_or_strict')
774                if not module.check_mode and not os.path.exists(b_dest):
775                    os.makedirs(b_dest)
776                    b_src = to_bytes(os.path.join(module.params['src'], ""), errors='surrogate_or_strict')
777                    diff_files_changed = copy_diff_files(b_src, b_dest, module)
778                    left_only_changed = copy_left_only(b_src, b_dest, module)
779                    common_dirs_changed = copy_common_dirs(b_src, b_dest, module)
780                    owner_group_changed = chown_recursive(b_dest, module)
781                    if diff_files_changed or left_only_changed or common_dirs_changed or owner_group_changed:
782                        changed = True
783                if module.check_mode and not os.path.exists(b_dest):
784                    changed = True
785
786    res_args = dict(
787        dest=dest, src=src, md5sum=md5sum_src, checksum=checksum_src, changed=changed
788    )
789    if backup_file:
790        res_args['backup_file'] = backup_file
791
792    if not module.check_mode:
793        file_args = module.load_file_common_arguments(module.params, path=dest)
794        res_args['changed'] = module.set_fs_attributes_if_different(file_args, res_args['changed'])
795
796    module.exit_json(**res_args)
797
798
799if __name__ == '__main__':
800    main()
801