win_copy: rewrite with new tests and functionality (#27678)

* win_copy rewrite with new tests and functionality

* minor pep fixes

* Handle UTF-8 filenames in zip

* fix for template

* when zip assemblies are not available in .net revert to old behaviour of copying one by one

* typo fix

* some more typos

* updated logic to correctly handle when new directories can be created

* removed testing file as it is not needed

* updated documentation based on PR
pull/28428/head
Jordan Borean 7 years ago committed by Matt Davis
parent 688823014f
commit 8e40ac54dd

@ -1,94 +1,144 @@
#!powershell #!powershell
# This file is part of Ansible # 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 <http://www.gnu.org/licenses/>.
# WANT_JSON
# POWERSHELL_COMMON
$params = Parse-Args $args -supports_check_mode $true
# (c) 2015, Jon Hawkesworth (@jhawkesworth) <figs@unity.demon.co.uk>
# Copyright (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#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 $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 $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 = Get-AnsibleParam -obj $params -name "original_basename" -type "str"
# original_basename gets set if src and dest are dirs # used in query and remote mode
# but includes subdir if the source folder contains sub folders $force = Get-AnsibleParam -obj $params -name "force" -type "bool" -default $true
# e.g. you could get subdir/foo.txt
# 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 = @{ $result = @{
changed = $false changed = $false
dest = $dest
original_basename = $original_basename
src = $src
} }
if (($force -eq $false) -and (Test-Path -Path $dest)) { if ($diff_mode) {
$result.msg = "file already exists" $result.diff = @{}
Exit-Json $result
} }
Function Copy-Folder($src, $dest) { Function Copy-File($source, $dest) {
if (Test-Path -Path $dest) { $diff = ""
if (-not (Get-Item -Path $dest -Force).PSIsContainer) { $copy_file = $false
Fail-Json $result "If src is a folder, dest must also be a folder. src: $src, dest: $dest" $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 { } else {
try { $copy_file = $true
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)"
} }
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"
} }
foreach ($item in Get-ChildItem -Path $src) { if (Test-Path -Path $dest -PathType Leaf) {
$dest_path = Join-Path -Path $dest -ChildPath $item.PSChildName Remove-Item -Path $dest -Force -Recurse | Out-Null
if ($item.PSIsContainer) { $diff += "-$dest`n"
Copy-Folder -src $item.FullName -dest $dest_path
} else {
Copy-File -src $item.FullName -dest $dest_path
} }
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"
Function Copy-File($src, $dest) { # make sure we set the attributes accordingly
if (Test-Path -Path $dest) { if (-not $check_mode) {
if ((Get-Item -Path $dest -Force).PSIsContainer) { $source_file = Get-Item -Path $source -Force
Fail-Json $result "If src is a file, dest must also be a file. src: $src, dest: $dest" $dest_file = Get-Item -Path $dest -Force
$dest_file.Attributes = $source_file.Attributes
$dest_file.SetAccessControl($source_file.GetAccessControl())
} }
$result.changed = $true
} }
$src_checksum = Get-FileChecksum -Path $src # ugly but to save us from running the checksum twice, let's return it for
$dest_checksum = Get-FileChecksum -Path $dest # the main code to add it to $result
if ($src_checksum -ne $dest_checksum) { return ,@{ diff = $diff; checksum = $source_checksum }
try { }
Copy-Item -Path $src -Destination $dest -Force -WhatIf:$check_mode
} catch { Function Copy-Folder($source, $dest) {
Fail-Json $result "Failed to copy file: $($_.Exception.Message)" $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 $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())
}
} }
# Verify the file we copied is the same $child_items = Get-ChildItem -Path $source -Force
$dest_checksum_verify = Get-FileChecksum -Path $dest foreach ($child_item in $child_items) {
if (-not ($check_mode) -and ($src_checksum -ne $dest_checksum_verify)) { $dest_child_path = Join-Path -Path $dest -ChildPath $child_item.Name
Fail-Json $result "Copied file does not match checksum. src: $src_checksum, dest: $dest_checksum_verify. Failed to copy file from $src to $dest" 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) { Function Get-FileSize($path) {
@ -108,56 +158,193 @@ Function Get-FileSize($path) {
$size $size
} }
if (-not (Test-Path -Path $src)) { if ($mode -eq "query") {
Fail-Json $result "Cannot copy src file: $src as it does not exist" # 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 = @()
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 copying from remote we need to get the original folder path and name and change dest to this path foreach ($directory in $directories) {
if ($original_basename) { $dirname = $directory.dest
$parent_path = Split-Path -Path $original_basename -Parent
if ($parent_path.length -gt 0) { $dirpath = Join-Path -Path $dest -ChildPath $dirname
$dest_folder = Join-Path -Path $dest -ChildPath $parent_path $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
}
}
# 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 { try {
New-Item -Path $dest_folder -Type directory -Force -WhatIf:$check_mode Add-Type -Assembly System.IO.Compression.FileSystem | Out-Null
$result.changed = $true Add-Type -Assembly System.IO.Compression | Out-Null
$result.zip_available = $true
} catch { } catch {
Fail-Json $result "Failed to create directory $($dest_folder): $($_.Exception.Message)" $result.zip_available = $false
} }
$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"
} }
if ((Get-Item -Path $dest -Force).PSIsContainer) { Add-Type -Assembly System.IO.Compression.FileSystem | Out-Null
$dest = Join-Path $dest -ChildPath $original_basename 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 the source is a container prepare for some recursive magic if (-not ($entry_target_path.EndsWith("`\") -or $entry_target_path.EndsWith("/"))) {
if ((Get-Item -Path $src -Force).PSIsContainer) { if (-not $check_mode) {
if (Test-Path -Path $dest) { [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $entry_target_path, $true)
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" }
} }
$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"
} }
$folder_name = (Get-Item -Path $src -Force).Name if (Test-Path -Path $src -PathType Container) {
$dest_path = Join-Path -Path $dest -ChildPath $folder_name # we are copying a directory or the contents of a directory
Copy-Folder -src $src -dest $dest_path $result.operation = 'folder_copy'
if ($result.changed -eq $true) { if ($src.EndsWith("/") -or $src.EndsWith("`\")) {
$result.operation = "folder_copy" # 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 {
Copy-File -src $src -dest $dest
if ($result.changed -eq $true) {
$result.operation = "file_copy"
} }
$result.original_basename = (Get-Item -Path $src -Force).Name } else {
$result.checksum = Get-FileChecksum -Path $src # 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
}
if ($check_mode) { # the file might not exist if running in check mode
# When in check mode the dest won't exit, just get the source size if (-not $check_mode -or (Test-Path -Path $dest -PathType Leaf)) {
$result.size = Get-FileSize -path $src
} else {
$result.size = Get-FileSize -path $dest $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
} }
Exit-Json $result Exit-Json -obj $result

@ -1,22 +1,11 @@
#!/usr/bin/python #!/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# (c) 2015, Jon Hawkesworth (@jhawkesworth) <figs@unity.demon.co.uk>
#
# This file is part of Ansible # This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify # (c) 2015, Jon Hawkesworth (@jhawkesworth) <figs@unity.demon.co.uk>
# it under the terms of the GNU General Public License as published by # Copyright (c) 2017 Ansible Project
# the Free Software Foundation, either version 3 of the License, or # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# (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 <http://www.gnu.org/licenses/>.
ANSIBLE_METADATA = {'metadata_version': '1.1', ANSIBLE_METADATA = {'metadata_version': '1.1',
@ -27,8 +16,8 @@ ANSIBLE_METADATA = {'metadata_version': '1.1',
DOCUMENTATION = r''' DOCUMENTATION = r'''
--- ---
module: win_copy module: win_copy
version_added: "1.9.2" version_added: '1.9.2'
short_description: Copies files to remote locations on windows hosts. short_description: Copies files to remote locations on windows hosts
description: description:
- The C(win_copy) module copies a file on the local box to remote windows locations. - 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. - 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 - Remote absolute path where the file should be copied to. If src is a
directory, this must be a directory too. directory, this must be a directory too.
- Use \ for path separators or \\ when in "double quotes". - 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 required: true
force: force:
version_added: "2.3" version_added: "2.3"
description: description:
- If set to C(yes), the remote file will be replaced when content is - If set to C(yes), the file will only be transferred if the content
different than the source. is different than destination.
- If set to C(no), the remote file will only be transferred if the - If set to C(no), the file will only be transferred if the
destination does not exist. destination does not exist.
default: True - If set to C(no), no checksuming of the content is performed which can
choices: help improve performance on larger files.
- yes default: 'yes'
- no 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: remote_src:
description: description:
- If False, it will search for src at originating/master machine, if True - If False, it will search for src at originating/master machine, if True
it will go to the remote/target machine for the src. it will go to the remote/target machine for the src.
default: False default: 'no'
choices: type: bool
- True
- False
version_added: "2.3" version_added: "2.3"
src: src:
description: description:
- Local path to a file to copy to the remote server; can be absolute or - 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, relative.
if path ends with "/", only inside contents of that directory are copied - If path is a directory, it is copied (including the source folder name)
to destination. Otherwise, if it does not end with "/", the directory recursively to C(dest).
itself with all contents is copied. This behavior is similar to Rsync. - 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 required: true
notes: notes:
- For non-Windows targets, use the M(copy) module instead. - 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''' EXAMPLES = r'''
- name: Copy a single file - name: Copy a single file
win_copy: win_copy:
src: /srv/myfiles/foo.conf src: /srv/myfiles/foo.conf
dest: c:\Temp\foo.conf dest: c:\Temp\renamed-foo.conf
- name: Copy files/temp_files to c:\temp
- 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: win_copy:
src: files/temp_files/ src: files/temp_files/
dest: c:\Temp dest: c:\Temp
- name: Copy a single file where the source is on the remote host - name: Copy a single file where the source is on the remote host
win_copy: win_copy:
src: C:\temp\foo.txt src: C:\temp\foo.txt
dest: C:\ansible\foo.txt dest: C:\ansible\foo.txt
remote_src: True remote_src: True
- name: Copy a folder recursively where the source is on the remote host - name: Copy a folder recursively where the source is on the remote host
win_copy: win_copy:
src: C:\temp src: C:\temp
dest: C:\ansible dest: C:\ansible
remote_src: True remote_src: True
- name: Set the contents of a file - name: Set the contents of a file
win_copy: win_copy:
dest: C:\temp\foo.txt dest: C:\temp\foo.txt
@ -121,12 +148,12 @@ checksum:
sample: 6e642bb8dd5c2e027bf21dd923337cbb4214f827 sample: 6e642bb8dd5c2e027bf21dd923337cbb4214f827
size: size:
description: size of the target, after execution 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 type: int
sample: 1220 sample: 1220
operation: operation:
description: whether a single file copy took place or a folder copy description: whether a single file copy took place or a folder copy
returned: changed returned: success
type: string type: string
sample: file_copy sample: file_copy
original_basename: original_basename:

@ -1,29 +1,522 @@
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
#
# This file is part of Ansible # This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify # Copyright (c) 2017 Ansible Project
# it under the terms of the GNU General Public License as published by # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# 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 <http://www.gnu.org/licenses/>.
# Make coding more python3-ish # Make coding more python3-ish
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __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 import ActionBase
from ansible.plugins.action.copy import ActionModule as CopyActionModule from ansible.utils.hashing import checksum
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
# Even though CopyActionModule inherits from ActionBase, we still need to # remove the content temp file if it was created
# directly inherit from ActionBase to appease the plugin loader. self._remove_tempfile_if_content_defined(content, content_tempfile)
class ActionModule(CopyActionModule, ActionBase): return result
pass

@ -0,0 +1 @@
test_win_copy_path: C:\ansible\win_copy

@ -1,2 +0,0 @@
dependencies:
- prepare_win_tests

@ -1,563 +1,24 @@
# test code for the copy module and action plugin ---
# (c) 2014, Michael DeHaan <michael.dehaan@gmail.com> - name: create empty folder
file:
# This file is part of Ansible path: '{{role_path}}/files/subdir/empty'
#
# 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 <http://www.gnu.org/licenses/>.
- name: remove win_output_dir
win_file:
path: "{{win_output_dir}}"
state: absent
- name: recreate win_output_dir
win_file:
path: "{{win_output_dir}}"
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 state: directory
delegate_to: localhost
- name: test recursive copy to directory - name: create test folder
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: win_file:
path: "{{win_output_dir}}\\subtrailing\\" path: '{{test_win_copy_path}}'
state: directory state: directory
- name: test recursive copy to directory - block:
win_copy: - name: run tests for local to remote
src: subdir/ include_tasks: tests.yml
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 - name: run tests for remote to remote
assert: include_tasks: remote_tests.yml
that:
- not check_copy_folder_remote_existing|changed
- name: cleanup output dir always:
- name: remove test folder
win_file: win_file:
path: "{{win_output_dir}}" path: '{{test_win_copy_path}}'
state: absent state: absent

@ -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'

@ -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
Loading…
Cancel
Save