Wes Ellis./ a personal notebook
Technology. Stories. Side projects.
A few things worth writing down.
← Back to Script Library

SCRIPT LIBRARY · POWERSHELL

Count KACE Devices by Location from a CSV Export

Export your KACE device inventory to CSV and get a per-location (or per-label) device count, optionally just the machines added this week.

AT A GLANCEGet-KaceLocationReport.ps1
What it does
Reads a device inventory CSV exported from the Quest KACE SMA and counts devices per location, label, or any other column. Can limit the count to devices created or seen in the last N days.
Requires
  • PowerShell 7+ or Windows PowerShell 5.1
  • No modules
  • A device list exported from the KACE SMA as CSV
Permissions
Whatever lets you see and export the device list in the SMA console. The script itself only reads a file.
Runs on
Windows, macOS, Linux
Tested
Parse-checked and run against a sample CSV export in PowerShell 7.4

"How many machines do we have at each site, and how many showed up this week?" It's a simple question. It's also the kind of thing that gets asked the afternoon before a budget meeting.

The first version of this post answered it with a custom SQL report written against the appliance's database. That works, but it depends on table names and joins that aren't documented for customers and can shift between versions, and it only helps people who have report-writing rights. This version takes the boring, reliable route: export the device list from the SMA console as CSV, then let PowerShell do the grouping.

Since the columns in an export depend on which ones you had showing, nothing is assumed. You tell the script which column to group by and which date column to filter on, and if you guess wrong it lists the columns it did find.

Get-KaceLocationReport.ps1Download
<#
.SYNOPSIS
    Counts devices per location (or label, or any column) from a KACE SMA inventory CSV export.
.DESCRIPTION
    Reads a device inventory CSV exported from the Quest KACE SMA console and groups it by the
    column you choose. Optionally keeps only devices created or last seen within the last N days.
    Column names differ depending on which columns you had showing when you exported, so every
    column is a parameter. If one isn't found, the script lists the columns that are there.
.PARAMETER Path
    The CSV file exported from KACE.
.PARAMETER GroupBy
    The column to group on, for example Location or Labels. Default: Location.
.PARAMETER DateColumn
    A date column to filter on, for example Created. Only used with -Days.
.PARAMETER Days
    Keep only rows whose DateColumn falls in the last N days.
.PARAMETER Delimiter
    If the GroupBy column holds several values (labels usually do), split on this character.
.PARAMETER IncludeBlank
    Count devices with an empty GroupBy value as '(none)' instead of dropping them.
.EXAMPLE
    .\Get-KaceLocationReport.ps1 -Path .\devices.csv -DateColumn Created -Days 7
.EXAMPLE
    .\Get-KaceLocationReport.ps1 -Path .\devices.csv -GroupBy Labels -Delimiter ','
#>
[CmdletBinding()]
param(
    [Parameter(Mandatory)][ValidateScript({ Test-Path -LiteralPath $_ -PathType Leaf })][string]$Path,
    [string]$GroupBy = 'Location',
    [string]$DateColumn,
    [ValidateRange(1, 3650)][int]$Days,
    [string]$Delimiter,
    [switch]$IncludeBlank
)

$ErrorActionPreference = 'Stop'

$rows = @(Import-Csv -LiteralPath $Path)
if ($rows.Count -eq 0) { throw "'$Path' has no data rows." }

$columns = $rows[0].PSObject.Properties.Name
$needed  = @($GroupBy) + @(if ($Days) { $DateColumn })
foreach ($col in $needed) {
    if (-not $col) { throw '-Days needs -DateColumn too, so the script knows which date to check.' }
    if ($columns -notcontains $col) {
        throw "Column '$col' isn't in this export. Columns found: $($columns -join ', ')"
    }
}

if ($Days) {
    $cutoff  = (Get-Date).AddDays(-$Days)
    $skipped = 0
    $rows = foreach ($row in $rows) {
        $when = [datetime]::MinValue
        if ([datetime]::TryParse($row.$DateColumn, [ref]$when)) {
            if ($when -ge $cutoff) { $row }
        }
        else { $skipped++ }
    }
    $rows = @($rows)
    if ($skipped) { Write-Warning "$skipped row(s) had an empty or unreadable '$DateColumn' value and were left out." }
    Write-Verbose "$($rows.Count) device(s) with $DateColumn in the last $Days day(s)."
}

