- Create a certificate-signed RDP shortcut via Group Policy - Fri, Aug 9 2019
- Monitor web server uptime with a PowerShell script - Tue, Aug 6 2019
- How to build a PowerShell inventory script for Windows Servers - Fri, Aug 2 2019
Prerequisites
You're going to need a few prerequisites before we get started:
- PowerShell Remoting enabled and available to you on all Windows Servers
- Intermediate level PowerShell knowledge
Scaffolding
A server inventory script like this can get massive and unwieldy if not properly planned out. I do this via script scaffolding.
Scaffolding is a term used to refer to writing out the framework of a script. I'm not actually writing functional code just yet; I'm just building the "container" to fit it in.
First, I'll need a source where I can pull the server names from. One possibility is to pull the names from an Active Directory, although this isn't necessarily required. Below you can see I'm using the Get-AdComputer to find all servers in the Servers OU.
$serversOuPath = 'OU=Servers,DC=local' $servers = Get-ADComputer -SearchBase $serversOuPath -Filter * | Select-Object -ExpandProperty Name
Once I have the server names, I can then begin to scaffold out the code querying the name, operating system, memory, and free disk space of multiple servers. A rough framework of how this will happen is shown below. Notice that I've already built a foreach loop and will be creating an object for each server with four properties.
foreach ($server in $servers) { $output = [ordered]@{ 'ServerName' = $server 'OperatingSystem' = $null 'FreeDiskSpace (GB)' = $null 'Memory (GB)' = $null } [pscustomobject]$output }
Finding the operating system
Now I can begin filling in the values for the $output hashtable I've defined in my script scaffold. The first task is getting the operating system of each server. The best way to do that is to use CIM. I know I can pull the operating system name via Get-CimInstance like this:
PS51> (Get-CimInstance -ComputerName <server> -ClassName Win32_OperatingSystem).Caption Microsoft Windows Server 2019 Datacenter
Knowing how to find the operating system for a single server, I can add that code and replace the ComputerName value with the variable our server will have in our foreach loop.
foreach ($server in $servers) { $output = [ordered]@{ 'ServerName' = $server 'OperatingSystem' = (Get-CimInstance -ComputerName $server -ClassName Win32_OperatingSystem).Caption 'FreeDiskSpace (GB)' = $null 'Memory (GB)' = $null } [pscustomobject]$output }
Finding free disk space
The next value we need to come up with is the amount of free disk space on one server. We'll then expand that out to our script when we're done.
Again, we can use CIM to handle this, querying the Win32_LogicalDisk class this time.
PS51> Get-CimInstance -ComputerName <server> -ClassName Win32_LogicalDisk
This works, but we have a problem. When this report is run, we're going to have a single server per row, and a server can have more than one disk on it. If we leave this as-is, it's going to look bad when the reports runs.
Let's sum up all of the disk space and round it to the nearest GB so we have a birds-eye view of all disks.
PS51> [Math]::Round(((Get-CimInstance -ComputerName <server> -ClassName Win32_LogicalDisk | Measure-Object -Property FreeSpace -Sum).Sum / 1GB),1) 118.5
We can now pull the sum of free space from all disks on a single server. Let's use the same tactic as before and insert this code into our inventory script.
foreach ($server in $servers) { $output = [ordered]@{ 'ServerName' = $server 'OperatingSystem' = (Get-CimInstance -ComputerName $server -ClassName Win32_OperatingSystem).Caption 'FreeDiskSpace (GB)' = [Math]::Round(((Get-CimInstance -ComputerName $server -ClassName Win32_LogicalDisk | Measure-Object -Property FreeSpace -Sum).Sum / 1GB),1) 'Memory (GB)' = $n } [pscustomobject]$output }
Finding total memory
The next attribute to add to the script is total memory. We can, thankfully, use CIM again to grab this!
PS51> Get-CimInstance -ComputerName <server> -ClassName Win32_PhysicalMemory
We have the same problem as last time. The query to find total memory returns objects with properties. We just need a single string value. We can find the actual memory amount via the Capacity property. Once we have this, we can sum up the capacities from each object that's returned.
PS51> (Get-CimInstance -ComputerName <server> -ClassName Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum).Sum 3758096384
Notice above that the 3758096384 sure is a big number! That’s because the number that comes out of CIM is in bytes. We need to convert this to gigabytes to get a better representation.
PS51> (Get-CimInstance -ComputerName <server> -ClassName Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum).Sum /1GB 3.5
We can now plug this code into the inventory script.
(Get-CimInstance -ComputerName <server> -ClassName Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum).Sum /1GB foreach ($server in $servers) { $output = [ordered]@{ 'ServerName' = $server 'OperatingSystem' = (Get-CimInstance -ComputerName $server -ClassName Win32_OperatingSystem).Caption 'FreeDiskSpace (GB)' = [Math]::Round(((Get-CimInstance -ComputerName $server -ClassName Win32_LogicalDisk | Measure-Object -Property FreeSpace -Sum).Sum / 1GB),1) 'Memory (GB)' = (Get-CimInstance -ComputerName $server -ClassName Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum).Sum /1GB } [pscustomobject]$output }
When the above code runs, you will see an output like below. I just have a single machine to query but when you run this in a larger environment, you will see one server per row with each property populated.
We can now add a parameter to dynamically send it server names and finish off this script!
param( [CmdletBinding()] param( [Parameter()] [ValidateNotNullOrEmpty()] [string[]]$Server ) foreach ($server in $servers) { $output = [ordered]@{ 'ServerName' = $server 'OperatingSystem' = (Get-CimInstance -ComputerName $server -ClassName Win32_OperatingSystem).Caption 'FreeDiskSpace (GB)' = [Math]::Round(((Get-CimInstance -ComputerName $server -ClassName Win32_LogicalDisk | Measure-Object -Property FreeSpace -Sum).Sum / 1GB),1) 'Memory (GB)' = (Get-CimInstance -ComputerName $server -ClassName Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum).Sum /1GB } [pscustomobject]$output } )
If I save the above code as Get-SrvInv.ps1, I could then execute the script passing in a list of server names retrieved from Active Directory or another data source.
Notice below I'm using a text file of server names as an example.
PS51> .\Get-SrvInv.ps1 -Server (Get-Content -Path 'servers.txt')
Summary
Once you have the basics down of building an inventory report like this, you can start to plug in various code snippets to pull the information you need.
Subscribe to 4sysops newsletter!
If you'd like a deeper look into building a script like this, I encourage you to check out the Building a Tool mini-course that covers these concepts and goes into a lot more detail.
I would recommend a few adjustments for anyone looking to use this in production:
The last code sample has two param blocks.
The final block of code should be more like the following.
Users might need to enable PSRemotting and have admin credentials to connect to the server/workstation.