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 impvpull/4420/head
parent
76b5a9fb52
commit
3d1dd0e599
|
@ -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
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
'''
|
||||||
|
|
|
@ -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
|
||||||
'''
|
'''
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
'''
|
'''
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}}
|
||||||
|
|
|
@ -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()
|
|
@ -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'
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue