Nigel Boulton's Blog
10Apr/170

UK South Coast PowerShell User Group

My old friend and Microsoft PowerShell MVP Jonathan Medd is looking to test the viability of running a PowerShell meetup in Southampton on a regular basis. See his blog post here for more details. If you can make it on 26 April, please come along and register your interest – it would be great to see you there!

You can register for the initial Meetup here.

6Feb/131

Using JH Software’s Simple DNS Client Library for .NET in PowerShell

Last week I had a requirement to perform a DNS lookup within a PowerShell script, using a DNS server other than the one that the machine in question was configured to use. This turned out to be quite a challenge and, having found a library that looked like it would do it, I ended up having to call on the expertise of my good friend and PowerShell MVP Jonathan Medd to help me out!

As I'm sure I can't be the first person that has attempted this in PowerShell using the library in question (or something similar) I felt it was worth blogging the methodology…

I won't bore you with too much of the background, but I was amending a script that I wrote a few years back which used the .NET class System.Net.Dns to perform a DNS lookup. This worked fine until my ISP changed the behaviour of their DNS servers to return a search page in the event that the host record in question was not found. Although this is probably helpful to a human being, I wanted my script to raise an error in the event that the host record wasn't found - and this was no longer happening because my ISP's DNS now returns the IP address of the server delivering the search page instead. Therefore, to my script, the DNS lookup was appearing to be successful!

The obvious answer to this is to use a DNS server that doesn't behave in this way. Google's DNS is an example of this. I didn't want to change my entire infrastructure to use Google's DNS servers, so the most sensible approach was to have just the script use them. This is where the next problem became apparent – System.Net.DNS does not allow the use of alternative DNS servers.

After some Googling, I came across the excellent and free 'SimpleDNS DNS Client Library for .NET' from JH Software here. This allows you to specify DNS servers to perform lookups against. However (perhaps because I'm not a .NET programmer) I couldn't work out for the life of me how to set the required property, and that is where Jonathan helped me out. The methodology is described below. I can't take any credit for this – it's all his work Smile

The first step is obviously to download the SimpleDNS library from here, and put at least the Dynamic Link Library 'JHSoftware.DnsClient.dll' into a suitable location on the local machine, say 'C:\PowerShell\Libraries'. (There is a .CHM help file included in the download that is worth including too.)

Here is the entire code snippet used. An explanation of each line follows in the text below.

[System.Reflection.Assembly]::LoadFile("C:\PowerShell\Libraries\JHSoftware.DnsClient.dll")            

$ServerIPObj = [System.Net.IPAddress]::Parse("8.8.8.8"),[System.Net.IPAddress]::Parse("8.8.4.4")            

$OptionsObj = New-Object JHSoftware.DnsClient+RequestOptions
$OptionsObj.DnsServers = $ServerIPObj            

$IPVersion = [JHSoftware.DnsClient+IPVersion]::IPv4            

$HostAddress = [JHSoftware.DnsClient]::LookupHost("www.microsoft.com",$IPVersion,$OptionsObj)
$HostAddress[0].ToString()

First, the line:

[System.Reflection.Assembly]::LoadFile("C:\PowerShell\Libraries\JHSoftware.DnsClient.dll")

loads the library from the previously given location.

The next step is to create an array of .NET IPAddress objects which are set to Google's DNS server (8.8.8.8 and 8.8.4.4). This is done using the 'Parse' static method which converts a string into an IP address:

$ServerIPObj = [System.Net.IPAddress]::Parse("8.8.8.8"),[System.Net.IPAddress]::Parse("8.8.4.4")

Next, we need to create a new .NET object to hold the request options, and set the 'DnsServers' property of that. For VB.NET and C# examples (but no PowerShell Sad smile), see the programming guide here.

$OptionsObj = New-Object JHSoftware.DnsClient+RequestOptions
$OptionsObj.DnsServers = $ServerIPObj

The 'LookupHost' method requires an additional parameter because we are using 'RequestOptions' – an enumeration of the IP protocol version to be used, which is called DnsClient.IPVersion. The base class is System.Enum. To create this in PowerShell with the correct value you need use the method below, specifying the protocol version with the appropriate member name. There is a great post from Lincoln Atkinson on using enumerations in PowerShell here.

$IPVersion = [JHSoftware.DnsClient+IPVersion]::IPv4

Once this has been done, we have all the parameters we need to call the LookupHost method and return a result:

$HostAddress = [JHSoftware.DnsClient]::LookupHost("www.microsoft.com",$IPVersion,$OptionsObj)

Now let's take a look at $HostAddress:

Address           : 456734529
AddressFamily     : InterNetwork
ScopeId           :
IsIPv6Multicast   : False
IsIPv6LinkLocal   : False
IsIPv6SiteLocal   : False
IPAddressToString : 65.55.57.27

I wanted just the IP address as a string in a variable for the script to process further, and how to do this eluded me initially until I realised that the $HostAddress object was in fact being returned by the library as a System.Net.IPAddress object, and effectively as an array. The MSDN documentation for a similar method of the System.Net.Dns class, the GetHostAddresses method, helped me to work this out. So returning the IP address alone is simply a matter of:

$HostAddress[0].ToString()

…and that's it!

Jonathan and I decided that we would both blog this information as it could potentially be useful to a number of people. He has taken this a stage further and turned the code above into a very cool function, making it easy to drop into a script for future re-use (I will be incorporating this into my script!). You can see his blog post on this here.

10Sep/1111

Wake-on-LAN using a PowerShell Script

I currently have a home lab based on Simon Gallagher's excellent vTARDIS. This is an HP ProLiant ML115 which runs 8 virtual ESXi hosts arranged into a cluster which in turn run 30 Linux virtual machines. As you can imagine, this takes quite a while to start up! For this reason, I wanted a way to start the physical host programmatically (either on a scheduled basis or remotely via my home automation interface), in preparation for working on it or doing some learning. So a PowerShell script was obviously the answer! Within the lab itself I have a PowerCLI script which automatically powers up the various components in the correct order (but that's another story...).

