2013 PowerShell Scripting Games Advanced Event 2 – Attention to Detail is Everything

Here's my approach to the 2013 PowerShell Scripting Games Advanced Event 2:

When I start one of the Scripting Games events, I read and re-read the scenario because if you don't understand the requirements, you can't write an effect script, function, command, tool, etc. It's not a bad idea to print out the event scenario and highlight the high-points.

Here's the scenario for Advanced Event 2 -An Inventory Intervention, I'll place the items in bold that I would normally highlight on my printout:

Dr. Scripto finally has the budget to buy a few new virtualization host servers, but he needs to make some room in the data center to accommodate them. He thinks it makes sense to get rid of his lowest-powered old servers first… but he needs to figure out which ones those are.

This is just the first wave, too - there's more budget on the horizon so it's possible he'll need to run this little report a few times. Better make a reusable tool.

The phrase in bold in the previous paragraph means it needs to be a script, function, or maybe even part of a module that has error checking to prevent others you may hand this 'tool" off to from having issues with it (not a one-liner).

All of the virtualization hosts run Windows Server, but some of them don't have Windows PowerShell installed, and they're all running different OS versions. The oldest OS version is Windows 2000 Server (he knows, and he's embarrassed but he's just been so darn busy). The good news is that they all belong to the same domain, and that you can rely on having a Domain Admin account to work with.

The important points are support OS's as old as Windows 2000, some without PowerShell, they're all in the same domain and we have a domain admin account to use for access. It's best practice to run PowerShell as a standard user and specify alternate credentials when running tools such as this need additional rights instead of running PowerShell as a domain admin so we'll need to add a credential parameter to our tool.

The good Doctor has asked you to write a PowerShell tool that can show him each server's name, installed version of Windows, amount of installed physical memory, and number of installed processors. For processors, he'll be happy getting a count of cores, or sockets, or even both - whatever you can reliably provide across all these different versions of Windows. He has a few text files with computer names - he'd like to pipe the computer names, as strings, to you tool, and have your tool query those computers.

That's a lot of information. It's a good thing I have a few highlighters that are different colors. My first thought: "I'll need to use WMI to retrieve this information". The great thing about WMI is that it's been around forever and you could even find a VBScript that uses WMI to accomplish what you want and pull the WMI class out of it and use it in PowerShell.

Now to determine what WMI classes we need to use:

Windows Version: Win32_OperatingSystem. This one was too easy. You can search WMI using the Get-WMIObject cmdlet to find classes that may contain the data you're looking for. The class you want is going to begin with win32_ and normally your not going to want the classes that start with win32_perf so filter those out like so:


Based on these results, there's only two classes that might contain the data we're looking for: Win32_OperatingSystem or Win32_SystemOperatingSystem

I'll switch to the Get-CimInstance cmdlet at this point since I'm running PowerShell version 3 and it tab expands namespaces and classes so there's less typing. I'll pipe to Format-List -Property * -Force because by default only a fraction of the properties are displayed. I added the Force switch parameter because I want to see the properties where whoever wrote this is trying to protect me from myself. I can see the Caption is the OS name, and the CSName is the ComputerName:


Towards the bottom of the results, the Version property is the OS Version:


Test against all the different OS's we need to support and validate that class exists and returns the correct data for each OS starting with Windows 2000. I personally ran my commands against over 30 machines so I would receive different results from a wide variety of different hardware and operating systems. Don't run your commands in your company's production environment though.

Ok, that one class gives us the server name, and OS version which are two of the items we need. I'll also include the OS Name since OS's like Windows 8 and Server 2012 or Windows Server 2003 and XP have the same "version" numbers. That way if this tool is ever run on workstations and servers, we'll be able to differentiate between the two.

I've found in the scripting games it's generally ok to include a little more information as long as it still meets the requirements and works, but with each item you add that's not required, you open up another chance for problems. Add something that's not required and if it doesn't work, it's much worse that not adding that item or feature at all.

Now we need the memory and processor information. I'll do another WMI search to see what classes exist that might contain that information just like before:


The Win32_ComputerSystem class looks promising.


It's also a good idea to test the classes as you go to make sure they do indeed retrieve the data you want. While the Win32_ComputerSystem appeared to return the correct information on multiple modern operating systems, on Windows 2003 it doesn't provide the correct information as shown below. I know that this Dell PE 1950 Server has two physical processors with four cores each so the NumberOfProcessors property can't be used to retrieve the number of physical CPU sockets since this should be 2:


I also have a question for you, first read this again: For processors, he'll be happy getting a count of cores, or sockets, or even both - whatever you can reliably provide across all these different versions of Windows.

What does reliably mean? The online dictionary I used says "giving the same result on successive trials". Can you provide a count of processor cores RELIABLY across all the different versions of Windows beginning with Windows 2000? My answer is no so I did not provide this information. Same question for processor sockets: Can you RELIABLY provide sockets across all the different versions? Yes, so I provided that information.

