Windows: Add backup parameter to modules (#50033)

* Windows: Add backup parameter to modules

This PR adds a backup infrastructure for modules.

* Fixes based on review feedback

* Various fixes to check-mode and backup

* Add integration tests

* Fix win_xml integration test

* Add backup support to copy action plugin

* Added integration tests

* Improve test efficiencies and other minor impv
pull/4420/head
Dag Wieers 2019-02-25 02:37:25 +01:00 committed by Jordan Borean
parent 76b5a9fb52
commit 3d1dd0e599
15 changed files with 365 additions and 95 deletions

View File

@ -0,0 +1,33 @@
# Copyright (c): 2018, Dag Wieers (@dagwieers) <dag@wieers.com>
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
Function Backup-File {
<#
.SYNOPSIS
Helper function to make a backup of a file.
.EXAMPLE
Backup-File -path $path -WhatIf:$check_mode
#>
[CmdletBinding(SupportsShouldProcess=$true)]
Param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string] $path
)
Process {
$backup_path = $null
if (Test-Path -LiteralPath $path -PathType Leaf) {
$backup_path = "$path.$pid." + [DateTime]::Now.ToString("yyyyMMdd-HHmmss") + ".bak";
Try {
Copy-Item -LiteralPath $path -Destination $backup_path
} Catch {
throw "Failed to create backup file '$backup_path' from '$path'. ($($_.Exception.Message))"
}
}
return $backup_path
}
}
# This line must stay at the bottom to ensure all defined module parts are exported
Export-ModuleMember -Function Backup-File

View File

@ -5,6 +5,7 @@
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#Requires -Module Ansible.ModuleUtils.Legacy #Requires -Module Ansible.ModuleUtils.Legacy
#Requires -Module Ansible.ModuleUtils.Backup
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
@ -22,6 +23,7 @@ $copy_mode = Get-AnsibleParam -obj $params -name "_copy_mode" -type "str" -defau
# used in explode, remote and single mode # used in explode, remote and single mode
$src = Get-AnsibleParam -obj $params -name "src" -type "path" -failifempty ($copy_mode -in @("explode","process","single")) $src = Get-AnsibleParam -obj $params -name "src" -type "path" -failifempty ($copy_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
$backup = Get-AnsibleParam -obj $params -name "backup" -type "bool" -default $false
# used in single mode # 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"
@ -74,6 +76,10 @@ Function Copy-File($source, $dest) {
$diff += "+$file_dir\`n" $diff += "+$file_dir\`n"
} }
if ($backup) {
$result.backup_file = Backup-File -path $dest -WhatIf:$check_mode
}
if (Test-Path -Path $dest -PathType Leaf) { if (Test-Path -Path $dest -PathType Leaf) {
Remove-Item -Path $dest -Force -Recurse -WhatIf:$check_mode | Out-Null Remove-Item -Path $dest -Force -Recurse -WhatIf:$check_mode | Out-Null
$diff += "-$dest`n" $diff += "-$dest`n"
@ -390,6 +396,10 @@ if ($copy_mode -eq "query") {
} }
} }
if ($backup) {
$result.backup_file = Backup-File -path $remote_dest -WhatIf:$check_mode
}
Copy-Item -Path $src -Destination $remote_dest -Force | Out-Null Copy-Item -Path $src -Destination $remote_dest -Force | Out-Null
$result.changed = $true $result.changed = $true
} }

View File

