PowerShell Configuration Storage Module

This PowerShell configuration module will let you easily create, store, and retrieve configuration data in simple XML files. Of course there are a few ways to store config data for use in PowerShell scripts. One way is to save data structures and variable in a psd1 file and load direct, which is what I typical do. I suppose this could be used to manage XML for other reasons too. I will likely use this to pull configs from other sources to generate XML. It sounds better than manually populating a psd1 file. If nothing else I learned a little bit about XML and working with it in PowerShell.

Using This Module

Simply copy the code, save as PS-Config.psm1 and place it where you store your modules or add it to the default PowerShell modules location. Then you should be able to import and use like any other module. You can set the folder path but you may want to change the default path, and maybe filename, to something more relevant to your environment.

Problems

This code worked out of the box on Linux running PowerShell 7 so it may work on Windows running version 7. If you receive an error on Windows when instantiating create a file named PS-Config.psd1 (must be named the same as the module and exist in the same folder as the module) containing this line:

[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Xml")

PowerShell Configuration Storage Module Code

<#
.Synopsis
  PS-Config.psm1: Simple configuration storage and retrieval for PowerShell scripts.
  Charles Nichols, Dec. 3, 2022
 
.Description
  Allows script authors to easily create and grab configuration information
  for use in PowerShell scripts stored as simple XML files.
 
.Parameter ConfigFolder
  The folder where configs should be stored.
 
.Parameter ConfigFileName
  The name of the configuration file. 

.Parameter XMLPath 
   This is set when the class is instantiated.
 
.Example

# Import the module.
Import-Module "\path\to\PS-Config.psm1"

# Create a new empty XML.
$conf = New-Object PSConfigs
$conf.CreateNew("mohawke","Comments about this conf.")

# Add children.
$conf.AddNodeSection("Environment")
$conf.AddNodeSection("Servers")

# Add attributes to Environment.
$Data = @{}
$Data["domain"] = "columbus"
$Data["days"] = 2

$conf.AddAttributesFromHash("Environment", $Data)

# Add elements with values to Servers.
$Elements = @{}
$Elements["core"] = "server1"
$Elements["web"] = @("server2","server3")

$conf.AddElementsFromHash("Servers", $Elements)

# Get Attributes as HashTable and loop key,values.
$EnvAttribs = $conf.GetAttributesByElement("Environment")
foreach ($Rec in $EnvAttribs.GetEnumerator()) {
    Write-Host "Attributes: " $Rec.Name "=" $Rec.Value
}

# Get Elements as HashTable and loop key,values.
$Servers = $conf.GetChildElements("Servers")
foreach ($Rec in $Servers.GetEnumerator()) {
    foreach ($Server in $Rec.Value) {
        Write-Host "Element/Value: " $Rec.Name "=" $Server
    }
}

.NOTES
 Could use some more error handling.
#>

class PSConfigs {
    [string]$ConfigFolder = "/home/mohawke/Documents/powershell/configs"
    [string]$ConfigFileName = "default.xml"
    [string]$RootNode = "Configuration"
    [string]$XMLPath = $null
    
    hidden PSConfigs() {
        # Init: Combine the folder and file for a complete path.
        $this.XMLPath = Join-path -Path $this.ConfigFolder -ChildPath $this.ConfigFileName
    }

    [void]CreateNew($Contact="Integration Team", $Comment=""){
        # Create a new configuration xml file.
        $Created = (Get-Date).ToString("yyyy-MM-dd")

        if (-not(Test-Path -Path $this.ConfigFolder)) {
            New-Item -Path $this.ConfigFolder -ItemType Directory
        }

        if (Test-Path -Path $this.XMLPath) {
            Throw "Configuration file exists!"
        }
        # Create base file.
        $xmlWriter = New-Object System.Xml.XmlTextWriter($this.XMLPath ,$Null)
        $xmlWriter.Formatting = 'Indented'
        $xmlWriter.Indentation = 1
        $XmlWriter.IndentChar = "`t"
        # Create base doc.
        $xmlWriter.WriteStartDocument()
        $xmlWriter.WriteComment($Comment) 
        $xmlWriter.WriteStartElement($this.RootNode)
        $XmlWriter.WriteAttributeString('Contact', $Contact)
        $XmlWriter.WriteAttributeString('Date', $Created)
        # Save base file.
        $xmlWriter.WriteEndDocument()
        $xmlWriter.Flush()
        $xmlWriter.Close()
    }

    [xml]OpenXML() {
        # Open a file handle to xml.
        if (-not(Test-Path -Path $this.XMLPath)) {
            $ErrMsg = "File not found at {0}" -f $this.XMLPath
            Throw $ErrMsg
        }
        return [xml](Get-Content -Path $this.XMLPath)
    }

    [void]AddNodeSection($SectionName) {
        $xmlDoc = $this.OpenXML()

        $newElement = $xmlDoc.CreateElement($SectionName)
        $xmlDoc.$($this.RootNode).AppendChild($newElement)
        $xmlDoc.Save($this.XMLPath)
    }

    [void]AddAttributesFromHash($Child,$Hash) {
        $xmlDoc = $this.OpenXML()
        $Node = $xmlDoc.SelectSingleNode("/$($this.RootNode)/$($Child)")
        ForEach ($Item in $Hash.GetEnumerator()) {
            $Node.SetAttribute($Item.Key.Trim(), $Item.Value)
        }
        $xmlDoc.save($this.XMLPath)            
    }

    [void]AddAttribute($Child, $Attribute, $Value) {
        $xmlDoc = $this.OpenXML()
        $Node = $xmlDoc.SelectSingleNode("/$($this.RootNode)/$($Child)")
        $Node.SetAttribute($Attribute.Trim(), $Value)
        $xmlDoc.save($this.XMLPath)            
    }

    [void]AddElementsFromHash($Child,$Hash) {
        $xmlDoc = $this.OpenXML()
        $Node = $xmlDoc.SelectSingleNode("/$($this.RootNode)/$($Child)")
        ForEach ($Item in $Hash.GetEnumerator()) {
            $SubChild = $xmlDoc.CreateElement($Item.Key.Trim())
            $SubChild.InnerText = $Item.Value
            $Node.AppendChild($SubChild)
        }
        $xmlDoc.save($this.XMLPath) 
    }

    [void]AddElement($Child, $Element, $Value) {
        $xmlDoc = $this.OpenXML()
        $Node = $xmlDoc.SelectSingleNode("/$($this.RootNode)/$($Child)")
        $SubChild = $xmlDoc.CreateElement($Element.Trim())
        $SubChild.InnerText = $Value
        $Node.AppendChild($SubChild)
        $xmlDoc.save($this.XMLPath) 
    }

    [HashTable]GetChildElements($Child) {
        $xmlDoc = $this.OpenXML()
        $Nodes = $xmlDoc.SelectSingleNode("/$($this.RootNode)/$($Child)")
        $Elements = @{}
        ForEach ($Elem in $Nodes) {
            ForEach ($ChildNode in $Elem.ChildNodes) {
                $Elements[$ChildNode.ToString()] = $ChildNode.InnerText.Split(" ")
            }
        }
        return $Elements
    }

    [HashTable]GetAttributesByElement($Element) {
        $xmlDoc = $this.OpenXML()
        $Nodes = $xmlDoc.SelectSingleNode("/$($this.RootNode)/$($Element)")
        $Attributes = @{}
        $Nodes.Attributes | ForEach-Object {
            $Attributes[$_.LocalName] = $_.Value
        }
        return $Attributes
    }
}

# =============================================================================
# MAIN = TESTING
# =============================================================================
<#
#Import-Module "\path\to\PS-Config.psm1"
$conf.CreateNew("mohawke","Comments about this conf.")
$conf.AddNodeSection("Environment")
$conf.AddNodeSection("Servers")

$Data = @{}
$Data["domain"] = "columbus"
$Data["days"] = 2

$conf.AddAttributesFromHash("Environment", $Data)

$Elements = @{}
$Elements["core"] = "server1"
$Elements["web"] = @("server2","server3")

$conf.AddElementsFromHash("Servers", $Elements)

$EnvAttribs = $conf.GetAttributesByElement("Environment")
foreach ($Rec in $EnvAttribs.GetEnumerator()) {
    Write-Host "Attributes: " $Rec.Name "=" $Rec.Value
}

$Servers = $conf.GetChildElements("Servers")
foreach ($Rec in $Servers.GetEnumerator()) {
    foreach ($Server in $Rec.Value) {
        Write-Host "Element/Value: " $Rec.Name "=" $Server
    }
}
#>

<# RESULTS:
Attributes:  domain = columbus
Attributes:  days = 2
Element/Value:  web = server2
Element/Value:  web = server3
Element/Value:  core = server1
#>