It's also a good idea to lookup the WMI classes you've chosen to use on MSDN to see if there are any notes about the properties that you're planning to use. Doing this for the Win32_ComputerSystem class and viewing the NumberOfProcessors property would have allowed you to discover that it wouldn't display the processor sockets correctly for Windows 2000 and Windows 2003 machines:


Image Source: <https://msdn.microsoft.com/en-us/library/windows/desktop/aa394102(v=vs.85).aspx>

There's a hotfix to correct this problem on Windows 2003, but not on 2000: https://support.microsoft.com/kb/932370

Here's a trick to accurately retrieve the correct number of physical processors from any of the systems this tool is suppose to run against:


The TotalPhysicalMemory property in Win32_ComputerSystem is also incorrect. It shows what's available to Windows after things such as memory for video has been taken out or if the OS doesn't support the amount of physical memory in the server, it would only show what the OS is able to access.

Using this class and property would only return 4091MB of RAM on the same Dell PE 1950 as shown in the following image. I didn't know they made physcial memory chips that would add up to that amount (4091MB)? That's because they don't. Notice in the second example, it's actually very easy to determine the true amount of physical memory using the Win32_PhysicalMemory class. You'll need to Sum or add up all of the memory chips to retrieve this information, but it's not that difficult:


Oh, and in case he forgets how to use it - make sure this tool of yours has a full help display available.

The help is self explanatory. Run help about_Comment_Based_Help in PowerShell to see examples.

Are you casting the amount of memory as an integer in gigabytes? Guess what? It's not uncommon for a Windows 2000 Server to have less than half a gigabyte of RAM. Cast 256MB as an integer in gigabytes and you'll end up with zero. Is zero useful information? It's not. You could either leave a couple of decimal places (don't cast it as an integer), or cast it to megabytes as I did.