The script I developed is shown below. I decided to share it via my blog as a) it might be useful to someone and b) it demonstrates the use of some handy PowerShell 2 features, such as advanced functions, parameter sets, parameter validation and comment-based Help, along with other good stuff like regular expressions, error handling and writing to the Windows event log. In the distant past I used an ActiveX control called UltraWOL (from UltraJones software, who don't seem to be around anymore) and called it from VBScript scripts to facilitate Wake-on-LAN, but I wanted to find a nice 'PowerShell only' way of implementing it.

The script can be used in two ways: it can be provided with a CSV file which maps 'known' machines to their MAC addresses, and then used to wake up one of these machines by providing just the computer name as a parameter, or alternatively, simply by specifying a MAC address using the 'MACAddress' parameter, to have a Wake-on-LAN magic packet containing that MAC address sent. In either case, results are reported to standard output and also recorded in the Application event log.

The script is shown below, but for your convenience you can download a copy here (MD5 checksum: 0533313E71C42816217F948247C17F9E) to save you copying and pasting (and all the potential problems associated with doing that) should you want to make use of it. Below the listing is a brief outline of the operation of the script.

<#
.NOTES
	================================================================================
	Filename:	Wake-Machine.ps1

	Author:		Nigel Boulton, http://www.nigelboulton.co.uk

	Version:	1.00

	Date:		10 Sep 2011

	Mod dates:	

	Notes:		See http://www.nigelboulton.co.uk/2011/09/
							wake-on-lan-using-a-powershell-script/
				for further details of this script
	================================================================================
.SYNOPSIS
	Wakes up a machine using Wake-on-LAN
.DESCRIPTION
	If the ComputerName parameter is given and is the name of a known machine (specified
	in the CSV file 'MACLookup.csv', stored in the same folder as this script), sends a
	Wake-on-LAN magic packet containing the MAC address of that machine. The ComputerName
	parameter is the default, so if omitted the script will expect any argument given to
	be the name of a known machine

	The format for the CSV file is as follows. Header information as shown is required in
	this file:

	ComputerName,MACAddress
	pc1,00-16-da-2b-6f-b8

	If the alternative MACAddress parameter is given and is a valid MAC address, sends a
	Wake-on-LAN magic packet containing that MAC address. In this case the MACLookup.csv
	file is not used and is not required to be present

	Results are reported to standard output and also recorded in the Application event log
.EXAMPLE
    Wake-Machine.ps1 -ComputerName PC1

	Description
	-----------
	Sends a Wake-on-LAN magic packet for the known machine 'PC1'
.EXAMPLE
    Wake-Machine.ps1 PC1

	Description
	-----------
	Sends a Wake-on-LAN magic packet for the known machine 'PC1'
.EXAMPLE
	Wake-Machine.ps1 -MACAddress 00-16-DA-2B-6F-B8

	Description
	-----------
	Sends a Wake-on-LAN magic packet containing the MAC address 00-16-DA-2B-6F-B8
.LINK
	http://www.nigelboulton.co.uk/2011/09/wake-on-lan-using-a-powershell-script/
#>
[CmdletBinding(DefaultParameterSetName='ComputerName')]
param(
 [Parameter(Mandatory=$true,
      HelpMessage="Enter a known machine name. See Help for this script for further information",
      Position=0,
      ParameterSetName='ComputerName')]
    [ValidateNotNullOrEmpty()]
 [string]$ComputerName,            

 [Parameter(Position=0,
      ParameterSetName='MACAddress')]
 [ValidatePattern('^([0-9a-fA-F]{2}[:-]{0,1}){5}[0-9a-fA-F]{2}$')]
 [string]$MACAddress
)            

function Send-MagicPacket {
 param(
 [Parameter(Mandatory=$true,
      HelpMessage="Enter a valid MAC address")]
 [ValidatePattern('^([0-9a-fA-F]{2}[:-]{0,1}){5}[0-9a-fA-F]{2}$')]
 [string]$MAC
 )
 <#
	.NOTES
		================================================================================
		Purpose: 		To send a Wake-on-LAN magic packet with a specified MAC address
		Assumptions:
		Effects:
		Inputs:
		 $MAC:			MAC address to include in packet
		Calls:
		Returns:		

		Notes:			Based on http://thepowershellguy.com/blogs/posh/archive/
										2007/04/01/powershell-wake-on-lan-script.aspx
		================================================================================
	.SYNOPSIS
		Sends a Wake-on-LAN magic packet containing a specified MAC address
	.DESCRIPTION
	    Sends a Wake-on-LAN magic packet containing a specified MAC address. The MAC
		address is specified by the MAC parameter. The octets of the MAC address may
		be separated by dashes '-', colons ':' or nothing
	.EXAMPLE
		Send-MagicPacket -MAC 00-16-DA-2B-6F-B8

		Description
		-----------
		Sends a Wake-on-LAN magic packet containing the MAC address 00-16-DA-2B-6F-B8
	.EXAMPLE
		Send-MagicPacket -MAC 00:16:DA:2B:6F:B8

		Description
		-----------
		Sends a Wake-on-LAN magic packet containing the MAC address 00-16-DA-2B-6F-B8
	.EXAMPLE
		Send-MagicPacket -MAC 0016DA2B6FB8

		Description
		-----------
		Sends a Wake-on-LAN magic packet containing the MAC address 00-16-DA-2B-6F-B8
	.LINK
		http://www.nigelboulton.co.uk/2011/09/wake-on-lan-using-a-powershell-script/
	#>            

 # Use regex to strip out separators (: or -) if present and split string every second character
 # Piping to Where-Object {$_} avoids empty elements between each pair of characters
 $MACArray = ($MAC -replace '[-:]', [String]::Empty) -split '(.{2})' | Where-Object {$_}            

 $MACByteArray = $MACArray | ForEach-Object {[Byte]('0x' + $_)}
 $UDPClient = New-Object System.Net.Sockets.UdpClient
 $UDPClient.Connect(([System.Net.IPAddress]::Broadcast),4000)
 $Packet = [Byte[]](,0xFF * 6)
 $Packet += $MACByteArray * 16
 Write-Debug "Magic packet contents: $([bitconverter]::ToString($Packet))"
 [void]$UDPClient.Send($Packet, $Packet.Length)
 Write-Debug "Wake-on-LAN magic packet of $($Packet.Length) bytes sent to $($MAC.ToUpper())"
}            

#***********************************************************************************
# Start of script
#Requires -Version 2
Set-StrictMode -Version 2            

# User configurable values
$MACLookupFilePath = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) MACLookup.csv # Location and name of MAC address lookup file            

