PowerShell Problem Solver: Improving the Hot Fix Report

powershell hero
In the first article of this short series I laid out the business problem and began the process of creating a PowerShell tool to solve it. My goal is not necessarily to have a finished solution at the end, but rather to teach you along the way and understand the development process. Sometimes learning the thought process is just as important as language and syntax.

The basic script I wrote for the first article works fine as long as nothing goes wrong. But what if one of the computers is offline or I don’t have permission?

script errors
Script Errors (Image Credit: Jeff Hicks)

The script ran for the first computer, but when it failed for the second it terminated the pipeline and the last computer is never processed. In these situations, I think the best approach is to run the underlying command, Get-HotFix, once for each computer. Even though Get-HotFix will accept an array of names, that doesn’t mean you have to use it that way.  By processing computer names individually, you can handle errors individually. For that, you will need to use Try/Catch.

Try/Catch

With Try/Catch, you use two scriptblocks. In the Try {} block, put in as much PowerShell code that you want to “try” to run. Typically these are commands that might have predictable errors like an offline computer or bad credentials. The corresponding Catch {} block contains the code to run when an error is raised or caught. It is possible to have different Catch blocks for different types of errors, but for now we’ll use one to handle everything. The other important detail that trips up many novice PowerShell scripters is that the error must be terminating.
Without getting too sidetracked into PowerShell errors and exceptions, know that in order for Try/Catch to work, the command in the Try block must create a terminating exception. You can accomplish this by setting the ErrorActionPreference to ‘Stop’. However, you rarely change the preference variable. Instead, use the common parameter -ErrorAction and set the value to ‘Stop’.
Here’s the revised script.

#requires -version 3.0
#TryCatch-HotFixReport.ps1
Param([string[]]$Computername = $env:COMPUTERNAME)
foreach ($Computer in $Computername) {
    Try {
        Get-Hotfix -ComputerName $Computer -ErrorAction Stop |
        Select-Object -Property PSComputername,HotFixID,Description,InstalledBy,InstalledOn,
        @{Name="Online";Expression={$_.Caption}}
    } #Try
    Catch {
        Write-Warning "$($computer.toUpper()) Failed. $($_.exception.Message)"
    } #Catch
} #foreach computer

When an error is caught, the exception object is “caught” by the Catch block. The $_ reference in the Catch block is to the exception object. All I’m doing is displaying the exception message. But now look at the result:

handling exceptions
Handling Exceptions (Image Credit: Jeff Hicks)

Much better. Now the error can still occur, but it won’t stop the rest of the script from running.

At this point, I think core functionality has been met. We have a reporting tool that writes objects to the pipeline that we can use with other cmdlets like ConvertTo-HTML and Export-CSV. However, it is bit of a sledgehammer right now. For example, if I wanted to only get certain types of hotfixes like Security updates, or find updates installed by a given user, I’d have to run the command and then filter with Where-Object. Or what if I wanted to use alternate credentials which Get-HotFix supports? Clearly there’s a bit more we can do.
I’ll go ahead and duplicate the Description and Credential parameters to the script.

Param(
[string[]]$Computername = $env:COMPUTERNAME,
[string]$Description,
[System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty
)

This is the way I handle credentials. The script will accept a credential object, or if I enter a name I get the credential prompt. But how do I figure out how to pass these optional parameters? I find the best way is to use a hashtable and splatting.

Splatting

I think you’ll find the splatting technique very useful. You create a hashtable where the keys match the names of a PowerShell command’s parameters. The beauty of hashtables is that they are easy to change on the fly based on whatever conditions you want to test.

#create a hashtable of parameters to splat to Get-Hotfix
$params = @{
    ErrorAction = 'Stop'
    Computername = $Null
}
if ($Credential.UserName) {
    #add the credential
    $params.Add("Credential",$Credential)
}
if ($Description) {
    #add the description parameter
    $params.add("Description",$Description)
}

The variable $params is now a hashtable of all the parameters for Get-Hotfix. Notice I set Computername to NULL. This is because I need to set a different value for each computer.

foreach ($Computer in $Computername) {
    #add the computer name to the parameter hashtable
    $params.Computername = $Computer

All that remains is to pass the hashtable to the cmdlet. The tricky part is remembering the syntax.

Try {
        Get-Hotfix @params |

Remember, the variable name is params and the @ symbol instructs PowerShell to splat it to the command.

using additional script features
Using additional script features (Image Credit: Jeff Hicks)

When I ran this command, I was prompted for the administrator password and now I only get the items with a description property of ‘Hotfix’.  But there is one last detail before I wrap up.
I knew that ‘HotFix’ was a valid description choice. But if I had entered an invalid entry, the script would have run without error and provided no results. In these situations, the best solution is to add a parameter validation.

[ValidateSet("Security Update","HotFix","Update")]
[string]$Description,

The ValidateSet() test will verify that the user enters one of the choices.

Validating parameter value
Validating parameter value (Image Credit: Jeff Hicks)

As a bonus, you also get tab completion for the value, which you can try for yourself. Here’s the script as it stands now.

#requires -version 3.0
#AddFeatures-HotFixReport.ps1
Param(
[string[]]$Computername = $env:COMPUTERNAME,
[ValidateSet("Security Update","HotFix","Update")]
[string]$Description,
[System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty
)
#create a hashtable of parameters to splat to Get-Hotfix
$params = @{
    ErrorAction = 'Stop'
    Computername = $Null
}
if ($Credential.UserName) {
    #add the credential
    $params.Add("Credential",$Credential)
}
if ($Description) {
    #add the description parameter
    $params.add("Description",$Description)
}
foreach ($Computer in $Computername) {
    #add the computer name to the parameter hashtable
    $params.Computername = $Computer
    Try {
        Get-Hotfix @params |
        Select-Object -Property PSComputername,HotFixID,Description,InstalledBy,InstalledOn,
        @{Name="Online";Expression={$_.Caption}}
    } #Try
    Catch {
        Write-Warning "$($computer.toUpper()) Failed. $($_.exception.Message)"
    } #Catch
} #foreach computer

Of course, we’re not done, although this script is certainly usable as is.  But there is at least one more feature to implement and there is some polishing to be done to make this a complete PowerShell tool.