Here's the process I use to write my PowerShell scripts, functions, commands, tools, etc:

  1. Write pieces of the script, function, command, etc that pulls the necessary data and verify accuracy. If the data isn't accurate it doesn't matter how fancy your PowerShell code is (period).

  2. Tune your command for efficiency. Don't make multiple calls across the network to the same machine unless absolutely necessary. Only go to the well once and get all you need. Need all you get as well (don't pull data from a remote machine that you don't need - filter at the source, not the destination). Store the data in a variable and work with the variable instead of constantly creating network traffic back and forth. Same goes for calls to disk. They're expensive. Get what you need and store it in a variable. Be sure to only retrieve what you need. A wise man once told me don't buy the whole box of Whitman's Samplers if all you like is chocolate covered peanuts.

  3. Do step #1 again and make sure you're data is still accurate (Accuracy counts!)

  4. Apply best practices once you've completed #1, #2, and #3. Add error checking etc.

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#Requires -Version 3.0
  3function Get-SystemInventory {
  7Retrieves hardware and operating system information for systems running Windows 2000 and higher.
  9Get-SystemInventory is a function that retrieves the computer name, operating system version, amount of physical memory
 10(RAM) in megabytes, and number of processors (CPU's) sockets from one or more hosts specified via the ComputerName parameter
 11or via pipeline input. The TotalPhysicalMemory property of Win32_ComputerSystem was initially used, but that property is not
 12the total amount of physical system memory. It is the amount of system memory available to Windows after the OS takes some
 13out for video if necessary or if the OS doesn't support the amount of RAM in the machine which is common when running an older
 1432 bit operating system on hardware with a lot of physical memory. Displaying the amount of memory in megabytes was chosen
 15because this function can be run against older hosts where less than half a gigabyte of memory is common and casting 256MB for
 16example to an integer in gigabytes would display zero which is not useful information. The amount of processor sockets was
 17chosen for similar reasons because older operating systems aren't aware of CPU cores and that information wouldn't be reliably
 18provided across all operating systems this function could be run against. Initially, the NumberOfProcessors property in the
 19Win32_ComputerSystem class was used, but it does not provide an accurate count of CPU sockets on older operating systems with
 20multi-core processors per this MSDN article: http://msdn.microsoft.com/en-us/library/windows/desktop/aa394102(v=vs.85).aspx
 21The Cim cmdlets have been used to gain maximum efficiency so only one connection will be made to each computer. This tool has
 22also been future proofed so as more hosts are upgraded to newer Windows operating systems, they will be able to take advantage
 23of the WSMAN protocol because DCOM is blocked by default in the firewall of newer operating systems and possibly on firewalls
 24between the computer running this tool and the destination host. A ShowProtocol switch parameter has been provided so the
 25results can be filtered (Where-Object) or sorted (Sort-Object) to determine which computers are not being communicated with
 26using WSMAN which is another means of finding older hosts. A Credential parameter has been provided because it's best practice
 27to run PowerShell as a non-domain admin and provide the domain admin credentials specified in the scenario on an as needed per
 28individual command basis. This function requires PowerShell version 3 on the computer it is being run from, but PowerShell is
 29not required to be installed or enabled on the remote computers that it is being run against.
 30.PARAMETER ComputerName
 31Specifies the name of a target computer(s). The local computer is the default. You can also pipe this parameter value.
 32.PARAMETER Credential
 33Specifies a user account that has permission to perform this action. The default is the current user.
 37Get-SystemInventory -ComputerName 'Server1'
 39Get-SystemInventory -ComputerName 'Server1', 'Server2' -Credential 'Domain\UserName'
 41'Server1', 'Server2' | Get-SystemInventory
 43(Get-Content c:\ComputerNames.txt) | Get-SystemInventory
 45(Get-Content c:\ComputerNames.txt, c:\ServerNames.txt) | Get-SystemInventory -Credential (Get-Credential) -ShowProtocol
 52    param(
 53        [Parameter(ValueFromPipeline=$True)]
 54        [ValidateNotNullorEmpty()]
 55        [string[]]$ComputerName = $env:COMPUTERNAME,
 57        [System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty,
 59        [switch]$ShowProtocol
 60    )
 62    BEGIN {
 63        $Opt = New-CimSessionOption -Protocol Dcom
 64    }
 66    PROCESS {
 67        foreach ($Computer in $ComputerName) {
 69            Write-Verbose "Attempting to Query $Computer"
 70            $Problem = $false
 71            $SessionParams = @{
 72                ComputerName  = $Computer
 73                ErrorAction = 'Stop'
 74            }
 76            If ($PSBoundParameters['Credential']) {
 77               $SessionParams.credential = $Credential
 78            }
 80            if ((Test-WSMan -ComputerName $Computer -ErrorAction SilentlyContinue).productversion -match 'Stack: 3.0') {
 81                try {
 82                    $CimSession = New-CimSession @SessionParams
 83                    $CimProtocol = $CimSession.protocol
 84                    Write-Verbose "Successfully created a CimSession to $Computer using the $CimProtocol protocol."
 85                }
 86                catch {
 87                    $Problem = $True
 88                    Write-Verbose "Unable to connect to $Computer using the WSMAN protocol. Verify your credentials and try again."
 89                }
 90            }
 92            elseif (Test-Connection -ComputerName $Computer -Count 1 -Quiet -ErrorAction SilentlyContinue) {
 93                $SessionParams.SessionOption = $Opt
 94                try {
 95                    $CimSession = New-CimSession @SessionParams
 96                    $CimProtocol = $CimSession.protocol
 97                    Write-Verbose "Successfully created a CimSession to $Computer using the $CimProtocol protocol."
 98                }
 99                catch {
100                    $Problem = $True
101                    Write-Verbose  "Unable to connect to $Computer using the DCOM protocol. Verify your credenatials and that DCOM is allowed in the firewall on the remote host."
102                }
103            }
105            else {
106                $Problem = $True
107                Write-Verbose "Unable to connect to $Computer using the WSMAN or DCOM protocol. Verify $Computer is online and try again."
108            }
110            if (-not($Problem)) {
111                $OperatingSystem = Get-CimInstance -CimSession $CimSession -Namespace root/CIMV2 -ClassName Win32_OperatingSystem -Property CSName, Caption, Version
112                $PhysicalMemory = Get-CimInstance -CimSession $CimSession -Namespace root/CIMV2 -ClassName Win32_PhysicalMemory -Property Capacity |
113                                  Measure-Object -Property Capacity -Sum
114                $Processor = Get-CimInstance -CimSession $CimSession -Namespace root/CIMV2 -ClassName Win32_Processor -Property SocketDesignation |
115                             Select-Object -Property SocketDesignation -Unique
116                Remove-CimSession -CimSession $CimSession -ErrorAction SilentlyContinue
117            }
119            else {
120                $OperatingSystem = @{
121                    CSName= $Computer
122                    Caption = 'Failed to connect to computer'
123                    Version = 'Unknown'
124                }
125                $PhysicalMemory = @{
126                    Sum = 0
127                }
128                $Processor = @{
129                }
130                $CimProtocol = 'NA'
131            }
133            $SystemInfo = [ordered]@{
134                ComputerName = $OperatingSystem.CSName
135                'OS Name' = $OperatingSystem.Caption
136                'OS Version' = $OperatingSystem.Version
137                'Memory(MB)' =  $PhysicalMemory.Sum/1MB -as [int]
138                'CPU Sockets' = $Processor.SocketDesignation.Count
139            }
141            If ($PSBoundParameters['ShowProtocol']) {
142               $SystemInfo.'Connection Protocol' = $CimProtocol
143            }
145            New-Object PSObject -Property $SystemInfo
146        }
147    }