The script in this post is one I’ve been meaning to write about for a long while, because it is an example of a script automating a task that would be mind-numbingly boring and time-consuming to do by hand.
For context – consider an organisation with a distributed network of around 100 locations. Multiple AD domains are present across the domain, with a server per location running DHCP and DNS services. The environment is significantly heterogeneous, constituting a mix of personal and organisational devices – smartphones, VOIP phones, tablets, laptops, desktops. In this environment, no central record of existing hardware profiles or maintenance cover exists for these devices.
The immediate instinct is very often “Get a tool to do it, I like “. And yes, there are some nice tools, free and paid-for, that can do this. Now imagine you’re trying to do this in as little time as possible (ideally by the end of the current week, say), leveraging only the systems already deployed. And you either have, or can trivially deploy, the Group Policies and host firewall rules required to allow remote PowerShell for authorised sources. (Or, if you’re stuck with now-obsolete operating systems like Windows 7 or Server 2008 R2, executing the equivalent Get-WMIObject queries via wmic…)
You could do worse than something like this script. This script covers the hardware inventory collection. It has a companion script which is now sadly very obsolete whose function was to parse the output file generated by this script and use the vendor API to check for useful information like purchase date, warranty cover type, and warranty expiration. On one hand, this script should only ever be viewed as a stepping stone – because proper inventory management should be based on information being in a database system that can generate reports and automatic notifications about interesting or important events. But on the other hand, a spreadsheet might be a terrible tool for tracking inventory – but it’s at least less bad than not tracking inventory at all.
This version doesn’t really focus on the hardware specifications of the identified equipment, but it could easily be expanded to include this e.g. if you were also trying to evaluate hardware suitability for an OS or software upgrade. The Win32 classes OperatingSystem, Processor, ComputerSystem, LogicalDisk, NetworkAdapter and NetworkAdapterConfiguration can be used to collect a good baseline set of information about the hardware specifications of target devices.
Anyway, let’s look at the script.
# Setup $Domain1Cred=(Get-Credential -Message "Please enter local admin credentials for Domain 1" -Username "DOMAIN1\") $Domain2Cred=(Get-Credential -Message "Please enter local admin credentials for Domain 2" -Username "DOMAIN2\") [System.Collections.ArrayList]$machinelist=@() $scandate=(Get-Date -Format "yyyyMMdd") $dc=Read-Host("Enter the FQDN for a domain controller to query for DHCP servers") foreach ($d in (Invoke-Command -Computer $dc -Credential (get-Credential) -Scriptblock {Get-DHCPServerInDC})) { Write-Host $d.DNSName -foregroundcolor white $DHCPname = ($d.DNSName -split "\.")[0] try { # Get list of active DHCP leases $scope=Get-DHCPServerv4Scope -Computername $d.DNSName -ErrorAction Stop $leases=Get-DhcpServerv4Lease -ScopeId $scope.ScopeID -ComputerName $d.DNSName -ErrorAction Stop | ? {$_.AddressState -like "Active" -and (($_.Hostname -match "DTW|LTW|TB"))} | Sort-Object -Property LeaseExpiryTime -Descending [boolean]$serveronline=$true Write-Host "Connected to DHCP Server $($d.DNSName), lease list obtained..." -foregroundcolor white } catch { [boolean]$serveronline=$false Write-Host "Unable to connect to DHCP server $($d.DNSName)" -foregroundcolor red }
First up, we’re getting credential tokens for the domains in use across the environment. I’ve kept this script to two domains and used if/else statements, but if additional domains come into play, it can be easily expanded by changing to switch statements for domain-specific information. We’re also setting up an ArrayList because normal arrays are inefficient at adding items as they increase in size, and I was looking to retrieve data on several thousand devices. The scandate variable is just for reference because I anticipated a situation where this might need to be run multiple times, and it would be useful to know when a system was last online. The dc variable is set like that to save me having to load an entire module for Active Directory just to get a domain controller name.
After that, we use Invoke-Command to run Get-DHCPServerInDC to get back a list of authorised DHCP servers on the DC’s domain, and iterate through that list. For each DHCP server, we start by getting the scope and leases, filtering for active leases (to remove inactive reservations) and for hostnames matching our device naming conventions (to avoid unwanted devices). The ability to use | and separate multiple terms when using match is a really neat way of keeping Where-Object clauses short and readable. Lastly, we set a boolean called serveronline.
if ($serveronline) { # For each machine in $leases, check for most recent directory in C:\Users and match against $user foreach ($l in $leases) { $shortname=($l.Hostname -split "\.")[0] # Check shortname and select credentials for WMI query accordingly if ($l.Hostname -match "DOMAIN1") { $domain="DOMAIN1" $cred=$Domain1Cred } else { $domain="DOMAIN2" $cred=$Domain2Cred } try { Test-Connection $shortname -ErrorAction Stop | Out-Null; [boolean]$online=$true } catch { Write-Host "Machine $($shortname) could not be reached." -foregroundcolor red; [boolean]$online=$false; } if ($online) { # $serialno=(Get-WMIObject -Computername $shortname -Credential $cred -Class Win32_BIOS).SerialNumber $hwinfo=(Get-WMIObject -Computername $shortname -Credential $cred -Class Win32_ComputerSystemProduct -Property vendor,name,IdentifyingNumber) } # Create object, add to machinelist arraylist $Obj=New-Object System.Object $Obj | Add-Member -MemberType NoteProperty -Name "DHCPServer" -value $DHCPname -Force $Obj | Add-Member -MemberType NoteProperty -Name "Domain" -value $domain -Force $Obj | Add-Member -MemberType NoteProperty -Name "Date checked" -value $scandate -Force $Obj | Add-Member -MemberType NoteProperty -Name "Hostname" -value $shortname -Force $Obj | Add-Member -MemberType NoteProperty -Name "Make" -value $($hwinfo.Vendor) -Force $Obj | Add-Member -MemberType NoteProperty -Name "Model" -value $($hwinfo.Name) -Force $Obj | Add-Member -MemberType NoteProperty -Name "SerialNumber" -value $($hwinfo.IdentifyingNumber) -Force $machinelist.Add($Obj) # Clean up variables Remove-Variable -Name shortname,cred,online,hwinfo,Obj -Force -ErrorAction SilentlyContinue }
If the server is online, we iterate through the list of active leases. For each lease, we check the hostname and use the DNS suffix to determine domain membership, and set the domain and cred parameters accordingly. We then check that the host is online with Test-Connection, and if so use Get-WMIObject to query the Win32_ComputerSystemProduct class for three values. These correspond to make, model and serial number – the commented-out line shows that I initially assumed I’d have to use the Win32_BIOS class for the serial number. Lastly, a new object is created and the relevant information is added to it across a set of properties. The object is then added to the ArrayList, and variables from the foreach-lease loop are cleaned up.
} else { # Add entry to machinelist for DHCP Server to record that it is unreachable $Obj=New-Object System.Object $Obj | Add-Member -MemberType NoteProperty -Name "DHCPServer" -value $DHCPname -Force $Obj | Add-Member -MemberType NoteProperty -Name "Date checked" -value $scandate -Force $Obj | Add-Member -MemberType NoteProperty -Name "Hostname" -value "SERVER UNREACHABLE" -Force } # Clean up variables after processing all leases Remove-Variable -Name scope,leases,serveronline -Force -ErrorAction SilentlyContinue }
If the server isn’t online, a placeholder object is created and added to the arraylist to note that the server was inaccessible. Lastly, variables from the foreach-dhcpserver loop are cleaned up.
Write-Host "Finished checking DHCP servers, generating output file...." -foregroundcolor white $outputfile="C:\tmp\Scripts\Inventory\" + (Get-Date -Format "yyyy-MM-dd") + "_Inventory.csv" $machinelist | Export-CSV -Path $outputfile -Encoding UTF8 -NoClobber -NoTypeInformation
Once all DHCP servers have been checked, all that’s left is exporting the arraylist as a CSV. (And in an ideal world, using that CSV file to populate a database-driven system…)