# Initialise variables
$ComputerDisplayName = $Null ; $Msg = $Null            

# Register event log source if required (requires admin rights)
if (!(Test-Path HKLM:\SYSTEM\CurrentControlSet\Services\Eventlog\Application\$($MyInvocation.MyCommand.Name))) {
 Try {
  New-EventLog -LogName Application -Source $MyInvocation.MyCommand.Name -ErrorAction Stop
 }
 Catch {
  Write-Host "WARNING: Unable to register event log source. You must run this script at least once as an administrator to do this" -ForegroundColor Red -BackgroundColor Black
 }
}            

if ($PsCmdlet.ParameterSetName -eq 'ComputerName') { # Computer name specified            

 # Import CSV file of MAC addresses for known machines into a hash table.
 # This makes it easy to check whether a given machine exists in this list,
 # and retrieve its MAC address if so
 $MACLookup = @{}
 Import-CSV -Path $MACLookupFilePath | ForEach-Object {$MACLookup[$_.ComputerName] = $_.MACAddress}            

 # Find MAC address for machine in hash table (this is case insensitive)
 if ($MACLookup.ContainsKey($ComputerName) -eq $True) {
  $MACAddress = $MACLookup.Get_Item($ComputerName)
  $ComputerDisplayName = "($($ComputerName.ToUpper()))"
  # Validate MAC address
  if ($MACAddress -notmatch '^([0-9a-fA-F]{2}[:-]{0,1}){5}[0-9a-fA-F]{2}$') {
   $Msg = "ERROR: Invalid MAC address specified: '$MACAddress'. Verify the MAC address for the computer '$ComputerName' in the CSV file '$MACLookupFilePath'"
   Write-Host $Msg -ForegroundColor Red -BackgroundColor Black
   Try {
    Write-EventLog -LogName Application -Source $MyInvocation.MyCommand.Name -EventID 1001 -EntryType Error -Message $Msg -Category 0
   }
   Catch {
    Write-Host "WARNING: Unable to write to event log. You must run this script at least once as an administrator to register the event log source" -ForegroundColor Red -BackgroundColor Black
   }
   Throw "Invalid MAC address"
  }
 } else {
  $Msg = "ERROR: Unrecognised computer name: '$($ComputerName.ToUpper())'. Add the computer name and MAC address to the CSV file '$MACLookupFilePath'"
  Write-Host $Msg -ForegroundColor Red -BackgroundColor Black
  Try {
   Write-EventLog -LogName Application -Source $MyInvocation.MyCommand.Name -EventID 1001 -EntryType Error -Message $Msg -Category 0
  }
  Catch {
   Write-Host "WARNING: Unable to write to event log. You must run this script at least once as an administrator to register the event log source" -ForegroundColor Red -BackgroundColor Black
  }
  Throw "Unrecognised computer name"
 }
}            

