Skip to main content

PowerShell Scripting Games 2013 - Advanced Practice Solution

This is my solution to the advanced practice event for the PowerShell scripting games. The beauty of this script compared to some of the other entries I saw posted, is that it totally relies upon the built-in capabilities of PowerShell 3.0 for the majority of the required features.

The Solution

  1. <#  
  2. .SYNOPSIS  
  3.     Get-ComputerUpTime displays the current up time (hours, minutes, seconds) of the specified computers.  
  6.     Connects to the computer with CIM to retrieve the Win32_OperatingSystem LastBootUpTime property and returns   
  7.     the computer name and the up time duration represented as total number of hours, minutes, and seconds.  
  9. .PARAMETER ComputerName  
  10.     The list of computers to retrieve the uptime from.  
  12. .INPUTS  
  13.     An array of strings or an array of objects that contain a property ComputerName.  
  15. .OUTPUTS  
  16.     PSCustomObject. Get-ComputerUpTime will return the computer name, total hours, minutes, and seconds since the computer operating system was loaded.  
  18. .EXAMPLE  
  19.     Get-ComputerUpTime localhost  
  21.     ComputerName Hours Minutes Seconds  
  22.     ------------ ----- ------- -------  
  23.     localhost    1980       28       8  
  25.     Get the computer up time of a single computer.  
  27. .EXAMPLE  
  28.     Get-ComputerUpTime dc,webserver  
  30.     Get the computer up time of the cmoputers dc and webserver.  
  32. .EXAMPLE  
  33.     Get-ComputerUpTime 2>"${ENV:TEMP}\Get-ComputerUpTime.log"  
  35.     Redirect failed computer up time retrieval error messages to a log.  
  37. .EXAMPLE  
  38.     Get-ADComputer -Filter * -SearchBase "OU=Domain Controllers,$((Get-ADRootDSE -Properties DefaultNamingContext).DefaultNamingContext)" -Properties DnsHostName | Select-Object -Expand DnsHostName | Get-ComputerUpTime | Sort-Object ComputerName | ConvertTo-Html -Title 'Domain Controller Up Time Report'  
  40.     Generate an HTML report of all domain controller up time, assuming the ActiveDirectory PowerShell module is installed.  
  41. #>  
  42. function Get-ComputerUpTime {  
  43.     [CmdletBinding()]  
  44.     param (  
  45.         [ValidateNotNullOrEmpty()]  
  46.         [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]  
  47.         [string[]] $ComputerName  
  48.     )  
  50.     process {  
  51.         Get-CimInstance -ComputerName $ComputerName -Class Win32_OperatingSystem -Property CSName,LastBootUpTime |   
  52.             Select-Object -Property @{Name='ComputerName';Expression={$_.CSName}},@{Name='UpTime';Expression={(Get-Date) - $_.LastBootUpTime}} |  
  53.             Select-Object -Property ComputerName,@{Name='Hours';Expression={[int]$_.UpTime.TotalHours}},@{Name='Minutes';Expression={$_.UpTime.Minutes}},@{Name='Seconds';Expression={$_.UpTime.Seconds}}  
  54.     }  
  55. }  

Dissecting the Solution

Lets tear this thing apart based on the requirements as outlined here:

Assumptions: You can be assured that all of the servers in the list are in the domain, and that you’ll have authority to query them. They’re all running Windows Server 2008 R2 or later, and they all have Windows PowerShell 3.0 installed. There are no firewalls or other blockages between you and those computers, and your command can assume that all of the servers are online and reachable.

The fact that we get to use PowerShell 3.0 opens up a lot of possiblities no matter what the requirements are, but I think the majority of the script could get done in 2.0 if Get-WmiObject was used instead of Get-CimInstance. The beauty of Get-CimInstance is that it works against non-Windows hosts just as well as Windows servers as long as they implement the CIM protocols, e.g. network switches.

Requirement: You need to write a command that accepts one or more computer names by means of a parameter.

This one is easy, just use a parameter of type [string[]] as shown on line 47.

Requirement: That parameter must accept direct input, or piped-in string values.

The [Parameter] attribute requires the parameter be passed in one form or another, lets the first argument be used so named parameters aren't required, and ValueFromPipeline allows strings an array of to be sent from a previous pipeline command, and if the object on the pipeline has a ComputerName property, that value will be used.

Requirement: Your command must prompt for computer names if none are provided when the command is run.

Plural use of computer names is tricky, the [ValidateNotNullOrEmpty] attribute on line 45 will continue to prompt for multiple values until an empty string is entered.

Requirement: Your command output must include each computer’s name (even if you were given an IP address, you must display the name), and the number of hours, minutes, and seconds the computer has been online since its last restart.

I was using the Get-WmiObject command, but have been reading a lot of about CIM in PowerShell 3.0 as I have been working with workflows lately. So, using the Get-CimInstance allows me to pass all the computer objects that I need to retrieve to the command as well as get the destinations actual computer name from Win32_OperatingSystem's CSName property and of course the LastBootUpTime value which is the date the operating system booted (line 51).

