Modules/Public/Get-S2DPhysicalDiskInventory.ps1
|
function Get-S2DPhysicalDiskInventory { <# .SYNOPSIS Inventories all physical disks in the S2D cluster with health, capacity, and wear data. .DESCRIPTION Queries each cluster node for physical disk properties, reliability counters, and storage pool membership. Classifies each disk as Cache or Capacity tier, detects symmetry anomalies across nodes, and surfaces firmware inconsistencies. Requires an active session established with Connect-S2DCluster, or use the -CimSession parameter to target a specific node directly. .PARAMETER NodeName Limit results to one or more specific node names. .PARAMETER CimSession Override the module session and use this CimSession directly. Useful for ad-hoc calls without a full Connect-S2DCluster session. .EXAMPLE # After Connect-S2DCluster Get-S2DPhysicalDiskInventory .EXAMPLE Get-S2DPhysicalDiskInventory | Format-Table NodeName, FriendlyName, Role, Size, HealthStatus, WearPercentage .EXAMPLE Get-S2DPhysicalDiskInventory -NodeName "node01", "node02" .OUTPUTS PSCustomObject[] — one object per physical disk #> [CmdletBinding()] param( [Parameter()] [string[]] $NodeName, [Parameter()] [CimSession] $CimSession ) $session = Resolve-S2DSession -CimSession $CimSession # Determine nodes to query $nodes = if ($NodeName) { $NodeName } elseif ($Script:S2DSession.Nodes) { $Script:S2DSession.Nodes } else { # Fall back: run on the connected node and trust it has visibility of all pool disks $null } $allDisks = @() # Helper: get disks from a single CIM target $getDisksBlock = { param([CimSession]$cs, [string]$targetNode) $cimParams = @{ ErrorAction = 'SilentlyContinue' } if ($cs) { $cimParams['CimSession'] = $cs } if ($targetNode -and -not $cs) { $cimParams['ComputerName'] = $targetNode } $physDisks = Get-PhysicalDisk @cimParams $reliabilityData = @{} try { Get-PhysicalDisk @cimParams | ForEach-Object { $rd = $_ | Get-StorageReliabilityCounter @cimParams -ErrorAction SilentlyContinue if ($rd) { $reliabilityData[$_.UniqueId] = $rd } } } catch { } $physDisks | ForEach-Object { $disk = $_ $rel = $reliabilityData[$disk.UniqueId] [PSCustomObject]@{ NodeName = $targetNode UniqueId = $disk.UniqueId FriendlyName = $disk.FriendlyName SerialNumber = $disk.SerialNumber Model = $disk.Model MediaType = $disk.MediaType BusType = $disk.BusType FirmwareVersion = $disk.FirmwareVersion Manufacturer = $disk.Manufacturer Usage = $disk.Usage CanPool = $disk.CanPool HealthStatus = $disk.HealthStatus OperationalStatus = $disk.OperationalStatus PhysicalLocation = $disk.PhysicalLocation SlotNumber = $disk.SlotNumber Size = $disk.Size # Reliability counters — null-safe Temperature = if ($rel) { $rel.Temperature } else { $null } WearPercentage = if ($rel) { $rel.Wear } else { $null } PowerOnHours = if ($rel) { $rel.PowerOnHours } else { $null } ReadErrors = if ($rel) { $rel.ReadErrorsUncorrected } else { $null } WriteErrors = if ($rel) { $rel.WriteErrorsUncorrected } else { $null } } } } if ($session -and $nodes) { foreach ($node in $nodes) { Write-Verbose " Collecting physical disks from node '$node'..." try { $nodeCim = New-CimSession -ComputerName $node -ErrorAction Stop $disks = & $getDisksBlock $nodeCim $node $allDisks += $disks $nodeCim | Remove-CimSession } catch { Write-Warning "Could not collect disks from node '$node': $_" } } } elseif ($session) { $allDisks = & $getDisksBlock $session $Script:S2DSession.ClusterName } else { # Local mode $allDisks = & $getDisksBlock $null $env:COMPUTERNAME } # Classify each disk as Cache or Capacity based on Usage and MediaType $poolDisks = @{} try { $poolCimParams = @{ ErrorAction = 'SilentlyContinue' } if ($session) { $poolCimParams['CimSession'] = $session } $pool = Get-StoragePool @poolCimParams | Where-Object IsPrimordial -eq $false | Select-Object -First 1 if ($pool) { $pool | Get-PhysicalDisk @poolCimParams | ForEach-Object { $poolDisks[$_.UniqueId] = $true } } } catch { } # Build output objects with computed fields $result = $allDisks | ForEach-Object { $disk = $_ # Role classification: Usage 'Journal' = cache; otherwise pool membership + media type heuristic $role = switch ($disk.Usage) { 'Journal' { 'Cache' } default { if ($poolDisks[$disk.UniqueId]) { # Heuristic: in tiered pool, faster media (NVMe/SSD) = cache if mixed with HDD 'Capacity' # Will be refined by Get-S2DCacheTierInfo in Phase 2 } else { 'Unknown' } } } $cap = if ($disk.Size -gt 0) { [S2DCapacity]::new($disk.Size) } else { $null } [PSCustomObject]@{ NodeName = $disk.NodeName UniqueId = $disk.UniqueId FriendlyName = $disk.FriendlyName SerialNumber = $disk.SerialNumber Model = $disk.Model MediaType = $disk.MediaType BusType = $disk.BusType FirmwareVersion = $disk.FirmwareVersion Manufacturer = $disk.Manufacturer Role = $role Usage = $disk.Usage CanPool = $disk.CanPool HealthStatus = $disk.HealthStatus OperationalStatus = $disk.OperationalStatus PhysicalLocation = $disk.PhysicalLocation SlotNumber = $disk.SlotNumber Size = $cap SizeBytes = $disk.Size Temperature = $disk.Temperature WearPercentage = $disk.WearPercentage PowerOnHours = $disk.PowerOnHours ReadErrors = $disk.ReadErrors WriteErrors = $disk.WriteErrors } } # Symmetry check — warn if node disk counts differ if ($result) { $byNode = $result | Group-Object NodeName if ($byNode.Count -gt 1) { $counts = $byNode | Select-Object Name, Count $unique = $counts.Count | Select-Object -Unique if (@($unique).Count -gt 1) { Write-Warning "Disk symmetry anomaly detected: $(($counts | ForEach-Object { "$($_.Name)=$($_.Count)" }) -join ', ')" } } } # Cache collected data for report generation $Script:S2DSession.CollectedData['PhysicalDisks'] = $result $result } |