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.