To convert the LastBootUpTime property from a DateTime to a TimeSpan so the second part of the requirement can be satisfied line 52 just subtracts the LastBootUpTime value from the current date.

Requirement: Your output should consist of four properties, with hours, minutes, and seconds broken out into their own individual properties.

Easy enough to use Select-Object to convert the TimeSpan value of UpTime to TotalHours, Minutes, and Seconds on line 53.

Requirement: When your command runs, it should optionally display each computer name or IP address that it is about to connect to.

This is where I get creative and use built-in capabilities of the -Verbose switch. To utilize this, the [CmdletBinding()] attribute needs to be on your params. This is also the beauty of Get-CimInstance, it produces verbose output about connecting to the destination which satisfies this requirement without any real code required.

Requirement: If an error occurs connecting to a computer, you should display an appropriate error message that includes the failed computer name or IP address.

Again, another advantage of the Get-CimInstance command, the error message if it cannot connect will display the PSComputerName value in the error output.

Requirement: Your command must also provide a switch that causes failed computer names or IP addresses to be logged to a specified text file. If your command is run without that switch, no logging should occur.

This one could be a toss up, on one hand I get done what they want, but not necessarily with a parameter, but with the 2> output redirection for the error, again built-in capabilities of PowerShell which should not be ignored; don't reinvent the PowerShell is a handy best-practice that will save you significant amounts of time and code.

Requirement: The output of your command must be pipe-able to a CSV file, XML file, or HTML file, if someone so chooses – but you do not need to provide those features in the command you write.

Since the script outputs PSCustomObject, that is no problem, in fact if you look at the last example in the comment-based help it shows how you could use the Active Directory module to get all the domain controller up times into a nifty little HTML file.

Final Thoughts

That really is it. I have seen some overly complicated scripts. You'd be surprised how much people try to compensate for PowerShell when they don't really need to. While I believe I bent the rules a little with that error logging to a file, I think that is the right call for the requirement.

Why did I include the comment-based help, well to show the different usages maybe, and it is a good practice if you are going to be provided a module of commands to someone to have sufficient help in them so they don't have to email you.

Did you signup for the PowerShell Scripting Games yet? If not head over to and signup, it isn't too late, the games start Monday.


Popular posts from this blog

PowerShell SupportsShouldProcess Worst & Best Practices

This has been a very big discussion within the Scripting Games 2013 community and I want to add my two cents in an official blog post.

I've left several people comments on how they might be misunderstanding how SupportsShouldProcess works, but I also realize, everyone of these individuals has given me more insight into its use and perhaps, how it should best be utilized.

For those of you that don't know, SupportsShouldProcess is a parameter on the CmdletBinding attribute you can place on your cmdlets that automatically adds the -WhatIf and -Confirm parameters. These will naturally flow into other cmdlets you use that also SupportsShouldProcess, e.g. New-Item, Move-Item.

The major discussion has been around, should you just let the other cmdlets handle the $PSCmdlet.ShouldProcess feature, and if not how should you implement it. ShouldProcess has the following definitions.

Generate Random SecureString Key

Ever need to encrypt a SecureString that can be used across multiple servers? I suggest storing this BASE64 value in a secure location only accessible by the account(s) that need to decrypt the SecureString.
$secret = 'secret1234'$key    = [Convert]::ToBase64String((1..32 |% { [byte](Get-Random -Minimum 0 -Maximum 255) }))  $encryptedSecret = ConvertTo-SecureString -AsPlainText -Force -String $secret | ConvertFrom-SecureString -Key ([Convert]::FromBase64String($key))  $encryptedSecret | Select-Object @{Name='Key';Expression={$key}},@{Name='EncryptedSecret';Expression={$encryptedSecret}} | fl  $ss = ConvertTo-SecureString -Key ([Convert]::FromBase64String($key)) -String $encryptedSecret(New-Object System.Management.Automation.PSCredential 'SECURESTRING',$ss).GetNetworkCredential().Password

PowerShell Error Handling Behavior Debunked

Note: I am using simple error messages as an example, please reference the best practices and guidelines I outlined on when to use custom error messages.

I have been churning in my mind for the last few days all the entries in the 2013 Scripting Games and how they handle errors, or lack thereof.

I am coming to the conclusion through some testing that the simple fact of seeing a try..catch or throw statements does not mean there is proper error handling.

I've been testing several variations and forms of error handling, so lets start with the basics.
function Test-WriteError {      [CmdletBinding()] param()  "Test-WriteError::ErrorActionPreference = $ErrorActionPreference"Move-Item -Path 'C:\Does\Not\Exists.log' -Destination 'C:\No\Where'"Test-WriteError::End"}   Test-WriteError::ErrorActionPreference = Continue
Move-Item : Cannot find path 'C:\Does\Not\Exists.log' because it does not exist.
At line:6 char:5
+     Move-Item -Path 'C:\Does\N…