Getting computer models in a domain using Powershell
From Svendsen Tech Powershell Wiki
Contents |
Getting Computer Model As Reported by the System BIOS
Here you will find various ways of getting the computer hardware models, as reported by the BIOS, of computers in a domain in a corporate, educational or similar environment. This is an absolute overkill, batter-to-death solution for a slightly obscure use case.
You can use the WMI script using PowerShell jobs as an example on how to use Start-Job, Wait-Job, etc. with PowerShell. It's a pretty decent example, I think... There's an article about an example PowerShell job wrapper using PowerShell jobs here.
The PsExec wrapper can also be used as an example on how to wrap any (PsExec) command in a PowerShell script and parse the output for a list of computers. For a pretty sophisticated generic PsExec wrapper that can process any PsExec output, see this PowerShell PsExec Wrapper article.
To see how to get a list of computers from Active Directory, see the following article: Getting Computer Names From AD Using PowerShell.
Here is the quick one-liner some of you might be looking for, so I will put it at the top:
PS C:\> (gwmi Win32_ComputerSystem).Model LIFEBOOK S7010 PS C:\>
Or a slight variation, as demonstrated below.
Side note: Most vendors like HP, Dell, Fujitsu-Siemens, etc. do label their motherboards, but with home-built computers you will often find that there's a default or non-descriptive name, like in this example. It's actually an Asus motherboard.
PS C:\> Get-WmiObject Win32_ComputerSystem | Select -Expand Model System Product Name PS C:\>
To target a remote computer, simply add the parameter "-ComputerName server01" to gwmi/Get-WmiObject.
From WSUS
If you have WSUS set up against the desired target computers, this will be an easy and efficient way to get the information which has already been collected and stored in a database.
Sample Output
Below is some sample output from computers in an institution.
PS C:\powershell\wsus> .\get-wsus-models.ps1 > models.txt PS C:\powershell\wsus> type .\models.txt | Select-Object -first 10 Found 974 computers Found 127 unique models Name Value ---- ----- OptiPlex 760 64 HP Compaq dc7600 Small Form Factor 55 OptiPlex GX620 53 HP Compaq dc7900 Small Form Factor 49 HP Compaq 8000 Elite CMT PC 43
Script Code
Below is sample PowerShell code to be run on a WSUS server (download Get-models-wsus.ps1.txt). If you get the error "Exception calling "GetUpdateServer" with "0" argument(s): "Exception of type 'Microsoft.UpdateServices.Administration.WsusInvalidServerException' was thrown."" - please make sure you are running the script from a PowerShell console window with elevated privileges (right-click and choose "Run as administrator").
[reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration") | Out-Null
# This function removes text between parentheses
function massageModel {
param([Parameter(Mandatory=$true)][string] $private:model)
$private:model = $private:model -replace '\s*\([^)]+\)\s*$', ''
$private:model = $private:model -replace '\s+$', ''
return $private:model
}
# Create a WSUS object
if (!$wsus) {
$wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer()
}
# Create a computer scope object and set the criteria to "All" update installation states
# to target all computers in the WSUS database.
$computerScope = New-Object Microsoft.UpdateServices.Administration.ComputerTargetScope
$computerScope.IncludedInstallationStates = [Microsoft.UpdateServices.Administration.UpdateInstallationStates]::All
# Get all the computer objects
$computers = $wsus.GetComputerTargets($computerScope)
'Found ' + $computers.Count + ' computers'
# Initialize hash
$models = @{}
# Store "massaged" models in a hash (keys are unique)
# and count the number of models.
$computers | Foreach-Object { $model = massageModel $_.Model; $models.$model += 1 }
'Found ' + $models.count + ' unique models'
# Output the data, sorted with the most common models
# first and then alphabetically.
$models.GetEnumerator() |
Sort-Object -Property @{Expression='Value';Descending=$true},@{Expression='Name';Descending=$false} |
Format-Table -AutoSize
You can of course adapt this as necessary.
Using WMI with and without jobs
Using these scripts requires that remote WMI access is set up and working - and of course that the target computers are online. The ones that aren't online will just be skipped / fail / time out. Adapt as necessary.
I'm putting up two scripts. One using jobs and one passing the computer names or IP addresses to Get-WMIObject in batches of 50. The reason behind this (batches) is that I experienced some errors of the type "Get-WmiObject : Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))", and Get-WMIObject stopped processing the parameters.
Apparently this error was deemed to be terminating. Also see this article for more information on using a try/catch statement to catch the WMI error, and also for tweaking the WMI timeout value. I should post an updated version with a custom WMI timeout.
With batches of 50 it will work a bit better, but if you hit upon a terminating error, I think the entire batch's data is lost. This script takes one parameter, which is the input file containing one target host (IP or DNS name) per line. See Getting computer names from AD using Powershell for information on how to extract the computer names from AD.
- Download Get-models-wmi.ps1.txt here (right-click and "save as" - but you probably want the jobs version below).
The solution using jobs is the one I have pasted the code for on this page as it seems most robust.
This takes two parameters where the second is optional and defaults to 50. The first is the file containing one target host (IP or DNS name) per line - and the second is the number of concurrent jobs to run. 50 concurrent jobs might be a bit high (or low?) - I haven't benchmarked. Running 50 jobs will require about 1.5-2 GB of RAM (at least in my test environments) and 100 jobs should require 3-4 GB of RAM.
Find a generic job wrapper script and read more about PowerShell jobs in this article. Each powershell.exe process (one for each job) uses minimum about 27 MB RAM on my test computers.
I ran the script against 1400 computers on a server with 4 GB RAM, with 100 jobs, and it went OK, using most of the available RAM when starting the jobs. It will be CPU-intensive when starting the jobs. See Getting computer names from AD using Powershell for information on how to extract the computer names from AD.
PowerShell's Start-Job cmdlet leaves a lot to be desired when it comes to resource usage and efficiency. At least it allows for concurrency if you have the hardware for it.
- Download Get-models-wmi-jobs.ps1.txt here (right-click and "save as").
Sample Output
Here is an example run against 1446 computers in a domain, some of which are offline/unavailable, and some sample output:
PS C:\powershell\get-models-wmi> .\get-models-wmi-jobs2.ps1 computers.txt 100 Start time: 05/28/2011 04:27:36 Found 1446 computers. Starting jobs. This might take a good long while depending on the amount of computers and the hardware being used. Starting jobs: host01, host02, host03, host04, host05, host06, host07, host08 Starting jobs: host09, host10, host11, host12, host13, host14, host15, host16 <### snip output ###> Total computer count: 100 <snip> Total computer count: 200 <snip> Total computer count: 1400 <snip> Found 85 unique models Start time: 05/28/2011 04:27:36 End time: 05/28/2011 04:55:20 Output files: computer-models-with-computername.txt, computer-models-count.txt, computer-models-noping.txt PS C:\powershell\get-models-wmi> (type .\computer-models-noping.txt).count 766 PS C:\powershell\get-models-wmi> (Select-String 'ERROR' .\computer-models-with-computername.txt).count 41 PS C:\powershell\get-models-wmi> type .\computer-models-count.txt | Select-Object -first 10 Name Value ---- ----- OptiPlex 760 55 OptiPlex GX620 44 ERROR 41 HP Compaq dc7600 Small Form Factor 41 HP Compaq dc7900 Small Form Factor 38 HP Compaq 8000 Elite CMT PC 34 HP Compaq dc7900 Convertible Minitower 27
Script Code
Here is the code that makes up the job-oriented script (download Get-models-wmi-jobs.ps1.txt):
param([Parameter(Mandatory=$true)] $ComputerFile, $jobCount = 50)
# Initialize the models hash which will be populated while processing job output
$models = @{}
# This function strips stuff in parentheses and trailing whitespace from the string passed,
# which in my experience is usually what you want when looking at the BIOS model string.
function Massage-Model {
param([Parameter(Mandatory=$true)][string] $private:model)
$private:model = $private:model -replace '\s*\([^)]+\)\s*', ''
$private:model = $private:model -replace '\s+$', ''
return $private:model
}
function Process-Jobs {
param($private:runningJobs)
#''
'Processing job batch...'
Write-Host -NoNewLine 'Processing jobs: '
$private:jobCounter = 0
foreach ($private:jobHash in $private:runningJobs) {
$private:jobCounter++
$private:computerName = $private:jobHash.Name
if ($private:jobCounter -eq 8) {
$private:jobCounter = 0
Write-Host $private:computerName
Write-Host -NoNewLine 'Processing jobs: '
}
else {
Write-Host -NoNewLine "$private:computerName, "
}
Wait-Job $private:jobHash.Job | Out-Null
$private:output = Receive-Job -ErrorAction SilentlyContinue $private:JobHash.Job
if ($private:output -ieq 'No ping reply') {
$private:computerName | Out-File -Append $noPingFile
}
elseif ($private:output -match '\S+') {
$models.$private:computerName = Massage-Model $private:output
}
else {
$models.$private:computerName = 'ERROR'
}
}
''
Write-Host -NoNewLine 'Starting jobs: '
}
# Output files, didn't bother making them parameters...
$modelsFile = 'computer-models-with-computername.txt'
$modelsCountFile = 'computer-models-count.txt'
$noPingFile = 'computer-models-noping.txt'
# Prompt before overwriting files.
if ( (Test-Path $modelsFile) -or (Test-Path $modelsCountFile) -or (Test-Path $noPingFile) ) {
"Output file(s) '$modelsFile', '$modelsCountFile' or '$noPingFile' exist."
$private:answer = Read-Host 'Overwrite? (Y/n) [yes]'
if ($private:answer -match '^n') { 'Aborting.'; exit 0 }
Remove-Item $noPingFile -ErrorAction SilentlyContinue
}
# Get the start time and display it before starting processing.
$startTime = Get-Date
"Start time: $startTime"
if (!(Test-Path -PathType leaf $computerFile)) {
"Error: $computerFile does not exist. Exiting..."
exit 0
}
# Get the computers. Skip lines with only whitespace (including blank lines).
$computers = Get-Content $ComputerFile | Where-Object { $_ -match '\S+' }
'Found ' + $computers.Count + ' computers.'
'Starting jobs.'
'This might take a good long while depending on the amount of computers and the hardware being used.'
# Having issues with passing large arrays to GWMI -computer and many concurrent jobs,
# trying batches of 50 jobs
$runningJobs = @()
$private:computerCounter = 0
$private:tempComputerCounter = 0
$private:jobCounter = 0
Write-Host -NoNewLine 'Starting jobs: '
foreach ($computer in $computers) {
$private:computerCounter++
$private:tempComputerCounter++
if ($private:tempComputerCounter -eq 8) {
$private:tempComputerCounter = 0
Write-Host $computer
Write-Host -NoNewLine 'Starting jobs: '
}
else {
Write-Host -NoNewLine "$computer, "
}
#Start-Sleep -Milliseconds 500
$private:job = Start-Job -ArgumentList $computer -ScriptBlock {
param($private:computer)
if (Test-Connection -Quiet -Count 1 $private:computer) {
(Get-WMIObject -computer $private:computer -Class Win32_ComputerSystem).Model
}
else {
'No ping reply'
}
}
$runningJobs += @{ 'Name' = $computer; 'Job' = $private:job; }
$private:jobCounter++
if ($private:jobCounter -eq $jobCount) {
''; "Total computer count: $private:computerCounter"
Process-Jobs $runningJobs
$private:jobCounter = 0
$runningJobs = @()
}
}
''
# Process the remainder
Process-Jobs $runningJobs
''
$models.GetEnumerator() | Sort-Object -property @{Expression='Name';Descending=$false},@{Expression='Value';Descending=$false} |
Format-Table -AutoSize | Out-File $modelsFile
$modelsCount = @{}
$models.Keys | Foreach { $modelsCount.$($models.$_) += 1 }
'Found ' + $modelsCount.Count + ' unique models'
$modelsCount.GetEnumerator() | Sort-Object -property @{Expression='Value';Descending=$true},@{Expression='Name';Descending=$false} |
Format-Table -AutoSize | Out-File $modelsCountFile
@"
Start time: $startTime
End time: $(Get-Date)
Output files: $modelsFile, $modelsCountFile, $noPingFile
"@
Using psexec
This requires psexec.exe which you can download from Microsoft Technet - Sysinternals (http://www.sysinternals.com redirects there, because it is now Microsoft-owned) and the get-model.vbs file which you can download here or below.
The script takes one parameter, which is the input file containing one computer name (or IP address) per line. See Getting computer names from AD using Powershell for information on how to extract the computer names from AD.
You could also do this with the "ultimate" generic PsExec Wrapper script I wrote.
Sample Output
PS C:\powershell\get-models-psexec> .\get-models-psexec.ps1 .\sample-computers-small-clean.txt Output file(s) 'computername-and-model-psexec.txt' or 'computer-models-count-psexec.txt' exist. Overwrite? (Y/n) [yes]: Found 35 computers. Processing computers with psexec.exe. This might take a good long while depending on the amount of computers and their availability. Processing comp1... Processing comp2... #### snip output ####> Found 19 unique models. Start time: 05/27/2011 06:05:25 End time: 05/27/2011 06:06:26 Input file: .\sample-computers-small-clean.txt Output files: computername-and-model-psexec.txt, computer-models-count-psexec.txt PS C:\powershell\get-models-psexec> type .\computer-models-count-psexec.txt | Select-Object -first 8 Name Value ---- ----- HP Compaq dc7600 Small Form Factor 4 OptiPlex 745 3 HP Compaq dc7800p Small Form Factor 2 OptiPlex 780 2 EVO 1 PS C:\powershell\get-models-psexec> type .\computername-and-model-psexec.txt | Select-Object -first 7 Name Value ---- ----- comp1 HP Compaq dc7100 CMT comp2 ERROR: No ping reply comp3 EVO comp4 HP Compaq dc7600 Convertible Minitower PS C:\powershell\get-models-psexec> (Select-String 'No ping reply' .\computername-and-model-psexec.txt).count 9
Script Code
- Download the script here (Get-model-psexec.ps1.txt)
- Download the VBScript it uses here (Get-model.zip)
- Go to Microsoft Technet - Sysinternals to download psexec.exe from the PsTools suite
param([Parameter(Mandatory=$true)] $ComputerFile)
# This is a psexec wrapper that can easily be adapted to other needs.
# It is made to retrieve the computer hardware model string as reported
# by the system BIOS. It requires psexec.exe to work and that WMI can
# be queried locally. The computer must also respond to ICMP echo (ping).
$startTime = Get-Date
# Check that we have the necessary files in the current directory
if ( ! ((Test-Path 'psexec.exe') -and (Test-Path 'get-model.vbs'))) {
'You need psexec.exe and get-model.vbs in the current working directory.'
'See www.powershelladmin.com for more information'
'Exiting.'
exit
}
# Output files, didn't bother making them parameters...
$modelsFile = 'computername-and-model-psexec.txt'
$modelsCountFile = 'computer-models-count-psexec.txt'
# Prompt before overwriting files.
if ( (Test-Path $modelsFile) -or (Test-Path $modelsCountFile) ) {
"Output file(s) '$modelsFile' or '$modelsCountFile' exist."
$private:answer = Read-Host 'Overwrite? (Y/n) [yes]'
if ($private:answer -match '^n') { 'Aborting.'; exit 0 }
}
function Trim-Model {
param([Parameter(Mandatory=$true)][string] $private:model)
$private:model = $private:model -replace '\s*\([^)]+\)\s*', ''
$private:model = $private:model -replace '\s+$', ''
return $private:model
}
function Get-Model {
param([string] $private:computer)
if ( ! (Test-Path "\\$private:computer\c$") ) {
return 'ERROR: Cannot access remote C:'
}
# Copy the file to the root of C: (let's just assume C: is writeable)
$private:destination = '\\' + $private:computer + '\c$'
# Try to copy, check $? for failure (false)
Copy-Item '.\get-model.vbs' -Destination $private:destination
if ( ! $? ) {
return 'ERROR: Could not copy get-model.vbs to remote C:'
}
# Run the psexec command
$private:output = .\psexec.exe \\$private:computer cscript //nologo c:\get-model.vbs . 2> temp-get-models-psexec.tmp
if ($private:output -match '### Model: (.+)') {
return Trim-Model $matches[1]
}
# If we get here something failed with psexec or the VBScript
return 'ERROR: Unexpected output from get-model.vbs'
}
# Read in computer names, or IP addresses, from $computerFile and store in the $computers array
$computers = Get-Content $computerFile
'Found ' + $computers.Count + ' computers.'
'Processing computers with psexec.exe.'
'This might take a good long while depending on the amount of computers and their availability.'
# Initialize hash and populate it
$models = @{}
$computers | Foreach { "Processing $_..."; if (Test-Connection -Quiet -Count 1 $_) { $models.$_ = Get-Model $_ } else { $models.$_ = 'ERROR: No ping reply' } }
# Dump data to file
$models.GetEnumerator() | Sort-Object -property @{Expression='Name';Descending=$false},@{Expression='Value';Descending=$false} |
Format-Table -AutoSize | Out-File $modelsFile
# Count the number of each model, skip error lines
$modelsCount = @{}
$models.Keys | Foreach { if ($models.$_ -notmatch '^ERROR:') { $modelsCount.$($models.$_) += 1 } }
'Found ' + $modelsCount.Count + ' unique models.'
# Dump data to file
$modelsCount.GetEnumerator() | Sort-Object -property @{Expression='Value';Descending=$true},@{Expression='Name';Descending=$false} |
Format-Table -AutoSize | Out-File $modelsCountFile
@"
Start time: $startTime
End time: $(Get-Date)
Input file: $computerFile
Output files: $modelsFile, $modelsCountFile
"@
