Advanced Event 3 – 2013 Scripting Games

We have made it to the midpoint of the Scripting Games.  I think that a lot has been learned and there is still more to learn.  This is really the whole reason for the games; to get people to learn something new about Powershell.  Let’s see what was asked for in Event 3, A Disk Decision.

In this event, we were to use some of our past techniques to gather fixed-disk size information from computers in our network.  Once the information was retrieved, it should be captured into an HTML file with very specific formatting.

HTML_Report_Sample

Here are the items that were specified in the requirements:

  • The tab should contain the text, “Disk Free Space Report.”  This should be specified with the <title> tag.
  • The header of the report should contain the text, “Local Fixed Disk Report.”  This text is required to be rendered as “Header 2” of <h2> tag.  If the computer name can be included, then that is a bonus.
  • The disk information should be shown in GB for the size, and MB for the free space and must be rounded to two decimal places.
  • The report must have a horizontal rule <hr /> at the end of the report, followed by the date and time that the report was generated.

The requirements also state that all firewall rules and authentication has been taken care of.  Some people take this to mean that the user running the function has the access to gather the data from the computers.  I don’t operate under that assumption.  I take it to mean that the user has access to administrative credentials that can access the data.  Because of my assumption, I almost always put a Credential parameter in my scripts, and I did this time as well.  Adding the credential parameter does not add much to the script and makes it much more portable.  For instance, when I am judging some of the other scripts, I am not always running them from a computer that is on my home domain.  In order to test these scripts without the credential parameter, I have to use a Remote Desktop connection to one of my computers and run the script from there.  Having the credential allows me to connect from a computer that is not a member of my domain.

The first thing I did was to set up my basic parameters.  I knew I needed a ComputerName parameter and a Path parameter.  Learning from my mistakes in Event 1, I am going with the standardized names that most other Powershell cmdlets use.  I have included a couple of aliases for the ComputerName parameter to allow for some flexibility.  While the ComputerName parameter does accept pipeline input, I have specified a default value.  I did the same for the Path parameter as well.  Doing so just allows the user to be able to run the basic function and get some data from the local computer.  As I was formulating the script, I also thought of another parameter that I had seen on standard Powershell cmdlets…the PassThru parameter.  Wow, I can set up extra functionality here. The PassThru parameter allows the data that I collect to be sent out to the pipeline for further processing, if so desired.  The idea here is that it is great that the HTML files are being generated, but what if i want to know immediately what disk on what computer is about to fill up?  I can pipe the output of this function with the PassThru parameter out to a sort-object cmdlet and BINGO, now I know.

On to the BEGIN block.  You might notice above that I did not supply a ValidateScript tag to verify that the Path folder exists.  I did this on purpose.  I want to test that path in the BEGIN block so that I can create it if it doesn’t exist; something you cannot do in the ValidateScript block, that I know of.  I also set up some variables that will be used multiple times in the PROCESS block.  Some of these will be used in the ConvertTo-Html cmdlet later and a couple are used to make the script a bit more readable.  The “content” variables were created in a generic way so that I could pass in the necessary information when the string is used.  The “format” variables were created so that I wouldn’t have to spend so many characters trying to “select” out the data in the format I needed.

As we enter the PROCESS block, I start with the standard foreach loop that is required when you are dealing with the possibility of using pipeline input and/or array input at the parameter name.  If you use pipeline input, each computer name comes across the pipeline one at a time, so when it hits this foreach, it only iterates one time.  However, if you submit a string array of computer names with the ComputerName parameter, then this foreach loop will iterate over each element of that array.

As I get each computer name, I check first to see that it is online and simply report to the user if the computer is offline.  If it is online, then I begin setting up the parameter hash table for the WMI request.  I provide a check to see if the Credential parameter was supplied and also that I am not connecting to the local computer, and add the Credential parameter into my hash table, if it is needed.  A lesson I learned in Event 2 that would have helped me out.  In hindsight, I probably could have also gone ahead and set up the hash table for ConvertTo-Html in the beginning.

Now we can actually get some data to play with.  I simply pass the WMI hash table to the Get-WmiObject cmdlet.  When the data comes back, I use the Select-Object cmdlet to get the data that I need and here is where I use the format variables I created above.  I’ll let you decide which way is cleaner.

Short way:

