Skip to content

Instantly share code, notes, and snippets.

@GeoffWilliams
Created July 17, 2018 04:02
Show Gist options
  • Select an option

  • Save GeoffWilliams/df7345a6cd440f825759460b844d4e39 to your computer and use it in GitHub Desktop.

Select an option

Save GeoffWilliams/df7345a6cd440f825759460b844d4e39 to your computer and use it in GitHub Desktop.
configure an application by looking up stuff from a .json file and writing XMLs from it
#<#
#.Synopsis
# Rewrite configuration files based on hiera data
#.Description
# ..
#
#.Parameter DataFile
# Exported data from hiera (JSON)
#.Parameter AppData
# File to read configurable settings from
#.Parameter DryRun
# In dry run mode, we exit with status 100 to indicate changes needed but do not save the file
#
#.Example
# # Reconfigure all files
# configure_app
#
##>
param(
[string] $DataFile = "C:\vagrant\mock.json",
[string] $AppData = "C:\vagrant\appdata.txt",
[switch] $DryRun = $false
)
function ChildKeyFromXpath {
param(
[string] $XPath,
[switch] $IgnoreMissing = $false
)
$childKeyCapture = [regex]::Match($xPath, '@[^=]+=''([^'']+?)'']/[^/]+$').captures.groups
if ($childKeyCapture.length -ne 2) {
if (-not $IgnoreMissing) {
write-error "unable to find childKey in $($xPath) - xPath needs to map to an element in hiera eg //foo[@key='bar']/@value to lookup bar"
exit 1
}
$childKey = $null
} else {
$childKey = $childKeyCapture[1]
}
return $childKey
}
function ReadXml {
param(
[string] $FilePath
)
# Read all the xml and do the edit at the requested xPath
if(-not [System.IO.File]::Exists($FilePath)){
write-error "File not found reading $($FilePath)"
exit 1
}
[xml] $xml = Get-Content($FilePath)
if ($xml -eq $null -or $xml.ChildNodes.Count -eq 0 ) {
write-error "Could not parse XML from $($FilePath) - invalid file content"
exit 1
}
return $xml
}
# Ensure XML fragment exists at correct location
function Ensure-XmlFragment {
param(
[string]$FilePath,
[string]$XPath,
[string]$ParentKey
)
# remove the `ENSURE:` from the start of the XPath
$XPath = $XPath -replace "ENSURE:", ""
$xml = ReadXml -FilePath $FilePath
# XPath should look like this:
# //configuration/system.serviceModel/client/endpoint[@name='SystemEventsService']/identity
# which means we need to get a reference to:
# 1. //configuration/system.serviceModel/client/endpoint[@name='SystemEventsService']
# 2. the child `identity`
$captures = [regex]::Match($xPath, '^(.*?)/([^/]+)$').captures.groups
$xPathBase = $captures[1]
$targetElement = $captures[2]
$nodes = (Select-Xml -Xml $xml -XPath $xPathBase)
if ($nodes.node.count -eq 0) {
write-error "XPath expression $($xPathBase) matches no nodes"
exit 1
}
$childKey = ChildKeyFromXpath -xPath $XPath -IgnoreMissing
$data = DataLookup -parentKey $ParentKey -childKey $childKey
$target = $nodes.node.$targetElement
if ($target -eq $null) {
# child (`identity` in above example) doesn't exist yet - create it
$target = $xml.CreateElement($targetElement)
$nodes.node.AppendChild($target) | Out-Null
}
# update the xml string inside the target (`identity`) to reflect the data from hiera
if ($target.InnerXml -ne $data) {
$target.InnerXml = $data
if ($DryRun) {
write-host "needs updating"
exit 100
} else {
$xml.save($FilePath)
}
}
}
# Set an XML attribute to a particular values
function Set-AttributeValue {
Param(
[string]$FilePath,
[string]$XPath,
[string]$ParentKey
)
$xPathSplit = $XPath -split("/@")
if ($xPathSplit.Length -ne 2) {
write-error "xpath input is not correct - needs to capture an element, eg //foo[@key='bar']/@value but you sent '$($XPath)'"
exit 1
}
$xPathBase = $xPathSplit[0]
$xPathAttrib = $xPathSplit[1]
$childKey = ChildKeyFromXpath -xPath $xPath
# lookup the value to use for this config item
$value = DataLookup -parentKey $parentKey -childKey $childKey
$xml = ReadXml -FilePath $FilePath
$nodes = (Select-Xml -Xml $xml -XPath $xPathBase)
if ($nodes -eq $null) {
# There must be a "slot" for us to insert values in the XMLs - we are not in the business
# of creating parent nodes any more
write-error "missing parent - create $($xPathBase) first and be aware of namespaces in xpath expressions"
exit 1
} else {
if ($nodes.Node.Attributes[$xPathAttrib] -ne $null) {
if ($nodes.Node.Attributes[$xPathAttrib].Value -eq $value) {
$changesNeeded = $false
} else {
$nodes.Node.Attributes[$xPathAttrib].Value = $value
$changesNeeded = $true
}
} else {
$nodes.Node.SetAttribute($xPathAttrib,$value)
$changesNeeded = $true
}
if ($changesNeeded -and $DryRun) {
write-host "Changes are required"
exit 100
}
}
if (-not $DryRun) {
$xml.Save($FilePath)
}
}
#.Synopsys
# Lookup data (from JSON file simulating hiera lookup)
#.Parameter parentKey
# The key to ask hiera for, eg `profile::foo::bar`
#.Parameter childKey
# Assuming the data returned from looking up `parentKey` is a hash, return the contents of this child element. If omitted,
# Return all data looked up from parentKey
function DataLookup {
param(
[String] $parentKey,
[String] $childKey
)
$json = Get-Content $DataFile | ConvertFrom-Json
if ([string]::IsNullOrEmpty($childKey)) {
$data = $json.$parentKey
} else {
$data = $json.$parentKey.$childKey
}
if ($data -eq $null) {
write-error "No data in hiera for '$($parentKey)' element '$($childKey)' - fix this!"
exit 25
}
return $data
}
$workingFile = $null
# appdata.txt is a simple textfile listing each app setting that must be managed
# Format:
# [c:\file\to\edit.xml]
# XPATH==PARENT KEY IN HIERA
# ENSURE:XPATH==PARENT KEY IN HIERA
# Real example:
# [C:\vagrant\web_config.xml]
# //configuration/appSettings/add[@key='smtpHostName']/@value==boards::ipadserver::settings::appSettings
# ENSURE://configuration/system.serviceModel/client/endpoint[@name='SystemEventsService']/identity==boards::ipadserver::spn
#
# The XPATH *must* take the form above, since this tells the script:
# 1. How to find existing settings (attribute predicate: `key=SmtpHostName`, `name=SystemEventService`)
# 2. Which attribute or element to write to (at the end of the XPATH: `/@value`, `/identity`)
# 3. The element under the parent key to find the data in hiera (the value the predicate searches for: `smtpHostName`, `SystemEventService`)
# 4. How to perform the update (line starts `ENSURE` we are adding a blob of xmltext otherwise we are setting an attribute)
foreach($line in Get-Content $AppData) {
# skip blank lines and comments
$line = $line.trim()
if ((-not ($line -match '^\s*[\n\r]*$')) -and (-not ($line -match '^\s*#'))) {
if ($line.StartsWith('[')) {
# Specify the current working file
$workingFile = $line -replace "[][]", ""
write-host "Working file set to $($workingFile)"
} else {
# must be something to lookup
$lineSplit = $line -split('==')
$xPath = $lineSplit[0]
$parentKey = $lineSplit[1]
if ($lineSplit.Length -eq 2) {
if ($workingFile -eq $null) {
write-error "There is no working file! specify it in square brackets in appdata.txt before items to configure"
exit 1
}
if($line.StartsWith("ENSURE:")) {
Ensure-XmlFragment -FilePath $workingFile -XPath $xPath -ParentKey $parentKey
} else {
Set-AttributeValue -FilePath $workingFile -XPath $xPath -ParentKey $parentKey
}
} else {
write-error "item to lookup is not in correct format - should be XPATH==HIERA KEY but got: \n $($line)"
exit 1
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment