Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

win_xml module for manipulating XML files on Windows #26404

Merged
merged 1 commit into from
Aug 31, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
239 changes: 239 additions & 0 deletions lib/ansible/modules/windows/win_xml.ps1
Original file line number Diff line number Diff line change
@@ -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

dagwieers marked this conversation as resolved.
Show resolved Hide resolved
$debug_level = Get-AnsibleParam -obj $params -name "_ansible_verbosity" -type "int"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this being used for anything?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is used to determine the debug level See below.

$debug = $debug_level -gt 2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, is this being used for anything. I'm also not sure what the Ansible practice is around returning extra values based on the verbosity level set.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The $debug variable is used to determine whether to return XML comparison errors. I can take all the functionality out or take the debug gate out. This was work in concert with removing the emission of the errors to a log on the remote server.


$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")) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@richardcs I am trying to remove an element from xml file but its not working for me. I am trying following, Please correct me if i am doing wrong.

- name: Running XML Transformations with state Absent win_xml: path: "path\to\xml\file" xpath: "/configuration/configSections/sectionGroup/section[@name='authentication']" state: "absent" fragment: " "

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the xpath be //configuration/configSections/sectionGroup/section[@name='authentication'] - double initial slash?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@petemounce it worked for other paramerters without adding double initial slash. its not working only if i am trying to delete an element.

Copy link
Contributor Author

@richardcs richardcs Apr 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont think you can have an empty fragment. The xpath should find the root of your XML where the child fragment is, then it will remove that child fragment.

So to guess maybe your xpath is "/configuration/configSections/sectionGroup/" and your fragment is "section[@name='authentication']"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@petemounce "//" means find it at the root, which is probably artificially the case, but not explicitly necessary .

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@richardcs one last thing that i want to know, Can we add the capability to delete even the attributes from the elements?
We are trying to remove only the attributes from the xml element.
For. Ex <language default="eng" style="arial"></language>
We want to delete the default attribute only.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the current module supports absent for attributes. If you give me the .xml you are working with I can probably add it.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@richardcs Here is the xml file.

<?xml version="1.0" encoding="utf-8"?>
<!--
  For more information on how to configure your ASP.NET application, please visit
  http://go.microsoft.com/fwlink/?LinkId=169433
  -->
<configuration>
  <configSections>
      <sectionGroup name="applicationSettings" type="System.Configuration.ApplicationSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
          <section name="GKE.DS.ALM.Patterns.DotNet.App.Web.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
      </sectionGroup>
	 <sectionGroup name="GKE.ds.security" type="GKE.DS.Security.Configuration.SecuritySectionGroup, GKE.DS.Security">
      <section name="authorization" type="GKE.DS.Security.Configuration.AuthorizationSection, GKE.DS.Security" />
      <section name="authentication" type="GKE.DS.Security.Configuration.AuthenticationSection, GKE.DS.Security" />    
     </sectionGroup>
	<!--
  Adding a multiline comment
  second line comment
  third line
  -->
  </configSections>
  <system.web>
    <compilation debug="true" targetFramework="4.5" />
    <httpRuntime targetFramework="4.5" />
  </system.web>
  <appSettings>
  </appSettings>
<applicationSettings>
    <GKE.DS.ALM.Patterns.DotNet.App.Web.Properties.Settings>
      <setting name="Setting1" serializeAs="String">
        <value>Setting1Value</value>
      </setting>
    </GKE.DS.ALM.Patterns.DotNet.App.Web.Properties.Settings>
  </applicationSettings>
</configuration>

we want to remove a attribute serializeAs="String" which is present in <setting name="Setting1" serializeAs="String"> element.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abhijeetka I modified the win_xml module to support your use case. It was tested with this playbook.

---
 - name: win_xml module test with absent attribute
   hosts: []
   tasks:
     - name: run the win_xml module
       win_xml:
          path: C:\\Users\richardcs\Temp\test.xml
          xpath: '/configuration/applicationSettings/GKE.DS.ALM.Patterns.DotNet.App.Web.Properties.Settings/setting'
          type: attribute
          fragment: String
          attribute: serializeAs
          state: absent

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@richardcs that works like a charm ...!! Thanks for adding this functionality.

[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 changes: 90 additions & 0 deletions lib/ansible/modules/windows/win_xml.py
Original file line number Diff line number Diff line change
@@ -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'''
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you also able to add the backup and err return values and a description for those.

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 change: 1 addition & 0 deletions test/integration/targets/win_xml/aliases
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
shippable/windows/group1
4 changes: 4 additions & 0 deletions test/integration/targets/win_xml/files/config.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<config>
<string key="foo">bar</string>
</config>
49 changes: 49 additions & 0 deletions test/integration/targets/win_xml/files/log4j.xml
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions test/integration/targets/win_xml/meta/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dependencies:
- prepare_win_tests