Entries in PowerShell (1)

Friday
Mar302012

PowerShell - Use an XML File for Script Settings and Logging

One of the guys I work with was looking to automate some software installation on new machines, sort of location-specific modifications to a firm-wide image, and wanted a way to have a script run steps, reboot and restart at the next step. He was effectively looking for a way to maintain information and state between times that the script ran. I thought using a local XML file to store states and log events into, treating it as a database that could remain on the machine afterwards for reference in troubleshooting might work well for him and created some code he could use.

I wound up building a few functions that would allow his script to create and interact with the local XML file so that he could both store and retrieve relevant values and log events along the way. This is broken down into functions that can be placed in a single script or that you can place in other files, whatever floats your boat.

Example of file

With the functions below you can:
  • Read and write status step value
  • Create, read and update configuration-params
  • Write only to log entries with some activities being auto logged
<setup>
  <status step="3" />
  <configuration>
    <param type="script version">1.0</param>
    <param type="ip address">129.168.1.108</param>
  </configuration>
  <log>
    <entry timestamp="03/20/2012 14:33:34">Log created</entry>
    <entry timestamp="03/20/2012 14:33:34">W00t!</entry>
    <entry timestamp="03/20/2012 17:18:44">Config/Param update. Old: 129.168.1.17, New: 129.168.1.18</entry>
    <entry timestamp="03/20/2012 17:23:01">Error, attempted to update step in log file to a blank value.</entry>
    <entry timestamp="03/20/2012 17:41:10">Stage value update. Old: 2, New: 3</entry>
  </log>
</setup>

Main - Control flow from a single function

Example of setting the script up and how to call the functions;
## Load Location-based Variables
##############################################################################
## Declare variables with :Script scope
##############################################################################

## Need Configured
	$Script:scriptversion = "1.0"
	$Script:logfilefolder = "C:\temp"	# will be created if does not exist
	$Script:logfilename = "C:\temp\MachineSetupLog.xml"	# will be created if does not exist

## General Use
	$Script:currentstep = "Error"  # This variable will hold your current step counter if needed in IF/THEN

##############################################################################	
## Control - Main
##############################################################################
	Function Main{	
	
		## initialize log and get step value (in case of restart)
		PrepLogFile
		GetStepFromLog
		
		## updates the step value in the log and the script variable $currentstep, both at once
		UpdateStepInLog("2")
		
		## add arbitrary text to log as an entry, timestamp added by function
		#AddEntryToLog("W00t!")
		
		## add param entry to log will overwrite if already exists, logging old and new values
		##  format is: AddConfigurationEntry <param type> <param value> <log entry (1/0)>
		##	The third parameter is a 0=false 1=true setting if the change should show old/new values in the log
		##    set this to 0 when removing or changing a sensitive item such as a password.
		AddConfigurationEntry "ip address" "192.168.0.1" 1
		
		## read parameters back: $foo = GetConfigurationEntry("IP Address")
		#PopUp(GetConfigurationEntry("ip address"))
		#PopUp(GetConfigurationEntry("script version"))
		
		##Done
		PopUp("Main Complete")
	}

Popup Creator Function

For debug really, function for popping up an alert to notify of some value/state
##############################################################################
## function PopUp - pops a message to user
##  usage: PopUp(GetConfigurationEntry("ip address")) 
##  usage: PopUp("More Coffee!")
##############################################################################
	Function PopUp([string]$messageIn){
		$a = new-object -comobject wscript.shell
		$b = $a.popup($messageIn,0,"CBH Script Alert",1)
	}

Exit Script Function

This was provided to call when an error state was detected or the need to shut the process down before the natural end occured, it logs the event and the message you pass to it to the log.
##############################################################################
## function ExitScript - Logs message and ends execution [ARG-STRING]
##  usage:  ExitScript("1 cannot = 2, aborted.")
##############################################################################
Function ExitScript($logmessage){
	$endingmessage = "Ending Execution with status: " + $logmessage
	AddEntryToLog($endingmessage)
	exit
}

Creates folder and log if they do not exist

Note the 2 string variables in the main function used here to specify the folder and file path. This also creates the blank XML template with a few values filled in. Mod as needed.
##############################################################################
## function PrepLogFile - Creates folder and log if they do not exist
##  usage: PrepLogFile
##  usage notes: be sure to set values of $Script:logfilefolder and $Script:logfilename beforehand
##############################################################################
Function PrepLogFile{
#Create folder if needed
	if (!(Test-Path -path $logfilefolder))
	{
		New-Item $logfilefolder -type directory
	}
	## Create our temp file if needed (with content, elements on different lines for readability later)
	if(!(Test-Path $logfilename))
	{
		$date = Get-Date
		$blanklog = "<entry timestamp='" + $date + "'>Log created</entry>"
		$blankparam = "<param type='script version'>" + $scriptversion + "</param>"

		add-content $logfilename "<setup>"
		add-content $logfilename "<status step='0' />"
		add-content $logfilename "<configuration>"
		add-content $logfilename $blankparam	
		add-content $logfilename "</configuration>"
		add-content $logfilename "<log>"
		add-content $logfilename $blanklog
		add-content $logfilename "</log>"
		add-content $logfilename "</setup>"
		$logfilename.Save($logfilename)
	}
}