@ -43,8 +43,18 @@ options:
with "/" or "\", or C(src) is a directory. with "/" or "\", or C(src) is a directory.
- If C(src) and C(dest) are files and if the parent directory of C(dest) - If C(src) and C(dest) are files and if the parent directory of C(dest)
doesn't exist, then the task will fail. doesn't exist, then the task will fail.
required: yes
type: path type: path
required: yes
backup:
description:
- Determine whether a backup should be created.
- When set to C(yes), create a backup file including the timestamp information
so you can get the original file back if you somehow clobbered it incorrectly.
- No backup is taken when C(remote_src=False) and multiple files are being
copied.
type: bool
default: no
version_added: '2.8'
force: force:
description: description:
- If set to C(yes), the file will only be transferred if the content - If set to C(yes), the file will only be transferred if the content
@ -107,6 +117,12 @@ EXAMPLES = r'''
src: /srv/myfiles/foo.conf src: /srv/myfiles/foo.conf
dest: C:\Temp\renamed-foo.conf dest: C:\Temp\renamed-foo.conf
- name: Copy a single file, but keep a backup
win_copy:
src: /srv/myfiles/foo.conf
dest: C:\Temp\renamed-foo.conf
backup: yes
- name: Copy a single file keeping the filename - name: Copy a single file keeping the filename
win_copy: win_copy:
src: /src/myfiles/foo.conf src: /src/myfiles/foo.conf
@ -141,6 +157,11 @@ EXAMPLES = r'''
''' '''
RETURN = r''' RETURN = r'''
backup_file:
description: Name of the backup file that was created.
returned: if backup=yes
type: str
sample: C:\Path\To\File.txt.11540.20150212-220915.bak
dest: dest:
description: Destination file/path. description: Destination file/path.
returned: changed returned: changed

View File