Send-MagicPacket -MAC $MACAddress
$Msg = ("Wake-on-LAN magic packet sent to $($MACAddress.ToUpper()) $ComputerDisplayName").Trim()
Write-Output $Msg
Try {
 Write-EventLog -LogName Application -Source $MyInvocation.MyCommand.Name -EventID 1000 -EntryType Information -Message $Msg -Category 0
}
Catch {
 Write-Host "WARNING: Unable to write to event log. You must run this script at least once as an administrator to register the event log source" -ForegroundColor Red -BackgroundColor Black
}

The first section of the script is the comment-based Help which can be displayed by typing Get-Help .\Wake-Machine.ps1 in a PowerShell console in the usual way (I'd advise that you do this to help understand the operation of the script before attempting to implement it).

Following that are the definitions for the two possible parameters. Note their parameter set names - these are called 'ComputerName' and 'MACAddress' respectively. You will notice that the default parameter set has been set as 'ComputerName'. This means that if the parameter name is omitted, the script will expect any argument given to be the name of a known machine in the CSV file.

The CSV file must be called 'MACLookup.csv' and stored in the same folder as this script. The format for the CSV file is as follows - header information as shown below is required in the file. A sample CSV file is included with the downloadable script:

ComputerName,MACAddress
pc1,00-16-da-2b-6f-b8

If the 'MACAddress' parameter is given, a regular expression is then used with parameter validation (ValidatePattern) to verify that it is a valid MAC address. The octets of the MAC address may be separated by dashes '-', colons ':' or nothing.

Coming on to the main body of the script you will notice that I use

Set-StrictMode -Version 2

This is a good practice which ensures that you don't get caught out by mis-typed variable names. I use this in all my scripts. For more information see this TechNet document.

The next significant operation is registering an event log source for the script in the Windows Application event log, if that hasn't already been done. This requires you to be an administrator, so it will be necessary to run the script once as an administrator if you will normally run it as a non-admin. If you don't do this, the script will continue without writing the result to the event log.

The next section is only executed if the 'ComputerName' parameter set is in operation: the MACLookup.csv file is read into a hash table, then this is queried to see whether the computer name specified by the parameter exists, and the corresponding MAC address is retrieved if so, and then validated by the same regular expression as before. If not, the script writes this fact to the event log and throws an error.

Finally, the 'Send-MagicPacket' function is called to send the Wake-on-LAN magic packet and the result is reported to standard output and the event log.

The Send-MagicPacket function is of course at the heart of the script functionality. It is based on a great post by The PowerShell Guy that you can find here (thanks /\/\o\/\/). I tweaked it slightly because I thought it would be more flexible to be able to specify the octets of the MAC address separated by either dashes, colons or nothing. I also added some comment-based Help so that you can easily copy and paste this function into other scripts or a PowerShell module, to make use of it elsewhere. You will notice that you can use the PowerShell common parameter 'Debug' to troubleshoot creation and sending of the magic packet if necessary. See Jeffery Hicks (The Lonely Administrator)'s excellent post here for more information on how and why you might do that.

I hope this script is of use to you (or perhaps the techniques within it)  – please leave a comment to let me know if so.

7May/1125

Problems with PowerShell Comment-based Help

I was writing a script recently and came across a couple of "gotchas" when attempting to use PowerShell 2's Comment-based Help at script level. A simple test script to demonstrate what I am about to describe is shown below:

<#
.SYNOPSIS
    Tests whether PowerShell 2 Comment-based Help is working as expected
.DESCRIPTION
    Displays Comment-based Help for this script
.EXAMPLE
    Test-CommentBasedHelp.ps1
.NOTES
    None
#>            
Write-Host "Hello World"


The expected output from this is as follows:

Expected output from test script showing Comment-based Help working

"Get-Help about_comment_based_help" says

"SYNTAX FOR COMMENT-BASED HELP IN SCRIPTS

  Comment-based Help for a script can appear in one of the following two

  locations in the script.

  -- At the beginning of the script file. Script Help can be preceded in the 
     script only by comments and blank lines.

  -- If the first item in the script body (after the Help) is a function 
     declaration, there must be at least two blank lines between the end of the 
     script Help and the function declaration. Otherwise, the Help is 
     interpreted as being Help for the function, not Help for the script.

  -- At the end of the script file."

Looks straightforward enough. However, if you try the following:

# Comment            
<#
.SYNOPSIS
    Tests whether PowerShell 2 Comment-based Help is working as expected
.DESCRIPTION
    Displays Comment-based Help for this script
.EXAMPLE
    Test-CommentBasedHelp.ps1
.NOTES
    None
#>            
Write-Host "Hello World"

The output isn't as expected. You just get the name of the script returned, as shown below:

Output from test script showing just script name returned

What's that about…? Well, this is the first "gotcha". Although Get-Help says that Script Help can be preceded in the script by comments and blank lines, it's easy to miss the text further up that says

"All of the lines in a comment-based Help topic must be contiguous. If a comment-based Help topic follows a comment that is not part of the Help topic, there must be at least one blank line between the last non-Help comment line and the beginning of the comment-based Help."

So to avoid breaking it, you need to use the following syntax:

# Comment            

<#
.SYNOPSIS
    Tests whether PowerShell 2 Comment-based Help is working as expected
.DESCRIPTION
    Displays Comment-based Help for this script
.EXAMPLE
    Test-CommentBasedHelp.ps1
.NOTES
    None
#>            
Write-Host "Hello World"

One tiny blank line can make such a difference!

The same applies if you put anything inside the comment block before the first keyword. For example, the following is not valid:

<#
Comment
.SYNOPSIS
    Tests whether PowerShell 2 Comment-based Help is working as expected
.DESCRIPTION
    Displays Comment-based Help for this script
.EXAMPLE
    Test-CommentBasedHelp.ps1
.NOTES
    None
#>            
Write-Host "Hello World"


The second "gotcha" is around the fact that Get-Help says that "at the end of the script file" is a valid location for Comment-based Help for a script. This is true, but if you do this, be aware of the fact that if you subsequently sign your script, a signature block is added to the end of the script which means that your Comment-Based Help block is no longer at the end of the file, and you will get the symptom described above.

Write-Host "Hello World"            
<#
.SYNOPSIS
    Tests whether PowerShell 2 Comment-based Help is working as expected
.DESCRIPTION
    Displays Comment-based Help for this script
.EXAMPLE
    Test-CommentBasedHelp.ps1
.NOTES
    None
#>            

# SIG # Begin signature block            
# MIID/wYJKddoZIhvcNAQcCoIID8DCCA+wCAQExCzAJBgUrDgMCGgUAMGkGCisGAQ            
# gjcWqCA56QSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYp            
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUXPsShpFvys7oIWj23R6GpiQb            
# l46gggIdMIICGTCCAYKgAwIBAgIQSqeTRu71Hp1OJu+xx6ASfzANBgkqhkiG9w0B            
# AQQFADAYMRYwFAYDVQQDEw1OaWdlbCBCd5b3VsdG9uMCAXDTAwMDEwMTAwMDAwMF            
# DzIwOTkwMTAxMDAwMDAwWjAYMRYwFA68YDVQQDEw1OaWdlbCBCb3VsdG9uMIGfMA            
# CSqGSIb3DQEBAQPd789yhiCBiQKBgQC0+WAMn64J4oKsTIKsbBH5cTB4fEnfafzG            
# 1G+QkkgHpimfbT0Y+XrfmqKP6G/ailX3BHvwYOMmuSARqutfF6Rv9AQ7B/Sl8BgH            
# +AztcWg+jNko9dTidqexjH+bunpbzFMIJ6Lnzr+xSBvAbQR8oWtOwodQASW0G4Ra            
# b7+u5VZBaQIDAQABo2IwYDATBgNVHSUEDDAKBggrBgEFBQcDAzBJBgNVHQEEQjBA            
# gBCJKelkj8xj96uouh6cXclzoRowGDEWMBQGA1UEAxMNTmlnZWwgQm91bHRvboIQ            
# SqeTRu71Hp1OJu+xx6ASfzAHJy578}iG9w0BAQQFAAOBgQBw/WwbWGAHyyGjDhpb            
# Z7i8duiLHBBRYfUpczIh02jXPU+DfWa7atfwuFyxeilUDTszZ/2dOplH8l394j3H            
# yy8ZqXTf796zLqWXmvZn85rkgm16rRXqzDBheHidyTP3cPRPn7ehCahAAqpmHS0y            
# H7X3bevXIvMwDSXpL47nCCfWUDGCAUwwggFIAgEBMCwwGDEWMBQv0GA1UEAxMNTm            
# ZWwgQm91bHRvbgIQSqeTRu71Hp1OJu+xx6ASfzAJBgUrDgMCGgUAoHgwGAYKKwYB            
# BAGCNwIBDDEKMAhjY7guigAoAAoQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNw            
# BgorBgEEAYI3ACBgELMQ4wDAYKKwYBBAGCNwIBFTAjBgkqhkiG9w0BCQQxFgQULr            
# hd1Ib4bCuTXkm35KwTLDIiK58wDQYJKoZIcxhvcNAQEBBQAEgYCPumseo6AGAZFD            
# R37Tj8Kx6E0E6+MHqMHZ1TcLjO3E/lZqzFW7cCTJOcIH6Yg78r2DiToGXISdJkk8            
# 9sBB3nbsvQHWsWOYdRVwH8VueRg9paSa3CMj87E500z6bElejYGOi9VVfDZ8xBwm            
# rY4aAWd5A2dDpnojJQLC1yCv8w==            
# SIG # End signature block

Update: Please see the comment below from June Blender at Microsoft, who writes PowerShell Help. She has kindly updated the PowerShell Online Help topic about_Comment_Based_Help (available here) to reflect this "gotcha". Some further good news is that this change made it into the Windows PowerShell 2.0 Core Help May 2011 Update, which provides updated PowerShell Help in a CHM format (handy for searching!).

Hope this information is useful to you, I spent more time than I would have liked chasing this around!

Filed under: PowerShell 25 Comments
26Mar/110

Regular Expressions in PowerShell – Tome Tanasovski

Tome Tanasovski (@toenuff on Twitter) who runs the NYC PowerShell User Group did a brilliant presentation on Regular Expressions in PowerShell to the UK PowerShell User Group earlier this week - by far the best I've seen to date.

Richard Siddaway (who runs the UK PowerShell User Group) has kindly made the recording available here and Tome's presentation slides, scripts and cheat sheet here. Well worth a look!

Many thanks to Tome, and of course to Richard for organising this event.

Filed under: PowerShell No Comments
28Feb/113

Converting a PowerShell Array into a .Net Framework ArrayList

I was writing a PowerShell script earlier today and needed to take some data I had in an array and put it into a .Net Framework ArrayList. It took me some searching online to find out how to do this so I thought I’d blog it here…

ArrayLists are a powerful way of managing data – one of their biggest advantages is that it is easy to manipulate data by adding or removing elements, as shown below. With default PowerShell arrays there is no simple way to remove elements.

To demonstrate how ArrayLists work, try this code:

$ArrayList = New-Object System.Collections.ArrayList
$ArrayList.Add("New Element 1")
$ArrayList.Add("New Element 2")
$ArrayList.Count
$ArrayList.Remove("New Element 2")
$ArrayList.Count

Note that the return value from the Add method is the element number that was added. You can always cast this to void or pipe it to Out-Null if you don’t need it.

You can also specify where to add or remove elements:

$ArrayList.Add(0,"New Element 3") 	# Adds an element at the beginning of the array list
$ArrayList.RemoveAt(0) 				# Removes the first element

Anyway, back to the point... I had an array containing a list of files (which came from Get-ChildItem), and I wanted to create an ArrayList and populate this with that data, because I wanted to be able to remove each file from the list later on in the script. There are two approaches that can be used for this:

From an existing array:

$arrFiles = Get-ChildItem
$colFiles = New-Object System.Collections.ArrayList
$colfiles.AddRange($arrFiles)

Or more simply, populate the ArrayList directly:

$colFiles = New-Object System.Collections.ArrayList(,(Get-ChildItem))

Note the comma within the parameters – the comma is the array construction operator in PowerShell.

Forgive my artistic license in choosing a title for this post – we’re not really converting an array as such, but I thought it would be the most likely thing somebody seeking this information would search for.

Filed under: PowerShell 3 Comments
7Feb/110

Checking whether a Hotfix is installed on Multiple Machines using PowerShell Remoting

In my last post, I said that I would post a way of verifying whether a particular hotfix had been installed on a number of machines, so here it is...

This approach uses good old PowerShell remoting again - as I have said before, this is an incredibly powerful way of executing PowerShell code on a number of machines, and well worth investing the time to set it up in your environment.

...So, I'd deployed the hotfix to all the servers in the farm, and I needed a quick way of verifying that it had been successfully installed on each of them. This was a variation on an approach I'd taken in the past (an enhancement kindly provided by Jeffrey Snover) - with one important difference: this time I was making good use of the PowerShell custom objects that the script returns. The script code is shown below:

$Servers = $(1..176 | foreach {"SERVER$_"})

Invoke-Command -ComputerName $Servers -ScriptBlock {
	$Result = Get-Hotfix | where {$_.hotfixid -eq 'KB2464876'}
	if ($Result) {
        New-Object PSObject -Property @{Host = hostname; Value = $true}
    } else {
		New-Object PSObject -Property @{Host = hostname; Value = $false}
    }
}

Running this script as shown below allowed me to get a quick indication of any servers that did not have the hotfix installed. This was achieved by querying for returned custom objects whose "Value" property was not equal to True:

./Check-Hotfix.ps1 | Where-Object {$_.Value -ne $True} | Select-Object Host

...and the result was:

Host
----

As I got no servers returned in the result (which of course is good), I wanted a confidence check, so I ran it this way to prove that the code was in fact running as expected:

./Check-Hotfix.ps1 | Where-Object {$_.Value -ne $False} | Select-Object Host

Host
----
SERVER1
SERVER2
SERVER3
SERVER4
.

As I mentioned in my last post, there are a number of ways you can build the list of servers. I used a numbered range, but by simply substituting the line which sets the $Servers variable, you can easily read a list of machine names from a text file (or a CSV file of course):

$Servers = Get-Content '\\Fileserver\Hotfixes\ServerList.txt'

Variations on the above approach can potentially allow you to do almost anything that you can do with PowerShell locally, across your entire server estate. For example, recently I used the same method to check which servers had a specific unwanted value in one of the Terminal Server "shadow" keys. This rogue value was being written into user profiles when users were directed to the servers in question and causing unexpected behaviour in subsequent sessions.

One of the best things about this is that, in most cases, you can develop and test the functional part of the code locally and then simply drop it into the scriptblock. Nice!

Filed under: PowerShell No Comments
20Jan/1111

Installing a Windows Hotfix on Multiple Machines using a PowerShell Script

A little while ago I was given a hotfix by Microsoft PSS for an issue we had been experiencing with the WMI repository intermittently becoming corrupted on Windows 2008 servers. As you will know from my previous posts we have quite a few servers, so after testing the hotfix carefully I was looking for a way to deploy this across the server estate with minimum effort. We do have a third-party deployment product, but this is geared around deploying regular Microsoft security patches as opposed to hotfixes intended to address specific issues. I wanted a way to do this semi-interactively so that I could monitor progress and deal with any issues arising during the deployment process.

My good friend and colleague Jonathan Medd kindly did the initial research on this for me (I figured if anybody could find a way to do it then he could!). We suspected that we might have some trouble getting PowerShell remoting to do this (see my previous post here), and after some searching and testing it soon became clear that this was in fact the case.

The approach we settled on was still based around PowerShell (of course) but we ended up having to make use of the trusty old utility PsExec, originally written by Mark Russinovich of Sysinternals, who now come under the Microsoft umbrella.

In this solution, PsExec.exe calls WUSA.exe, which is the Windows Update Stand-alone Installer. This is a means of installing update packages programmatically. Update packages have an .msu file extension. I can’t get out of the habit of calling them hotfixes though, sorry... WUSA is a simple and effective utility that surprisingly, I hadn’t encountered before. The final script code is shown below:

$Servers = $(1..176 | foreach {"SERVER$_"})

$HotfixPath = '\\Fileserver\Hotfixes\KB2464876\Windows6.0-KB2464876-x86.msu'

foreach ($Server in $Servers){
	if (Test-Path "\\$Server\c$\Temp"){
		Write-Host "Processing $Server..."
		# Copy update package to local folder on server
		Copy-Item $Hotfixpath "\\$Server\c$\Temp"
		# Run command as SYSTEM via PsExec (-s switch)
		& E:\SysinternalsSuite\PsExec -s \\$Server wusa C:\Temp\Windows6.0-KB2464876-x86.msu /quiet /norestart
		if ($LastExitCode -eq 3010) {
			$ConfirmReboot = $False
		} else {
			$ConfirmReboot = $True
		}
		# Delete local copy of update package
		Remove-Item "\\$Server\c$\Temp\Windows6.0-KB2464876-x86.msu"
		Write-Host "Restarting $Server..."
		Restart-Computer -ComputerName $Server -Force -Confirm:$ConfirmReboot
		Write-Host
	} else {
		Write-Host "Folder C:\Temp does not exist on the target server"
	}
}

To avoid authentication issues, we have PsExec run WUSA as SYSTEM (-s switch), which means that the update package needs to be available locally, so the script copies it to C:\Temp on the machine in question first. During testing, we were using the -i (interactive) switch, but doing this caused error 1008 "ERROR_NO_TOKEN" when I tried to run it for real – this appears to happen if you are not logged on to the server being processed.

The hotfix (sorry, "update package") in question required a restart after installation. I wanted the process to be as automated as possible, but still interactive, as I mentioned earlier. I wanted the restart to be performed automatically and for the script to proceed to the next server unprompted if the package installed as expected, but to prompt me if not so that I could troubleshoot.

To achieve this, WUSA installs the package with the /norestart switch. Because of this, the error code returned from WUSA (via PsExec) is 3010. In fact this isn’t an error. By testing PowerShell’s built in $LastExitCode variable it's possible to have the script proceed with the removal of the local file and the subsequent restart of the machine automatically if this is the result.

Of course, you can build the list of servers in a number of ways in the code above. I used a numbered range, but by simply substituting the line which sets the $Servers variable, you can easily read a list of machine names from a text file:

$Servers = Get-Content '\\Fileserver\Hotfixes\ServerList.txt'

I ran this against 176 servers in groups of about 50 in a few hours and it worked faultlessly. After all the servers had restarted I needed a quick way of verifying that the hotfix had been successfully installed on all of them. Shortly I will publish a further post which outlines how I did that. It’s neat and simple and potentially useful to be able to check for the presence of any hotfix on a number of machines.

Filed under: PowerShell 11 Comments
5Dec/104

Checking File Associations with the help of PowerShell Remoting

I recently needed to check the file association for .JPG files across a whole Citrix server estate, as we’d received reports of files of these types not always opening as expected. Because PowerShell remoting is enabled on every server, this was a very easy job..!

The code snippet below is what I used. A script block is run remotely on each server using the Invoke-Command cmdlet. The script block then uses the Get-ItemProperty cmdlet to read the registry to get the default file association for .JPG files (via the “jpegfile” class) and reports OK if it is as expected, or the actual value that is set if not:

1..176 | ForEach-Object {
	$ServerName = "SERVER$_"
	Write-Host "$($ServerName): " -NoNewLine
	Invoke-Command -ComputerName $ServerName -ScriptBlock {
		$Value = Get-ItemProperty "Registry::HKEY_CLASSES_ROOT\jpegfile\shell\open\command" "(Default)" | Select-Object -ExpandProperty "(Default)"
		if ($Value -eq "C:\Windows\System32\rundll32.exe `"C:\Program Files\Windows Photo Gallery\PhotoViewer.dll`", ImageView_Fullscreen %1") {
			Write-Host "OK"
		} else {
			Write-Host $Value
		}
	}
}

Update: Please see the comment below from Jeffrey Snover. Jeffrey's approach makes use of the concurrency feature of Invoke-Command, which executes the command on 32 servers simultaneously (by default) and so returns the results substantially faster.

In my opinion, PowerShell remoting is by far the best version 2.0 feature. If you don’t already have it enabled across your server estate I’d strongly recommend doing so as it can save hours of effort.

Depending on your environment, there may be a few hoops you have to jump through to get remoting working properly, but it will be time well spent.  PowerShell MVP Jonathan Medd has an excellent post on his blog on Enabling PowerShell 2.0 Remoting in an Enterprise and Ravikanth Chaganti has produced a helpful multi-part PowerShell 2.0 remoting guide.

Be aware though that unfortunately there are some things that can’t be run remotely – a month or two ago I was doing some work with WSUS and discovered that it’s not possible to call the IUpdateSession::CreateUpdateDownloader method remotely for example. Shame!

Filed under: PowerShell 4 Comments