# Turn each device into one or more group names.
$values = foreach ($row in $rows) {
    $raw   = [string]$row.$GroupBy
    $parts = if ($Delimiter) { $raw -split [regex]::Escape($Delimiter) } else { , $raw }
    $parts = @($parts | ForEach-Object { $_.Trim() } | Where-Object { $_ -and $_ -ne 'unassigned' })
    if ($parts.Count) { $parts }
    elseif ($IncludeBlank) { '(none)' }
}

$total = $rows.Count
$values | Group-Object | Sort-Object @{ Expression = 'Count'; Descending = $true }, Name | ForEach-Object {
    [pscustomobject]@{
        $GroupBy   = $_.Name
        Devices    = $_.Count
        PercentAll = if ($total) { [math]::Round(100 * $_.Count / $total, 1) } else { 0 }
    }
}

Parameters

ParameterTypeDefaultWhat it's for
-Pathstring—The CSV file you exported from KACE.
-GroupBystringLocationThe column to count by. Location and Labels are the usual ones, but any column works.
-DateColumnstring—A date column to filter on, like Created or Last Inventory. Only used with -Days.
-Daysint—Keep only devices whose DateColumn is within the last N days.
-Delimiterstring—Split the GroupBy column on this character first. Handy for labels, where one device often has several.
-IncludeBlankswitch—Count devices with no value as '(none)' instead of leaving them out. Worth doing now and then, because those are the ones nobody's tracking.

Run it

Devices per location, biggest site first.

.\Get-KaceLocationReport.ps1 -Path .\devices.csv

Only devices created in the last week, including the ones with no location set.

.\Get-KaceLocationReport.ps1 -Path .\devices.csv -DateColumn Created -Days 7 -IncludeBlank

Count by label instead. Devices with two labels count under both.

.\Get-KaceLocationReport.ps1 -Path .\devices.csv -GroupBy Labels -Delimiter ','

Save it for the meeting.

.\Get-KaceLocationReport.ps1 -Path .\devices.csv | Export-Csv .\devices-by-location.csv -NoTypeInformation

What you'll see

Example outputvalues are illustrative
WARNING: 3 row(s) had an empty or unreadable 'Created' value and were left out.

Location       Devices PercentAll
--------       ------- ----------
Building A          14       46.7
Main Office          9         30
Warehouse            5       16.7
(none)               2        6.7

How it works

  1. Get the export. In the SMA admin console, open the device inventory list, make sure the columns you want are showing, and export the list as CSV. That's the only KACE-side step.
  2. Check the columns. The script reads the first row and confirms the -GroupBy column (and the -DateColumn, if you're filtering) actually exists. If not, you get the real column names in the error, so fixing it takes one try.
  3. Filter by date, if asked. Each row's date is parsed with [datetime]::TryParse. Rows inside the window stay. Rows with a blank or unreadable date are counted and reported in a single warning instead of being silently dropped.
  4. Split, trim, count. Values are trimmed, optionally split on your delimiter, and blanks and "unassigned" are skipped (or counted as (none) with -IncludeBlank). Group-Object does the counting.
  5. Return objects. One row per group, biggest first, with the count and its share of all devices.

Take it further

  • Compare two exports. Run it against last month's export and this month's, then join them on location to see which sites are growing.
  • Stay in the appliance. If you'd rather not export at all, the SMA's report wizard can build a grouped device report without writing any SQL, and schedule it to email you.
  • Automate the export. The SMA has a REST API that can return inventory as JSON. Once you've confirmed the endpoints and fields for your appliance version against Quest's API guide, save the results with Export-Csv and point this script at the file.

Things that'll trip you up

  • Export what you want to count. A list export generally includes the columns you have showing. If Location or your date column isn't visible in the device list, add it with the column chooser before you export, or the script will tell you it isn't there.
  • Dates and regional settings. The script parses dates with your computer's regional format. If the export uses day-first dates and your PC expects month-first, some rows will be skipped with a warning. Check the count in that warning before trusting the numbers.
  • Labels can add up to more than 100%. When you split a multi-value column, one device can count in several groups. PercentAll is still out of the total number of devices, so the column won't sum to 100.
  • "unassigned" is dropped on purpose. The old SQL report skipped it and so does this. Use -IncludeBlank if you want those devices counted as (none).