@ -3,6 +3,7 @@
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#Requires -Module Ansible.ModuleUtils.Legacy #Requires -Module Ansible.ModuleUtils.Legacy
#Requires -Module Ansible.ModuleUtils.Backup
function WriteLines($outlines, $path, $linesep, $encodingobj, $validate, $check_mode) { function WriteLines($outlines, $path, $linesep, $encodingobj, $validate, $check_mode) {
Try { Try {
@ -42,14 +43,14 @@ function WriteLines($outlines, $path, $linesep, $encodingobj, $validate, $check_
# Commit changes to the path # Commit changes to the path
$cleanpath = $path.Replace("/", "\"); $cleanpath = $path.Replace("/", "\");
Try { Try {
Copy-Item $temppath $cleanpath -force -ErrorAction Stop -WhatIf:$check_mode; Copy-Item -Path $temppath -Destination $cleanpath -Force -WhatIf:$check_mode;
} }
Catch { Catch {
Fail-Json @{} "Cannot write to: $cleanpath ($($_.Exception.Message))"; Fail-Json @{} "Cannot write to: $cleanpath ($($_.Exception.Message))";
} }
Try { Try {
Remove-Item $temppath -force -ErrorAction Stop -WhatIf:$check_mode; Remove-Item -Path $temppath -Force -WhatIf:$check_mode;
} }
Catch { Catch {
Fail-Json @{} "Cannot remove temporary file: $temppath ($($_.Exception.Message))"; Fail-Json @{} "Cannot remove temporary file: $temppath ($($_.Exception.Message))";
@ -60,19 +61,6 @@ function WriteLines($outlines, $path, $linesep, $encodingobj, $validate, $check_
} }
# Backup the file specified with a date/time filename
function BackupFile($path, $check_mode) {
$backuppath = $path + "." + [DateTime]::Now.ToString("yyyyMMdd-HHmmss");
Try {
Copy-Item $path $backuppath -WhatIf:$check_mode;
}
Catch {
Fail-Json @{} "Cannot copy backup file! ($($_.Exception.Message))";
}
return $backuppath;
}
# Implement the functionality for state == 'present' # Implement the functionality for state == 'present'
function Present($path, $regexp, $line, $insertafter, $insertbefore, $create, $backup, $backrefs, $validate, $encodingobj, $linesep, $check_mode, $diff_support) { function Present($path, $regexp, $line, $insertafter, $insertbefore, $create, $backup, $backrefs, $validate, $encodingobj, $linesep, $check_mode, $diff_support) {
@ -198,7 +186,9 @@ function Present($path, $regexp, $line, $insertafter, $insertbefore, $create, $b
# Write backup file if backup == "yes" # Write backup file if backup == "yes"
If ($backup) { If ($backup) {
$result.backup = BackupFile $path $check_mode; $result.backup_file = Backup-File -path $path -WhatIf:$check_mode
# Ensure backward compatibility (deprecate in future)
$result.backup = $result.backup_file
} }
$after = WriteLines $lines $path $linesep $encodingobj $validate $check_mode; $after = WriteLines $lines $path $linesep $encodingobj $validate $check_mode;
@ -278,7 +268,9 @@ function Absent($path, $regexp, $line, $backup, $validate, $encodingobj, $linese
# Write backup file if backup == "yes" # Write backup file if backup == "yes"
If ($backup) { If ($backup) {
$result.backup = BackupFile $path $check_mode; $result.backup_file = Backup-File -path $path -WhatIf:$check_mode
# Ensure backward compatibility (deprecate in future)
$result.backup = $result.backup_file
} }
$after = WriteLines $left $path $linesep $encodingobj $validate $check_mode; $after = WriteLines $left $path $linesep $encodingobj $validate $check_mode;

View File

@ -24,6 +24,13 @@ options:
type: path type: path
required: yes required: yes
aliases: [ dest, destfile, name ] aliases: [ dest, destfile, name ]
backup:
description:
- Determine whether a backup should be created.
- When set to C(yes), create a backup file including the timestamp information
so you can get the original file back if you somehow clobbered it incorrectly.
type: bool
default: no
regexp: regexp:
description: description:
- The regular expression to look for in every line of the file. For C(state=present), the pattern to replace if found; only the last line found - The regular expression to look for in every line of the file. For C(state=present), the pattern to replace if found; only the last line found
@ -70,11 +77,6 @@ options:
- Used with C(state=present). If specified, the file will be created if it does not already exist. By default it will fail if the file is missing. - Used with C(state=present). If specified, the file will be created if it does not already exist. By default it will fail if the file is missing.
type: bool type: bool
default: no default: no
backup:
description:
- Create a backup file including the timestamp information so you can get the original file back if you somehow clobbered it incorrectly.
type: bool
default: no
validate: validate:
description: description:
- Validation to run before copying into place. Use %s in the command to indicate the current file to validate. - Validation to run before copying into place. Use %s in the command to indicate the current file to validate.
@ -160,3 +162,18 @@ EXAMPLES = r'''
regexp: '(^name=)' regexp: '(^name=)'
line: '$1JohnDoe' line: '$1JohnDoe'
''' '''
RETURN = r'''
backup:
description:
- Name of the backup file that was created.
- This is now deprecated, use C(backup_file) instead.
returned: if backup=yes
type: str
sample: C:\Path\To\File.txt.11540.20150212-220915.bak
backup_file:
description: Name of the backup file that was created.
returned: if backup=yes
type: str
sample: C:\Path\To\File.txt.11540.20150212-220915.bak
'''

View File

@ -40,8 +40,16 @@ options:
dest: dest:
description: description:
- Location to render the template to on the remote machine. - Location to render the template to on the remote machine.
type: str type: path
required: yes required: yes
backup:
description:
- Determine whether a backup should be created.
- When set to C(yes), create a backup file including the timestamp information
so you can get the original file back if you somehow clobbered it incorrectly.
type: bool
default: no
version_added: '2.8'
newline_sequence: newline_sequence:
description: description:
- Specify the newline sequence to use for templating files. - Specify the newline sequence to use for templating files.
@ -99,6 +107,9 @@ notes:
which changes the variable interpolation markers to [% var %] instead of {{ var }}. which changes the variable interpolation markers to [% var %] instead of {{ var }}.
This is the best way to prevent evaluation of things that look like, but should not be Jinja2. This is the best way to prevent evaluation of things that look like, but should not be Jinja2.
raw/endraw in Jinja2 will not work as you expect because templates in Ansible are recursively evaluated." raw/endraw in Jinja2 will not work as you expect because templates in Ansible are recursively evaluated."
- You can use the M(win_copy) module with the C(content:) option if you prefer the template inline,
as part of the playbook.
seealso: seealso:
- module: template - module: template
- module: win_copy - module: win_copy
@ -117,4 +128,13 @@ EXAMPLES = r'''
src: unix/config.conf.j2 src: unix/config.conf.j2
dest: C:\share\unix\config.conf dest: C:\share\unix\config.conf
newline_sequence: '\n' newline_sequence: '\n'
backup: yes
'''
RETURN = r'''
backup_file:
description: Name of the backup file that was created.
returned: if backup=yes
type: str
sample: C:\Path\To\File.txt.11540.20150212-220915.bak
''' '''

View File

@ -4,6 +4,7 @@
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#Requires -Module Ansible.ModuleUtils.Legacy #Requires -Module Ansible.ModuleUtils.Legacy
#Requires -Module Ansible.ModuleUtils.Backup
Set-StrictMode -Version 2 Set-StrictMode -Version 2
@ -79,12 +80,6 @@ function Compare-XmlDocs($actual, $expected) {
} }
} }
function BackupFile($path) {
$backuppath = $path + "." + [DateTime]::Now.ToString("yyyyMMdd-HHmmss");
Copy-Item $path $backuppath;
return $backuppath;
}
$params = Parse-Args $args -supports_check_mode $true $params = Parse-Args $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
@ -94,7 +89,7 @@ $debug = $debug_level -gt 2
$dest = Get-AnsibleParam $params "path" -type "path" -FailIfEmpty $true -aliases "dest", "file" $dest = Get-AnsibleParam $params "path" -type "path" -FailIfEmpty $true -aliases "dest", "file"
$fragment = Get-AnsibleParam $params "fragment" -type "str" -FailIfEmpty $true -aliases "xmlstring" $fragment = Get-AnsibleParam $params "fragment" -type "str" -FailIfEmpty $true -aliases "xmlstring"
$xpath = Get-AnsibleParam $params "xpath" -type "str" -FailIfEmpty $true $xpath = Get-AnsibleParam $params "xpath" -type "str" -FailIfEmpty $true
$backup = Get-AnsibleParam $params "backup" -type "bool" -Default $false $backup = Get-AnsibleParam $params "backup" -type "bool" -default $false
$type = Get-AnsibleParam $params "type" -type "str" -Default "element" -ValidateSet "element", "attribute", "text" $type = Get-AnsibleParam $params "type" -type "str" -Default "element" -ValidateSet "element", "attribute", "text"
$attribute = Get-AnsibleParam $params "attribute" -type "str" -FailIfEmpty ($type -eq "attribute") $attribute = Get-AnsibleParam $params "attribute" -type "str" -FailIfEmpty ($type -eq "attribute")
$state = Get-AnsibleParam $params "state" -type "str" -Default "present" $state = Get-AnsibleParam $params "state" -type "str" -Default "present"
@ -174,15 +169,15 @@ if ($type -eq "element") {
} }
if ($changed) { if ($changed) {
$result.changed = $true if ($backup) {
if (!$check_mode) { $result.backup_file = Backup-File -path $dest -WhatIf:$check_mode
if ($backup) { # Ensure backward compatibility (deprecate in future)
$result.backup = BackupFile($dest) $result.backup = $result.backup_file
}
$xmlorig.Save($dest)
} else {
$result.msg += " check mode"
} }
if (-not $check_mode) {
$xmlorig.Save($dest)
}
$result.changed = $true
} else { } else {
$result.msg = "not changed" $result.msg = "not changed"
} }
@ -190,17 +185,17 @@ if ($type -eq "element") {
$node = $xmlorig.SelectSingleNode($xpath, $namespaceMgr) $node = $xmlorig.SelectSingleNode($xpath, $namespaceMgr)
[bool]$add = ($node.get_InnerText() -ne $fragment) [bool]$add = ($node.get_InnerText() -ne $fragment)
if ($add) { if ($add) {
$result.changed = $true if ($backup) {
if (-Not $check_mode) { $result.backup_file = Backup-File -path $dest -WhatIf:$check_mode
if ($backup) { # Ensure backward compatibility (deprecate in future)
$result.backup = BackupFile($dest) $result.backup = $result.backup_file
}
$node.set_InnerText($fragment)
$xmlorig.Save($dest)
$result.msg = "text changed"
} else {
$result.msg = "text changed check mode"
} }
$node.set_InnerText($fragment)
if (-not $check_mode) {
$xmlorig.Save($dest)
}
$result.changed = $true
$result.msg = "text changed"
} else { } else {
$result.msg = "not changed" $result.msg = "not changed"
} }
@ -208,33 +203,35 @@ if ($type -eq "element") {
$node = $xmlorig.SelectSingleNode($xpath, $namespaceMgr) $node = $xmlorig.SelectSingleNode($xpath, $namespaceMgr)
[bool]$add = !$node.HasAttribute($attribute) -Or ($node.$attribute -ne $fragment) [bool]$add = !$node.HasAttribute($attribute) -Or ($node.$attribute -ne $fragment)
if ($add -And ($state -eq "present")) { if ($add -And ($state -eq "present")) {
$result.changed = $true if ($backup) {
if (-Not $check_mode) { $result.backup_file = Backup-File -path $dest -WhatIf:$check_mode
if ($backup) { # Ensure backward compatibility (deprecate in future)
$result.backup = BackupFile($dest) $result.backup = $result.backup_file
}
if (!$node.HasAttribute($attribute)) {
$node.SetAttributeNode($attribute, $xmlorig.get_DocumentElement().get_NamespaceURI())
}
$node.SetAttribute($attribute, $fragment)
$xmlorig.Save($dest)
$result.msg = "text changed"
} else {
$result.msg = "text changed check mode"
} }
if (!$node.HasAttribute($attribute)) {
$node.SetAttributeNode($attribute, $xmlorig.get_DocumentElement().get_NamespaceURI())
}
$node.SetAttribute($attribute, $fragment)
if (-not $check_mode) {
$xmlorig.Save($dest)
}
$result.changed = $true
$result.msg = "text changed"
} elseif (!$add -And ($state -eq "absent")) { } elseif (!$add -And ($state -eq "absent")) {
$result.changed = $true if ($backup) {
if (-Not $check_mode) { $result.backup_file = Backup-File -path $dest -WhatIf:$check_mode
if ($backup) { # Ensure backward compatibility (deprecate in future)
$result.backup = BackupFile($dest) $result.backup = $result.backup_file
}
$node.RemoveAttribute($attribute)
$xmlorig.Save($dest)
$result.msg = "text changed"
} }
$node.RemoveAttribute($attribute)
if (-not $check_mode) {
$xmlorig.Save($dest)
}
$result.changed = $true
$result.msg = "text changed"
} else { } else {
$result.msg = "not changed" $result.msg = "not changed"
} }
} }
Exit-Json $result Exit-Json $result

View File

@ -23,7 +23,7 @@ options:
path: path:
description: description:
- The path of remote servers XML. - The path of remote servers XML.
type: str type: path
required: true required: true
aliases: [ dest, file ] aliases: [ dest, file ]
fragment: fragment:
@ -39,7 +39,9 @@ options:
required: true required: true
backup: backup:
description: description:
- Whether to backup the remote server's XML before applying the change. - Determine whether a backup should be created.
- When set to C(yes), create a backup file including the timestamp information
so you can get the original file back if you somehow clobbered it incorrectly.
type: bool type: bool
default: no default: no
type: type:
@ -75,6 +77,11 @@ EXAMPLES = r'''
''' '''
RETURN = r''' RETURN = r'''
backup_file:
description: Name of the backup file that was created.
returned: if backup=yes
type: str
sample: C:\Path\To\File.txt.11540.20150212-220915.bak
msg: msg:
description: What was done. description: What was done.
returned: always returned: always
@ -85,9 +92,4 @@ err:
returned: always, for type element and -vvv or more returned: always, for type element and -vvv or more
type: list type: list
sample: attribute mismatch for actual=string sample: attribute mismatch for actual=string
backup:
description: Name of the backup file, if created.
returned: changed
type: str
sample: C:\config.xml.19700101-000000
''' '''

View File

@ -260,7 +260,7 @@ class ActionModule(ActionBase):
if content is not None: if content is not None:
os.remove(content_tempfile) os.remove(content_tempfile)
def _copy_single_file(self, local_file, dest, source_rel, task_vars, tmp): def _copy_single_file(self, local_file, dest, source_rel, task_vars, tmp, backup):
if self._play_context.check_mode: if self._play_context.check_mode:
module_return = dict(changed=True) module_return = dict(changed=True)
return module_return return module_return
@ -275,7 +275,8 @@ class ActionModule(ActionBase):
dest=dest, dest=dest,
src=tmp_src, src=tmp_src,
_original_basename=source_rel, _original_basename=source_rel,
_copy_mode="single" _copy_mode="single",
backup=backup,
) )
) )
copy_args.pop('content', None) copy_args.pop('content', None)
@ -286,7 +287,7 @@ class ActionModule(ActionBase):
return copy_result return copy_result
def _copy_zip_file(self, dest, files, directories, task_vars, tmp): def _copy_zip_file(self, dest, files, directories, task_vars, tmp, backup):
# create local zip file containing all the files and directories that # create local zip file containing all the files and directories that
# need to be copied to the server # need to be copied to the server
if self._play_context.check_mode: if self._play_context.check_mode:
@ -317,7 +318,8 @@ class ActionModule(ActionBase):
dict( dict(
src=tmp_src, src=tmp_src,
dest=dest, dest=dest,
_copy_mode="explode" _copy_mode="explode",
backup=backup,
) )
) )
copy_args.pop('content', None) copy_args.pop('content', None)
@ -342,6 +344,7 @@ class ActionModule(ActionBase):
local_follow = boolean(self._task.args.get('local_follow', False), strict=False) local_follow = boolean(self._task.args.get('local_follow', False), strict=False)
force = boolean(self._task.args.get('force', True), strict=False) force = boolean(self._task.args.get('force', True), strict=False)
decrypt = boolean(self._task.args.get('decrypt', True), strict=False) decrypt = boolean(self._task.args.get('decrypt', True), strict=False)
backup = boolean(self._task.args.get('backup', False), strict=False)
result['src'] = source result['src'] = source
result['dest'] = dest result['dest'] = dest
@ -383,7 +386,8 @@ class ActionModule(ActionBase):
_copy_mode="remote", _copy_mode="remote",
dest=dest, dest=dest,
src=source, src=source,
force=force force=force,
backup=backup,
) )
) )
new_module_args.pop('content', None) new_module_args.pop('content', None)
@ -472,7 +476,7 @@ class ActionModule(ActionBase):
force=force, force=force,
files=source_files['files'], files=source_files['files'],
directories=source_files['directories'], directories=source_files['directories'],
symlinks=source_files['symlinks'] symlinks=source_files['symlinks'],
) )
) )
# src is not required for query, will fail path validation is src has unix allowed chars # src is not required for query, will fail path validation is src has unix allowed chars
@ -493,20 +497,19 @@ class ActionModule(ActionBase):
# we only need to copy 1 file, don't mess around with zips # we only need to copy 1 file, don't mess around with zips
file_src = query_return['files'][0]['src'] file_src = query_return['files'][0]['src']
file_dest = query_return['files'][0]['dest'] file_dest = query_return['files'][0]['dest']
copy_result = self._copy_single_file(file_src, dest, file_dest, result.update(self._copy_single_file(file_src, dest, file_dest,
task_vars, self._connection._shell.tmpdir) task_vars, self._connection._shell.tmpdir, backup))
if result.get('failed') is True:
result['msg'] = "failed to copy file %s: %s" % (file_src, result['msg'])
result['changed'] = True result['changed'] = True
if copy_result.get('failed') is True:
result['failed'] = True
result['msg'] = "failed to copy file %s: %s" % (file_src, copy_result['msg'])
elif len(query_return['files']) > 0 or len(query_return['directories']) > 0: elif len(query_return['files']) > 0 or len(query_return['directories']) > 0:
# either multiple files or directories need to be copied, compress # either multiple files or directories need to be copied, compress
# to a zip and 'explode' the zip on the server # to a zip and 'explode' the zip on the server
# TODO: handle symlinks # TODO: handle symlinks
result.update(self._copy_zip_file(dest, source_files['files'], result.update(self._copy_zip_file(dest, source_files['files'],
source_files['directories'], source_files['directories'],
task_vars, self._connection._shell.tmpdir)) task_vars, self._connection._shell.tmpdir, backup))
result['changed'] = True result['changed'] = True
else: else:
# no operations need to occur # no operations need to occur

View File

@ -172,6 +172,24 @@
that: that:
- copy_file_again is not changed - copy_file_again is not changed
- name: copy single file (backup)
win_copy:
content: "{{ lookup('file', 'foo.txt') }}\nfoo bar"
dest: '{{test_win_copy_path}}\foo-target.txt'
backup: yes
register: copy_file_backup
- name: check backup_file
win_stat:
path: '{{ copy_file_backup.backup_file }}'
register: backup_file
- name: assert copy single file (backup)
assert:
that:
- copy_file_backup is changed
- backup_file.stat.exists == true
- name: copy single file to folder (check mode) - name: copy single file to folder (check mode)
win_copy: win_copy:
src: foo.txt src: foo.txt

View File

@ -43,12 +43,17 @@
win_lineinfile: dest={{win_output_dir}}/test.txt state=present line="New line at the beginning" insertbefore="BOF" backup=yes win_lineinfile: dest={{win_output_dir}}/test.txt state=present line="New line at the beginning" insertbefore="BOF" backup=yes
register: result register: result
- name: check backup_file
win_stat:
path: '{{ result.backup_file }}'
register: backup_file
- name: assert that the line was inserted at the head of the file - name: assert that the line was inserted at the head of the file
assert: assert:
that: that:
- "result.changed == true" - result.changed == true
- "result.msg == 'line added'" - result.msg == 'line added'
- "result.backup != ''" - backup_file.stat.exists == true
- name: stat the backup file - name: stat the backup file
win_stat: path={{result.backup}} win_stat: path={{result.backup}}

View File

@ -0,0 +1,89 @@
#!powershell
# Copyright: (c) 2019, Dag Wieers (@dagwieers) <dag@wieers.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#AnsibleRequires -CSharpUtil Ansible.Basic
#Requires -Module Ansible.ModuleUtils.Backup
$module = [Ansible.Basic.AnsibleModule]::Create($args, @{})
Function Assert-Equals {
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true)][AllowNull()]$Actual,
[Parameter(Mandatory=$true, Position=0)][AllowNull()]$Expected
)
$matched = $false
if ($Actual -is [System.Collections.ArrayList] -or $Actual -is [Array]) {
$Actual.Count | Assert-Equals -Expected $Expected.Count
for ($i = 0; $i -lt $Actual.Count; $i++) {
$actual_value = $Actual[$i]
$expected_value = $Expected[$i]
Assert-Equals -Actual $actual_value -Expected $expected_value
}
$matched = $true
} else {
$matched = $Actual -ceq $Expected
}
if (-not $matched) {
if ($Actual -is [PSObject]) {
$Actual = $Actual.ToString()
}
$call_stack = (Get-PSCallStack)[1]
$module.Result.test = $test
$module.Result.actual = $Actual
$module.Result.expected = $Expected
$module.Result.line = $call_stack.ScriptLineNumber
$module.Result.method = $call_stack.Position.Text
$module.FailJson("AssertionError: actual != expected")
}
}
$tmp_dir = $module.Tmpdir
$tests = @{
"Test backup file with missing file" = {
$actual = Backup-File -path (Join-Path -Path $tmp_dir -ChildPath "missing")
$actual | Assert-Equals -Expected $null
}
"Test backup file in check mode" = {
$orig_file = Join-Path -Path $tmp_dir -ChildPath "file-check.txt"
Set-Content -Path $orig_file -Value "abc"
$actual = Backup-File -path $orig_file -WhatIf
(Test-Path -LiteralPath $actual) | Assert-Equals -Expected $false
$parent_dir = Split-Path -Path $actual
$backup_file = Split-Path -Path $actual -Leaf
$parent_dir | Assert-Equals -Expected $tmp_dir
($backup_file -match "^file-check\.txt\.$pid\.\d{8}-\d{6}\.bak$") | Assert-Equals -Expected $true
}
"Test backup file" = {
$content = "abc"
$orig_file = Join-Path -Path $tmp_dir -ChildPath "file.txt"
Set-Content -Path $orig_file -Value $content
$actual = Backup-File -path $orig_file
(Test-Path -LiteralPath $actual) | Assert-Equals -Expected $true
$parent_dir = Split-Path -Path $actual
$backup_file = Split-Path -Path $actual -Leaf
$parent_dir | Assert-Equals -Expected $tmp_dir
($backup_file -match "^file\.txt\.$pid\.\d{8}-\d{6}\.bak$") | Assert-Equals -Expected $true
(Get-Content -Path $actual -Raw) | Assert-Equals -Expected "$content`r`n"
}
}
foreach ($test_impl in $tests.GetEnumerator()) {
$test = $test_impl.Key
&$test_impl.Value
}
$module.Result.res = 'success'
$module.ExitJson()

View File

@ -163,3 +163,13 @@
that: that:
- not add_type_test is failed - not add_type_test is failed
- add_type_test.res == 'success' - add_type_test.res == 'success'
- name: call module with BackupFile tests
backup_file_test:
register: backup_file_test
- name: assert call module with BackupFile tests
assert:
that:
- not backup_file_test is failed
- backup_file_test.res == 'success'

View File

@ -119,6 +119,39 @@
that: that:
- '"FC: no differences encountered" in diff_result.stdout' - '"FC: no differences encountered" in diff_result.stdout'
# TEST BACKUP
- name: test backup (check_mode)
win_template:
src: foo.j2
dest: '{{ win_output_dir }}/foo.unix.templated'
backup: yes
register: cm_backup_result
check_mode: yes
- name: verify that a backup_file was returned
assert:
that:
- cm_backup_result is changed
- cm_backup_result.backup_file is not none
- name: test backup (normal mode)
win_template:
src: foo.j2
dest: '{{ win_output_dir }}/foo.unix.templated'
backup: yes
register: nm_backup_result
- name: check backup_file
win_stat:
path: '{{ nm_backup_result.backup_file }}'
register: backup_file
- name: verify that a backup_file was returned
assert:
that:
- nm_backup_result is changed
- backup_file.stat.exists == true
- name: create template dest directory - name: create template dest directory
win_file: win_file:
path: '{{win_output_dir}}\directory' path: '{{win_output_dir}}\directory'

View File

@ -70,7 +70,7 @@
- name: check attribute change result - name: check attribute change result
assert: assert:
that: that:
- not attribute_changed_result is changed - attribute_changed_result is not changed
# This testing is for https://github.com/ansible/ansible/issues/48471 # This testing is for https://github.com/ansible/ansible/issues/48471
# The issue was that an .xml with no encoding declaration, but a UTF8 BOM # The issue was that an .xml with no encoding declaration, but a UTF8 BOM
@ -105,6 +105,26 @@
that: that:
- sha1_checksum.stat.checksum == 'e3e18c3066e1bfce9a5cf87c81353fa174440944' - sha1_checksum.stat.checksum == 'e3e18c3066e1bfce9a5cf87c81353fa174440944'
- name: change a text value in a file with UTF8 BOM and armenian characters in the description
win_xml:
path: "{{ win_output_dir }}\\plane-utf8-bom-armenian-characters.xml"
xpath: '/plane/year'
type: text
fragment: '1989'
backup: yes
register: test_backup
- name: check backup_file
win_stat:
path: '{{ test_backup.backup_file }}'
register: backup_file
- name: Check backup_file
assert:
that:
- test_backup is changed
- backup_file.stat.exists == true
- name: change a text value in a file with UTF-16 BE BOM and Chinese characters in the description - name: change a text value in a file with UTF-16 BE BOM and Chinese characters in the description
win_xml: win_xml:
path: "{{ win_output_dir }}\\plane-utf16be-bom-chinese-characters.xml" path: "{{ win_output_dir }}\\plane-utf16be-bom-chinese-characters.xml"