Learn about Windows PowerShell
(Note: These solutions were written for Advanced Event 1 of the 2010 Scripting Games.)
Kirk Munro, the world's first self-proclaimed Poshoholic, is a Microsoft MVP and Windows PowerShell Solutions Architect who has worked in the IT industry for more than 13 years. He has spent the majority of that time working with IT administrators, creating and developing software solutions to facilitate systems management. At Quest Software, Kirk works on Windows PowerShell solutions, most notably PowerGUI, where he is responsible for the architecture of new PowerGUI features and the management and development of the PowerPacks used to extend the PowerGUI administrative console.
· Blog: http://poshoholic.com
· Twitter: http://twitter.com/Poshoholic
· MVP Profile: http://tinyurl.com/Poshoholic
· MVP Canada Profile: http://blogs.technet.com/canitpro/archive/2008/04/03/mvp-profile-kirk-munro.aspx
· LinkedIn Profile: http://www.linkedin.com/in/kirkmunro
------------
Module: http://gallery.technet.microsoft.com/ScriptCenter/en-us/b4bbc1e3-426c-47c1-954c-4ba36d598584
Manifest: http://gallery.technet.microsoft.com/ScriptCenter/en-us/0034cd8b-1a01-4e50-922e-74bbdac14657
This event is very typical of what you might encounter in the real world. Your boss wants you to “check computers on the network” (whatever that means) and log a timestamp in the registry when the check has completed. If you were handed a task like this, you might think you can’t really take action on it because it raises more questions than answers. What you might not realize, though, is that you can create a Windows PowerShell script for this that solves this task even though you didn’t get a lot of details from your boss.
The first thing I did when I was writing this task was fire up the PowerGUI Script Editor (my Windows PowerShell editor of choice), and use the PowerShell v2 snippets to create a module. The “function (module, public, advanced)” snippet sets up my function framework in one empty file and the “module manifest” snippet sets up the module manifest in another empty file. I called my function Invoke-NetworkCheck, and then I saved the two files as AdvancedEvent1.psm1 and AdvancedEvent1.psd1, respectively, in an AdvancedEvent1 folder in my personal Modules folder (My Documents\WindowsPowerShell\Modules). After this was in place, I was able to update the function with the functionality I needed and the documentation describing it and the manifest with the version and other settings, leaving me with a module that I can send to my boss so that he can try it out.
For the functionality in the module, there are a few design decisions I made up front when planning out my function, as follows:
The complete Invoke-NetworkCheck function definition is shown here:
function Invoke-NetworkCheck {
<#
.SYNOPSIS
Runs a network check on local and remote computers.
.DESCRIPTION
The Invoke-NetworkCheck command runs a network check on local and remote computers and returns all output from the commands, including errors. The default network check is a lookup of the Windows PowerShell version; however, this may be replaced with any script block. With a single Invoke-NetworkCheck command, you can run the network check on multiple computers.
When the network check has finished running, the current date and time will be written to the registry in the location identified by RegistryPath and RegistryValue.
To run a single network check on a remote computer, use the ComputerName parameter.
You can also use Invoke-NetworkCheck on a local computer to evaluate the network check on the local machine. Windows PowerShell converts the script block to a command and runs the command immediately in the current scope.
Before using Invoke-NetworkCheck to run commands on a remote computer, read about_Remote.
.PARAMETER ScriptBlock
Specifies the network check to run. Enclose the network check script in curly braces ( { } ) to create a script block. This parameter is required.
Any variables in the network check are evaluated on the remote computer.
.PARAMETER RegistryPath
Specifies a path to the Registry key where a timestamp will be written when the network check task has finished. The timestamp will only be written once after all computers have been checked. The value of RegistryPath is used exactly as it is typed. No characters are interpreted as wildcards.
.PARAMETER RegistryValue
Specifies the name of the registry value where a timestamp will be written after the network check task has finished. The timestamp will only be written once after all computers have been checked. The value of RegistryValue is used exactly as it is typed. No characters are interpreted as wild card characters.
.PARAMETER Credential
Specifies a user account that has permission to perform this action. The default is the current user.
Type a user name, such as "Poshoholic" or "PoshStudios\Poshoholic", or enter a variable that contains a PSCredential object, such as one generated by the Get-Credential cmdlet. When you type a user name, you will be prompted for a password.
.PARAMETER ComputerName
Specifies the computers on which the command runs. The default is the local computer.
When you use the ComputerName parameter, Windows PowerShell creates a temporary connection that is used only to run the specified command and is then closed.
Type the NETBIOS name, IP address, or fully-qualified domain name of one or more computers in a comma-separated list. To specify the local computer, type the computer name, "localhost", or a dot (.).
To use an IP address in the value of the ComputerName parameter, the command must include the Credential parameter. Also, the computer must be configured for HTTPS transport or the IP address of the remote computer must be included in the WinRM TrustedHosts list on the local computer. For instructions for adding a computer name to the TrustedHosts list, see "How to Add a Computer to the Trusted Host List" in about_Remote_Troubleshooting.
Note: On Windows Vista and later versions of Windows, to include the local computer in the value of the ComputerName parameter, you must open Windows PowerShell with the "Run as administrator" option.
.EXAMPLE
PS C:\> Invoke-NetworkCheck
Major Minor Build Revision PSComputerName
----- ----- ----- -------- --------------
6 1 7600 16385 localhost
Description
-----------
This command invokes the default network check on the local computer.
The default network check is to look up the Windows PowerShell version. The script runs on the local computer through a remote connection and the results are returned with the computer name added in the PSComputerName property.
On Windows Vista and later versions of Windows, you must open Windows PowerShell with the "Run as administrator" option in order for the Invoke-NetworkCheck command to work against the local computer.
PS C:\> Get-Content C:\Computers.txt | Invoke-NetworkCheck
6 1 7600 16385 dc
6 1 7600 16385 exchange
This command invokes the default network check on the computers identified in the computers.txt file.
The default network check is to look up the Windows PowerShell version. The script runs on each computer through a remote connection and the results are returned with the computer name added in the PSComputerName property.
The results of this command are returned in the order in which the scripts finish running on the remote computers, which may not be the same order as the computers that are listed in C:\Computers.txt.
PS C:\> Get-Content C:\Computers.txt | Invoke-NetworkCheck -ScriptBlock {Get-Service wuauserv} -Verbose
VERBOSE: Invoking network check on 'dc'.
Status Name DisplayName PSComputerName
------ ---- ----------- --------------
Stopped wuauserv Windows Update dc
VERBOSE: Invoking network check on 'exchange'.
Running wuauserv Windows Update exchange
VERBOSE: Network Check Completed: 03/28/2010 21:55:15
This command invokes a custom network check on the computers identified in the computers.txt file, and shows verbose output in the results.
The custom network check gets the Windows Update service from each remote computer. The script runs on each computer through a remote connection and the results are returned with the computer name added in the PSComputerName property.
.INPUTS
System.String
You can pipe a string to Invoke-NetworkCheck.
.OUTPUTS
Output of the invoked network check
Invoke-NetworkCheck returns the output of the invoked network check (the network check is the value of the ScriptBlock parameter).
.NOTES
-- On Windows Vista and later versions of Windows, to use the ComputerName parameter of Invoke-Command to run a command on the local computer, you must open Windows PowerShell with the "Run as administrator" option.
-- When you run commands on multiple computers, Windows PowerShell connects to the computers in the order in which they appear in the list. However, the command output is displayed in the order that it is received from the remote computers, which might be different.
-- Errors that result from the command that Invoke-NetworkCheck runs are included in the command results. Errors that would be terminating errors in a local command are treated as non-terminating errors in a network check. This strategy ensures that terminating errors on one computer do not terminate the command on all computers on which it is run. This practice is used even when a remote command is run on a single computer.
-- If the remote computer is not in a domain that the local computer trusts, the computer might not be able to authenticate the user's credentials. To add the remote computer to the list of "trusted hosts" in WS-Management, use the following command in the WSMAN provider, where <Remote-Computer-Name> is the name of the remote computer:
Set-Item -LiteralPath WSMan:\Localhost\Client\TrustedHosts -Value <Remote-Computer-Name>.
.LINK
Invoke-Command
Test-Connection
about_Remote
WS-Management Provider
Registry Provider
about_functions_advanced
about_comment_based_help
#>
[CmdletBinding()]
param(
[Parameter()]
[System.Management.Automation.ScriptBlock]
${ScriptBlock} = {$PSVersionTable.BuildVersion},
[ValidateNotNullOrEmpty()]
[System.String]
${RegistryPath} = 'Registry::HKEY_CURRENT_USER\SOFTWARE\Scripting Games 2010\Advanced Event 1',
${RegistryValue} = 'LastUpdate',
[ValidateNotNull()]
[System.Management.Automation.Credential()]
${Credential} = [System.Management.Automation.PSCredential]::Empty,
[Parameter(ValueFromPipeline=$true)]
[System.String[]]
${ComputerName} = @('.')
)
begin {
try {
# Windows PowerShell 2.0 bug: https://connect.microsoft.com/PowerShell/feedback/details/545212/test-connection-throws-an-exception-if-you-pass-s-m-a-pscredential-empty-into-the-credential-parameter
# Workaround: Set up a hashtable for optional parameters to work around the Test-Connection bug
$optionalParameters = @{}
if ($Credential -and ($Credential -ne [System.Management.Automation.PSCredential]::Empty)) {
$optionalParameters.Credential = $Credential
}
catch {
throw
process {
if ($ComputerName) {
if (Test-Connection -ComputerName $ComputerName -Count 1 -ErrorAction SilentlyContinue @optionalParameters) {
Write-Verbose "Invoking network check on '$ComputerName'."
Write-Debug "Invoking network check on '$ComputerName'."
Invoke-Command -ComputerName $ComputerName -ScriptBlock $ScriptBlock @optionalParameters
} else {
Write-Warning "'$ComputerName' did not respond to the ICMP request."
end {
if (-not (Test-Path -LiteralPath $RegistryPath)) {
New-Item -Path $RegistryPath -Force
# Windows PowerShell 2.0 bug: https://connect.microsoft.com/PowerShell/feedback/details/482461/set-itemproperty-error-with-date-format
# Workaround: Remove the property so that the date is re-written in the correct format for the current culture every time this is run
Remove-ItemProperty -LiteralPath $RegistryPath -Name $RegistryValue -ErrorAction SilentlyContinue
$now = Get-Date
Set-ItemProperty -LiteralPath $RegistryPath -Name $RegistryValue -Value $now
Write-Verbose "Network Check Completed: $now"
After executing the Invoke-NetworkCheck function, I was able to see the timestamp in my registry:
Andrew Willett is the solutions architect for The Internet Group. Based in London, he spends most of his time designing and developing Microsoft-based architectures. Andrew’s LinkedIn profile: andrewwillett.com
Full script: http://gallery.technet.microsoft.com/ScriptCenter/en-us/a6c11f5d-c90d-41a9-8dfd-76bedf6710f4
This year for the Scripting Games I was asked to write a script for the first Advanced event. This builds upon the Beginner event, which called for a script that updates a registry key. There’s nothing too challenging here, but what’s good about this event is it gives us the opportunity to look at some of the common challenges that scripters face, such as command-line help/usage and how to parse parameters.
First things first: Let’s look at how we write the registry key. I’m going to start here and work back to the main subroutine as I wrote it.
A quick Bing search takes us to SetStringValue Method of the StdRegProv Class on MSDN, which details how to set a string value in the registry and has some VBScript code we can adapt. A call to
GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & computer &_
"\root\default:StdRegProv")
returns a StdRegProv class instance (from WMI), which provides methods that manipulate system registry keys and values; with the computer variable determining the computer to connect to. Passing a period or localhost connects to the local machine.
Next, we look at setting the value using the SetStringValue method. Notice from the documentation that if the key does not exist, the method call will fail. So we need to call CreateKey first to create it if it doesn’t exist:
registryObject.CreateKey hDefKey, sSubKeyName
registryObject.SetStringValue hDefKey, sSubKeyName, sValueName, sValue
You will notice that both calls require us to pass the registry tree/hive as a separate parameter, hDefKey. Tempting as it is to just default to HKEY_LOCAL_MACHINE, I implemented some basic string manipulation in the parseRegistryPath method that strips out the hive and returns the integer representation and the remainder of the registry key path. Utilising Case Else is an easy way to do some error handling if the input isn’t what you expect. I call this early on in the script to ensure any bad data gets flagged and rejected before the actual operation against the computer occurs. This is a good habit to get into when designing scripts that take user input.
Executing the WMI call, and indeed the actual command, could fail for a variety of reasons, such as connectivity, permissions, etc. So I wrapped the contents of the performCheck method in some error handling to trap any errors and write them to the command line.
The printUsage method encapsulates printing the command-line usage to the command line. This would look pretty nasty if you were to run the script interactively (in other words, without using cscript.exe), popping up a million modal dialog boxes. So for the sake of user experience, it has a bit of code that changes the display format depending on how it’s run.
The next part is how to parse command-line parameters. There are a few ways of doing this, ranging from the quick and dirty to the time consuming but thorough. In this case I’ve chosen a middle-of-the-road iterative design with some basic error handling, but there’s nothing to say this is a best practice or the way you should do it.
The iteration steps through each of the parameters, and then looks to parse them to the relevant variables. WScript.Arguments(i + 1) allows you to step forward to the next parameter when they come as a pair, such as –computers file.txt, not forgetting to increment i an extra time to step over it. I pass a single computer as a single-element array to keep the code consistent through the script.
Some weaknesses in this approach: There’s no handling of either/or scenarios, so one could pass –computer and –computers (with the last one taking precedence), and the error message doesn’t tell you which parameter is at fault. If you need a Rolls Royce solution, you could do this using regular expressions, but that’s overkill here.
Finally, the Main subroutine pulls all this together into something human-readable. Create the variables for the parameters, set them to the default, parse the parameters, and loop through performing the check on each computer. And voila! The result is shown in the following image.
If you want to know exactly what we will be looking at tomorrow, follow us on Twitter or Facebook. If you have any questions, send e-mail to us at scripter@microsoft.com or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.
Ed Wilson and Craig Liebendorfer, Scripting Guys