diff --git a/lib/ansible/modules/windows/win_copy.ps1 b/lib/ansible/modules/windows/win_copy.ps1 index 7a8441205b..2f521f7da5 100644 --- a/lib/ansible/modules/windows/win_copy.ps1 +++ b/lib/ansible/modules/windows/win_copy.ps1 @@ -1,94 +1,144 @@ #!powershell # This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . -# WANT_JSON -# POWERSHELL_COMMON +# (c) 2015, Jon Hawkesworth (@jhawkesworth) +# Copyright (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -$params = Parse-Args $args -supports_check_mode $true +#Requires -Module Ansible.ModuleUtils.Legacy.psm1 +$ErrorActionPreference = 'Stop' + +$params = Parse-Args -arguments $args -supports_check_mode $true $check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false +$diff_mode = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false -$src = Get-AnsibleParam -obj $params -name "src" -type "path" -failifempty $true +# there are 4 modes to win_copy which are driven by the action plugins: +# explode: src is a zip file which needs to be extracted to dest, for use with multiple files +# query: win_copy action plugin wants to get the state of remote files to check whether it needs to send them +# remote: all copy action is happening remotely (remote_src=True) +# single: a single file has been copied, also used with template +$mode = Get-AnsibleParam -obj $params -name "mode" -type "str" -default "single" -validateset "explode","query","remote","single" + +# used in explode, remote and single mode +$src = Get-AnsibleParam -obj $params -name "src" -type "path" -failifempty ($mode -in @("explode","process","single")) $dest = Get-AnsibleParam -obj $params -name "dest" -type "path" -failifempty $true -$force = Get-AnsibleParam -obj $params -name "force" -type "bool" -default $true + +# used in single mode $original_basename = Get-AnsibleParam -obj $params -name "original_basename" -type "str" -# original_basename gets set if src and dest are dirs -# but includes subdir if the source folder contains sub folders -# e.g. you could get subdir/foo.txt +# used in query and remote mode +$force = Get-AnsibleParam -obj $params -name "force" -type "bool" -default $true + +# used in query mode, contains the local files/directories/symlinks that are to be copied +$files = Get-AnsibleParam -obj $params -name "files" -type "list" +$directories = Get-AnsibleParam -obj $params -name "directories" -type "list" +$symlinks = Get-AnsibleParam -obj $params -name "symlinks" -type "list" $result = @{ changed = $false - dest = $dest - original_basename = $original_basename - src = $src } -if (($force -eq $false) -and (Test-Path -Path $dest)) { - $result.msg = "file already exists" - Exit-Json $result +if ($diff_mode) { + $result.diff = @{} } -Function Copy-Folder($src, $dest) { - if (Test-Path -Path $dest) { - if (-not (Get-Item -Path $dest -Force).PSIsContainer) { - Fail-Json $result "If src is a folder, dest must also be a folder. src: $src, dest: $dest" +Function Copy-File($source, $dest) { + $diff = "" + $copy_file = $false + $source_checksum = $null + if ($force) { + $source_checksum = Get-FileChecksum -path $source + } + + if (Test-Path -Path $dest -PathType Container) { + Fail-Json -obj $result -message "cannot copy file from $source to $($dest): dest is already a folder" + } elseif (Test-Path -Path $dest -PathType Leaf) { + if ($force) { + $target_checksum = Get-FileChecksum -path $dest + if ($source_checksum -ne $target_checksum) { + $copy_file = $true + } } } else { - try { - New-Item -Path $dest -ItemType Directory -Force -WhatIf:$check_mode - $result.changed = $true - } catch { - Fail-Json $result "Failed to create new folder $dest $($_.Exception.Message)" - } + $copy_file = $true } - foreach ($item in Get-ChildItem -Path $src) { - $dest_path = Join-Path -Path $dest -ChildPath $item.PSChildName - if ($item.PSIsContainer) { - Copy-Folder -src $item.FullName -dest $dest_path - } else { - Copy-File -src $item.FullName -dest $dest_path + if ($copy_file) { + $file_dir = [System.IO.Path]::GetDirectoryName($dest) + # validate the parent dir is not a file and that it exists + if (Test-Path -Path $file_dir -PathType Leaf) { + Fail-Json -obj $result -message "cannot copy file from $source to $($dest): object at dest parent dir is not a folder" + } elseif (-not (Test-Path -Path $file_dir)) { + # directory doesn't exist, need to create + New-Item -Path $file_dir -ItemType Directory -WhatIf:$check_mode | Out-Null + $diff += "+$file_dir\`n" } - } -} -Function Copy-File($src, $dest) { - if (Test-Path -Path $dest) { - if ((Get-Item -Path $dest -Force).PSIsContainer) { - Fail-Json $result "If src is a file, dest must also be a file. src: $src, dest: $dest" + if (Test-Path -Path $dest -PathType Leaf) { + Remove-Item -Path $dest -Force -Recurse | Out-Null + $diff += "-$dest`n" } - } - $src_checksum = Get-FileChecksum -Path $src - $dest_checksum = Get-FileChecksum -Path $dest - if ($src_checksum -ne $dest_checksum) { - try { - Copy-Item -Path $src -Destination $dest -Force -WhatIf:$check_mode - } catch { - Fail-Json $result "Failed to copy file: $($_.Exception.Message)" + if (-not $check_mode) { + # cannot run with -WhatIf:$check_mode as if the parent dir didn't + # exist and was created above would still not exist in check mode + Copy-Item -Path $source -Destination $dest -Force | Out-Null } + $diff += "+$dest`n" + + # make sure we set the attributes accordingly + if (-not $check_mode) { + $source_file = Get-Item -Path $source -Force + $dest_file = Get-Item -Path $dest -Force + $dest_file.Attributes = $source_file.Attributes + $dest_file.SetAccessControl($source_file.GetAccessControl()) + } + $result.changed = $true } - # Verify the file we copied is the same - $dest_checksum_verify = Get-FileChecksum -Path $dest - if (-not ($check_mode) -and ($src_checksum -ne $dest_checksum_verify)) { - Fail-Json $result "Copied file does not match checksum. src: $src_checksum, dest: $dest_checksum_verify. Failed to copy file from $src to $dest" + # ugly but to save us from running the checksum twice, let's return it for + # the main code to add it to $result + return ,@{ diff = $diff; checksum = $source_checksum } +} + +Function Copy-Folder($source, $dest) { + $diff = "" + $copy_folder = $false + + if (-not (Test-Path -Path $dest -PathType Container)) { + $parent_dir = [System.IO.Path]::GetDirectoryName($dest) + if (Test-Path -Path $parent_dir -PathType Leaf) { + Fail-Json -obj $result -message "cannot copy file from $source to $($dest): object at dest parent dir is not a folder" + } + if (Test-Path -Path $dest -PathType Leaf) { + Fail-Json -obj $result -message "cannot copy folder from $source to $($dest): dest is already a file" + } + + New-Item -Path $dest -ItemType Container -WhatIf:$check_mode | Out-Null + $diff += "+$dest\`n" + $result.changed = $true + + if (-not $check_mode) { + $source_folder = Get-Item -Path $source -Force + $dest_folder = Get-Item -Path $source -Force + $dest_folder.Attributes = $source_folder.Attributes + $dest_folder.SetAccessControl($source_folder.GetAccessControl()) + } } + + $child_items = Get-ChildItem -Path $source -Force + foreach ($child_item in $child_items) { + $dest_child_path = Join-Path -Path $dest -ChildPath $child_item.Name + if ($child_item.PSIsContainer) { + $diff += (Copy-Folder -source $child_item.Fullname -dest $dest_child_path) + } else { + $diff += (Copy-File -source $child_item.Fullname -dest $dest_child_path).diff + } + } + + return $diff } Function Get-FileSize($path) { @@ -108,56 +158,193 @@ Function Get-FileSize($path) { $size } -if (-not (Test-Path -Path $src)) { - Fail-Json $result "Cannot copy src file: $src as it does not exist" -} +if ($mode -eq "query") { + # we only return a list of files/directories that need to be copied over + # the source of the local file will be the key used + $will_change = $false + $changed_files = @() + $changed_directories = @() + $changed_symlinks = @() -# If copying from remote we need to get the original folder path and name and change dest to this path -if ($original_basename) { - $parent_path = Split-Path -Path $original_basename -Parent - if ($parent_path.length -gt 0) { - $dest_folder = Join-Path -Path $dest -ChildPath $parent_path - try { - New-Item -Path $dest_folder -Type directory -Force -WhatIf:$check_mode - $result.changed = $true - } catch { - Fail-Json $result "Failed to create directory $($dest_folder): $($_.Exception.Message)" + foreach ($file in $files) { + $filename = $file.dest + $local_checksum = $file.checksum + + $filepath = Join-Path -Path $dest -ChildPath $filename + if (Test-Path -Path $filepath -PathType Leaf) { + if ($force) { + $checksum = Get-FileChecksum -path $filepath + if ($checksum -ne $local_checksum) { + $will_change = $true + $changed_files += $file + } + } + } elseif (Test-Path -Path $filepath -PathType Container) { + Fail-Json -obj $result -message "cannot copy file to dest $($filepath): object at path is already a directory" + } else { + $will_change = $true + $changed_files += $file } } - if ((Get-Item -Path $dest -Force).PSIsContainer) { - $dest = Join-Path $dest -ChildPath $original_basename - } -} + foreach ($directory in $directories) { + $dirname = $directory.dest -# If the source is a container prepare for some recursive magic -if ((Get-Item -Path $src -Force).PSIsContainer) { - if (Test-Path -Path $dest) { - if (-not (Get-Item -Path $dest -Force).PSIsContainer) { - Fail-Json $result "If src is a folder, dest must also be a folder. src: $src, dest: $dest" + $dirpath = Join-Path -Path $dest -ChildPath $dirname + $parent_dir = [System.IO.Path]::GetDirectoryName($dirpath) + if (Test-Path -Path $parent_dir -PathType Leaf) { + Fail-Json -obj $result -message "cannot copy folder to dest $($dirpath): object at parent directory path is already a file" + } + if (Test-Path -Path $dirpath -PathType Leaf) { + Fail-Json -obj $result -message "cannot copy folder to dest $($dirpath): object at path is already a file" + } elseif (-not (Test-Path -Path $dirpath -PathType Container)) { + $will_change = $true + $changed_directories += $directory } } - $folder_name = (Get-Item -Path $src -Force).Name - $dest_path = Join-Path -Path $dest -ChildPath $folder_name - Copy-Folder -src $src -dest $dest_path - if ($result.changed -eq $true) { - $result.operation = "folder_copy" + # TODO: Handle symlinks + + # Detect if the PS zip assemblies are available, this will control whether + # the win_copy plugin will use explode as the mode or single + try { + Add-Type -Assembly System.IO.Compression.FileSystem | Out-Null + Add-Type -Assembly System.IO.Compression | Out-Null + $result.zip_available = $true + } catch { + $result.zip_available = $false } -} else { - Copy-File -src $src -dest $dest - if ($result.changed -eq $true) { - $result.operation = "file_copy" + + $result.will_change = $will_change + $result.files = $changed_files + $result.directories = $changed_directories + $result.symlinks = $changed_symlinks +} elseif ($mode -eq "explode") { + # a single zip file containing the files and directories needs to be + # expanded this will always result in a change as the calculation is done + # on the win_copy action plugin and is only run if a change needs to occur + if (-not (Test-Path -Path $src -PathType Leaf)) { + Fail-Json -obj $result -message "Cannot expand src zip file file: $src as it does not exist" } - $result.original_basename = (Get-Item -Path $src -Force).Name - $result.checksum = Get-FileChecksum -Path $src + + Add-Type -Assembly System.IO.Compression.FileSystem | Out-Null + Add-Type -Assembly System.IO.Compression | Out-Null + + $archive = [System.IO.Compression.ZipFile]::Open($src, [System.IO.Compression.ZipArchiveMode]::Read, [System.Text.Encoding]::UTF8) + foreach ($entry in $archive.Entries) { + $entry_target_path = [System.IO.Path]::Combine($dest, $entry.FullName) + $entry_dir = [System.IO.Path]::GetDirectoryName($entry_target_path) + + if (-not (Test-Path -Path $entry_dir)) { + New-Item -Path $entry_dir -ItemType Directory -WhatIf:$check_mode | Out-Null + } + + if (-not ($entry_target_path.EndsWith("`\") -or $entry_target_path.EndsWith("/"))) { + if (-not $check_mode) { + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $entry_target_path, $true) + } + } + } + + $result.changed = $true +} elseif ($mode -eq "remote") { + # all copy actions are happening on the remote side (windows host), need + # too copy source and dest using PS code + $result.src = $src + $result.dest = $dest + + if (-not (Test-Path -Path $src)) { + Fail-Json -obj $result -message "Cannot copy src file: $src as it does not exist" + } + + if (Test-Path -Path $src -PathType Container) { + # we are copying a directory or the contents of a directory + $result.operation = 'folder_copy' + if ($src.EndsWith("/") -or $src.EndsWith("`\")) { + # copying the folder's contents to dest + $diff = "" + $child_files = Get-ChildItem -Path $src -Force + foreach ($child_file in $child_files) { + $dest_child_path = Join-Path -Path $dest -ChildPath $child_file.Name + if ($child_file.PSIsContainer) { + $diff += Copy-Folder -source $child_file.FullName -dest $dest_child_path + } else { + $diff += (Copy-File -source $child_file.FullName -dest $dest_child_path).diff + } + } + } else { + # copying the folder and it's contents to dest + $dest = Join-Path -Path $dest -ChildPath (Get-Item -Path $src -Force).Name + $result.dest = $dest + $diff = Copy-Folder -source $src -dest $dest + } + } else { + # we are just copying a single file to dest + $result.operation = 'file_copy' + + $source_basename = (Get-Item -Path $src -Force).Name + $result.original_basename = $source_basename + + if ($dest.EndsWith("/") -or $dest.EndsWith("`\")) { + $dest = Join-Path -Path $dest -ChildPath (Get-Item -Path $src -Force).Name + $result.dest = $dest + } else { + # check if the parent dir exists, this is only done if src is a + # file and dest if the path to a file (doesn't end with \ or /) + $parent_dir = Split-Path -Path $dest + if (Test-Path -Path $parent_dir -PathType Leaf) { + Fail-Json -obj $result -message "object at destination parent dir $parent_dir is currently a file" + } elseif (-not (Test-Path -Path $parent_dir -PathType Container)) { + Fail-Json -obj $result -message "Destination directory $parent_dir does not exist" + } + } + $copy_result = Copy-File -source $src -dest $dest + $diff = $copy_result.diff + $result.checksum = $copy_result.checksum + } + + # the file might not exist if running in check mode + if (-not $check_mode -or (Test-Path -Path $dest -PathType Leaf)) { + $result.size = Get-FileSize -path $dest + } else { + $result.size = $null + } + if ($diff_mode) { + $result.diff.prepared = $diff + } +} elseif ($mode -eq "single") { + # a single file is located in src and we need to copy to dest, this will + # always result in a change as the calculation is done on the Ansible side + # before this is run. This should also never run in check mode + if (-not (Test-Path -Path $src -PathType Leaf)) { + Fail-Json -obj $result -message "Cannot copy src file: $src as it does not exist" + } + + # the dest parameter is a directory, we need to append original_basename + if ($dest.EndsWith("/") -or $dest.EndsWith("`\")) { + $remote_dest = Join-Path -Path $dest -ChildPath $original_basename + $parent_dir = Split-Path -Path $remote_dest + + # when dest ends with /, we need to create the destination directories + if (Test-Path -Path $parent_dir -PathType Leaf) { + Fail-Json -obj $result -message "object at destination parent dir $parent_dir is currently a file" + } elseif (-not (Test-Path -Path $parent_dir -PathType Container)) { + New-Item -Path $parent_dir -ItemType Directory | Out-Null + } + } else { + $remote_dest = $dest + $parent_dir = Split-Path -Path $remote_dest + + # check if the dest parent dirs exist, need to fail if they don't + if (Test-Path -Path $parent_dir -PathType Leaf) { + Fail-Json -obj $result -message "object at destination parent dir $parent_dir is currently a file" + } elseif (-not (Test-Path -Path $parent_dir -PathType Container)) { + Fail-Json -obj $result -message "Destination directory $parent_dir does not exist" + } + } + + Copy-Item -Path $src -Destination $remote_dest -Force | Out-Null + $result.changed = $true } -if ($check_mode) { - # When in check mode the dest won't exit, just get the source size - $result.size = Get-FileSize -path $src -} else { - $result.size = Get-FileSize -path $dest -} - -Exit-Json $result +Exit-Json -obj $result diff --git a/lib/ansible/modules/windows/win_copy.py b/lib/ansible/modules/windows/win_copy.py index 555b8fce96..f7ccd25eff 100644 --- a/lib/ansible/modules/windows/win_copy.py +++ b/lib/ansible/modules/windows/win_copy.py @@ -1,22 +1,11 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# (c) 2015, Jon Hawkesworth (@jhawkesworth) -# # This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . + +# (c) 2015, Jon Hawkesworth (@jhawkesworth) +# Copyright (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) ANSIBLE_METADATA = {'metadata_version': '1.1', @@ -27,8 +16,8 @@ ANSIBLE_METADATA = {'metadata_version': '1.1', DOCUMENTATION = r''' --- module: win_copy -version_added: "1.9.2" -short_description: Copies files to remote locations on windows hosts. +version_added: '1.9.2' +short_description: Copies files to remote locations on windows hosts description: - The C(win_copy) module copies a file on the local box to remote windows locations. - For non-Windows targets, use the M(copy) module instead. @@ -44,59 +33,97 @@ options: - Remote absolute path where the file should be copied to. If src is a directory, this must be a directory too. - Use \ for path separators or \\ when in "double quotes". + - If C(dest) ends with \ then source or the contents of source will be + copied to the directory without renaming. + - If C(dest) is a nonexistent path, it will only be created if C(dest) ends + with "/" or "\", or C(src) is a directory. + - If C(src) and C(dest) are files and if the parent directory of C(dest) + doesn't exist, then the task will fail. required: true force: version_added: "2.3" description: - - If set to C(yes), the remote file will be replaced when content is - different than the source. - - If set to C(no), the remote file will only be transferred if the + - If set to C(yes), the file will only be transferred if the content + is different than destination. + - If set to C(no), the file will only be transferred if the destination does not exist. - default: True - choices: - - yes - - no + - If set to C(no), no checksuming of the content is performed which can + help improve performance on larger files. + default: 'yes' + type: bool + local_follow: + version_added: '2.4' + description: + - This flag indicates that filesystem links in the source tree, if they + exist, should be followed. + default: 'yes' + type: bool remote_src: description: - If False, it will search for src at originating/master machine, if True it will go to the remote/target machine for the src. - default: False - choices: - - True - - False + default: 'no' + type: bool version_added: "2.3" src: description: - Local path to a file to copy to the remote server; 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 Rsync. + relative. + - If path is a directory, it is copied (including the source folder name) + recursively to C(dest). + - If path is a directory and ends with "/", only the inside contents of + that directory are copied to the destination. Otherwise, if it does not + end with "/", the directory itself with all contents is copied. + - If path is a file and dest ends with "\", the file is copied to the + folder with the same filename. required: true notes: - For non-Windows targets, use the M(copy) module instead. -author: "Jon Hawkesworth (@jhawkesworth)" +- Currently win_copy does not support copying symbolic links from both local to + remote and remote to remote. +- It is recommended that backslashes C(\) are used instead of C(/) when dealing + with remote paths. +- Because win_copy runs over WinRM, it is not a very efficient transfer + mechanism. If sending large files consider hosting them on a web service and + using M(win_get_url) instead. +author: +- Jon Hawkesworth (@jhawkesworth) +- Jordan Borean (@jborean93) ''' EXAMPLES = r''' - name: Copy a single file win_copy: src: /srv/myfiles/foo.conf - dest: c:\Temp\foo.conf -- name: Copy files/temp_files to c:\temp + dest: c:\Temp\renamed-foo.conf + +- name: Copy a single file keeping the filename + win_copy: + src: /src/myfiles/foo.conf + dest: c:\temp\ + +- name: Copy folder to c:\temp (results in C:\Temp\temp_files) + win_copy: + src: files/temp_files + dest: c:\Temp + +- name: Copy folder contents recursively win_copy: src: files/temp_files/ dest: c:\Temp + - name: Copy a single file where the source is on the remote host win_copy: src: C:\temp\foo.txt dest: C:\ansible\foo.txt remote_src: True + - name: Copy a folder recursively where the source is on the remote host win_copy: src: C:\temp dest: C:\ansible remote_src: True + - name: Set the contents of a file win_copy: dest: C:\temp\foo.txt @@ -121,12 +148,12 @@ checksum: sample: 6e642bb8dd5c2e027bf21dd923337cbb4214f827 size: description: size of the target, after execution - returned: changed (src is a file or remote_src == True) + returned: changed, src is a file type: int sample: 1220 operation: description: whether a single file copy took place or a folder copy - returned: changed + returned: success type: string sample: file_copy original_basename: diff --git a/lib/ansible/plugins/action/win_copy.py b/lib/ansible/plugins/action/win_copy.py index caaa992775..c97049b523 100644 --- a/lib/ansible/plugins/action/win_copy.py +++ b/lib/ansible/plugins/action/win_copy.py @@ -1,29 +1,522 @@ -# (c) 2012-2014, Michael DeHaan -# # This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . + +# Copyright (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # Make coding more python3-ish from __future__ import (absolute_import, division, print_function) __metaclass__ = type +import json +import os +import os.path +import tempfile +import traceback +import zipfile + +from ansible.errors import AnsibleError +from ansible.module_utils._text import to_bytes, to_native, to_text +from ansible.module_utils.parsing.convert_bool import boolean from ansible.plugins.action import ActionBase -from ansible.plugins.action.copy import ActionModule as CopyActionModule +from ansible.utils.hashing import checksum -# Even though CopyActionModule inherits from ActionBase, we still need to -# directly inherit from ActionBase to appease the plugin loader. -class ActionModule(CopyActionModule, ActionBase): - pass +def _walk_dirs(topdir, base_path=None, local_follow=False, trailing_slash_detector=None, checksum_check=False): + """ + Walk a filesystem tree returning enough information to copy the files. + This is similar to the _walk_dirs function in ``copy.py`` but returns + a dict instead of a tuple for each entry and includes the checksum of + a local file if wanted. + + :arg topdir: The directory that the filesystem tree is rooted at + :kwarg base_path: The initial directory structure to strip off of the + files for the destination directory. If this is None (the default), + the base_path is set to ``top_dir``. + :kwarg local_follow: Whether to follow symlinks on the source. When set + to False, no symlinks are dereferenced. When set to True (the + default), the code will dereference most symlinks. However, symlinks + can still be present if needed to break a circular link. + :kwarg trailing_slash_detector: Function to determine if a path has + a trailing directory separator. Only needed when dealing with paths on + a remote machine (in which case, pass in a function that is aware of the + directory separator conventions on the remote machine). + :kawrg whether to get the checksum of the local file and add to the dict + :returns: dictionary of dictionaries. All of the path elements in the structure are text string. + This separates all the files, directories, and symlinks along with + import information about each:: + + { + 'files'; [{ + src: '/absolute/path/to/copy/from', + dest: 'relative/path/to/copy/to', + checksum: 'b54ba7f5621240d403f06815f7246006ef8c7d43' + }, ...], + 'directories'; [{ + src: '/absolute/path/to/copy/from', + dest: 'relative/path/to/copy/to' + }, ...], + 'symlinks'; [{ + src: '/symlink/target/path', + dest: 'relative/path/to/copy/to' + }, ...], + + } + + The ``symlinks`` field is only populated if ``local_follow`` is set to False + *or* a circular symlink cannot be dereferenced. The ``checksum`` entry is set + to None if checksum_check=False. + + """ + # Convert the path segments into byte strings + + r_files = {'files': [], 'directories': [], 'symlinks': []} + + def _recurse(topdir, rel_offset, parent_dirs, rel_base=u'', checksum_check=False): + """ + This is a closure (function utilizing variables from it's parent + function's scope) so that we only need one copy of all the containers. + Note that this function uses side effects (See the Variables used from + outer scope). + + :arg topdir: The directory we are walking for files + :arg rel_offset: Integer defining how many characters to strip off of + the beginning of a path + :arg parent_dirs: Directories that we're copying that this directory is in. + :kwarg rel_base: String to prepend to the path after ``rel_offset`` is + applied to form the relative path. + + Variables used from the outer scope + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :r_files: Dictionary of files in the hierarchy. See the return value + for :func:`walk` for the structure of this dictionary. + :local_follow: Read-only inside of :func:`_recurse`. Whether to follow symlinks + """ + for base_path, sub_folders, files in os.walk(topdir): + for filename in files: + filepath = os.path.join(base_path, filename) + dest_filepath = os.path.join(rel_base, filepath[rel_offset:]) + + if os.path.islink(filepath): + # Dereference the symlnk + real_file = os.path.realpath(filepath) + if local_follow and os.path.isfile(real_file): + # Add the file pointed to by the symlink + r_files['files'].append( + { + "src": real_file, + "dest": dest_filepath, + "checksum": _get_local_checksum(checksum_check, real_file) + } + ) + else: + # Mark this file as a symlink to copy + r_files['symlinks'].append({"src": os.readlink(filepath), "dest": dest_filepath}) + else: + # Just a normal file + r_files['files'].append( + { + "src": filepath, + "dest": dest_filepath, + "checksum": _get_local_checksum(checksum_check, filepath) + } + ) + + for dirname in sub_folders: + dirpath = os.path.join(base_path, dirname) + dest_dirpath = os.path.join(rel_base, dirpath[rel_offset:]) + real_dir = os.path.realpath(dirpath) + dir_stats = os.stat(real_dir) + + if os.path.islink(dirpath): + if local_follow: + if (dir_stats.st_dev, dir_stats.st_ino) in parent_dirs: + # Just insert the symlink if the target directory + # exists inside of the copy already + r_files['symlinks'].append({"src": os.readlink(dirpath), "dest": dest_dirpath}) + else: + # Walk the dirpath to find all parent directories. + new_parents = set() + parent_dir_list = os.path.dirname(dirpath).split(os.path.sep) + for parent in range(len(parent_dir_list), 0, -1): + parent_stat = os.stat(u'/'.join(parent_dir_list[:parent])) + if (parent_stat.st_dev, parent_stat.st_ino) in parent_dirs: + # Reached the point at which the directory + # tree is already known. Don't add any + # more or we might go to an ancestor that + # isn't being copied. + break + new_parents.add((parent_stat.st_dev, parent_stat.st_ino)) + + if (dir_stats.st_dev, dir_stats.st_ino) in new_parents: + # This was a a circular symlink. So add it as + # a symlink + r_files['symlinks'].append({"src": os.readlink(dirpath), "dest": dest_dirpath}) + else: + # Walk the directory pointed to by the symlink + r_files['directories'].append({"src": real_dir, "dest": dest_dirpath}) + offset = len(real_dir) + 1 + _recurse(real_dir, offset, parent_dirs.union(new_parents), + rel_base=dest_dirpath, + checksum_check=checksum_check) + else: + # Add the symlink to the destination + r_files['symlinks'].append({"src": os.readlink(dirpath), "dest": dest_dirpath}) + else: + # Just a normal directory + r_files['directories'].append({"src": dirpath, "dest": dest_dirpath}) + + # Check if the source ends with a "/" so that we know which directory + # level to work at (similar to rsync) + source_trailing_slash = False + if trailing_slash_detector: + source_trailing_slash = trailing_slash_detector(topdir) + else: + source_trailing_slash = topdir.endswith(os.path.sep) + + # Calculate the offset needed to strip the base_path to make relative + # paths + if base_path is None: + base_path = topdir + if not source_trailing_slash: + base_path = os.path.dirname(base_path) + if topdir.startswith(base_path): + offset = len(base_path) + + # Make sure we're making the new paths relative + if trailing_slash_detector and not trailing_slash_detector(base_path): + offset += 1 + elif not base_path.endswith(os.path.sep): + offset += 1 + + if os.path.islink(topdir) and not local_follow: + r_files['symlinks'] = {"src": os.readlink(topdir), "dest": os.path.basename(topdir)} + return r_files + + dir_stats = os.stat(topdir) + parents = frozenset(((dir_stats.st_dev, dir_stats.st_ino),)) + # Actually walk the directory hierarchy + _recurse(topdir, offset, parents, checksum_check=checksum_check) + + return r_files + + +def _get_local_checksum(get_checksum, local_path): + if get_checksum: + return checksum(local_path) + else: + return None + + +class ActionModule(ActionBase): + + WIN_PATH_SEPARATOR = "\\" + + def _create_content_tempfile(self, content): + ''' Create a tempfile containing defined content ''' + fd, content_tempfile = tempfile.mkstemp() + f = os.fdopen(fd, 'wb') + content = to_bytes(content) + try: + f.write(content) + except Exception as err: + os.remove(content_tempfile) + raise Exception(err) + finally: + f.close() + return content_tempfile + + def _create_zip_tempfile(self, files, directories): + tmpdir = tempfile.mkdtemp() + zip_file_path = os.path.join(tmpdir, "win_copy.zip") + zip_file = zipfile.ZipFile(zip_file_path, "w") + + # need to write in byte string with utf-8 encoding to support unicode + # characters in the filename. + for directory in directories: + directory_path = to_bytes(directory['src'], errors='surrogate_or_strict') + archive_path = to_bytes(directory['dest'], errors='surrogate_or_strict') + zip_file.write(directory_path, archive_path, zipfile.ZIP_DEFLATED) + + for file in files: + file_path = to_bytes(file['src'], errors='surrogate_or_strict') + archive_path = to_bytes(file['dest'], errors='surrogate_or_strict') + zip_file.write(file_path, archive_path, zipfile.ZIP_DEFLATED) + + return zip_file_path + + def _remove_tempfile_if_content_defined(self, content, content_tempfile): + if content is not None: + os.remove(content_tempfile) + + def _create_directory(self, dest, source_rel, task_vars): + dest_path = self._connection._shell.join_path(dest, source_rel) + file_args = self._task.args.copy() + file_args.update( + dict( + path=dest_path, + state="directory" + ) + ) + file_args.pop('content', None) + + file_result = self._execute_module(module_name='file', module_args=file_args, task_vars=task_vars) + return file_result + + def _copy_single_file(self, local_file, dest, source_rel, task_vars): + if self._play_context.check_mode: + module_return = dict(changed=True) + return module_return + + # copy the file across to the server + tmp_path = self._make_tmp_path() + tmp_src = self._connection._shell.join_path(tmp_path, 'source') + self._transfer_file(local_file, tmp_src) + + copy_args = self._task.args.copy() + copy_args.update( + dict( + dest=dest, + src=tmp_src, + original_basename=source_rel, + mode="single" + ) + ) + copy_args.pop('content', None) + + copy_result = self._execute_module(module_name="copy", module_args=copy_args, task_vars=task_vars) + self._remove_tmp_path(tmp_path) + + return copy_result + + def _copy_zip_file(self, dest, files, directories, task_vars): + # create local zip file containing all the files and directories that + # need to be copied to the server + try: + zip_file = self._create_zip_tempfile(files, directories) + except Exception as e: + module_return = dict( + changed=False, + failed=True, + msg="failed to create tmp zip file: %s" % to_text(e), + exception=traceback.format_exc() + ) + return module_return + + zip_path = self._loader.get_real_file(zip_file) + + if self._play_context.check_mode: + module_return = dict(changed=True) + os.remove(zip_path) + os.removedirs(os.path.dirname(zip_path)) + return module_return + + # send zip file to remote + tmp_path = self._make_tmp_path() + tmp_src = self._connection._shell.join_path(tmp_path, 'source') + self._transfer_file(zip_path, tmp_src) + + # run the explode operation of win_copy on remote + copy_args = self._task.args.copy() + copy_args.update( + dict( + src=tmp_src, + dest=dest, + mode="explode" + ) + ) + copy_args.pop('content', None) + os.remove(zip_path) + os.removedirs(os.path.dirname(zip_path)) + + module_return = self._execute_module(module_args=copy_args, task_vars=task_vars) + self._remove_tmp_path(tmp_path) + return module_return + + def run(self, tmp=None, task_vars=None): + ''' handler for file transfer operations ''' + if task_vars is None: + task_vars = dict() + + result = super(ActionModule, self).run(tmp, task_vars) + + source = self._task.args.get('src', None) + content = self._task.args.get('content', None) + dest = self._task.args.get('dest', None) + remote_src = boolean(self._task.args.get('remote_src', False), strict=False) + follow = boolean(self._task.args.get('follow', False), strict=False) + force = boolean(self._task.args.get('force', True), strict=False) + + result['src'] = source + result['dest'] = dest + + result['failed'] = True + if (source is None and content is None) or dest is None: + result['msg'] = "src (or content) and dest are required" + elif source is not None and content is not None: + result['msg'] = "src and content are mutually exclusive" + elif content is not None and dest is not None and ( + dest.endswith(os.path.sep) or dest.endswith(self.WIN_PATH_SEPARATOR)): + result['msg'] = "dest must be a file if content is defined" + else: + del result['failed'] + + if result.get('failed'): + return result + + # If content is defined make a temp file and write the content into it + content_tempfile = None + if content is not None: + try: + # if content comes to us as a dict it should be decoded json. + # We need to encode it back into a string and write it out + if isinstance(content, dict) or isinstance(content, list): + content_tempfile = self._create_content_tempfile(json.dumps(content)) + else: + content_tempfile = self._create_content_tempfile(content) + source = content_tempfile + except Exception as err: + result['failed'] = True + result['msg'] = "could not write content temp file: %s" % to_native(err) + return result + # all actions should occur on the remote server, run win_copy module + elif remote_src: + new_module_args = self._task.args.copy() + new_module_args.update( + dict( + mode="remote", + dest=dest, + src=source, + force=force + ) + ) + new_module_args.pop('content', None) + result.update(self._execute_module(module_args=new_module_args, task_vars=task_vars)) + return result + # find_needle returns a path that may not have a trailing slash on a + # directory so we need to find that out first and append at the end + else: + trailing_slash = source.endswith(os.path.sep) + try: + # find in expected paths + source = self._find_needle('files', source) + except AnsibleError as e: + result['failed'] = True + result['msg'] = to_text(e) + result['exception'] = traceback.format_exc() + return result + + if trailing_slash != source.endswith(os.path.sep): + if source[-1] == os.path.sep: + source = source[:-1] + else: + source = source + os.path.sep + + # A list of source file tuples (full_path, relative_path) which will try to copy to the destination + source_files = {'files': [], 'directories': [], 'symlinks': []} + + # If source is a directory populate our list else source is a file and translate it to a tuple. + if os.path.isdir(to_bytes(source, errors='surrogate_or_strict')): + result['operation'] = 'folder_copy' + + # Get a list of the files we want to replicate on the remote side + source_files = _walk_dirs(source, local_follow=follow, + trailing_slash_detector=self._connection._shell.path_has_trailing_slash, + checksum_check=force) + + # If it's recursive copy, destination is always a dir, + # explicitly mark it so (note - win_copy module relies on this). + if not self._connection._shell.path_has_trailing_slash(dest): + dest = "%s%s" % (dest, self.WIN_PATH_SEPARATOR) + + check_dest = dest + # Source is a file, add details to source_files dict + else: + result['operation'] = 'file_copy' + + original_basename = os.path.basename(source) + result['original_basename'] = original_basename + + # check if dest ends with / or \ and append source filename to dest + if self._connection._shell.path_has_trailing_slash(dest): + check_dest = dest + filename = original_basename + result['dest'] = self._connection._shell.join_path(dest, filename) + else: + # replace \\ with / so we can use os.path to get the filename or dirname + unix_path = dest.replace(self.WIN_PATH_SEPARATOR, os.path.sep) + filename = os.path.basename(unix_path) + check_dest = os.path.dirname(unix_path) + + file_checksum = _get_local_checksum(force, source) + source_files['files'].append( + dict( + src=source, + dest=filename, + checksum=file_checksum + ) + ) + result['checksum'] = file_checksum + result['size'] = os.path.getsize(to_bytes(source, errors='surrogate_or_strict')) + + # find out the files/directories/symlinks that we need to copy to the server + query_args = self._task.args.copy() + query_args.update( + dict( + mode="query", + dest=check_dest, + force=force, + files=source_files['files'], + directories=source_files['directories'], + symlinks=source_files['symlinks'] + ) + ) + + query_args.pop('content', None) + query_return = self._execute_module(module_args=query_args, task_vars=task_vars) + + if query_return.get('failed', False) is True: + result.update(query_return) + return result + + if query_return['will_change'] is False: + # no changes need to occur + result['failed'] = False + result['changed'] = False + return result + + if query_return['zip_available'] is True and result['operation'] != 'file_copy': + # if the PS zip utils are available and we need to copy more than a + # single file/folder, create a local zip file of all the changed + # files and send that to the server to be expanded + # TODO: handle symlinks + result.update(self._copy_zip_file(dest, source_files['files'], source_files['directories'], task_vars)) + else: + # the PS zip assemblies are not available or only a single file + # needs to be copied. Instead of zipping up into one task this + # will handle each file/folder as an individual task + # TODO: Handle symlinks + + for directory in query_return['directories']: + file_result = self._create_directory(dest, directory['dest'], task_vars) + + result['changed'] = file_result.get('changed', False) + if file_result.get('failed', False) is True: + self._remove_tempfile_if_content_defined(content, content_tempfile) + result['failed'] = True + result['msg'] = "failed to create directory %s" % file_result['msg'] + return result + + for file in query_return['files']: + copy_result = self._copy_single_file(file['src'], dest, file['dest'], task_vars) + + result['changed'] = copy_result.get('changed', False) + if copy_result.get('failed', False) is True: + self._remove_tempfile_if_content_defined(content, content_tempfile) + result['failed'] = True + result['msg'] = "failed to copy file %s: %s" % (file['src'], copy_result['msg']) + return result + + # remove the content temp file if it was created + self._remove_tempfile_if_content_defined(content, content_tempfile) + return result diff --git a/test/integration/targets/win_copy/defaults/main.yml b/test/integration/targets/win_copy/defaults/main.yml new file mode 100644 index 0000000000..dcb00b54d0 --- /dev/null +++ b/test/integration/targets/win_copy/defaults/main.yml @@ -0,0 +1 @@ +test_win_copy_path: C:\ansible\win_copy diff --git a/test/integration/targets/win_copy/meta/main.yml b/test/integration/targets/win_copy/meta/main.yml deleted file mode 100644 index d328716dfa..0000000000 --- a/test/integration/targets/win_copy/meta/main.yml +++ /dev/null @@ -1,2 +0,0 @@ -dependencies: - - prepare_win_tests diff --git a/test/integration/targets/win_copy/tasks/main.yml b/test/integration/targets/win_copy/tasks/main.yml index bea2be8660..0e25c514ca 100644 --- a/test/integration/targets/win_copy/tasks/main.yml +++ b/test/integration/targets/win_copy/tasks/main.yml @@ -1,563 +1,24 @@ -# test code for the copy module and action plugin -# (c) 2014, Michael DeHaan +--- +- name: create empty folder + file: + path: '{{role_path}}/files/subdir/empty' + state: directory + delegate_to: localhost -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -- name: remove win_output_dir - win_file: - path: "{{win_output_dir}}" - state: absent - -- name: recreate win_output_dir - win_file: - path: "{{win_output_dir}}" +- name: create test folder + win_file: + path: '{{test_win_copy_path}}' state: directory -- name: copy an empty file - win_copy: - src: empty.txt - dest: "{{win_output_dir}}\\empty.txt" - register: copy_empty_result - -- name: check copy empty result - assert: - that: - - copy_empty_result|changed - - copy_empty_result.checksum == 'da39a3ee5e6b4b0d3255bfef95601890afd80709' - -- name: stat the empty file - win_stat: - path: "{{win_output_dir}}/empty.txt" - register: stat_empty_result - -- name: check that empty file really was created - assert: - that: - - stat_empty_result.stat.exists - - stat_empty_result.stat.size == 0 - -- name: copy an empty file again - win_copy: - src: empty.txt - dest: "{{win_output_dir}}/empty.txt" - register: copy_empty_again_result - -- name: check copy empty again result - assert: - that: - - not copy_empty_again_result|changed - - copy_empty_again_result.checksum == 'da39a3ee5e6b4b0d3255bfef95601890afd80709' - -- name: initiate a basic copy - win_copy: - src: foo.txt - dest: "{{win_output_dir}}\\foo.txt" - register: copy_result - -- name: check that the basic copy of the file was created - win_stat: - path: "{{win_output_dir}}\\foo.txt" - register: copy_result_stat - -- name: check basic copy result - assert: - that: - - copy_result|changed - - copy_result.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6' - - copy_result_stat.stat.exists == True - -- name: initiate a basic copy again - win_copy: - src: foo.txt - dest: "{{win_output_dir}}\\foo.txt" - register: copy_result_again - -- name: check basic copy result again - assert: - that: - - not copy_result_again|changed - - copy_result_again.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6' - -- name: copy file that exists on remote but checksum different and force is False - win_copy: - src: empty.txt - dest: "{{win_output_dir}}\\foo.txt" - force: False - register: copy_result_no_force_different - -- name: get stat on remote file for assertion - win_stat: - path: "{{win_output_dir}}\\foo.txt" - register: copy_result_no_force_different_stat - -- name: check that nothing changed when not forcing file and dest exists - assert: - that: - - not copy_result_no_force_different|changed - - copy_result_no_force_different_stat.stat.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6' - -- name: copy file that doesn't exist on remote and force is False - win_copy: - src: empty.txt - dest: "{{win_output_dir}}\\no_force.txt" - force: False - register: copy_result_no_force - -- name: get stat on remote file for assertion - win_stat: - path: "{{win_output_dir}}\\no_force.txt" - register: copy_result_no_force_stat - -- name: check that there was a change when not forcing file and dest does not exist - assert: - that: - - copy_result_no_force|changed - - copy_result_no_force_stat.stat.exists == True - - copy_result_no_force_stat.stat.checksum == 'da39a3ee5e6b4b0d3255bfef95601890afd80709' - -- name: make an output subdirectory - win_file: - path: "{{win_output_dir}}\\sub" - state: directory - -- name: test recursive copy to directory - win_copy: - src: subdir - dest: "{{win_output_dir}}\\sub" - register: recursive_copy_result - -- name: get stats on files within sub directory - win_find: - paths: "{{win_output_dir}}\\sub" - recurse: True - register: recurse_find_results - -- name: assert recursive copy worked - assert: - that: - - recursive_copy_result|changed - - recurse_find_results.examined == 7 # checks that it found 4 folders and 3 files - -- name: test recursive copy to directory again - win_copy: - src: subdir - dest: "{{win_output_dir}}\\sub" - register: recursive_copy_result_again - -- name: assert recursive copy worked - assert: - that: - - not recursive_copy_result_again|changed - -# Recursive folder copy with trailing slash (see issue 23559) - - -- name: make an output subdirectory - win_file: - path: "{{win_output_dir}}\\subtrailing\\" - state: directory - -- name: test recursive copy to directory - win_copy: - src: subdir/ - dest: "{{win_output_dir}}\\subtrailing\\" - register: recursive_copy_result2 - -- name: get stats on files within sub directory - win_find: - paths: "{{win_output_dir}}\\subtrailing\\" - recurse: True - register: recurse_find_results2 - -- name: assert recursive copy worked - assert: - that: - - recursive_copy_result2|changed - - recurse_find_results2.examined == 6 # checks that it found 3 folders and 3 files. -# Note this is different from the test above because, by including the trailing -# slash on the source, we only get the *contents* of the source folder -# without the trailing slash, we would get the source folder *and* its -# contents. -# See 'src' parameter documentation -# here: http://docs.ansible.com/ansible/win_copy_module.html - -- name: test recursive copy to directory again with source slash - win_copy: - src: subdir/ - dest: "{{win_output_dir}}\\subtrailing\\" - register: recursive_copy_result_again2 - -- name: assert recursive copy worked - assert: - that: - - not recursive_copy_result_again2|changed - -# test 'content' parameter -- name: create file with content - win_copy: - content: abc - dest: "{{win_output_dir}}\\content.txt" - register: content_result - -- name: get stat on creating file with content - win_stat: - path: "{{win_output_dir}}\\content.txt" - register: content_stat - -- name: assert content copy worked - assert: - that: - - content_result|changed - - content_stat.stat.exists == True - - content_stat.stat.checksum == 'a9993e364706816aba3e25717850c26c9cd0d89d' - -- name: create file with content again - win_copy: - content: abc - dest: "{{win_output_dir}}\\content.txt" - register: content_result_again - -- name: assert content copy again didn't change - assert: - that: - - not content_result_again|changed - -- name: copy file with different content - win_copy: - content: 123 - dest: "{{win_output_dir}}\\content.txt" - register: content_different_result - -- name: get stat on file with different content - win_stat: - path: "{{win_output_dir}}\\content.txt" - register: content_different_stat - -- name: assert different content copy worked - assert: - that: - - content_different_result|changed - - content_different_stat.stat.checksum == '40bd001563085fc35165329ea1ff5c5ecbdbbeef' - -- name: copy remote file - win_copy: - src: "{{win_output_dir}}\\foo.txt" - dest: "{{win_output_dir}}\\foobar.txt" - remote_src: True - register: remote_file_result - -- name: get stat on new remote file - win_stat: - path: "{{win_output_dir}}\\foobar.txt" - register: remote_file_stat - -- name: assert remote copy worked - assert: - that: - - remote_file_result|changed - - remote_file_result.size == 8 - - remote_file_result.src == '{{win_output_dir|regex_replace('\\', '\\\\')}}\\foo.txt' - - remote_file_result.dest == '{{win_output_dir|regex_replace('\\', '\\\\')}}\\foobar.txt' - - remote_file_result.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6' - - remote_file_result.operation == 'file_copy' - - remote_file_result.original_basename == 'foo.txt' - - remote_file_stat.stat.exists == True - - remote_file_stat.stat.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6' - -- name: copy remote file again - win_copy: - src: "{{win_output_dir}}\\foo.txt" - dest: "{{win_output_dir}}\\foobar.txt" - remote_src: True - register: remote_file_result_again - -- name: assert remote copy again did not change - assert: - that: - - not remote_file_result_again|changed - -- name: copy remote folder - win_copy: - src: "{{win_output_dir}}\\sub" - dest: "{{win_output_dir}}\\sub2" - remote_src: True - register: remote_folder_result - -- name: get stat on new remote folder contents - win_find: - paths: "{{win_output_dir}}\\sub2" - recurse: True - register: remote_folder_stat - -- name: assert remote copy worked - assert: - that: - - remote_folder_result|changed - - remote_folder_result.size == 11 - - remote_folder_result.src == '{{win_output_dir|regex_replace('\\', '\\\\')}}\\sub' - - remote_folder_result.dest == '{{win_output_dir|regex_replace('\\', '\\\\')}}\\sub2' - - remote_folder_result.operation == 'folder_copy' - - remote_folder_stat.examined == 8 # 5 folders 3 files - -- name: copy remote folder again - win_copy: - src: "{{win_output_dir}}\\sub" - dest: "{{win_output_dir}}\\sub2" - remote_src: True - register: remote_folder_result_again - -- name: assert remote copy again did not change - assert: - that: - - not remote_folder_result_again|changed - -- name: fail to copy when source doesn't exist - win_copy: - src: false-file - dest: "{{win_output_dir}}\\fale-file.txt" - register: fail_missing_source - failed_when: not (fail_missing_source|failed) - -- name: fail when copying remote src file when src doesn't exist - win_copy: - src: "{{win_output_dir}}\\fake.txt" - dest: "{{win_output_dir}}\\real.txt" - remote_src: True - register: fail_remote_missing_src - failed_when: "fail_remote_missing_src.msg != 'Cannot copy src file: ' + win_output_dir + '\\\\fake.txt as it does not exist'" - -- name: fail when copying remote src folder to file - win_copy: - src: "{{win_output_dir}}\\sub" - dest: "{{win_output_dir}}\\foo.txt" - remote_src: True - register: fail_remote_folder_to_file - failed_when: "'If src is a folder, dest must also be a folder. src' not in fail_remote_folder_to_file.msg" - -- name: fail when copying remote src file to folder - win_copy: - src: "{{win_output_dir}}\\foo.txt" - dest: "{{win_output_dir}}\\sub" - remote_src: True - register: fail_remote_file_to_folder - failed_when: "'If src is a file, dest must also be a file. src' not in fail_remote_file_to_folder.msg" - -- name: run check mode copy new file - win_copy: - src: foo.txt - dest: "{{win_output_dir}}\\foo-check.txt" - register: check_copy_file - check_mode: yes - -- name: get stat on new file - win_stat: - path: "{{win_output_dir}}\\foo-check.txt" - register: check_stat_file - -- name: assert check would change but file doesn't exist - assert: - that: - - check_copy_file|changed - - check_stat_file.stat.exists == False - -- name: run check mode copy existing file - win_copy: - src: foo.txt - dest: "{{win_output_dir}}\\foo.txt" - register: check_copy_file_existing - check_mode: yes - -- name: assert check wouldn't change existing file - assert: - that: - - not check_copy_file_existing|changed - -- name: run check mode copy existing file with force False - win_copy: - src: empty.txt - dest: "{{win_output_dir}}\\foo.txt" - force: False - register: check_copy_existing_no_force - check_mode: yes - -- name: assert check wouldn't change existing file - assert: - that: - - not check_copy_existing_no_force|changed - -- name: run check mode copy new file with force False - win_copy: - src: empty.txt - dest: "{{win_output_dir}}\\no-force-check.txt" - force: False - register: check_copy_no_force - check_mode: yes - -- name: get stat on new file - win_stat: - path: "{{win_output_dir}}\\no-force-check.txt" - register: check_copy_no_force_stat - -- name: assert check wouldn't create file but change registered - assert: - that: - - check_copy_no_force|changed - - check_copy_no_force_stat.stat.exists == False - -- name: run check mode copy new folder - win_copy: - src: subdir - dest: "{{win_output_dir}}\\sub-check" - register: check_copy_folder - check_mode: yes - -- name: get stat on new folder - win_stat: - path: "{{win_output_dir}}\\sub-check" - register: check_stat_folder - -- name: assert check would change but folder doesn't exist - assert: - that: - - check_copy_folder|changed - - check_stat_folder.stat.exists == False - -- name: run check mode copy existing folder - win_copy: - src: subdir - dest: "{{win_output_dir}}\\sub" - register: check_copy_folder_existing - check_mode: yes - -- name: assert check wouldn't change existing file - assert: - that: - - not check_copy_folder_existing|changed - -- name: run check mode copy new contents - win_copy: - content: abc - dest: "{{win_output_dir}}\\content-check.txt" - register: check_content_file - check_mode: yes - -- name: get stat on content file - win_stat: - path: "{{win_output_dir}}\\content-check.txt" - register: check_stat_content - -- name: assert check would change but content file doesn't exist - assert: - that: - - check_content_file|changed - - check_stat_content.stat.exists == False - -- name: run check mode copy existing contents - win_copy: - content: 123 - dest: "{{win_output_dir}}\\content.txt" - register: check_content_file_existing - check_mode: yes - -- name: assert check wouldn't change exisitng content file - assert: - that: - - not check_content_file_existing|changed - -- name: run check mode copy new contents - win_copy: - content: abc - dest: "{{win_output_dir}}\\content.txt" - register: check_different_content_file - -- name: get stat on check mode file with different content - win_stat: - path: "{{win_output_dir}}\\content.txt" - register: check_different_content_stat - -- name: assert check content changed but file wasn't touched - assert: - that: - - check_different_content_file|changed - -- name: run check mode copy new file remote src - win_copy: - src: "{{win_output_dir}}\\foo.txt" - dest: "{{win_output_dir}}\\foo-check.txt" - remote_src: True - register: check_copy_file_remote - check_mode: yes - -- name: get stat on new file - win_stat: - path: "{{win_output_dir}}\\foo-check.txt" - register: check_stat_file_remote - -- name: assert check would change but file doesn't exist - assert: - that: - - check_copy_file_remote|changed - - check_stat_file_remote.stat.exists == False - -- name: run check mode copy existing file remote src - win_copy: - src: "{{win_output_dir}}\\foo.txt" - dest: "{{win_output_dir}}\\foo.txt" - remote_src: True - register: check_copy_file_remote_existing - check_mode: yes - -- name: assert check would change but file doesn't exist - assert: - that: - - not check_copy_file_remote_existing|changed - -- name: run check mode copy new folder remote src - win_copy: - src: "{{win_output_dir}}\\sub" - dest: "{{win_output_dir}}\\sub-check" - remote_src: True - register: check_copy_folder_remote - check_mode: yes - -- name: get stat on new file - win_stat: - path: "{{win_output_dir}}\\sub-check" - register: check_stat_folder_remote - -- name: assert check would change but folder doesn't exist - assert: - that: - - check_copy_folder_remote|changed - - check_stat_folder_remote.stat.exists == False - -- name: run check mode copy existing folder remote src - win_copy: - src: "{{win_output_dir}}\\sub" - dest: "{{win_output_dir}}\\sub2" - remote_src: True - register: check_copy_folder_remote_existing - check_mode: yes - -- name: assert check wouldn't change existing folder - assert: - that: - - not check_copy_folder_remote_existing|changed - -- name: cleanup output dir - win_file: - path: "{{win_output_dir}}" - state: absent +- block: + - name: run tests for local to remote + include_tasks: tests.yml + + - name: run tests for remote to remote + include_tasks: remote_tests.yml + + always: + - name: remove test folder + win_file: + path: '{{test_win_copy_path}}' + state: absent diff --git a/test/integration/targets/win_copy/tasks/remote_tests.yml b/test/integration/targets/win_copy/tasks/remote_tests.yml new file mode 100644 index 0000000000..589bfaf970 --- /dev/null +++ b/test/integration/targets/win_copy/tasks/remote_tests.yml @@ -0,0 +1,414 @@ +--- +- name: fail when source does not exist remote + win_copy: + src: fakesource + dest: fakedest + remote_src: yes + register: fail_remote_invalid_source + failed_when: "fail_remote_invalid_source.msg != 'Cannot copy src file: fakesource as it does not exist'" + +- name: setup source folder for remote tests + win_copy: + src: files/ + dest: '{{test_win_copy_path}}\source\' + +- name: setup remote failure tests + win_file: + path: '{{item.path}}' + state: '{{item.state}}' + with_items: + - { 'path': '{{test_win_copy_path}}\target\folder', 'state': 'directory' } + - { 'path': '{{test_win_copy_path}}\target\file', 'state': 'touch' } + - { 'path': '{{test_win_copy_path}}\target\subdir', 'state': 'touch' } + +- name: fail source is a file but dest is a folder + win_copy: + src: '{{test_win_copy_path}}\source\foo.txt' + dest: '{{test_win_copy_path}}\target\folder' + remote_src: yes + register: fail_remote_file_to_folder + failed_when: "'dest is already a folder' not in fail_remote_file_to_folder.msg" + +- name: fail source is a file but dest is a folder + win_copy: + src: '{{test_win_copy_path}}\source\' + dest: '{{test_win_copy_path}}\target\' + remote_src: yes + register: fail_remote_folder_to_file + failed_when: "'dest is already a file' not in fail_remote_folder_to_file.msg" + +- name: fail source is a file dest parent dir is also a file + win_copy: + src: '{{test_win_copy_path}}\source\foo.txt' + dest: '{{test_win_copy_path}}\target\file\foo.txt' + remote_src: yes + register: fail_remote_file_parent_dir_file + failed_when: fail_remote_file_parent_dir_file.msg != 'object at destination parent dir ' + test_win_copy_path + '\\target\\file is currently a file' + +- name: fail source is a folder dest parent dir is also a file + win_copy: + src: '{{test_win_copy_path}}\source\subdir' + dest: '{{test_win_copy_path}}\target\file' + remote_src: yes + register: fail_remote_folder_parent_dir_file + failed_when: "'object at dest parent dir is not a folder' not in fail_remote_folder_parent_dir_file.msg" + +- name: fail to copy a remote file with parent dir that doesn't exist and filename is set + win_copy: + src: '{{test_win_copy_path}}\source\foo.txt' + dest: '{{test_win_copy_path}}\missing-dir\foo.txt' + remote_src: yes + register: fail_remote_missing_parent_dir + failed_when: "'Destination directory ' + test_win_copy_path + '\\missing-dir does not exist' not in fail_remote_missing_parent_dir.msg" + +- name: remove target after remote failure tests + win_file: + path: '{{test_win_copy_path}}\target' + state: absent + +- name: create remote target after cleaning + win_file: + path: '{{test_win_copy_path}}\target' + state: directory + +- name: copy single file remote (check mode) + win_copy: + src: '{{test_win_copy_path}}\source\foo.txt' + dest: '{{test_win_copy_path}}\target\foo-target.txt' + remote_src: yes + register: remote_copy_file_check + check_mode: yes + +- name: get result of copy single file remote (check mode) + win_stat: + path: '{{test_win_copy_path}}\target\foo-target.txt' + register: remote_copy_file_actual_check + +- name: assert copy single file remote (check mode) + assert: + that: + - remote_copy_file_check|changed + - remote_copy_file_actual_check.stat.exists == False + +- name: copy single file remote + win_copy: + src: '{{test_win_copy_path}}\source\foo.txt' + dest: '{{test_win_copy_path}}\target\foo-target.txt' + remote_src: yes + register: remote_copy_file + +- name: get result of copy single file remote + win_stat: + path: '{{test_win_copy_path}}\target\foo-target.txt' + register: remote_copy_file_actual + +- name: assert copy single file remote + assert: + that: + - remote_copy_file|changed + - remote_copy_file.operation == 'file_copy' + - remote_copy_file.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6' + - remote_copy_file.size == 8 + - remote_copy_file.original_basename == 'foo.txt' + - remote_copy_file_actual.stat.exists == True + - remote_copy_file_actual.stat.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6' + +- name: copy single file remote (idempotent) + win_copy: + src: '{{test_win_copy_path}}\source\foo.txt' + dest: '{{test_win_copy_path}}\target\foo-target.txt' + remote_src: yes + register: remote_copy_file_again + +- name: assert copy single file remote (idempotent) + assert: + that: + - not remote_copy_file_again|changed + +- name: copy single file into folder remote (check mode) + win_copy: + src: '{{test_win_copy_path}}\source\foo.txt' + dest: '{{test_win_copy_path}}\target\' + remote_src: yes + register: remote_copy_file_to_folder_check + check_mode: yes + +- name: get result of copy single file into folder remote (check mode) + win_stat: + path: '{{test_win_copy_path}}\target\foo.txt' + register: remote_copy_file_to_folder_actual_check + +- name: assert copy single file into folder remote (check mode) + assert: + that: + - remote_copy_file_to_folder_check|changed + - remote_copy_file_to_folder_actual_check.stat.exists == False + +- name: copy single file into folder remote + win_copy: + src: '{{test_win_copy_path}}\source\foo.txt' + dest: '{{test_win_copy_path}}\target\' + remote_src: yes + register: remote_copy_file_to_folder + +- name: get result of copy single file into folder remote + win_stat: + path: '{{test_win_copy_path}}\target\foo.txt' + register: remote_copy_file_to_folder_actual + +- name: assert copy single file into folder remote + assert: + that: + - remote_copy_file_to_folder|changed + - remote_copy_file_to_folder.operation == 'file_copy' + - remote_copy_file_to_folder.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6' + - remote_copy_file_to_folder.size == 8 + - remote_copy_file_to_folder.original_basename == 'foo.txt' + - remote_copy_file_to_folder_actual.stat.exists == True + - remote_copy_file_to_folder_actual.stat.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6' + +- name: copy single file into folder remote (idempotent) + win_copy: + src: '{{test_win_copy_path}}\source\foo.txt' + dest: '{{test_win_copy_path}}\target\' + remote_src: yes + register: remote_copy_file_to_folder_again + +- name: assert copy single file into folder remote + assert: + that: + - not remote_copy_file_to_folder_again|changed + +- name: copy single file to missing folder (check mode) + win_copy: + src: '{{test_win_copy_path}}\source\foo.txt' + dest: '{{test_win_copy_path}}\target\missing\' + remote_src: yes + register: remote_copy_file_to_missing_folder_check + check_mode: yes + +- name: get result of copy single file to missing folder remote (check mode) + win_stat: + path: '{{test_win_copy_path}}\target\missing\foo.txt' + register: remote_copy_file_to_missing_folder_actual_check + +- name: assert copy single file to missing folder remote (check mode) + assert: + that: + - remote_copy_file_to_missing_folder_check|changed + - remote_copy_file_to_missing_folder_check.operation == 'file_copy' + - remote_copy_file_to_missing_folder_actual_check.stat.exists == False + +- name: copy single file to missing folder remote + win_copy: + src: '{{test_win_copy_path}}\source\foo.txt' + dest: '{{test_win_copy_path}}\target\missing\' + remote_src: yes + register: remote_copy_file_to_missing_folder + +- name: get result of copy single file to missing folder remote + win_stat: + path: '{{test_win_copy_path}}\target\missing\foo.txt' + register: remote_copy_file_to_missing_folder_actual + +- name: assert copy single file to missing folder remote + assert: + that: + - remote_copy_file_to_missing_folder|changed + - remote_copy_file_to_missing_folder.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6' + - remote_copy_file_to_missing_folder.operation == 'file_copy' + - remote_copy_file_to_missing_folder.size == 8 + - remote_copy_file_to_missing_folder_actual.stat.exists == True + - remote_copy_file_to_missing_folder_actual.stat.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6' + +- name: clear target for folder to folder test + win_file: + path: '{{test_win_copy_path}}\target' + state: absent + +- name: copy folder to folder remote (check mode) + win_copy: + src: '{{test_win_copy_path}}\source' + dest: '{{test_win_copy_path}}\target' + remote_src: yes + register: remote_copy_folder_to_folder_check + check_mode: yes + +- name: get result of copy folder to folder remote (check mode) + win_stat: + path: '{{test_win_copy_path}}\target' + register: remote_copy_folder_to_folder_actual_check + +- name: assert copy folder to folder remote (check mode) + assert: + that: + - remote_copy_folder_to_folder_check|changed + - remote_copy_folder_to_folder_check.operation == 'folder_copy' + - remote_copy_folder_to_folder_actual_check.stat.exists == False + +- name: copy folder to folder remote + win_copy: + src: '{{test_win_copy_path}}\source' + dest: '{{test_win_copy_path}}\target' + remote_src: yes + register: remote_copy_folder_to_folder + +- name: get result of copy folder to folder remote + win_find: + paths: '{{test_win_copy_path}}\target' + recurse: yes + file_type: directory + register: remote_copy_folder_to_folder_actual + +- name: assert copy folder to folder remote + assert: + that: + - remote_copy_folder_to_folder|changed + - remote_copy_folder_to_folder.operation == 'folder_copy' + - remote_copy_folder_to_folder_actual.examined == 11 + - remote_copy_folder_to_folder_actual.matched == 6 + - remote_copy_folder_to_folder_actual.files[0].filename == 'source' + - remote_copy_folder_to_folder_actual.files[1].filename == 'subdir' + - remote_copy_folder_to_folder_actual.files[2].filename == 'empty' + - remote_copy_folder_to_folder_actual.files[3].filename == 'subdir2' + - remote_copy_folder_to_folder_actual.files[4].filename == 'subdir3' + - remote_copy_folder_to_folder_actual.files[5].filename == 'subdir4' + +- name: copy folder to folder remote (idempotent) + win_copy: + src: '{{test_win_copy_path}}\source' + dest: '{{test_win_copy_path}}\target' + remote_src: yes + register: remote_copy_folder_to_folder_again + +- name: assert copy folder to folder remote (idempotent) + assert: + that: + - not remote_copy_folder_to_folder_again|changed + +- name: change remote file after folder to folder test + win_copy: + content: bar.txt + dest: '{{test_win_copy_path}}\target\source\foo.txt' + +- name: remote remote folder after folder to folder test + win_file: + path: '{{test_win_copy_path}}\target\source\subdir\subdir2\subdir3\subdir4' + state: absent + +- name: copy folder to folder remote after change + win_copy: + src: '{{test_win_copy_path}}\source' + dest: '{{test_win_copy_path}}\target' + remote_src: yes + register: remote_copy_folder_to_folder_after_change + +- name: get result of copy folder to folder remote after change + win_find: + paths: '{{test_win_copy_path}}\target\source' + recurse: yes + patterns: ['foo.txt', 'qux.txt'] + register: remote_copy_folder_to_folder_after_change_actual + +- name: assert copy folder after changes + assert: + that: + - remote_copy_folder_to_folder_after_change|changed + - remote_copy_folder_to_folder_after_change_actual.matched == 2 + - remote_copy_folder_to_folder_after_change_actual.files[0].checksum == 'b54ba7f5621240d403f06815f7246006ef8c7d43' + - remote_copy_folder_to_folder_after_change_actual.files[1].checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6' + +- name: clear target folder before folder contents to remote test + win_file: + path: '{{test_win_copy_path}}\target' + state: absent + +- name: copy folder contents to folder remote with backslash (check mode) + win_copy: + src: '{{test_win_copy_path}}\source\' + dest: '{{test_win_copy_path}}\target' + remote_src: yes + register: remote_copy_folder_content_backslash_check + check_mode: yes + +- name: get result of copy folder contents to folder remote with backslash (check mode) + win_stat: + path: '{{test_win_copy_path}}\target' + register: remote_copy_folder_content_backslash_actual_check + +- name: assert copy folder content to folder remote with backslash (check mode) + assert: + that: + - remote_copy_folder_content_backslash_check|changed + - remote_copy_folder_content_backslash_actual_check.stat.exists == False + +- name: copy folder contents to folder remote with backslash + win_copy: + src: '{{test_win_copy_path}}\source\' + dest: '{{test_win_copy_path}}\target' + remote_src: yes + register: remote_copy_folder_content_backslash + +- name: get result of copy folder contents to folder remote with backslash + win_find: + paths: '{{test_win_copy_path}}\target' + recurse: yes + file_type: directory + register: remote_copy_folder_content_backslash_actual + +- name: assert copy folder content to folder remote with backslash + assert: + that: + - remote_copy_folder_content_backslash|changed + - remote_copy_folder_content_backslash.operation == 'folder_copy' + - remote_copy_folder_content_backslash_actual.examined == 10 + - remote_copy_folder_content_backslash_actual.matched == 5 + - remote_copy_folder_content_backslash_actual.files[0].filename == 'subdir' + - remote_copy_folder_content_backslash_actual.files[1].filename == 'empty' + - remote_copy_folder_content_backslash_actual.files[2].filename == 'subdir2' + - remote_copy_folder_content_backslash_actual.files[3].filename == 'subdir3' + - remote_copy_folder_content_backslash_actual.files[4].filename == 'subdir4' + +- name: copy folder contents to folder remote with backslash (idempotent) + win_copy: + src: '{{test_win_copy_path}}\source\' + dest: '{{test_win_copy_path}}\target' + remote_src: yes + register: remote_copy_folder_content_backslash_again + +- name: assert copy folder content to folder remote with backslash (idempotent) + assert: + that: + - not remote_copy_folder_content_backslash_again|changed + +- name: change remote file after folder content to folder test + win_copy: + content: bar.txt + dest: '{{test_win_copy_path}}\target\foo.txt' + +- name: remote remote folder after folder content to folder test + win_file: + path: '{{test_win_copy_path}}\target\subdir\subdir2\subdir3\subdir4' + state: absent + +- name: copy folder content to folder remote after change + win_copy: + src: '{{test_win_copy_path}}/source/' + dest: '{{test_win_copy_path}}/target/' + remote_src: yes + register: remote_copy_folder_content_to_folder_after_change + +- name: get result of copy folder content to folder remote after change + win_find: + paths: '{{test_win_copy_path}}\target' + recurse: yes + patterns: ['foo.txt', 'qux.txt'] + register: remote_copy_folder_content_to_folder_after_change_actual + +- name: assert copy folder content to folder after changes + assert: + that: + - remote_copy_folder_content_to_folder_after_change|changed + - remote_copy_folder_content_to_folder_after_change_actual.matched == 2 + - remote_copy_folder_content_to_folder_after_change_actual.files[0].checksum == 'b54ba7f5621240d403f06815f7246006ef8c7d43' + - remote_copy_folder_content_to_folder_after_change_actual.files[1].checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6' diff --git a/test/integration/targets/win_copy/tasks/tests.yml b/test/integration/targets/win_copy/tasks/tests.yml new file mode 100644 index 0000000000..b5f3d8dc30 --- /dev/null +++ b/test/integration/targets/win_copy/tasks/tests.yml @@ -0,0 +1,392 @@ +--- +- name: fail no source or content + win_copy: + dest: dest + register: fail_no_source_content + failed_when: fail_no_source_content.msg != 'src (or content) and dest are required' + +- name: fail content but dest isn't a file, unix ending + win_copy: + content: a + dest: a/ + register: fail_dest_not_file_unix + failed_when: fail_dest_not_file_unix.msg != 'dest must be a file if content is defined' + +- name: fail content but dest isn't a file, windows ending + win_copy: + content: a + dest: a\ + register: fail_dest_not_file_windows + failed_when: fail_dest_not_file_windows.msg != 'dest must be a file if content is defined' + +- name: fail to copy a file with parent dir that doesn't exist and filename is set + win_copy: + src: foo.txt + dest: '{{test_win_copy_path}}\missing-dir\foo.txt' + register: fail_missing_parent_dir + failed_when: "'Destination directory ' + test_win_copy_path + '\\missing-dir does not exist' not in fail_missing_parent_dir.msg" + +- name: copy with content (check mode) + win_copy: + content: a + dest: '{{test_win_copy_path}}\file' + register: copy_content_check + check_mode: yes + +- name: get result of copy with content (check mode) + win_stat: + path: '{{test_win_copy_path}}\file' + register: copy_content_actual_check + +- name: assert copy with content (check mode) + assert: + that: + - copy_content_check|changed + - copy_content_check.checksum == '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8' + - copy_content_check.operation == 'file_copy' + - copy_content_check.size == 1 + - copy_content_actual_check.stat.exists == False + +- name: copy with content + win_copy: + content: a + dest: '{{test_win_copy_path}}\file' + register: copy_content + +- name: get result of copy with content + win_stat: + path: '{{test_win_copy_path}}\file' + register: copy_content_actual + +- name: assert copy with content + assert: + that: + - copy_content|changed + - copy_content.checksum == '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8' + - copy_content.operation == 'file_copy' + - copy_content.size == 1 + - copy_content_actual.stat.exists == True + - copy_content_actual.stat.checksum == '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8' + +- name: copy with content (idempotent) + win_copy: + content: a + dest: '{{test_win_copy_path}}\file' + register: copy_content_again + +- name: assert copy with content (idempotent) + assert: + that: + - not copy_content_again|changed + +- name: copy with content change when missing + win_copy: + content: b + dest: '{{test_win_copy_path}}\file' + force: no + register: copy_content_when_missing + +- name: assert copy with content change when missing + assert: + that: + - not copy_content_when_missing|changed + +- name: copy single file (check mode) + win_copy: + src: foo.txt + dest: '{{test_win_copy_path}}\foo-target.txt' + register: copy_file_check + check_mode: yes + +- name: get result of copy single file (check mode) + win_stat: + path: '{{test_win_copy_path}}\foo-target.txt' + register: copy_file_actual_check + +- name: assert copy single file (check mode) + assert: + that: + - copy_file_check|changed + - copy_file_check.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6' + - copy_file_check.operation == 'file_copy' + - copy_file_check.size == 8 + - copy_file_actual_check.stat.exists == False + +- name: copy single file + win_copy: + src: foo.txt + dest: '{{test_win_copy_path}}\foo-target.txt' + register: copy_file + +- name: get result of copy single file + win_stat: + path: '{{test_win_copy_path}}\foo-target.txt' + register: copy_file_actual + +- name: assert copy single file + assert: + that: + - copy_file|changed + - copy_file.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6' + - copy_file.operation == 'file_copy' + - copy_file.size == 8 + - copy_file_actual.stat.exists == True + - copy_file_actual.stat.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6' + +- name: copy single file (idempotent) + win_copy: + src: foo.txt + dest: '{{test_win_copy_path}}\foo-target.txt' + register: copy_file_again + +- name: assert copy single file (idempotent) + assert: + that: + - not copy_file_again|changed + +- name: copy single file to folder (check mode) + win_copy: + src: foo.txt + dest: '{{test_win_copy_path}}\' + register: copy_file_to_folder_check + check_mode: yes + +- name: get result of copy single file to folder (check mode) + win_stat: + path: '{{test_win_copy_path}}\foo.txt' + register: copy_file_to_folder_actual_check + +- name: assert copy single file to folder (check mode) + assert: + that: + - copy_file_to_folder_check|changed + - copy_file_to_folder_check.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6' + - copy_file_to_folder_check.operation == 'file_copy' + - copy_file_to_folder_check.size == 8 + - copy_file_to_folder_actual_check.stat.exists == False + +- name: copy single file to folder + win_copy: + src: foo.txt + dest: '{{test_win_copy_path}}\' + register: copy_file_to_folder + +- name: get result of copy single file to folder + win_stat: + path: '{{test_win_copy_path}}\foo.txt' + register: copy_file_to_folder_actual + +- name: assert copy single file to folder + assert: + that: + - copy_file_to_folder|changed + - copy_file_to_folder.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6' + - copy_file_to_folder.operation == 'file_copy' + - copy_file_to_folder.size == 8 + - copy_file_to_folder_actual.stat.exists == True + - copy_file_to_folder_actual.stat.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6' + +- name: copy single file to folder (idempotent) + win_copy: + src: foo.txt + dest: '{{test_win_copy_path}}\' + register: copy_file_to_folder_again + +- name: assert copy single file to folder (idempotent) + assert: + that: + - not copy_file_to_folder_again|changed + +- name: copy single file to missing folder (check mode) + win_copy: + src: foo.txt + dest: '{{test_win_copy_path}}\missing\' + register: copy_file_to_missing_folder_check + check_mode: yes + +- name: get result of copy single file to missing folder (check mode) + win_stat: + path: '{{test_win_copy_path}}\missing\foo.txt' + register: copy_file_to_missing_folder_actual_check + +- name: assert copy single file to missing folder (check mode) + assert: + that: + - copy_file_to_missing_folder_check|changed + - copy_file_to_missing_folder_check.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6' + - copy_file_to_missing_folder_check.operation == 'file_copy' + - copy_file_to_missing_folder_check.size == 8 + - copy_file_to_missing_folder_actual_check.stat.exists == False + +- name: copy single file to missing folder + win_copy: + src: foo.txt + dest: '{{test_win_copy_path}}\missing\' + register: copy_file_to_missing_folder + +- name: get result of copy single file to missing folder + win_stat: + path: '{{test_win_copy_path}}\missing\foo.txt' + register: copy_file_to_missing_folder_actual + +- name: assert copy single file to missing folder + assert: + that: + - copy_file_to_missing_folder|changed + - copy_file_to_missing_folder.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6' + - copy_file_to_missing_folder.operation == 'file_copy' + - copy_file_to_missing_folder.size == 8 + - copy_file_to_missing_folder_actual.stat.exists == True + - copy_file_to_missing_folder_actual.stat.checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6' + +- name: copy folder (check mode) + win_copy: + src: files + dest: '{{test_win_copy_path}}\recursive\folder' + register: copy_folder_check + check_mode: yes + +- name: get result of copy folder (check mode) + win_stat: + path: '{{test_win_copy_path}}\recursive\folder' + register: copy_folder_actual_check + +- name: assert copy folder (check mode) + assert: + that: + - copy_folder_check|changed + - copy_folder_check.operation == 'folder_copy' + - copy_folder_actual_check.stat.exists == False + +- name: copy folder + win_copy: + src: files + dest: '{{test_win_copy_path}}\recursive\folder' + register: copy_folder + +- name: get result of copy folder + win_find: + paths: '{{test_win_copy_path}}\recursive\folder' + recurse: yes + file_type: directory + register: copy_folder_actual + +- name: assert copy folder + assert: + that: + - copy_folder|changed + - copy_folder.operation == 'folder_copy' + - copy_folder_actual.examined == 11 # includes files and folders, the below is the nested order + - copy_folder_actual.matched == 6 + - copy_folder_actual.files[0].filename == 'files' + - copy_folder_actual.files[1].filename == 'subdir' + - copy_folder_actual.files[2].filename == 'empty' + - copy_folder_actual.files[3].filename == 'subdir2' + - copy_folder_actual.files[4].filename == 'subdir3' + - copy_folder_actual.files[5].filename == 'subdir4' + +- name: copy folder (idempotent) + win_copy: + src: files + dest: '{{test_win_copy_path}}\recursive\folder' + register: copy_folder_again + +- name: assert copy folder (idempotent) + assert: + that: + - not copy_folder_again|changed + +- name: change the text of a file in the remote source + win_copy: + content: bar.txt + dest: '{{test_win_copy_path}}\recursive\folder\files\foo.txt' + +- name: remove folder for test of recursive copy + win_file: + path: '{{test_win_copy_path}}\recursive\folder\files\subdir\subdir2\subdir3\subdir4' + state: absent + +- name: copy folder after changes + win_copy: + src: files + dest: '{{test_win_copy_path}}\recursive\folder' + register: copy_folder_after_change + +- name: get result of copy folder after changes + win_find: + paths: '{{test_win_copy_path}}\recursive\folder\files' + recurse: yes + patterns: ['foo.txt', 'qux.txt'] + register: copy_folder_after_changes_actual + +- name: assert copy folder after changes + assert: + that: + - copy_folder_after_change|changed + - copy_folder_after_changes_actual.matched == 2 + - copy_folder_after_changes_actual.files[0].checksum == 'b54ba7f5621240d403f06815f7246006ef8c7d43' + - copy_folder_after_changes_actual.files[1].checksum == 'c79a6506c1c948be0d456ab5104d5e753ab2f3e6' + +- name: copy folder's contents (check mode) + win_copy: + src: files/ + dest: '{{test_win_copy_path}}\recursive-contents\' + register: copy_folder_contents_check + check_mode: yes + +- name: get result of copy folder'scontents (check mode) + win_stat: + path: '{{test_win_copy_path}}\recursive-contents' + register: copy_folder_contents_actual_check + +- name: assert copy folder's contents (check mode) + assert: + that: + - copy_folder_contents_check|changed + - copy_folder_contents_check.operation == 'folder_copy' + - copy_folder_contents_actual_check.stat.exists == False + +- name: copy folder's contents + win_copy: + src: files/ + dest: '{{test_win_copy_path}}\recursive-contents\' + register: copy_folder_contents + +- name: get result of copy folder + win_find: + paths: '{{test_win_copy_path}}\recursive-contents' + recurse: yes + file_type: directory + register: copy_folder_contents_actual + +- name: assert copy folder + assert: + that: + - copy_folder_contents|changed + - copy_folder_contents.operation == 'folder_copy' + - copy_folder_contents_actual.examined == 10 # includes files and folders, the below is the nested order + - copy_folder_contents_actual.matched == 5 + - copy_folder_contents_actual.files[0].filename == 'subdir' + - copy_folder_contents_actual.files[1].filename == 'empty' + - copy_folder_contents_actual.files[2].filename == 'subdir2' + - copy_folder_contents_actual.files[3].filename == 'subdir3' + - copy_folder_contents_actual.files[4].filename == 'subdir4' + +- name: fail to copy file to a folder + win_copy: + src: foo.txt + dest: '{{test_win_copy_path}}\recursive-contents' + register: fail_file_to_folder + failed_when: "'object at path is already a directory' not in fail_file_to_folder.msg" + +- name: fail to copy folder to a file + win_copy: + src: subdir/ + dest: '{{test_win_copy_path}}\recursive-contents\foo.txt' + register: fail_folder_to_file + failed_when: "'object at parent directory path is already a file' not in fail_folder_to_file.msg" + +- name: remove test folder after local to remote tests + win_file: + path: '{{test_win_copy_path}}' + state: absent