2013 PowerShell Scripting Games Advanced Event 3 – Bringing Bad Habits from the GUI to PowerShell

I’m seeing a trend with a lot of PowerShell scripters. Many of us have a GUI, non-scripting background and we moved to PowerShell because we didn't want to be button monkeys clicking through the same options in the GUI day in and day out. I've always heard, if you’re going to do something more than once, do it (script it) in PowerShell.

The trend I’m seeing is many are bringing their bad habit of wanting to repeat themselves over and over, day in and day out, to PowerShell. They write the same code over and over. I need a new tool to accomplish a task today. I’ll rewrite the same code that I've written every time I've had to accomplish a similar task again today so there will be fifty thousand copies of similar code out there, each in a different function to accomplish a similar task. No, no, no! Now you’re thinking like a script monkey instead of a button monkey, the only difference is you’re repeating yourself in PowerShell instead of the GUI.

A new day and a new way. Write reusable code, write it once and be done with it! That way when a modification needs to be made, it only needs to be made once or at least a lot less. Redundant data is a bad thing. What? I thought redundant data was what we wanted in IT? No (period). Redundancy of systems to prevent failure and to keep them online in case of a failure is a good thing. Redundancy in the form of backups is another good thing and all of these will assist you in keeping your job, but having multiple copies of the same data that are all being modified independently and not being synchronized is a nightmare.

I chose to first write a reusable function that would query the logical disk information and return it as generic objects that could be used to accomplish many different tasks. Want to create an HTML report as in this scenario, no problem. Want to display the output on the screen or place it in a text file, or use it as part of a larger project, those aren't problems either, and there's no reason to rewrite another function to retrieve the same information, ever. Hard coding this into the rest of the script or function is effectively the same thing and placing formatting cmdlets in you script or function because then you can't do much else with it. What are you going to pipe the output to if it's producing HTML files and not objects? Nothing, right? At least I can pipe format cmdlets to out cmdlets.

sg2013-event2a.png

Here’s the output of that function:

sg2013-event2b1.png

Note, I pulled the computer name from the computer with the other information. This is very important. Getting on my soapbox for a moment. What is PowerShell? Ultimately, it's a means to an end. At the end of the day if you don't accomplish the task at hand, it does not matter how perfect your PowerShell code is. The client, customer, or your boss won't care that you did something really cool and used best practices because ultimately the code didn't accomplish the task at hand. We should strive for all the above though, really cool, efficient code that accomplishes the goal your trying to accomplish and do it accurately!

Never depend on someone imputing the real computer name into your tool. It's all too common to have a DNS CNAME record for many machines which is not the true computer name. The IP address or something such as localhost could also be provided. If you're naming your HTML file and displaying the name that was provided via input on the webpage, there's a good chance that it's not the actual "Computer Name". You're already querying WMI and at least for the class I chose, Win32_LogicalDisk, the computer name can be retrieved from it as well so there's not much overhead in retrieving one additional property to achieve a higher level of accuracy. Climbing off my soapbox now.

Ultimately, the Get-DiskInformation function would be placed in a separate module. You can see in the image below that I placed it in the begin block of my advanced script. Never heard of an advanced script? Neither had I. I didn't even know you could do all those cool advanced function things in a script. Placing the Get-DiskInformation function in the process block would cause it to run one time for each computer that is piped in so don't put it there because it only needs to be loaded once, then it's in memory and can be called as many times as necessary without having to reload it.

While we're on the subject of a script, you're probably wondering why I chose a script instead of a function for this tool? Think about how it's going to be used, read between the lines in otherwords. Is someone actually going to manually run this script? Probably not. How fast is the data going to be out of date? Possibly very quick since it's a static HTML file. It's probably going to be setup as a scheduled task. That said, if it were a function it would need a script or some code placed inside of the scheduled task to dot source it which could get messy. I thought it would be cleaner to have everything in a script that could be called using PowerShell.exe in task scheduler. To me that approach was cleaner.

Don't forget about validating the path. That seems to be a common problem. I honestly didn't know how to do that myself and I took a few hits in event 1 because of it.

sg2013-event2c.png

One small detail I did as well, as shown in the remainder of the script in the following image was to change the computer name to lower case for the output file because I prefer HTML files to be in lower case, but that's just my personal preference. I didn't change the case of the computer name that's displayed on the web page though.

sg2013-event2d.png

Update 02/09/14:

