TB

MoppleIT Tech Blog

Welcome to my personal blog where I share thoughts, ideas, and experiences.

Fleet Inventory with Reusable CimSessions: Faster PowerShell Queries Across Windows Servers

When you inventory a Windows fleet, the slow part often isn’t the query itself—it’s the repeated connection handshakes. By opening CimSessions once and reusing them across multiple queries, you can dramatically speed up cross-machine data collection, reduce network chatter, and make your inventory runs more predictable. In this post, you’ll learn how to bulk-create CimSessions, query multiple WMI/CIM classes efficiently, project only the fields you need, compute summaries client-side, and clean up safely every time.

Why Reusable CimSessions Beat One-Off Calls

The handshake tax

Each remote call to a Windows machine over WS-Man/WinRM includes authentication and session negotiation. If you query three classes across 100 servers using one-off calls, you’re paying that handshake tax 300 times. Reusing CimSessions replaces many of those handshakes with a single, durable session per computer.

Predictability and resilience

  • Predictable run time: You connect once per node and run as many queries as you need.
  • Batch error handling: If a session fails to open, you catch and log it up front, then proceed with the rest.
  • Fewer moving parts: One pool of sessions, multiple queries, single cleanup.

When to use CimSessions

  • Recurring fleet inventory and health checks
  • On-demand diagnostics across multiple servers
  • Pre-deployment validation in CI/CD pipelines

Implementation: Bulk Sessions, Minimal Fields, Clean Shutdown

End-to-end example

The following script demonstrates the pattern: create sessions in bulk, run multiple class queries, project only the fields you need, join results client-side, and dispose sessions in finally so nothing leaks.

$computers = @('srv01','srv02','srv03')
$sessions = New-CimSession -ComputerName $computers -ErrorAction Stop
try {
  $os = Get-CimInstance -ClassName Win32_OperatingSystem -CimSession $sessions -ErrorAction Stop |
    Select-Object PSComputerName, Caption,
      @{N='UptimeDays';E={[int]((Get-Date) - $_.LastBootUpTime).TotalDays}}

  $c = Get-CimInstance -ClassName Win32_LogicalDisk -CimSession $sessions -Filter "DeviceID='C:'" -ErrorAction Stop |
    Select-Object PSComputerName, @{N='FreeGB';E={[math]::Round($_.FreeSpace/1GB,2)}}

  $by = $c | Group-Object PSComputerName -AsHashTable -AsString
  $os | ForEach-Object {
    [pscustomobject]@{
      Computer   = $_.PSComputerName; OS = $_.Caption; UptimeDays = $_.UptimeDays;
      FreeGB     = if ($by.ContainsKey($_.PSComputerName)) { $by[$_.PSComputerName].FreeGB } else { $null }
    }
  } | Sort-Object Computer | Format-Table -AutoSize
} catch {
  Write-Warning ('Failed: {0}' -f $_.Exception.Message)
} finally {
  $sessions | Remove-CimSession -ErrorAction SilentlyContinue
}

Highlights:

  • Bulk session creation: New-CimSession -ComputerName $computers opens sessions to all targets in one shot.
  • Multiple queries per session: Collect OS and disk data through the same session pool.
  • Client-side projection: Use Select-Object with calculated properties for only the fields you need.
  • Local join: Materialize a hashtable index with Group-Object -AsHashTable for quick lookups by computer.
  • Guaranteed cleanup: finally closes every session even if an error occurs.

Project only what you need

The fastest byte is the one you never transfer. Project at the source when possible, and compute summaries client-side:

  • Minimize payload: Select just a few fields (e.g., Caption, LastBootUpTime).
  • Compute locally: Convert boot time to uptime days on the client, and derive FreeGB and percentages locally to avoid repeated remote queries.
  • Consistent shape: Compose results as [pscustomobject] with predictable property names.
$os = Get-CimInstance Win32_OperatingSystem -CimSession $sessions |
  Select-Object PSComputerName, Caption,
    @{N='UptimeDays';E={[int]((Get-Date) - $_.LastBootUpTime).TotalDays}}

$disk = Get-CimInstance Win32_LogicalDisk -CimSession $sessions -Filter "DeviceID='C:'" |
  Select-Object PSComputerName,
    @{N='FreeGB';E={[math]::Round($_.FreeSpace/1GB,2)}},
    @{N='SizeGB';E={[math]::Round($_.Size/1GB,2)}},
    @{N='FreePct';E={[math]::Round(100*($_.FreeSpace/$_.Size),1)}}

Error handling and safe disposal

  • Wrap the session lifecycle in try/catch/finally.
  • Use -ErrorAction Stop so you can trap and respond to failures consistently.
  • Always remove sessions in finally to avoid resource leaks and open handles.
$sessions = New-CimSession -ComputerName $computers -ErrorAction Stop
try {
  # queries
}
catch {
  Write-Warning ('Failed: {0}' -f $_.Exception.Message)
}
finally {
  $sessions | Remove-CimSession -ErrorAction SilentlyContinue
}

Operational Tips, Patterns, and Hardening

Wrap it as a reusable function

Package the pattern as a function that returns objects. Downstream tooling can format, export, or alert as needed.

