Voting has started for the second assignment of the 2013 Powershell Scripting Games. This second task was very tough for me because I was on holiday at the seaside and had no fixed Internet connection and no Windows 2000 nor Windows 2003 test machine to test my script with. Anyhow I managed to answer the assignment by using MSDN and checking there which WMI classes I could query depending on the operatin system version.
But let's go back a bit. Here's the full task assignment:
"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.
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 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."
When I saw the scenario I was I little scared by the fact of having to handle all the possible operating systems since Windows 2000. I highlighted the critical points that need to be addressed by the script. I for sure came up with a function in roder to answer to the need of a reusable tool.
Here's the script I wrote:
- #Requires -Version 3.0
- Function Get-Inventory {
- <#
- .SYNOPSIS
- Collect important information about a system's hardware in order to decide which ones are too low-powered.
- .DESCRIPTION
- This function will collect the following information about the target computer:
- - computer name,
- - operating system version,
- - the amount of physical memory,
- - number of processors,
- - number of cores,
- - number of sockets
- This function has the three classic Begin, Process and End block.
- - In the Begin block I prepare the ScriptBlock which contains the WMI queries and which is called in the process block.
- - In the process block I perform Job setup and Jobs throttling.
- - In the End block I return the results in the form of a object or of a GridView.
- .PARAMETER ComputerName
- Gets the services running on the specified computer. The default is the local computer.
- Type the NetBIOS name, an IP address, or a fully qualified domain name of a remote computer.
- To specify the local computer, type the computer name, a dot (.), or "localhost".
- This parameter does not rely on Windows PowerShell remoting.
- You can use the ComputerName parameter even if your computer is not configured to run remote commands.
- .PARAMETER Credential
- Specifies a user account that has permission to perform this action.
- This parameter allows you to supply either a PSCredential object or you can specify the domain\username and it will open up
- the credential window to type in a password.
- .PARAMETER Grid
- Use this switch to visualize inventory data in the new GridView format for better filtering.
- .EXAMPLE
- To report on localhost:
- Get-Inventory
- .EXAMPLE
- To report on a list of servers from a text file:
- Get-Inventory -ComputerName (Get-Content c:\servers.txt)
- .EXAMPLE
- To report on all available servers in a domain, and see the results in a grid, use the following syntax:
- Get-Inventory -ComputerName ( ([adsisearcher]"objectCategory=computer").findall() | ForEach-Object { $PSItem.properties.cn } ) -Grid
- .EXAMPLE
- To report against a specif server:
- Get-Inventory -ComputerName "alphaserver"
- .NOTES
- Entry for 2013SG Advanced Event 2. To use, dot source script and run 'Get-Inventory'.
- Use 'get-help Get-Inventory -full' to see how to run complete help.
- #>
- [CmdletBinding()]
- param(
- [Parameter(Mandatory=$false, ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$true)]
- # Binding by default to localhost.
- [Alias('CN','__Server')]
- [String[]]$ComputerName = $env:COMPUTERNAME,
- [Parameter(Mandatory=$false)]
- [System.Management.Automation.PSCredential]
- [System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty,
- [Parameter(Mandatory=$false)]
- [switch]$Grid = $false
- ) # End parameter definition.
- Begin {
- Write-Verbose "Started at $(get-date)."
- # Setting up an array to keep al the objects returned from this function.
- $AllComputerData = @()
- # Setting up an array to store all the remote jobs.
- $Jobs = @()
- # Number of max computers to query at the same time. This parameter throttle WMI queries in order not to overload the Inventory server.
- $MaxConcurrentJobs = 10
- # Checking if current is has administrator token for WMI queries.
- Write-Verbose "Checking for admin rights."
- $CurrentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
- $TestAdmin = (New-Object Security.Principal.WindowsPrincipal $CurrentUser).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
- if (!$TestAdmin)
- {
- Throw "This script must be run with elevated privileges. Try tu use -Credential parameter and suplly administrative credentials. Exiting..."
- }
- else
- {
- Write-Verbose "Admin rights OK. Continuing."
- }
- # Scriptblock. Here is where everything happens. This scriptblock is called inside the Process block for each server to query.
- $Sb = {
- # The parameter $Computer is passed by Start-Job as an argument for consistency. I could have used args[0] too.
- param($Computer)
- # Checking if ping works..
- if(!(Test-Connection $Computer -Quiet -ea 0 -Count 2))
- {
- # Ping failed!!!
- # Creating a dummy object for servers not pinging. I added the Ordered key to keep columns in the order I want.
- $Result = New-Object –TypeName PSObject –Property ([ordered]@{
- 'Computer Name'= $computer
- 'State' = "Not pinging"
- 'Operating System'= "N/A"
- 'Physical Memory'= "N/A"
- 'Number Of Processors'= "N/A"
- 'Number of Cores'= "N/A"
- 'Number of Sockets'= "N/A"
- })
- } # End if.
- else
- {
- # Server pings. Checking WMI in a Try Catch block.
- try
- {
- #Adding splatting to keep the line for WMI queries not too long
- $Parms = @{
- "Computername" = $Computer;
- "ErrorAction" = "Stop";
- "ErrorVariable" = "MyErr";
- "Impersonation" = "impersonate"
- }
- # Retrieving the three needed WMI instances: Win32_OperatingSystem, Win32_ComputerSystem and Win32_Processor.
- $OperatingSystem = Get-WmiObject Win32_OperatingSystem -property Caption @Parms
- $ComputerSystem = Get-WmiObject Win32_ComputerSystem -property Name,TotalPhysicalMemory,NumberOfProcessors @Parms
- $Processor = Get-WmiObject Win32_Processor -Property SocketDesignation,Numberofcores @Parms
- # Retrieving information about cores and sockets
- $NumberOfCores = 0
- $NumberOfSockets = 0
- [string]$String = $null
- foreach ($Proc in $Processor)
- {
- # Checking if the NumberOfcores property is present in Win32_Processor. If it is not the case, then it's a Windows 2003 or older.
- if ($Proc.numberofcores -eq $null)
- {
- # Checking if SocketDesignation property is available for each instance of Win32_Processor.
- # If it is present I increment the $NumberOfSockets counter.
- If (-not $String.contains($Proc.SocketDesignation))
- {
- $String = $String + $Proc.SocketDesignation
- $NumberOfSockets++
- }
- # In Windows 2003 and older there is one instance of Win32_Processor for each core.
- # So I am incrementing this counter for each instance.
- $NumberOfCores++
- }
- else
- {
- # Since Windows Server 2008 and Windows Vista:
- # - for the Number of Cores, there is a NumberOfCores property. Easy.
- # - for the Nymber of Sockets, each instance of Win32_Processor is a socket. Incrementing this counter for each instance.
- $NumberOfCores = $NumberOfCores + $Proc.numberofcores
- $NumberOfSockets++
- }
- } # End of foreach loop used to retrieve information on sockets and cores.
- # Retrieving Physical Memory
- $PhysicalMemory = $ComputerSystem.TotalPhysicalMemory
- # Converting installed RAM to the most appropriate unit. it's a bit long but it makes results easy to read for humans.
- Switch ($PhysicalMemory)
- {
- {($PhysicalMemory -gt 1024) -and ($PhysicalMemory -le 1048576)}
- {
- # Better to use KB.
- $PhysicalMemory = $PhysicalMemory/1Kb
- $PhysicalMemory = [math]::Round($PhysicalMemory, 2)
- $PhysicalMemory = [string]$PhysicalMemory + ' KB'
- Break
- }
- {($PhysicalMemory -gt 1048576) -and ($PhysicalMemory -le 1073741824)}
- {
- # Better to use MB.
- $PhysicalMemory = $PhysicalMemory/1Mb
- $PhysicalMemory = [math]::Round($PhysicalMemory, 2)
- $PhysicalMemory = [string]$PhysicalMemory + ' MB'
- Break
- }
- {($PhysicalMemory -gt 1073741824)}
- {
- # Alot of memory here. Better to use GB.
- $PhysicalMemory = $PhysicalMemory/1Gb
- $PhysicalMemory = [math]::Round($PhysicalMemory, 2)
- $PhysicalMemory = [string]$PhysicalMemory + ' GB'
- }
- default
- {
- # Bytes maybe.
- $PhysicalMemory = [math]::Round($PhysicalMemory, 2)
- $PhysicalMemory = [string]$PhysicalMemory + ' B'
- }
- } # End Switch.
- # Preparing a PSObject and filling it with hardware and operating system information. Keeping columns in order with Ordered key.
- $Result = New-Object –TypeName PSObject –Property ([ordered]@{
- 'Computer Name'= $ComputerSystem.Name
- 'State' = "Ping and WMI OK"
- 'Operating System'= $OperatingSystem.Caption
- 'Physical Memory'= $PhysicalMemory
- 'Number Of Processors'= $ComputerSystem.NumberOfProcessors
- 'Number of Cores'=$NumberOfCores
- 'Number of Sockets'=$NumberOfSockets
- })
- } # End Try, starting Catch.
- Catch
- {
- # Cought WMI error.
- "WMI Query for $computer failed."
- "The error was: $MyErr"
- # Preparing a dummy object for servers that did not answer WMI query.
- $Result = New-Object –TypeName PSObject –Property ([ordered]@{
- 'Computer Name'= $computer
- 'State' = "Ping OK but WMI Query failed"
- 'Operating System'= "N/A"
- 'Physical Memory'= "N/A"
- 'Number Of Processors'= "N/A"
- 'Number of Cores'= "N/A"
- 'Number of Sockets'= "N/A"
- })
- } # End Catch block.
- } # End if else
- # Returning the result for this server to the calling function.
- Return $Result
- } # End of Scriptblock
- } # End Begin section
- Process {
- foreach ($Computer in $ComputerName) {
- # Preparing a progress bar to be used if the number of servers to query is greater than 1.
- # The parameters for the progress bar are stored in a variable named $ProgressSplatting.
- if ($ComputerName.Count -gt 1) {
- $ProgressSplatting = @{
- Activity = 'Inventorying:'
- Status = $Computer
- Id = 1
- PercentComplete = ([array]::IndexOf($ComputerName, $Computer))/$ComputerName.Count*100
- }
- Write-Progress @ProgressSplatting
- }
- #Replacing dot and localhost with proper computername.
- switch ($computer)
- {
- "." {$computer="$env:COMPUTERNAME"}
- "localhost" {$computer="$env:COMPUTERNAME"}
- }
- Write-Verbose "Starting job for $Computer"
- # Adding a job for each server in the pipe.
- $Jobs += Start-Job -ArgumentList $Computer -ScriptBlock $sb
- $Running = @($Jobs | Where-Object {$PSItem.State -eq 'Running'})
- # Throttling jobs
- while ($Running.Count -ge $MaxConcurrentJobs)
- {
- write-verbose "Waiting for all jobs to complete"
- $Finished = Wait-Job -Job $Jobs -Any
- $Running = @($Jobs | Where-Object {$PSItem.State -eq 'Running'})
- } # End while loop.
- } # End of foreach loop against each server.
- # Waiting for every job inside $Jobs array to finish.
- Wait-Job -Job $Jobs > $Null
- Write-Verbose "All jobs finished!"
- } # End Process block.
- End {
- # Receiving content of jobs and storing them in a $AllComputerData array.
- $Jobs | ForEach-Object { $AllComputerData += $PSItem | Receive-Job }
- Write-Verbose "Finished at $(get-date)."
- if($Grid)
- {
- # If you selected the -Grid parameters I am going to show the results in a GridView.
- Write-Verbose "Displaying output in a GridView."
- $AllComputerData | Out-GridView
- }
- else
- {
- # Returning an object for post-processing.
- Write-Verbose "Returning the array that contains all the inventory results."
- Return $AllComputerData
- } # End If
- } # End of End block
- } # End of Function Get-Inventory
- Get-Inventory -ComputerName . -Verbose
For the moment my script was very well received, since I am in the Top 10 as you can see here:
This is a huge result for me since I din't spend more than three hours coding and that I had no test machine apart from my laptop.
I tried to be as prolific as I could, so I added jobs, which is a must when you are querying a lot of remote hosts. I also added the possibility to show the result in a GridView, which is a new feature of Powershell.
To vote for me on this entry, visit the link below and click on the rightmost star (5 stars)!
Thanks for your support!