The link to my solution is no longer valid so I’m posting it here since I’ve received some requests for it:

  1<#
  2.SYNOPSIS
  3This is a script, not a function! It creates one HTML file per specified ComputerName that contains logical
  4disk information which are saved in the Path location.
  5.DESCRIPTION
  6    New-HTMLDiskReport.ps1 is a script that outputs HTML reports, one HTML file per computer is saved to the
  7folder specified in via the Path parameter for each computer that is provided via the ComputerName parameter.
  8A force parameter is was added because this script will probably be setup to run as a scheduled task, running
  9periodically and the files will need to be overwritten. The default is not to overwrite.
 10    Why was a script chosen instead of a function for the main functionality? It will probably be setup as a
 11scheduled tasks and you would have to write a script or embed some PowerShell into the scheduled tasks to dot
 12source a function and then run it where a script like this one can simply be called directly with the scheduled
 13task using PowerShell.exe.
 14    A separate function was chosen for retrieving the disk related data because it is now a reusable function
 15that could be used with other commands that are written and not just this one. Ultimately, it could be moved
 16into a module. PowerShell version 2.0 is required to run this script.
 17    The date will be generated once per computer instead placing it in the begin block and generating it once
 18for the entire script because the date on the report will not be accurate if this script is run against a
 19million computers. Calculating the date is a cheap operation which should not impede performance even when
 20calculating it once per computer.
 21.PARAMETER ComputerName
 22Specifies the name of a target computer(s). The local computer is the default. You can also pipe this parameter
 23value.
 24.PARAMETER Path
 25Specifies the file system path where the HTML files will be saved to. This is a mandatory parameter.
 26.PARAMETER Force
 27Switch parameter that when specified overwrites destination files if they already exist.
 28.EXAMPLE
 29.\New-HTMLDiskReport.ps1 -Path 'c:\inetpub\wwwroot'
 30.EXAMPLE
 31.\New-HTMLDiskReport.ps1 -ComputerName 'Server1, 'Server2' -Path 'c:\inetpub\wwwroot' -Force
 32.EXAMPLE
 33'Server1', 'Server2' | .\New-HTMLDiskReport.ps1 -Path 'c:\inetpub\wwwroot'
 34.EXAMPLE
 35'Server1', 'Server2' | .\New-HTMLDiskReport.ps1 -Path 'c:\inetpub\wwwroot' -Force
 36.EXAMPLE
 37Get-Content -Path c:\ServerNames.txt | .\New-HTMLDiskReport.ps1 -Path 'c:\inetpub\wwwroot' -Force
 38.INPUTS
 39System.String
 40.OUTPUTS
 41None
 42#>
 43
 44[CmdletBinding()]
 45param(
 46    [Parameter(ValueFromPipeline=$True)]
 47    [ValidateNotNullorEmpty()]
 48    [string[]]$ComputerName = $env:COMPUTERNAME,
 49
 50    [Parameter(Mandatory=$True)]
 51    [ValidateScript({Test-Path $_ -PathType 'Container'})]
 52    [string]$Path,
 53
 54    [switch]$Force
 55)
 56
 57BEGIN {
 58    $Params = @{
 59        ErrorAction = 'Stop'
 60        ErrorVariable = 'Issue'
 61    }
 62
 63    function Get-DiskInformation {
 64
 65    <#
 66    .SYNOPSIS
 67    Retrieves logical disk information to include SystemName, Drive, Size in Gigabytes, and FreeSpace in Megabytes.
 68    .DESCRIPTION
 69    Get-DiskInformation is a function that retrieves logical disk information from machines that are provided via the
 70    computer name parameter. This function is designed for re-usability and could possibly be moved into a module at
 71    a later date. PowerShell version 2.0 is required to run this function.
 72    .PARAMETER ComputerName
 73    Specifies the name of a target computer. The local computer is the default.
 74    .EXAMPLE
 75    Get-DiskInformation -ComputerName 'Server1'
 76    .INPUTS
 77    None
 78    .OUTPUTS
 79    System.Management.Automation.PSCustomObject
 80    #>
 81
 82        [CmdletBinding()]
 83        param(
 84            [ValidateNotNullorEmpty()]
 85            [string]$ComputerName = $env:COMPUTERNAME
 86        )
 87
 88        $Params = @{
 89            ComputerName = $ComputerName
 90            NameSpace = 'root/CIMV2'
 91            Class = 'Win32_LogicalDisk'
 92            Filter = 'DriveType = 3'
 93            Property = 'DeviceID', 'Size', 'FreeSpace', 'SystemName'
 94            ErrorAction = 'Stop'
 95            ErrorVariable = 'Issue'
 96        }
 97
 98        try {
 99            Write-Verbose -Message "Attempting to query logical disk information for $ComputerName."
100            $LogicalDisks = Get-WmiObject @Params
101            Write-Verbose -Message "Test-Connection was not used because it does not test DCOM connectivity, only ping."
102        }
103        catch {
104            Write-Warning -Message "$Issue.Message.Exception"
105        }
106
107        foreach ($disk in $LogicalDisks){
108
109        $DiskInfo = @{
110            'SystemName' = $disk.SystemName
111            'Drive' = $disk.DeviceID
112            'Size(GB)' = "{0:N2}" -f ($disk.Size / 1GB)
113            'FreeSpace(MB)' = "{0:N2}" -f ($disk.FreeSpace / 1MB)
114        }
115
116        New-Object PSObject -Property $DiskInfo
117
118        }
119    }
120
121}
122
123PROCESS {
124    foreach ($Computer in $ComputerName) {
125
126        $Problem = $false
127        $Params.ComputerName = $Computer
128
129        try {
130            Write-Verbose -Message "Calling the Get-DiskInformation function."
131            $DiskSpace = Get-DiskInformation @Params
132        }
133        catch {
134            $Problem = $True
135            Write-Warning -Message "$Issue.Exception.Message"
136        }
137
138        if (-not($Problem)) {
139
140            $DiskHTML = $DiskSpace | Select-Object -Property Drive, 'Size(GB)', 'FreeSpace(MB)' | ConvertTo-HTML -Fragment | Out-String
141            $MachineName = $DiskSpace | Select-Object -ExpandProperty SystemName -Unique
142            $Params.remove("ComputerName")
143
144            $HTMLParams = @{
145                'Title'="Drive Free Space Report"
146                'PreContent'="<H2>Local Fixed Disk Report for $($MachineName) </H2>"
147                'PostContent'= "$DiskHTML <HR> $(Get-Date)"
148            }
149
150            try {
151                Write-Verbose -Message "Attempting to create the filepath variable."
152                $FilePath = Join-Path -Path $Path -ChildPath "$($MachineName.ToLower()).html" @Params
153
154                Write-Verbose -Message "Attempting to convert to HTML and write the file to the $FilePath folder for $Computer"
155                ConvertTo-HTML @HTMLParams | Out-File -FilePath $FilePath -NoClobber:(-not($Force)) @Params
156            }
157            catch {
158                Write-Warning -Message "Use the -Force parameter to overwrite existing files. Error details: $Issue.Message.Exception"
159            }
160        }
161    }
162}

µ