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)
		}
	}
Thursday
Nov032011

SharePoint Popup Contextual Images

Also known as the 'hover over for screenshot' functionality.  This is a nice jquery driven code snippet that will take any link on your SharePoint page that has an image as its url and allow the user to simply hover over the link to see the image.

This has come in handy for providing contextual help in the form of screenshots right in with content.  A dead simple implementation might look like the following with a hyperlink column in your list and that being the only element in the view.

Yielding this type of thing when you hover over a link on your page:

Otherwise this just requires a reference to your local jquery file (or maybe not local, do what you want cowboy).


<script type="text/javascript" src="jquery.min.js"></script></span></div>
<script type="text/javascript"></span></div>
<br /></span></div>
function imagePreview(){</span></div>
arrOfImageTypes = ['jpg','jpeg','gif','png'];</span></div>
 $("table.ms-listviewtable td.ms-vb2 a").hover(function(e){</span></div>
 var href = this.href;</span></div>
 var img = href.substring(href.lastIndexOf('.')+1).toLowerCase();</span></div>
 if(href.indexOf('http')==0 && $.inArray(img,arrOfImageTypes)>-1){</span></div>
    $("body").append("<img id='preview' src='"+ this.href +"' alt='Image preview' />");</span></div>
 }</span></div>
 var obj = $("#preview");</span></div>
 var offset = $(this).offset();</span></div>
 var winHeight = $(window).height();</span></div>
 var winWidth = $(window).width();</span></div>
 var scrollLeft = $(window).scrollLeft();</span></div>
 var scrollTop = $(window).scrollTop();</span></div>
 var objHeight = obj.outerHeight();</span></div>
 var objWidth = obj.width()+15;</span></div>
 if(((winWidth+scrollLeft)-offset.left)<objWidth){</span></div>
 offset.left=((winWidth+scrollLeft)-objWidth);</span></div>
 }</span></div>
 var maxHeight = (winHeight+scrollTop)-offset.top;</span></div>
 if(objHeight>maxHeight){</span></div>
 if(offset.top-scrollTop>objHeight){</span></div>
 offset.top=offset.top-objHeight-20;</span></div>
 }</span></div>
 height = (objHeight<winHeight)?objHeight:winHeight;</span></div>
 }</span></div>
 obj.css({"position":"absolute","top":(offset.top+20)+"px","left":(offset.left+20),"border":"1px solid black"})</span></div>
 .fadeIn("fast");</span></div>
 },</span></div>
 function(){</span></div>
 $("#preview").remove();</span></div>
 });</span></div>
};</span></div>
<br /></span></div>
// Call the script on page load</span></div>
$(document).ready(function(){</span></div>
 imagePreview();</span></div>
});</span></div>
</script></span></div>
Tuesday
Oct112011

Google Stock Ticker for SharePoint

We looked at a number of controls to add a stock ticker to our intranet after it was requested that we do so. We looked at a few paid versions, considered building our own and in the end settled for using Google's phenomonal API for stock data.  Sure it is 15 minutes delayed so day trades should go find a version that you pay for.

Sample yerself some XML results here: http://www.google.com/ig/api?stock=.INX  You'll see in line 275 of the attached web part where the call to Google's API lives and can me modified.

SharePoint comes with a great data view web part that we just fed the Google output into, did a little visual tweaking to meet the space constraits we had and we were done.  Elegant, reliable, cheap.   Win.

I attached the exported web part that we used that you can import as a starting point if you'd like or if you want a little more control you can just build your own and not spend much more time in the process.

stock_ticker.webpart

Tuesday
Oct112011

Blog Reboot

Well, I've finally decided to attempt this again.  I think a portion of my reluctance to pick the effort back up was in managing my local instance of WordPress so I made a clean break to SquareSpace and put my money where my mouth is.  I've been recommending them to others for years now and even done a little freelance work on the platform.

I am generating some code at work that may prove helpful to others and have decided that I need to get it out and available to others, giving back to a community that I have been playing a one-sided game with for a while now.

I'll save a few posts from my old blog but plan on letting many of the others go, like old friends that you remember fondly but realize you may be better off with memories of than bumping into every few years.  I'm keeping the blog name though as I feel I never gave it attention enough to do it justice.  Here we go.

Tuesday
Sep202011

Um, Google, this isn't working

Scenario: My daughter, under 18, has been using Google Buzz to communicate with her friends now since it came out because we will not let her have a Facebook account yet.  Facebook is not a organization that I feel has the user's best interest in mind.  Google though I feel more comfortable with as she has a very level head on her shoulders.  Although none of us adults were using it, what I've come to learn is that apparently Buzz is the shiznit for the young folks.

So, Google is now killing the Buzz service that anyone could access, regardless of age and is barring young folks from using Google Plus.  The solution is that, en masse, they all open new Google accounts with fake ages.  This strikes me as bad for Google and not really what I would, as a parent, advocate.

So my daughter discussed this with me, we talked about social networking and have discussed sensible online behaviour for years now and asked me to help her get Google Plus enabled for her.  I think she is mature enough to handle this model, but Google locks you out really well, your only recourse is to wait until you turn 18.  No ability for a parent to override or opt in.

So now she has a bogus account just to interact over plus.  What did they expect?  They are inviting a huge user base to provide them with incorrect information that we all know they use for marketing purposes so protecting the integrity of that data seems like it would be a high priority.  Locking the service down to 18 years of age is asking for bad data.  This isn't working guys.