Bookmark and Share

 

(Note: These solutions were written for Advanced Event 1 of the 2010 Scripting Games.)

 

Advanced Event 1 (Windows PowerShell)

Image of Kirk MunroKirk 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.

 

·         Bloghttp://poshoholic.com

·         Twitterhttp://twitter.com/Poshoholic

·         MVP Profilehttp://tinyurl.com/Poshoholic

·         MVP Canada Profile: http://blogs.technet.com/canitpro/archive/2008/04/03/mvp-profile-kirk-munro.aspx

·         LinkedIn Profilehttp://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:

  • I decided to accept computer names as values from the pipeline. Accepting computer names this way allowed me to let users get computer names from wherever they want rather than be limited to computer names in a text file (of course, they can get them from a file too using Get-Content if they want to).
  • Anything that I didn’t know up front from my boss’s request translated into function parameters with default values. This included the registry path, the registry value name, and the network check itself.  Taking this approach allowed me to create a versatile function that could do any network check and write the timestamp in any registry location.
  • Being a good network citizen, I added a Credential parameter so that you don’t have to be in “god mode” to get the function to work.
  • The function internals are pretty straightforward. In the begin block, I create a hashtable with the optional Credential parameter so that I can use splatting when I am calling Test-Connection and Invoke-Command in the process block of the function (a good technique to use when dealing with optional credentials). In the process block, I ping each computer first with Test-Connection and if it responds, I invoke the network check with Invoke-Command. After all computers have been processed, I write the current timestamp to the registry in the end block.


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.

 

.EXAMPLE

      PS C:\> Get-Content C:\Computers.txt | Invoke-NetworkCheck

 

Major  Minor  Build  Revision PSComputerName

-----  -----  -----  -------- --------------

6      1      7600   16385    dc

6      1      7600   16385    exchange

 

Description

-----------

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.

.EXAMPLE

      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

 

      Description

      -----------

      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.

      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.

 

.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

 

.LINK

      Test-Connection

 

.LINK

      about_Remote

 

.LINK

      WS-Management Provider

 

.LINK

      Registry Provider

 

.LINK

      about_functions_advanced

 

.LINK

      about_comment_based_help

 

      #>

      [CmdletBinding()]

      param(

            [Parameter()]

            [System.Management.Automation.ScriptBlock]

            ${ScriptBlock} = {$PSVersionTable.BuildVersion},

 

            [Parameter()]

            [ValidateNotNullOrEmpty()]

            [System.String]

            ${RegistryPath} = 'Registry::HKEY_CURRENT_USER\SOFTWARE\Scripting Games 2010\Advanced Event 1',

 

            [Parameter()]

            [ValidateNotNullOrEmpty()]

            [System.String]

            ${RegistryValue} = 'LastUpdate',

 

            [Parameter()]

            [ValidateNotNull()]

            [System.Management.Automation.Credential()]

            ${Credential} = [System.Management.Automation.PSCredential]::Empty,

 

            [Parameter(ValueFromPipeline=$true)]

            [ValidateNotNullOrEmpty()]

            [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 {

            try {

                  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."

                        }

                  }

            }

            catch {

                  throw

            }

      }

      end {

            try {

                  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"

            }

            catch {

                  throw

            }

      }

}

 


After executing the Invoke-NetworkCheck function, I was able to see the timestamp in my registry:

Image of timestamp in registry

 

 

Advanced Event 1 (VBScript)


Image of Andrew Willett

 


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.


Image of script output

 

 

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