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.
<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>
## 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") }
############################################################################## ## 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) }
############################################################################## ## 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 }
############################################################################## ## 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) } }
############################################################################## ## 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 }
############################################################################## ## 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.") } }
############################################################################## ## 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 }
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) } }