$computerNameFormat=@{Label="ComputerName";Expression={$_.__SERVER}}
$freeSpaceFormat = @{Label="FreeSpace(MB)";Expression={ "{0:N2}" -f ($_.FreeSpace / 1MB)}}
$sizeFormat = @{Label="Size(GB)";Expression={ "{0:N2}" -f ($_.Size / 1GB)}}
$diskInfo = Get-WmiObject @WMIParameters | Select-Object -Property $computerNameFormat, DeviceId, $sizeFormat, $freeSpaceFormat

Long way:

$diskInfo = Get-WmiObject @WMIParameters | Select-Object -Property __SERVER, DeviceId, @{Label="Size(GB)";Expression={ "{0:N2}" -f ($_.Size / 1GB)}}, @{Label="Freespace(MB)";Expression={ "{0:N2}" -f $(_.FreeSpace / 1MB)}}

Now that I have the information, it is time to assemble it for the HTML file.  I start by building the hash table for my ConvertTo-Html cmdlet.  It is here that I pass in the format strings with the “-f” operator where needed.  Below that, I build the file name for this particular file using Join-Path.  I take the Path parameter from above and combine it with the “calculated” ComputerName property from the $diskInfo object’s first element.  The reason is that at this point, I know I have at least one fixed disk on the system.  The system is alive and I have been authenticated, so I think I can safely assume that this should be populated here.  Finally, I call the ConvertTo-Html cmdlet with my parameter hash table (called “splatting” in Powershell) and send the output to the named HTML file.

Let’s talk about the ConvertTo-Html cmdlet for a minute.  When I first looked at this task, the first thing I did was to call “Get-Help ConvertTo-Html -detailed” to see what was available for this cmdlet.  As it turns out, everything I needed was right there.  There is a title, precontent, postcontent, and property parameter.  The title parameter does what you expect.  It sets the title of the document, and this is what shows up in the tab of your browser.  The precontent parameter is where you put any markup that should go before your data.  This is where I put the

tag with the required text along with the placeholder for my computer name to be passed in.  Postcontent is where I placed the


tag along with the placeholder for the date/time object to be passed in.  And finally, the property parameter is to specify what properties from your data will be shown in the HTML table section of the file.

There is another option that I could have used.  There is a head parameter that can be used to enter CSS lines for the file.  However, had I used that, I would have to move the <title> text into the head parameter because when the head parameter is used, the title parameter is ignored.  Just something to keep in mind for your own scripts.

The final thing to do is to check if the PassThru parameter was used on the function, and if so, write the object out to the pipeline.  No puppies killed here for using Write-Host!  Love to hear what you think.  If you like my approach, please vote for my entry!

Here is the full source for my entry to review!