Pulls current step from log file

Calling this reads the value from your file and sets the script-level variable $Script:currentstep to the value of the step.
##############################################################################
## function GetStepFromLog - Pulls current step from log file into $Script:currentstep
##  usage: GetStepFromLog
##############################################################################
	Function GetStepFromLog{
		$xml = [xml] (Get-Content $logfilename)		#open file
		$Script:currentstep =  $xml.setup.status.GetAttribute("step")	#map currentstep to XML step value
	}

Updates the step value

This sets the step in your file and updates the script-level variable $Script:currentstep while it is at it. It also makes a timestamped log entry of this change, reflecting the old and new values.
##############################################################################
## function UpdateStepInLog - Set step to new value log file [ARG-STRING]
##  usage: UpdateStepInLog("42")
##############################################################################
	Function UpdateStepInLog($newstep){	
		if($newstep -ne "")
		{
			#Open xml file and change step to newstep value
			$xml = [xml] (Get-Content $logfilename)		#open file
			$status = $xml.setup.status		#map $status to that element in XML
			$status.step = $newstep		# set value
			$xml.Save($logfilename)		#save			
			GetStepFromLog		# Refresh $Script:currentstep from log we just changed
			$logmessage = "Stage value update. Old: " + $oldstep + ", New: " + $newstep
			AddEntryToLog($logmessage)
		}
		else
		{
			PopUp("Error, trying to update step in log file to a blank value")
			AddEntryToLog("Error, attempted to update step in log file to a blank value.")
		}
	}

Make log entry

This creates a timestamped entry in the log portion of the XML file for you. Use liberally.
##############################################################################
## function AddEntryToLog - Adds a new 'entry' node and datestamp to log file [ARG-STRING]
##  usage: AddEntryToLog("Look at me still logging when there's science to do.")
##############################################################################
	Function AddEntryToLog($message){
		$date = Get-Date	#for datestamp
		$xml = [xml] (Get-Content $logfilename)		#open file
		$newEntry = $xml.CreateElement("entry")		#create new entity 'entry'
		$newEntry.set_InnerXML($message)		#insert the message
		$newEntry.SetAttribute("timestamp",$date)	#populate timestamp attribute
		$xml.setup.log.AppendChild($newEntry)	#append to <log> entity
		$xml.Save($logfilename)		#save
	}

Add a configuration parameter

This will update the value if it exists already (and is a change) or add and entry if this is new. Additionally, it logs the old/new values when it updates an entry UNLESS you tell it to supress logging of the old, new values. This was added as the need to use these parameters for sensitive information might arise and though they may be purged at the end of the script, would remain in the log unless we allowed suppression.

If you call this with the log bit set to 0 it will supress logging the old/new value. If you pass a 1 it will log all details of your change. This is recommended.

##############################################################################
## function AddConfigurationEntry - Adds a new 'parameter' node and configuration portion of log file [ARG-STRING], [ARG-BOOL]
##  usage (standard): AddConfigurationEntry "ip address" "129.168.1.1" 1
##  usage (supress): AddConfigurationEntry "secret value" "foobar!" 0
##############################################################################
	Function AddConfigurationEntry{
		param([String]$paramtype, [String]$paramtext, [Bool]$logchange) # map parameters to variables
			# the boolean value '$logchange' will suppress the log entry when set to false
		$xml = [xml] (Get-Content $logfilename)		#open file
		
		#already an item?
		$logatend = "no"		# flag to log after we close the file
		$logmessage = ""	# holder in case we need to log
		$test = $xml.setup.configuration.param | where { $_.type -eq $paramtype} #look for pre-existing param
		if($test)
		{
			# already exists
			# only update if they are not the same value
			if($test.InnerXML -ne $paramtext) #test for inequality
			{
				# prepare to log this change, storing the old value and the new one
				if($logchange) # only do this if $logchange function param is true
				{
					$logmessage = "Config/Param update (" + $paramtype + "). Old: " + $test.InnerXML + ", New: " + $paramtext							
				}
				else # generic message
				{
					$logmessage = "Config/Param update (" + $paramtype + "). Values supressed."
				}
				$logatend = "yes"
				#set and save the change
				$test.InnerXML = $paramtext	# set value
				$xml.Save($logfilename)		#save				
			}
		}
		else # OK to add
		{
			$newEntry = $xml.CreateElement("param")		#create new entity 'param'
			$newEntry.set_InnerXML($paramtext)		#insert the message
			$newEntry.SetAttribute("type", $paramtype)	#populate type attribute
			$xml.setup.configuration.AppendChild($newEntry)	#append to <configuration> entity
			$xml.Save($logfilename)		#save		
		}
		if($logatend -eq "yes")
		{
			# logging was not working while the file was open, we moved it to the end here and it works.  boom.
			AddEntryToLog($logmessage)
		}
	}