Bootstrapping scripts

One of the problems here at work that we have is for some oddball reason people just can’t seem to get UNC paths to work right when it comes to DFS. For those cases we’ve had to resort to mapping these people directly to the server that is the primary location. Since these people are connecting via either cached Windows sessions or are on non-company computers (don’t ask me why we allow this, I don’t know), we can’t guarentee that the login script will run. Especially since it’s written in Powershell and that isn’t installed on your normal configuration. So far we’ve been patching things together using batch files, but these are proving unruly and people never seem to want to get the latest version. Therefore it was proposed that we make automatic updating scripts.

The the biggest problem was that we couldn’t have the scripts grab updates via UNC paths. The reason for this is that the server could change and Windows won’t answer UNC paths for another name besides the machine name. For example, you have a server called fileshare1 and this can change someday for whatever reason. This meant that we needed to use some other method of distribution, we decided HTTP was the best and simplest.

bootstrap.ps1

@'
	Name: bootstrap
	Version: 0.9
	Authors: Joshua Erickson
	Creation Date: 12/23/2008

	Description:
		Bootstrap is script that downloads/updates user initated scripts
		before actually running the desired script. These script typically
		rely on servers who's names can change over time or for scripts
		that must be updated frequently. Bootstrap can even update itself.

	Other Required Files:
		Local version file: ./version.xml
		Remote version file: http://some.server.local/scripting/version.xml

	Syntax:
		bootstrap.ps1 [ScriptName [ScriptName [ScriptName]]]

	Example:
		bootstrap.ps1 SomeScriptDefinedInVersionXml.ps1 SomeOtherScript.ps1

	Version 0.9 Notes:
		* Copy down latest scripts from a http server.
		* Can update itself.
		* Keeps track of installed scripts.
		* Runs desired script after updating it.

	Version 1.0 Plans
		* Have a dedicated subdomain for script updates.

	Future versions:
		-Argument passing. Might not be necessary since most scripts don't
		rely on arguments, but get everything they need all by themselves.
		-Optional alternate locations for downloading updates. Both in
		the script and defined in file elements in version.xml.
		-Forced updates regardless of version number.
		-Updates only script(s) that are called.
		-Downloads script and executes without saving it to disk.

'@ | Set-Variable "help";
#end headers;
if($Args -contains "--help") {
	$help;
	exit;
}

#
#	Define variables
#
$lroot = split-path -parent $MyInvocation.mycommand.path
$rroot = "http://some.server.local/scripting";

$wc = New-Object System.Net.WebClient

#
#	$lva // "local version array". Custom array with details about the scripts.
#	$rva // "remote version array". Custom array with details about the scripts.
#	$ulva // "updated local version array". Custom array with details about the
#				script that were downloaded.
#
$lva = @{};
$rva = @{};
$ulva = @{};

#
#	Get and load the version.xml files.
#
$rv = New-Object System.Xml.XmlDocument;
$rv.loadXML($wc.downloadstring("$rroot/version.xml"));

$lv = New-Object System.Xml.XmlDocument;
$lv.load("$lroot\version.xml");

#
#	What are the currently loaded versions?
#
foreach($f in ($lv.getElementsByTagName("file"))) {
	$lva.add($f.name,@{"loc"=$f.loc;"dest"=$f.dest;"version"=$f.version})
}

#
#	Check if there is a new version and download.
#
foreach($f in ($rv.getElementsByTagName("file"))) {
	$rva.add($f.name,@{"loc"=$f.loc;"dest"=$f.dest;"version"=$f.version})

	if($f.version -gt $lva[$f.name].version) {
		$rurl = "$rroot/" + [string]::join("/",(($f.loc),($f.name))).replace("//","/").replace("./","");
		$lpath = "$lroot\" + [string]::join("\",(($f.dest),($f.name))).replace("\\","\").replace(".\","");

		"Updating: $($f.name)"
		$wc.downloadstring($rurl) | set-content -encoding ascii $lpath;
		$ulva.add($f.name,$rva[$f.name]);
	}
}

#
#	Update local version.xml file with new version data.
#
foreach($i in $ulva.keys) {
	$f = $False;
	$f = ($lv.getElementsByTagName("file")) | Where-object { $_.name -eq $i }
	if($f -eq $Null) {
		$node = $lv.CreateNode("element","file","");
		$node.SetAttribute("name",$i);
		$node.normalize();
		$lv.scripts.AppendChild($node) | out-null;
		$lv.normalize();
		$f = ($lv.getElementsByTagName("file")) | Where-object { $_.name -eq $i }
	}

	$f.SetAttribute("version",$ulva[$i].version);
	$f.SetAttribute("loc",$ulva[$i].loc);
	$f.SetAttribute("dest",$ulva[$i].dest);
	$f.normalize();
}

#
#	Save local version.xml
#
if($ulva.count -gt 0) {
	$lv.save("$lroot\version.xml");
}

#
#	Run desired scripts.
#
foreach($scr in $args) {
	if($rva[$scr] -eq $Null) { continue; }

	$lscript = "$lroot\" + [string]::join("\",(($rva[$scr]["dest"]),($scr))).replace("\\","\").replace(".\","");
	"Running: $lscript";
	$t = (& $lscript);
	"`t" + [string]::join("`n`t",$t);
}

version.xml (syntax used for both server and client)

<scripts>
  <file name="bootstrap.ps1" version="0.9" loc="./" dest=".\" />
  <file name="vpndrivemap.ps1" version="0.1" loc="./cnatoolscripts" dest=".\scripts" />
  <file name="test.txt" version=".01" loc="./" dest=".\" />
</scripts>
You can leave a response, or trackback from your own site.

3 Responses to “Bootstrapping scripts”

  1. Lee Holmes says:

    That looks great. A couple points of feedback:

    - The param() statement is the ideal way to define script parameters. When you define a parameter through a param statement, you only need to type as much of the parameter name required to disambiguate it from other parameters.

    PS:34 > function foo { param([switch] $help) if($help) { “Help!” } }

    PS:35 > foo -h
    Help!

    - You might find Import-CliXml / Export-CliXml to be easier data structures to deal with (rather than digging through XML)

    PS > $versionInfo = @{ Version = 1.0; File = “http://foo” }
    PS > Export-Clixml -In $versionInfo -Path version.xml
    PS > $versionInfo = Import-CliXml version.xml
    PS > $versionInfo.Version
    1

    PS > $versionInfo.File
    http://foo

    - You might consider formatting your help instead as help comments. We intentionally designed them to work in V1, but light up in V2: http://blogs.msdn.com/powershell/archive/2008/12/23/advanced-functions-and-test-leapyear-ps1.aspx

    Hope this helps,

    Lee Holmes [MSFT]
    Windows PowerShell Development

  2. josh says:

    Thanks for the comment Mr. Holmes!

    The param()’s looks very promising, especially now that I’m branching off from simple straight up PS scripting to making libraries of code to handle things.

    Help comments handled by the shell?! No way! Another reason I can’t wait for V2!

    Thanks for the pointer on XML, however I went this route as I wanted to have the version.xml file as human readable as possible (and it was a fun challenge to learn about System.XML). I don’t know if Export-clixml has changed with V2, but I find that V1 has bloat that could be forgotten when updating the file by hand. Perhaps if I had a UI or another script that handled updating version.xml I would go that route. (I could however see that export-clixml could be used for the local version.xml!)

    Thanks again! It’s quite an honor to be critiqued by you!

    -Josh Erickson

  3. [...] Lee Holmes’ comments, I’ve updated the file and function with some of the suggestions he gave. Unfortunately, the [...]

Leave a Reply