Getting computer models in a domain using Powershell

From Svendsen Tech Powershell Wiki

Jump to: navigation, search

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.

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.

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

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
"@
Personal tools