win_xml module for manipulating XML files on Windows (#26404)
documentation fixups handling backup in a more ansible canonical way remove quotes from $dest Handle elements with only text child nodes
This commit is contained in:
parent
113336d6f1
commit
c759381b0b
7 changed files with 458 additions and 0 deletions
239
lib/ansible/modules/windows/win_xml.ps1
Normal file
239
lib/ansible/modules/windows/win_xml.ps1
Normal file
|
@ -0,0 +1,239 @@
|
|||
#!powershell
|
||||
|
||||
# Copyright: (c) 2018, 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
|
||||
|
||||
Set-StrictMode -Version 2
|
||||
|
||||
function Copy-Xml($dest, $src, $xmlorig) {
|
||||
if ($src.get_NodeType() -eq "Text") {
|
||||
$dest.set_InnerText($src.get_InnerText())
|
||||
}
|
||||
|
||||
if ($src.get_HasAttributes()) {
|
||||
foreach ($attr in $src.get_Attributes()) {
|
||||
$dest.SetAttribute($attr.get_Name(), $attr.get_Value())
|
||||
}
|
||||
}
|
||||
|
||||
if ($src.get_HasChildNodes()) {
|
||||
foreach ($childnode in $src.get_ChildNodes()) {
|
||||
if ($childnode.get_NodeType() -eq "Element") {
|
||||
$newnode = $xmlorig.CreateElement($childnode.get_Name(), $xmlorig.get_DocumentElement().get_NamespaceURI())
|
||||
Copy-Xml $newnode $childnode $xmlorig
|
||||
$dest.AppendChild($newnode) | Out-Null
|
||||
} elseif ($childnode.get_NodeType() -eq "Text") {
|
||||
$dest.set_InnerText($childnode.get_InnerText())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Compare-XmlDocs($actual, $expected) {
|
||||
if ($actual.get_Name() -ne $expected.get_Name()) {
|
||||
throw "Actual name not same as expected: actual=" + $actual.get_Name() + ", expected=" + $expected.get_Name()
|
||||
}
|
||||
##attributes...
|
||||
|
||||
if (($actual.get_NodeType() -eq "Element") -and ($expected.get_NodeType() -eq "Element")) {
|
||||
if ($actual.get_HasAttributes() -and $expected.get_HasAttributes()) {
|
||||
if ($actual.get_Attributes().Count -ne $expected.get_Attributes().Count) {
|
||||
throw "attribute mismatch for actual=" + $actual.get_Name()
|
||||
}
|
||||
for ($i=0;$i -lt $expected.get_Attributes().Count; $i =$i+1) {
|
||||
if ($expected.get_Attributes()[$i].get_Name() -ne $actual.get_Attributes()[$i].get_Name()) {
|
||||
throw "attribute name mismatch for actual=" + $actual.get_Name()
|
||||
}
|
||||
if ($expected.get_Attributes()[$i].get_Value() -ne $actual.get_Attributes()[$i].get_Value()) {
|
||||
throw "attribute value mismatch for actual=" + $actual.get_Name()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (($actual.get_HasAttributes() -and !$expected.get_HasAttributes()) -or (!$actual.get_HasAttributes() -and $expected.get_HasAttributes())) {
|
||||
throw "attribute presence mismatch for actual=" + $actual.get_Name()
|
||||
}
|
||||
}
|
||||
|
||||
##children
|
||||
if ($expected.get_ChildNodes().Count -ne $actual.get_ChildNodes().Count) {
|
||||
throw "child node mismatch. for actual=" + $actual.get_Name()
|
||||
}
|
||||
|
||||
for ($i=0;$i -lt $expected.get_ChildNodes().Count; $i =$i+1) {
|
||||
if (-not $actual.get_ChildNodes()[$i]) {
|
||||
throw "actual missing child nodes. for actual=" + $actual.get_Name()
|
||||
}
|
||||
Compare-XmlDocs $expected.get_ChildNodes()[$i] $actual.get_ChildNodes()[$i]
|
||||
}
|
||||
|
||||
if ($expected.get_InnerText()) {
|
||||
if ($expected.get_InnerText() -ne $actual.get_InnerText()) {
|
||||
throw "inner text mismatch for actual=" + $actual.get_Name()
|
||||
}
|
||||
}
|
||||
elseif ($actual.get_InnerText()) {
|
||||
throw "actual has inner text but expected does not for actual=" + $actual.get_Name()
|
||||
}
|
||||
}
|
||||
|
||||
function BackupFile($path) {
|
||||
$backuppath = $path + "." + [DateTime]::Now.ToString("yyyyMMdd-HHmmss");
|
||||
Copy-Item $path $backuppath;
|
||||
return $backuppath;
|
||||
}
|
||||
|
||||
$params = Parse-Args $args -supports_check_mode $true
|
||||
$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
|
||||
|
||||
$debug_level = Get-AnsibleParam -obj $params -name "_ansible_verbosity" -type "int"
|
||||
$debug = $debug_level -gt 2
|
||||
|
||||
$dest = Get-AnsibleParam $params "path" -type "path" -FailIfEmpty $true -aliases "dest", "file"
|
||||
$fragment = Get-AnsibleParam $params "fragment" -type "str" -FailIfEmpty $true -aliases "xmlstring"
|
||||
$xpath = Get-AnsibleParam $params "xpath" -type "str" -FailIfEmpty $true
|
||||
$backup = Get-AnsibleParam $params "backup" -type "bool" -Default $false
|
||||
$type = Get-AnsibleParam $params "type" -type "str" -Default "element" -ValidateSet "element", "attribute", "text"
|
||||
$attribute = Get-AnsibleParam $params "attribute" -type "str" -FailIfEmpty ($type -eq "attribute")
|
||||
$state = Get-AnsibleParam $params "state" -type "str" -Default "present"
|
||||
|
||||
$result = @{
|
||||
changed = $false
|
||||
}
|
||||
|
||||
If (-Not (Test-Path -Path $dest -PathType Leaf)){
|
||||
Fail-Json $result "Specified path $dest does not exist or is not a file."
|
||||
}
|
||||
|
||||
[xml]$xmlorig = $null
|
||||
Try {
|
||||
[xml]$xmlorig = Get-Content -Path $dest
|
||||
}
|
||||
Catch {
|
||||
Fail-Json $result "Failed to parse file at '$dest' as an XML document: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
$namespaceMgr = New-Object System.Xml.XmlNamespaceManager $xmlorig.NameTable
|
||||
$namespace = $xmlorig.DocumentElement.NamespaceURI
|
||||
$localname = $xmlorig.DocumentElement.LocalName
|
||||
|
||||
$namespaceMgr.AddNamespace($xmlorig.$localname.SchemaInfo.Prefix, $namespace)
|
||||
|
||||
if ($type -eq "element") {
|
||||
$xmlchild = $null
|
||||
Try {
|
||||
$xmlchild = [xml]$fragment
|
||||
} Catch {
|
||||
Fail-Json $result "Failed to parse fragment as XML: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
$child = $xmlorig.CreateElement($xmlchild.get_DocumentElement().get_Name(), $xmlorig.get_DocumentElement().get_NamespaceURI())
|
||||
Copy-Xml $child $xmlchild.DocumentElement $xmlorig
|
||||
|
||||
$node = $xmlorig.SelectSingleNode($xpath, $namespaceMgr)
|
||||
if ($node.get_NodeType() -eq "Document") {
|
||||
$node = $node.get_DocumentElement()
|
||||
}
|
||||
$elements = $node.get_ChildNodes()
|
||||
[bool]$present = $false
|
||||
[bool]$changed = $false
|
||||
if ($elements.get_Count()) {
|
||||
if ($debug) {
|
||||
$err = @()
|
||||
$result.err = {$err}.Invoke()
|
||||
}
|
||||
foreach ($element in $elements) {
|
||||
try {
|
||||
Compare-XmlDocs $child $element
|
||||
$present = $true
|
||||
break
|
||||
} catch {
|
||||
if ($debug) {
|
||||
$result.err.Add($_.Exception.ToString())
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$present -and ($state -eq "present")) {
|
||||
[void]$node.AppendChild($child)
|
||||
$result.msg = "xml added"
|
||||
$changed = $true
|
||||
} elseif ($present -and ($state -eq "absent")) {
|
||||
[void]$node.RemoveChild($element)
|
||||
$result.msg = "xml removed"
|
||||
$changed = $true
|
||||
}
|
||||
} else {
|
||||
if ($state -eq "present") {
|
||||
[void]$node.AppendChild($child)
|
||||
$result.msg = "xml added"
|
||||
$changed = $true
|
||||
}
|
||||
}
|
||||
|
||||
if ($changed) {
|
||||
$result.changed = $true
|
||||
if (!$check_mode) {
|
||||
if ($backup) {
|
||||
$result.backup = BackupFile($dest)
|
||||
}
|
||||
$xmlorig.Save($dest)
|
||||
} else {
|
||||
$result.msg += " check mode"
|
||||
}
|
||||
} else {
|
||||
$result.msg = "not changed"
|
||||
}
|
||||
} elseif ($type -eq "text") {
|
||||
$node = $xmlorig.SelectSingleNode($xpath, $namespaceMgr)
|
||||
[bool]$add = ($node.get_InnerText() -ne $fragment)
|
||||
if ($add) {
|
||||
$result.changed = $true
|
||||
if (-Not $check_mode) {
|
||||
if ($backup) {
|
||||
$result.backup = BackupFile($dest)
|
||||
}
|
||||
$node.set_InnerText($fragment)
|
||||
$xmlorig.Save($dest)
|
||||
$result.msg = "text changed"
|
||||
} else {
|
||||
$result.msg = "text changed check mode"
|
||||
}
|
||||
} else {
|
||||
$result.msg = "not changed"
|
||||
}
|
||||
} elseif ($type -eq "attribute") {
|
||||
$node = $xmlorig.SelectSingleNode($xpath, $namespaceMgr)
|
||||
[bool]$add = !$node.HasAttribute($attribute) -Or ($node.$attribute -ne $fragment)
|
||||
if ($add -And ($state -eq "present")) {
|
||||
$result.changed = $true
|
||||
if (-Not $check_mode) {
|
||||
if ($backup) {
|
||||
$result.backup = BackupFile($dest)
|
||||
}
|
||||
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"
|
||||
}
|
||||
} elseif (!$add -And ($state -eq "absent")) {
|
||||
$result.changed = $true
|
||||
if (-Not $check_mode) {
|
||||
if ($backup) {
|
||||
$result.backup = BackupFile($dest)
|
||||
}
|
||||
$node.RemoveAttribute($attribute)
|
||||
$xmlorig.Save($dest)
|
||||
$result.msg = "text changed"
|
||||
}
|
||||
} else {
|
||||
$result.msg = "not changed"
|
||||
}
|
||||
}
|
||||
|
||||
Exit-Json $result
|
90
lib/ansible/modules/windows/win_xml.py
Normal file
90
lib/ansible/modules/windows/win_xml.py
Normal file
|
@ -0,0 +1,90 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2018, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
# this is a windows documentation stub. actual code lives in the .ps1
|
||||
# file of the same name
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_xml
|
||||
version_added: "2.7"
|
||||
short_description: Add XML fragment to an XML parent
|
||||
description:
|
||||
- Adds XML fragments formatted as strings to existing XML on remote servers.
|
||||
options:
|
||||
path:
|
||||
description:
|
||||
- The path of remote servers XML.
|
||||
required: true
|
||||
aliases: [ dest, file ]
|
||||
fragment:
|
||||
description:
|
||||
- The string representation of the XML fragment to be added.
|
||||
required: true
|
||||
aliases: [ xmlstring ]
|
||||
xpath:
|
||||
description:
|
||||
- The node of the remote server XML where the fragment will go.
|
||||
required: true
|
||||
backup:
|
||||
description:
|
||||
- Whether to backup the remote server's XML before applying the change.
|
||||
type: bool
|
||||
default: 'no'
|
||||
type:
|
||||
description:
|
||||
- The type of XML you are working with.
|
||||
required: yes
|
||||
default: element
|
||||
choices:
|
||||
- element
|
||||
- attribute
|
||||
- text
|
||||
attribute:
|
||||
description:
|
||||
- The attribute name if the type is 'attribute'. Required if C(type=attribute).
|
||||
|
||||
author:
|
||||
- Richard Levenberg (@richardcs)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
# Apply our filter to Tomcat web.xml
|
||||
- win_xml:
|
||||
path: C:\apache-tomcat\webapps\myapp\WEB-INF\web.xml
|
||||
fragment: '<filter><filter-name>MyFilter</filter-name><filter-class>com.example.MyFilter</filter-class></filter>'
|
||||
xpath: '/*'
|
||||
|
||||
# Apply sslEnabledProtocols to Tomcat's server.xml
|
||||
- win_xml:
|
||||
path: C:\Tomcat\conf\server.xml
|
||||
xpath: '//Server/Service[@name="Catalina"]/Connector[@port="9443"]'
|
||||
attribute: 'sslEnabledProtocols'
|
||||
fragment: 'TLSv1,TLSv1.1,TLSv1.2'
|
||||
type: attribute
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
msg:
|
||||
description: what was done
|
||||
returned: always
|
||||
type: string
|
||||
sample: "xml added"
|
||||
err:
|
||||
description: xml comparison exceptions
|
||||
returned: always, for type element and -vvv or more
|
||||
type: list
|
||||
sample: attribute mismatch for actual=string
|
||||
backup:
|
||||
description: name of the backup file, if created
|
||||
returned: changed
|
||||
type: string
|
||||
sample: C:\config.xml.19700101-000000
|
||||
'''
|
1
test/integration/targets/win_xml/aliases
Normal file
1
test/integration/targets/win_xml/aliases
Normal file
|
@ -0,0 +1 @@
|
|||
shippable/windows/group1
|
4
test/integration/targets/win_xml/files/config.xml
Normal file
4
test/integration/targets/win_xml/files/config.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<config>
|
||||
<string key="foo">bar</string>
|
||||
</config>
|
49
test/integration/targets/win_xml/files/log4j.xml
Normal file
49
test/integration/targets/win_xml/files/log4j.xml
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
|
||||
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" debug="false">
|
||||
<appender name="stdout" class="org.apache.log4j.ConsoleAppender" >
|
||||
<layout class="org.apache.log4j.PatternLayout">
|
||||
<param name="ConversionPattern" value="[%d{dd/MM/yy hh:mm:ss:sss z}] %5p %c{2}: %m%n"/>
|
||||
</layout>
|
||||
</appender>
|
||||
|
||||
<appender name="file" class="org.apache.log4j.DailyRollingFileAppender">
|
||||
<param name="append" value="true" />
|
||||
<param name="encoding" value="UTF-8" />
|
||||
<param name="file" value="mylogfile.log" />
|
||||
<param name="DatePattern" value="'.'yyyy-MM-dd" />
|
||||
<layout class="org.apache.log4j.PatternLayout">
|
||||
<param name="ConversionPattern" value="[%-25d{ISO8601}] %-5p %x %C{1} -- %m\n" />
|
||||
</layout>
|
||||
</appender>
|
||||
|
||||
<logger name="org.springframework.security.web.FilterChainProxy" additivity="false">
|
||||
<level value="error"/>
|
||||
<appender-ref ref="file" />
|
||||
</logger>
|
||||
|
||||
<logger name="org.springframework.security.web.context.HttpSessionSecurityContextRepository" additivity="false">
|
||||
<level value="error"/>
|
||||
<appender-ref ref="file" />
|
||||
</logger>
|
||||
|
||||
<logger name="org.springframework.security.web.context.SecurityContextPersistenceFilter" additivity="false">
|
||||
<level value="error"/>
|
||||
<appender-ref ref="file" />
|
||||
</logger>
|
||||
|
||||
<logger name="org.springframework.security.web.access.intercept" additivity="false">
|
||||
<level value="error"/>
|
||||
<appender-ref ref="stdout" />
|
||||
</logger>
|
||||
|
||||
<logger name="org.apache.commons.digester" additivity="false">
|
||||
<level value="info"/>
|
||||
<appender-ref ref="stdout" />
|
||||
</logger>
|
||||
|
||||
<root>
|
||||
<priority value="debug"/>
|
||||
<appender-ref ref="stdout"/>
|
||||
</root>
|
||||
</log4j:configuration>
|
2
test/integration/targets/win_xml/meta/main.yml
Normal file
2
test/integration/targets/win_xml/meta/main.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
dependencies:
|
||||
- prepare_win_tests
|
73
test/integration/targets/win_xml/tasks/main.yml
Normal file
73
test/integration/targets/win_xml/tasks/main.yml
Normal file
|
@ -0,0 +1,73 @@
|
|||
# test code for the Windows xml module
|
||||
# (c) 2017, Richard Levenberg <richard.levenberg@cosocloud.com>
|
||||
|
||||
# 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/>.
|
||||
|
||||
- name: copy a test .xml file
|
||||
win_copy:
|
||||
src: config.xml
|
||||
dest: "{{win_output_dir}}\\config.xml"
|
||||
|
||||
- name: add an element that only has a text child node
|
||||
win_xml:
|
||||
path: "{{win_output_dir}}\\config.xml"
|
||||
fragment: '<string key="answer">42</string>'
|
||||
xpath: '/config'
|
||||
register: element_add_result
|
||||
|
||||
- name: check element add result
|
||||
assert:
|
||||
that:
|
||||
- element_add_result is changed
|
||||
|
||||
- name: try to add the element that only has a text child node again
|
||||
win_xml:
|
||||
path: "{{win_output_dir}}\\config.xml"
|
||||
fragment: '<string key="answer">42</string>'
|
||||
xpath: '/config'
|
||||
register: element_add_result_second
|
||||
|
||||
- name: check element add result
|
||||
assert:
|
||||
that:
|
||||
- not element_add_result_second is changed
|
||||
|
||||
- name: copy a test log4j.xml
|
||||
win_copy:
|
||||
src: log4j.xml
|
||||
dest: "{{win_output_dir}}\\log4j.xml"
|
||||
|
||||
- name: change an attribute to fatal logging
|
||||
win_xml:
|
||||
path: "{{win_output_dir}}\\log4j.xml"
|
||||
xpath: '/log4j:configuration/logger[@name="org.apache.commons.digester"]/level'
|
||||
type: attribute
|
||||
attribute: 'value'
|
||||
fragment: 'FATAL'
|
||||
|
||||
- name: try to change the attribute again
|
||||
win_xml:
|
||||
path: "{{win_output_dir}}\\log4j.xml"
|
||||
xpath: '/log4j:configuration/logger[@name="org.apache.commons.digester"]/level'
|
||||
type: attribute
|
||||
attribute: 'value'
|
||||
fragment: 'FATAL'
|
||||
register: attribute_changed_result
|
||||
|
||||
- name: check attribute change result
|
||||
assert:
|
||||
that:
|
||||
- not attribute_changed_result is changed
|
Loading…
Reference in a new issue