Summary: Microsoft PowerShell enthusiast, Jeff Wouters, talks about his experience with the 2013 Winter Scripting Games warm-up events.
Microsoft Scripting Guy, Ed Wilson, is here. Today we have a special guest blogger, Microsoft Windows PowerShell enthusiast, Jeff Wouters. Here is a little bit about Jeff…
Jeff Wouters (B ICT, MCITP, MCSA, MCSE) is a freelance technical consultant from the Netherlands with a main focus on high availability and automation. In Microsoft and Citrix products, he uses technologies such as virtualization, redundancy, clustering, and replication. He also has a great passion for Windows PowerShell and is a founding member of the Dutch PowerShell User Group in 2012.
Jeff has been a speaker at IT events such as E2E Virtualization Conference (formerly known as PubForum), BriForum Chicago, and NGN (Dutch IT community). He speaks and blogs mainly about Windows PowerShell and virtualization, but every now and then something else slips in that piques his interest. Jeff is also a contributing author for a book project where 30+ authors from all over the world are working together to create a Windows PowerShell deep-dive book that will be published in 2013.
Jeff’s contact information:
- Jeff Wouters’s Blog, The Scritping Dutchman
- Twitter: @JeffWouters
- Facebook: @JeffWouters
- LinkedIn: @JeffWouters
This year, I’m competing in the Windows PowerShell Scripting Games that will be launched at the Windows PowerShell Summit in April. As a teaser, and to test the new system, it was decided to do a little warm-up event.
Note The 2013 Winter Scripting Games warm-up events are over. The Scripting Wife wrote about her experience with the warm-up events in The Scripting Wife Talks About the First Warm-Up Event. The announcement for the 2013 Scripting Games (main event) will take place at the Windows PowerShell Summit in April. Stay tuned for more information.
I found that the first exercise of the 2013 Winter Scripting Games warm-ups is something that I was able to use in a few of the Windows PowerShell trainings I’ve been giving. It teaches people to break large scripting projects down into eatable pieces and investigate them. When you do this in a group, you’ll end up having some great discussions about why you are doing it the way you are doing when your colleague is doing it another way. So what I’m providing in this post is my solution, not THE solution.
Last year I participated in the Beginner class. I love challenging myself, so I added a little something to the exercise: To deliver every exercise within one hour after starting to script. I was very happy to actually succeed in that task, although at one event I was cutting it a bit close because I was trying to do something with the wrong cmdlet (I needed to use Get-WinEvent instead of Get-EventLog or Get-Event). But that’s a whole other discussion…
I don’t recommend trying to deliver your scripts within an hour. As I experienced last year, this will greatly diminish your learning experience, which is exactly opposite of the goal for the Scripting Games.
So let’s get back on topic. This year I’ve decided that I want to participate in the Advanced class—mostly because I’ve learned a great deal in the last year and I still want to challenge myself.
I hope that you’ll find this post useful in your coming scripting endeavors.
For the purpose of this post, I’ve split the exercise into separate bullets—we’ll cover them one at a time. I’ve numbered the paragraphs in this post the same as each bullet in the exercise so that you can easily find your way around this rather large post.
- You have been asked to create a Windows PowerShell advanced function named Get-DiskSizeInfo.
- It must accept one or more computer names as a parameter.
- It must use WMI or CIM to query each computer.
- For each computer, it must display the percentage of free space, drive letter, total size in gigabytes, and free space in gigabytes.
- The script must not display error messages.
- If a specified computer cannot be contacted, the function must log the computer name to ‘C:\Errors.txt’.
- Optional: Display verbose output showing the name of the computer being contacted.
Note Although I added a Help function when I wrote the script, I’ve not included it in this post because it would make it even bigger—and it’s big enough as it is, right?
1. The advanced function
The function has a few requirements. First, it has to be named Get-DiskSizeInfo. Secondly, it needs to be an advanced function.
Many of the students in my Windows PowerShell classes and workshops ask me how they can convert a function into an advanced function. It’s actually pretty easy, just add [CmdletBinding()] to it at the top like so:
function Get-DiskSizeInfo
{
[CmdletBinding()]
Param ()
}
See how easy it is? You don’t need to be a rocket scientist to write some Windows PowerShell commands.
2. A parameter
The second is that the function needs to accept one or more computer names as input via a parameter. You could define a bunch of parameters such as ComputerName1, ComputerName2, ComputerName3, but that’s just plain crazy.
If you were to create a single parameter and make it an array instead of a string, it would fit our needs just fine:
function Get-DiskSizeInfo
{
[CmdletBinding()]
Param (
[Parameter(Mandatory=$false)][array]$ComputerName=$Env:ComputerName
)
}
Note that I’ve made the parameter NOT mandatory because I’ve given a default value (the local computer name). If I were to make it mandatory, giving it a default value would be useless because it would prompt me to provide values for the ComputerName parameter (because it’s mandatory).
But this is a rather basic parameter. In fact, I would want to do more with it such as providing it aliases and allowing input from the pipeline. So let’s add some of that:
function Get-DiskSizeInfo
{
[CmdletBinding()]
Param (
[Parameter(Mandatory=$false,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ValueFromRemainingArguments=$false,Position=0)]
[ValidateNotNull()][ValidateNotNullOrEmpty()][Alias("Name","Computer")]
[array]$ComputerName=$Env:ComputerName
)
}
As you can see, I also want my function to be able to handle pipeline input. Therefore, I’ll be using a Begin-Process-End construction:
function Get-DiskSizeInfo
{
[CmdletBinding(SupportsShouldProcess=$true,PositionalBinding=$false,ConfirmImpact='Low')]
Param (
[Parameter(Mandatory=$false,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ValueFromRemainingArguments=$false,Position=0)]
[ValidateNotNull()][ValidateNotNullOrEmpty()][Alias("Name","Computer")]
[array]$ComputerName=$Env:ComputerName
)
Begin { }
Process { }
End { }
}
3. The command
In this case, I’ve chosen to use WMI because not all servers in my production environment support CIM. I could have used the COM protocol combined with the CIM cmdlets, but I have found simply using the WMI cmdlets to be easier. It wasn’t a requirement to NOT use WMI, so I am still working within the boundaries that are set by the exercise.
Get-WmiObject -Class Win32_LogicalDisk -Filter "DriveType=3"
But as usual, you’ll get too much information, and you’ll only want the properties that are required. Because there are some additional requirements, such as showing the output value of the total size in GB, I need to do some formatting:
Get-WmiObject -Class Win32_LogicalDisk -Filter "DriveType=3" | Select-Object @{Label="Drive";Expression={$_.DeviceID}},@{Label="FreeSpace(GB)";Expression={"{0:N1}" -f($_.FreeSpace/1GB)}},@{Label="Size(GB)";Expression={"{0:N1}" -f($_.Size/1GB)}},@{Label=”PercentFree”;Expression={"{0:N0}" -f(($_.freespace * 100) / $_.Size)}}
Note I’ve seen some people use Label and Expression, whereas others use Name and Expression. Both work just fine, so you can use whatever makes you happy.
Now include this code into the function:
function Get-DiskSizeInfo
{
[CmdletBinding(SupportsShouldProcess=$true,PositionalBinding=$false,ConfirmImpact='Low')]
Param ( [Parameter(Mandatory=$false,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ValueFromRemainingArguments=$false,Position=0)]
[ValidateNotNull()][ValidateNotNullOrEmpty()][Alias("Name","Computer")]
[array]$ComputerName=$Env:ComputerName
)
Begin { }
Process
{
Get-WmiObject -Class Win32_LogicalDisk -Filter "DriveType=3" | Select-Object @{Label="Drive";Expression={$_.DeviceID}},@{Label="FreeSpace(GB)";Expression={"{0:N1}" -f($_.FreeSpace/1GB)}},@{Label="Size(GB)";Expression={"{0:N1}" -f($_.Size/1GB)}},@{Label=”PercentFree”;Expression={"{0:N0}" -f(($_.freespace * 100) / $_.Size)}}
}
End { }
}
4. Display the information for each computer
Displaying the information is actually the easy part. Simply use a ForEach loop, and add a computer name parameter to the Get-WMIObject command:
function Get-DiskSizeInfo
{ [CmdletBinding(SupportsShouldProcess=$true,PositionalBinding=$false,ConfirmImpact='Low')]
Param ( [Parameter(Mandatory=$false,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ValueFromRemainingArguments=$false,Position=0)]
[ValidateNotNull()][ValidateNotNullOrEmpty()][Alias("Name","Computer")]
[array]$ComputerName=$Env:ComputerName
)
Begin { }
Process
{
Foreach ($Target in $ComputerName)
{
Get-WmiObject -Class Win32_LogicalDisk -Filter "DriveType=3" -ComputerName $Target -ErrorVariable $Errors -ErrorAction SilentlyContinue | Select-Object @{Label="Drive";Expression={$_.DeviceID}},@{Label="FreeSpace(GB)";Expression={"{0:N1}" -f($_.FreeSpace/1GB)}},@{Label="Size(GB)";Expression={"{0:N1}" -f($_.Size/1GB)}},@{Label=”PercentFree”;Expression={"{0:N0}" -f(($_.freespace * 100) / $_.Size)}}
}
}
End { }
}
5. No errors displayed
Handling errors can be a little tricky because there are two types of errors: terminating and non-terminating.
Terminating errors will actually terminate your script. So if such an error occurs, it’s the end of the script. If you’re executing the command for multiple objects, you wouldn’t want the script to be terminated half way through, right? So, how can you catch those errors?
Well, that’s it actually…you need to “catch” them with Try-Catch—and to not show them, you need to redirect or pipe them to Null.
In this case, I’ll only be catching the exceptions:
function Get-DiskSizeInfo
{ [CmdletBinding(SupportsShouldProcess=$true,PositionalBinding=$false,ConfirmImpact='Low')]
Param ( [Parameter(Mandatory=$false,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ValueFromRemainingArguments=$false,Position=0)]
[ValidateNotNull()][ValidateNotNullOrEmpty()][Alias("Name","Computer")]
[array]$ComputerName=$Env:ComputerName
)
Begin { }
Process
{
Try
{
Foreach ($Target in $ComputerName)
{
Get-WmiObject -Class Win32_LogicalDisk -Filter "DriveType=3" -ComputerName $Target -ErrorVariable $Errors -ErrorAction SilentlyContinue | Select-Object @{Label="Drive";Expression={$_.DeviceID}},@{Label="FreeSpace(GB)";Expression={"{0:N1}" -f($_.FreeSpace/1GB)}},@{Label="Size(GB)";Expression={"{0:N1}" -f($_.Size/1GB)}},@{Label=”PercentFree”;Expression={"{0:N0}" -f(($_.freespace * 100) / $_.Size)}}
}
}
catch [System.Exception]
{
$Error | Out-Null
}
Finally { }
}
End { }
}
Note For more information about how you can use Try/Catch/Finally, take a look at the Hey! Scripting Guy Blog, How Can I Use Try/Catch/Finally in Windows PowerShell?
The command itself can give errors. For example if a computer can’t be contacted, it will return that the RPC server is unavailable. You can solve this by adding the ErrorAction parameter to the Get-WMIObject cmdlet with a SilentlyContinue value:
function Get-DiskSizeInfo
{ [CmdletBinding(SupportsShouldProcess=$true,PositionalBinding=$false,ConfirmImpact='Low')]
Param ( [Parameter(Mandatory=$false,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ValueFromRemainingArguments=$false,Position=0)]
[ValidateNotNull()][ValidateNotNullOrEmpty()][Alias("Name","Computer")]
[array]$ComputerName=$Env:ComputerName
)
Begin { }
Process
{
Try
{
Foreach ($Target in $ComputerName)
{
Get-WmiObject -Class Win32_LogicalDisk -Filter "DriveType=3" -ComputerName $Target -ErrorAction SilentlyContinue | Select-Object @{Label="Drive";Expression={$_.DeviceID}},@{Label="FreeSpace(GB)";Expression={"{0:N1}" -f($_.FreeSpace/1GB)}},@{Label="Size(GB)";Expression={"{0:N1}" -f($_.Size/1GB)}},@{Label=”PercentFree”;Expression={"{0:N0}" -f(($_.freespace * 100) / $_.Size)}}
}
}
catch [System.Exception]
{
$_ | Out-Null
$Error | Out-Null
}
Finally { }
}
End { }
}
The reason I’m catching System.Exception here is that this is the base exception class. All other exception classes are derived from this one.
6. Error logging
No errors are shown, but how do we know if a system could not be contacted? It isn’t shown to the screen—we’ve just made sure that won’t happen. Also, one of the requirements was to write the name of that computer to an error log (C:\Errors.txt) when it can’t be contacted.
First I always like to define the file or even the path of the error log. We can do that at Begin { }:
Begin
{
$ErrorLogPath = "C:\Errors.txt"
}
If you have Windows PowerShell 3.0, you can use the Out-File cmdlet with the Append parameter. This parameter was introduced in Windows PowerShell 3.0. We need an error written to the log at each loop.
So how do we get those errors? We’ve just made it so that no errors are shown, so where are they?
Windows PowerShell comes with a bunch of error variables. One of those is $?. This variable gives you a $true or $false depending on if the last command completed successfully. So if the variable is false, we know that an error has occurred, right? An error means that the device could not be contacted, no matter what the reason. And we don’t care about the reason because that wasn’t one of the requirements. We only want to log that the device could not be contacted.
function Get-DiskSizeInfo
{ [CmdletBinding(SupportsShouldProcess=$true,PositionalBinding=$false,ConfirmImpact='Low')]
Param ( [Parameter(Mandatory=$false,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ValueFromRemainingArguments=$false,Position=0)]
[ValidateNotNull()][ValidateNotNullOrEmpty()][Alias("Name","Computer")]
[array]$ComputerName=$Env:ComputerName
)
Begin
{
$ErrorLogPath = "C:\Errors.txt"
}
Process
{
Try
{
Foreach ($Target in $ComputerName)
{
Get-WmiObject -Class Win32_LogicalDisk -Filter "DriveType=3" -ComputerName $Target -ErrorAction SilentlyContinue | Select-Object @{Label="Drive";Expression={$_.DeviceID}},@{Label="FreeSpace(GB)";Expression={"{0:N1}" -f($_.FreeSpace/1GB)}},@{Label="Size(GB)";Expression={"{0:N1}" -f($_.Size/1GB)}},@{Label=”PercentFree”;Expression={"{0:N0}" -f(($_.freespace * 100) / $_.Size)}}
if (!$?) {"Device $Target could not be contacted" | Out-File $ErrorLogPath -Append}
}
}
catch [System.Exception]
{
$_ | Out-Null
$Error | Out-Null
}
Finally { }
}
End { }
}
7. Display system name based on the Verbose parameter
This step took some searching because I had not done this before. How do we know if the Verbose parameter has been used?
There probably are some very creative ways of doing this, but do you know that you can use the $PSCmdlet variable? You can use this to check the command you’ve invoked for the presence of a parameter.
So if the parameter is present, we want to do something; and if it’s not, we want to do something else.
function Get-DiskSizeInfo
{ [CmdletBinding(SupportsShouldProcess=$true,PositionalBinding=$false,ConfirmImpact='Low')]
Param ( [Parameter(Mandatory=$false,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ValueFromRemainingArguments=$false,Position=0)]
[ValidateNotNull()][ValidateNotNullOrEmpty()][Alias("Name","Computer")]
[array]$ComputerName=$Env:ComputerName
)
Begin
{
$ErrorLogPath = "C:\Errors.txt"
}
Process
{
Try
{
If ($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent)
{
Foreach ($Target in $ComputerName)
{
Get-WmiObject -Class Win32_LogicalDisk -Filter "DriveType=3" -ComputerName $Target -ErrorAction SilentlyContinue | Select-Object SystemName,@{Label="Drive";Expression={$_.DeviceID}},@{Label="FreeSpace(GB)";Expression={"{0:N1}" -f($_.FreeSpace/1GB)}},@{Label="Size(GB)";Expression={"{0:N1}" -f($_.Size/1GB)}},@{Label=”PercentFree”;Expression={"{0:N0}" -f(($_.freespace * 100) / $_.Size)}}
if (!$?) {"Device $Target could not be contacted" | Out-File $ErrorLogPath -Append}
}
}
else
{
Foreach ($Target in $ComputerName)
{
Get-WmiObject -Class Win32_LogicalDisk -Filter "DriveType=3" -ComputerName $Target -ErrorAction SilentlyContinue | Select-Object @{Label="Drive";Expression={$_.DeviceID}},@{Label="FreeSpace(GB)";Expression={"{0:N1}" -f($_.FreeSpace/1GB)}},@{Label="Size(GB)";Expression={"{0:N1}" -f($_.Size/1GB)}},@{Label=”PercentFree”;Expression={"{0:N0}" -f(($_.freespace * 100) / $_.Size)}}
if (!$?) {"Device $Target could not be contacted" | Out-File $ErrorLogPath -Append}
}
}
}
catch [System.Exception]
{
$Error | Out-Null
}
finally {}
}
End
{
}
}
My conclusion is also my advice: Break down the exercise into eatable pieces and cover them one at a time. This will make your scripting life and learning experience a whole lot more effective and easier. Trust me on this one. Also take time to properly investigate each part, which will greatly improve your learning experience. You are going to encounter things in your investigation that you didn’t know. But be aware that those investigation will not take you too far away from your goal. Simply do as I do: Make a note of it and look at it sometime in the future…
~Jeff
Jeff, thank you so very much for writing about your experiences in the 2013 Scripting Games warm-up exercises.
I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.
Ed Wilson, Microsoft Scripting Guy