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.
Here’s the output of that function:
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.
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.
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}
µ