266 lines
10 KiB
PowerShell
266 lines
10 KiB
PowerShell
#!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
|
|
#Requires -Module Ansible.ModuleUtils.Backup
|
|
|
|
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 -dest $newnode -src $childnode -xmlorig $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 Save-ChangedXml($xmlorig, $result, $message, $check_mode, $backup) {
|
|
$result.changed = $true
|
|
if (-Not $check_mode) {
|
|
if ($backup) {
|
|
$result.backup_file = Backup-File -path $dest -WhatIf:$check_mode
|
|
# Ensure backward compatibility (deprecate in future)
|
|
$result.backup = $result.backup_file
|
|
}
|
|
$xmlorig.Save($dest)
|
|
$result.msg = $message
|
|
} else {
|
|
$result.msg += " check mode"
|
|
}
|
|
}
|
|
|
|
$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" -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"
|
|
$count = Get-AnsibleParam $params "count" -type "bool" -Default $false
|
|
|
|
$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."
|
|
}
|
|
|
|
$xmlorig = New-Object -TypeName System.Xml.XmlDocument
|
|
$xmlorig.XmlResolver = $null
|
|
Try {
|
|
$xmlorig.Load($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)
|
|
|
|
$nodeList = $xmlorig.SelectNodes($xpath, $namespaceMgr)
|
|
$nodeListCount = $nodeList.get_Count()
|
|
if ($count) {
|
|
$result.count = $nodeListCount
|
|
if (-not $fragment) {
|
|
Exit-Json $result
|
|
}
|
|
}
|
|
## Exit early if xpath did not match any nodes
|
|
if ($nodeListCount -eq 0) {
|
|
$result.msg = "The supplied xpath did not match any nodes. If this is unexpected, check your xpath is valid for the xml file at supplied path."
|
|
Exit-Json $result
|
|
}
|
|
|
|
$changed = $false
|
|
$result.msg = "not changed"
|
|
|
|
if ($type -eq "element") {
|
|
if ($state -eq "absent") {
|
|
foreach ($node in $nodeList) {
|
|
# there are some nodes that match xpath, delete without comparing them to fragment
|
|
if (-Not $check_mode) {
|
|
$removedNode = $node.get_ParentNode().RemoveChild($node)
|
|
$changed = $true
|
|
if ($debug) {
|
|
$result.removed += $result.removed + $removedNode.get_OuterXml()
|
|
}
|
|
}
|
|
}
|
|
} else { # state -eq 'present'
|
|
$xmlfragment = $null
|
|
Try {
|
|
$xmlfragment = [xml]$fragment
|
|
} Catch {
|
|
Fail-Json $result "Failed to parse fragment as XML: $($_.Exception.Message)"
|
|
}
|
|
|
|
foreach ($node in $nodeList) {
|
|
$candidate = $xmlorig.CreateElement($xmlfragment.get_DocumentElement().get_Name(), $xmlorig.get_DocumentElement().get_NamespaceURI())
|
|
Copy-Xml -dest $candidate -src $xmlfragment.DocumentElement -xmlorig $xmlorig
|
|
|
|
if ($node.get_NodeType() -eq "Document") {
|
|
$node = $node.get_DocumentElement()
|
|
}
|
|
$elements = $node.get_ChildNodes()
|
|
[bool]$present = $false
|
|
[bool]$changed = $false
|
|
$element_count = $elements.get_Count()
|
|
$nstatus = "node: " + $node.get_Value() + " element: " + $elements.get_OuterXml() + " Element count is $element_count"
|
|
Add-Warning $result $nstatus
|
|
if ($elements.get_Count()) {
|
|
if ($debug) {
|
|
$err = @()
|
|
$result.err = {$err}.Invoke()
|
|
}
|
|
foreach ($element in $elements) {
|
|
$estatus = "element is " + $element.get_OuterXml()
|
|
Add-Warning $result $estatus
|
|
try {
|
|
Compare-XmlDocs $candidate $element
|
|
$present = $true
|
|
break
|
|
} catch {
|
|
if ($debug) {
|
|
$result.err.Add($_.Exception.ToString())
|
|
}
|
|
}
|
|
}
|
|
if (-Not $present -and ($state -eq "present")) {
|
|
[void]$node.AppendChild($candidate)
|
|
$result.msg = $result.msg + "xml added "
|
|
$changed = $true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} elseif ($type -eq "text") {
|
|
foreach ($node in $nodeList) {
|
|
if ($node.get_InnerText() -ne $fragment) {
|
|
$node.set_InnerText($fragment)
|
|
$changed = $true
|
|
}
|
|
}
|
|
} elseif ($type -eq "attribute") {
|
|
foreach ($node in $nodeList) {
|
|
if ($state -eq 'present') {
|
|
if ($node.NodeType -eq 'Attribute') {
|
|
if ($node.Name -eq $attribute -and $node.Value -ne $fragment ) {
|
|
# this is already the attribute with the right name, so just set its value to match fragment
|
|
$node.Value = $fragment
|
|
$changed = $true
|
|
}
|
|
} else { # assume NodeType is Element
|
|
if ($node.$attribute -ne $fragment) {
|
|
if (!$node.HasAttribute($attribute)) { # add attribute to Element if missing
|
|
$node.SetAttributeNode($attribute, $xmlorig.get_DocumentElement().get_NamespaceURI())
|
|
}
|
|
#set the attribute into the element
|
|
$node.SetAttribute($attribute, $fragment)
|
|
$changed = $true
|
|
}
|
|
}
|
|
} elseif ($state -eq 'absent') {
|
|
if ($node.NodeType -eq 'Attribute') {
|
|
$attrNode = [System.Xml.XmlAttribute]$node
|
|
$parent = $attrNode.OwnerElement
|
|
$parent.RemoveAttribute($attribute)
|
|
$changed = $true
|
|
} else { # element node processing
|
|
if ($node.Name -eq $attribute ) { # note not caring about the state of 'fragment' at this point
|
|
$node.RemoveAttribute($attribute)
|
|
$changed = $true
|
|
}
|
|
}
|
|
} else {
|
|
Add-Warning $result "Unexpected state when processing attribute $($attribute), add was $add, state was $state"
|
|
}
|
|
}
|
|
}
|
|
if ($changed) {
|
|
if ($state -eq "absent") {
|
|
$summary = "$type removed"
|
|
} else {
|
|
$summary = "$type changed"
|
|
}
|
|
Save-ChangedXml -xmlorig $xmlorig -result $result -message $summary -check_mode $check_mode -backup $backup
|
|
}
|
|
|
|
Exit-Json $result
|