Function Get-DiskSpaceReport
{
<#
 .SYNOPSIS
 Gets hard disk size and free space information for Windows computers.

.DESCRIPTION
 This function allows the user to query one or many computers about fixed hard drives (DriveType 3). The function will create an HTML report for each
 server and save it in the default or alternate specified location. The user may supply alternate credentials to the script, if needed, otherwise the script
 will use the current user's credentials to connect to remote computers. There is a PassThru parameter available to allow the user to output the objects
 sent to the HTML reports out to the pipeline as well. This will allow the user to perform additional processing on the data if so desired.

.PARAMETER ComputerName
 Specifies the target computer for the management operation. The value can be a fully qualified domain name, a NetBIOS name, or an IP address. To specify the local computer,
 use the local computer name, use localhost, or use a dot (.) . The local computer is the default. When the remote computer is in a different domain from the user, you must
 use a fully qualified domain name. You can also pipe this parameter value to the Get-WmiObject cmdlet. The default for this parameter is the local computer.

.PARAMETER Path
 Specifies the path to the report directory. If the directory does not exist, it will be created. The default directory is C:\Temp.

.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 "User01", "Domain01\User01", or
 User@Contoso.com. Or, enter a PSCredential object, such as an object that is returned by the Get-Credential cmdlet. When you type a user name, you are prompted for a password.

.PARAMETER PassThru
 Returns an object representing the disk report information. By default, this function does not return output to the pipeline. While not a requirement
 for this exercise, it provides some additional functionality that may be useful.

.EXAMPLE
 PS C:\> $Env:COMPUTERNAME
 SERVER01
 PS C:\> Get-DiskSpaceReport

 This example will gather the local computer disk space report as the currently logged on user. The HTML report will be saved at C:\Temp\SERVER01.html

 .EXAMPLE
 PS C:\> Get-DiskSpaceReport -ComputerName Server01, Server02 -Path C:\Temp\Report

 This example will gather the disk space information for Server01 and Server02. Two HTML report files will be created as C:\Temp\Report\Server01.html and
 C:\Temp\Report\Server02.html

 .EXAMPLE
 PS C:\> Get-Content -Path C:\Temp\ServerList.txt | Get-DiskSpaceReport -Path C:\Temp\Report -Credential CONTOSO\jdoe -PassThru

 This example will get a list of servers from a file at C:\Temp\ServerList.txt. Each server name (one on each line) will pass through the
 pipeline to the Get-DiskSpaceReport function. The HTML report files will be stored in a folder C:\Temp\Report. The function will ask the user for
 a password for the user CONTOSO\jdoe and use those credentials to query the servers. Additionally, the script will output the disk space
 inforamtion objects out to the pipeline so that additional processing can be done on the data, if desired.
#>

[CmdletBinding()]
 Param(
 [Parameter(ValueFromPipeline=$true,
 ValueFromPipelineByPropertyName=$true)]
 [Alias("DnsHostName","PSComputerName")]
 [String[]]$ComputerName = $ENV:COMPUTERNAME,
 [String]$Path = "C:\Temp",
 [System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty,
 [switch]$PassThru
 )

 BEGIN
 {
 if (-not (Test-Path -Path $Path -PathType Container))
 {
 Write-Debug "$Path does not exist, creating..."
 New-Item -Path $Path -ItemType Directory -Force | out-null
 }
 Write-Debug "Setting up script-level variables."
 $ReportFiles = @()
 $preContent = "</pre>
<h2>Local Fixed Disk Report{0}</h2>
<pre>"
 $postContent = "</pre>

<hr />

<pre>{0}"
 $title = "Disk Free Space Report"
 $properties = "__SERVER, DeviceId, FreeSpace, Size"
 $computerNameFormat=@{Label="ComputerName";Expression={$_.__SERVER}}
 $freeSpaceFormat = @{Label="FreeSpace(MB)";Expression={ "{0:N2}" -f ($_.FreeSpace / 1MB)}}
 $sizeFormat = @{Label="Size(GB)";Expression={ "{0:N2}" -f ($_.Size / 1GB)}}

 }

 PROCESS
 {
 foreach ($computer in $ComputerName)
 {
 Write-Debug "Checking if $computer is reachable."
 if (Test-Connection -ComputerName $computer -Quiet -Count 1)
 {
 Write-Debug "$computer is online."
 $WMIParameters = @{
 Class="Win32_LogicalDisk"
 Filter="DriveType=3"
 Property=$properties
 Impersonation="Impersonate"
 Authentication="PacketPrivacy"
 ComputerName=""
 }
 if ($Credential -and ($computer -notlike $Env:COMPUTERNAME))
 {
 Write-Debug "Credential parameter supplied, adding to WMI Parameter list."
 $WMIParameters.Add("Credential", $Credential)
 }
 Write-Debug "Setting WMI ComputerName parameter to $computer."
 $WMIParameters.set_Item("ComputerName", $computer)
 try
 {
 Write-Debug "Connecting to and querying $computer."
 $diskInfo = Get-WmiObject @WMIParameters | Select-Object -Property $computerNameFormat, DeviceId, $sizeFormat, $freeSpaceFormat

 $ConvertHtmlParameters = @{
 PreContent=$($preContent -f " - $($diskInfo[0].ComputerName)")
 PostContent=$($postContent -f (Get-Date -Format "MM/dd/yyyy HH:mm:ss"))
 Property=@('DeviceId','Size(GB)','FreeSpace(MB)')
 Title=$title
 }

 $htmlFile = Join-Path -Path $Path -ChildPath "$($diskInfo[0].ComputerName).html"
 Write-Debug "File will be saved to $htmlFile."

 Write-Debug "Generating HTML file for $($diskInfo[0].ComputerName)."
 $diskInfo | ConvertTo-Html @ConvertHtmlParameters | Out-File -FilePath $htmlFile -Force

 Write-Debug "Adding file to array in case user wants to open all files created."
 $ReportFiles += $htmlFile
 if ($PassThru)
 {
 Write-Debug "User specified to output objects to the pipeline."
 Write-Output $diskInfo
 }
 }
 catch [System.UnauthorizedAccessException]
 {
 Write-Error "You do not have permissiosn to access $computer."
 }
 }
 else
 {
 Write-Error "$computer is not reachable."
 }
 }
 }

}

 

Advertisements
This entry was posted in Scripting Games 2013 and tagged , , , , , , , . Bookmark the permalink.