function Get-FleetInventory {
  [CmdletBinding()]
  param(
    [Parameter(Mandatory)][string[]]$ComputerName,
    [System.Management.Automation.PSCredential]$Credential,
    [int]$OperationTimeoutSec = 60,
    [switch]$UseSsl
  )

  $opt = New-CimSessionOption -Protocol WsMan
  $params = @{ ComputerName = $ComputerName; SessionOption = $opt; OperationTimeoutSec = $OperationTimeoutSec; ErrorAction = 'Stop' }
  if ($Credential) { $params.Credential = $Credential }
  $params.UseSsl = [bool]$UseSsl

  $sess = New-CimSession @params
  try {
    $os = Get-CimInstance -ClassName Win32_OperatingSystem -CimSession $sess -ErrorAction Stop |
      Select-Object PSComputerName, Caption,
        @{N='UptimeDays';E={[int]((Get-Date) - $_.LastBootUpTime).TotalDays}}

    $disk = Get-CimInstance -ClassName Win32_LogicalDisk -CimSession $sess -Filter "DeviceID='C:'" -ErrorAction Stop |
      Select-Object PSComputerName,
        @{N='FreeGB';E={[math]::Round($_.FreeSpace/1GB,2)}},
        @{N='SizeGB';E={[math]::Round($_.Size/1GB,2)}}

    $byDisk = $disk | Group-Object PSComputerName -AsHashTable -AsString
    foreach ($row in $os) {
      [pscustomobject]@{
        Computer   = $row.PSComputerName
        OS         = $row.Caption
        UptimeDays = $row.UptimeDays
        FreeGB     = if ($byDisk.ContainsKey($row.PSComputerName)) { $byDisk[$row.PSComputerName].FreeGB } else { $null }
        SizeGB     = if ($byDisk.ContainsKey($row.PSComputerName)) { $byDisk[$row.PSComputerName].SizeGB } else { $null }
      }
    }
  }
  finally {
    $sess | Remove-CimSession -ErrorAction SilentlyContinue
  }
}

# Usage
$cred = Get-Credential # if needed
$report = Get-FleetInventory -ComputerName (Get-Content .\servers.txt) -Credential $cred -UseSsl
$report | Sort-Object Computer | Export-Csv .\inventory.csv -NoTypeInformation

Notes:

  • Return objects, not formatted tables: Avoid Format-Table inside the function; format at the edge (console) or export to CSV/JSON.
  • Credentials: Prefer Kerberos in-domain (no -Credential needed). Use -Credential only when required.

Scale and reliability

  • Batching: For very large fleets, process in batches (e.g., 100-200 nodes per batch) to avoid transient port/resource pressure.
  • Timeouts: Tune -OperationTimeoutSec for slower links. Consider a pre-check with Test-WSMan to filter unreachable nodes.
  • Retries: Wrap New-CimSession in simple retry logic for transient network hiccups.
$all = Get-Content .\servers.txt
$batchSize = 150
for ($i=0; $i -lt $all.Count; $i+=$batchSize) {
  $slice = $all[$i..([math]::Min($i+$batchSize-1,$all.Count-1))]
  try {
    # invoke your Get-FleetInventory for $slice
  } catch {
    Write-Warning "Batch $i failed: $($_.Exception.Message)"
  }
}

Security best practices

  • Use Kerberos with constrained delegation where possible: Domain-joined environments benefit from seamless, secure authentication.
  • Prefer HTTPS listeners: Use -UseSsl to encrypt transport end-to-end across untrusted networks. Validate certificates; avoid broad TrustedHosts.
  • Least privilege: Inventory typically needs read-only rights. Limit administrative privileges to what the queries require.
  • Network scoping: Restrict WinRM to management subnets and enforce firewall rules.

Make the data actionable

  • Export for reporting: Export-Csv or ConvertTo-Json for dashboards (Power BI, Grafana via sidecar ingestion, or custom portals).
  • Alerting thresholds: Add client-side rules like FreeGB < 10 to flag low-disk servers.
  • Historical trends: Append runs to a time-series store (e.g., SQL, InfluxDB) to track drift and mean-time-to-failure indicators.
$now = Get-Date -Format o
$report | ForEach-Object { $_ | Add-Member -NotePropertyName CollectedAt -NotePropertyValue $now -PassThru } |
  Export-Csv .\inventory-history.csv -Append -NoTypeInformation

Troubleshooting quick wins

  • Verify WinRM/CIM availability: Test-WSMan -ComputerName srv01
  • Check firewall: Ensure TCP 5985 (HTTP) or 5986 (HTTPS) is open as required.
  • Class availability: Verify target class exists: Get-CimClass -ClassName Win32_OperatingSystem -CimSession $session
  • Latency clues: Enable verbose output to see where time is spent.

By reusing CimSessions, selecting only the fields you need, and computing summaries on the client, you get faster inventory, fewer handshakes, predictable queries, and safer cleanup. This pattern scales cleanly from a handful of servers to large fleets and fits naturally into CI/CD and scheduled automation.

Further reading: Microsoft Docs: CIM Cmdlets

← All Posts Home →