mqsft.diskconfigvalidation.psm1

# ==============================================================================
# mqsft.diskconfigvalidation.psm1 - AUTO-GENERATED by Create_Module_0_0_7.ps1 v0.0.7
# ------------------------------------------------------------------------------
# Module Version : 1.0.1
# Script Version : 0.0.7
# Built : 2026-02-24 21:48:17
# Source : C:\Modules\functions\mqsft.diskconfigvalidation
#
# DO NOT EDIT THIS FILE DIRECTLY.
# Edit source files in C:\Modules\functions\mqsft.diskconfigvalidation and re-run Create_Module_0_0_7.ps1.
# ==============================================================================

Set-StrictMode -Version Latest

# --- Source: Show-DiskReport.ps1 -----------------------------------------
function Show-DiskReport {

    $fmt     = "{0,-4} {1,-15} {2,-16} {3,-7} {4,-8} {5,-10} {6,-9} {7,-9} {8,-4} {9,-12} {10,-16} {11,-7} {12}"
    $divider = "-" * 135

    $physDisks  = Get-PhysicalDisk
    $physLookup = @{}
    foreach ($pd in $physDisks) { $physLookup[$pd.SerialNumber.Trim()] = $pd }

    $allDisks       = Get-Disk
    $nonSystemDisks = $allDisks | Where-Object { -not $_.IsSystem -and -not $_.IsBoot -and $_.BusType -ne "USB" -and $_.FriendlyName -notmatch "LOGICAL VOLUME" }
    $sizeGroups     = $nonSystemDisks | Group-Object { [math]::Round($_.Size / 1GB, 0) } | Sort-Object Count -Descending
    $dominantSizeGB = [int]$sizeGroups[0].Name
    $dominantName   = ($nonSystemDisks | Group-Object FriendlyName | Sort-Object Count -Descending | Select-Object -First 1).Name

    Write-Host ""
    Write-Host " DISK INVENTORY — $env:COMPUTERNAME" -ForegroundColor Cyan
    Write-Host " Dominant pool disk : $dominantName ($dominantSizeGB GB)" -ForegroundColor DarkGray
    Write-Host " NOTE: Offline disks are normal on a cluster node — not scored as spare signal" -ForegroundColor DarkGray
    Write-Host $divider -ForegroundColor DarkGray
    Write-Host ($fmt -f "No.","Friendly Name","Serial","Health","Status","Size GB","Size TB","Partition","Bus","PD Usage","S2D Status","CanPool","Role") -ForegroundColor Cyan
    Write-Host $divider -ForegroundColor DarkGray

    $allDisks | Sort-Object { [int]$_.Number } | ForEach-Object {
        $disk   = $_
        $sizeGB = [math]::Round($disk.Size / 1GB, 1)
        $sizeTB = [math]::Round($disk.Size / 1TB, 3)
        $serial = $disk.SerialNumber.Trim()
        $pd     = $physLookup[$serial]

        # Pre-compute everything - no inline if inside -f
        $pdUsage  = if ($pd) { $pd.Usage }        else { "Unknown" }
        $canPool  = if ($pd) { "$($pd.CanPool)" } else { "Unknown" }
        $pdHealth = if ($pd) { $pd.HealthStatus } else { $disk.HealthStatus }

        # Spare scoring - Offline removed as signal (normal on cluster nodes)
        $sizeOutlier   = ([math]::Round($sizeGB, 0)) -ne $dominantSizeGB
        $isHotSpare    = $pdUsage -eq "Hot Spare"
        $cantPool      = ($pd -and $pd.CanPool -eq $false)
        $modelMismatch = ($disk.FriendlyName -ne $dominantName) -and -not ($disk.IsSystem -or $disk.IsBoot) -and ($disk.BusType -ne "USB")

        $score  = 0
        $score += if ($sizeOutlier)   { 3 } else { 0 }   # strong - different size to pool
        $score += if ($isHotSpare)    { 3 } else { 0 }   # strong - explicitly flagged
        $score += if ($cantPool)      { 2 } else { 0 }   # medium - storage subsystem says no
        $score += if ($modelMismatch) { 1 } else { 0 }   # weak - different model

        $isSpare = $score -ge 3

        $s2d = switch ($true) {
            { $disk.IsSystem -or $disk.IsBoot }            { "N/A OS Disk";  break }
            { $disk.BusType -eq "USB" }                    { "N/A USB";      break }
            { $disk.FriendlyName -match "LOGICAL VOLUME" } { "N/A RAID";     break }
            { $isSpare }                                    { "⚠ Spare?";     break }
            { $disk.PartitionStyle -eq "RAW" }             { "✔ Ready";      break }
            { $disk.PartitionStyle -eq "GPT" }             { "⚠ Need Reset"; break }
            { $disk.PartitionStyle -eq "MBR" }             { "✗ MBR";        break }
            default                                         { "Unknown" }
        }

        $role = switch ($true) {
            { $disk.IsSystem -or $disk.IsBoot }            { "⚠ OS/BOOT";         break }
            { $disk.BusType -eq "USB" }                    { "⚠ USB";             break }
            { $disk.FriendlyName -match "LOGICAL VOLUME" } { "⚠ RAID VOL";        break }
            { $isSpare }                                    { "⚠ SPARE [$score/8]"; break }
            { $disk.PartitionStyle -eq "RAW" }             { "✔ S2D Ready";       break }
            default                                         { "S2D Candidate" }
        }

        $color = switch ($true) {
            { $disk.IsSystem -or $disk.IsBoot }            { "Red";        break }
            { $disk.BusType -eq "USB" }                    { "Magenta";    break }
            { $disk.FriendlyName -match "LOGICAL VOLUME" } { "Yellow";     break }
            { $isSpare }                                    { "DarkYellow"; break }
            { $disk.PartitionStyle -eq "RAW" }             { "Green";      break }
            { $disk.PartitionStyle -eq "GPT" }             { "Yellow";     break }
            default                                         { "White" }
        }

        Write-Host ($fmt -f
            ([int]$disk.Number),
            ($disk.FriendlyName -replace "HPE ",""),
            $serial,
            $pdHealth,
            $disk.OperationalStatus,
            "$sizeGB GB",
            "$sizeTB TB",
            $disk.PartitionStyle,
            $disk.BusType,
            $pdUsage,
            $s2d,
            $canPool,
            $role
        ) -ForegroundColor $color
    }

    Write-Host $divider -ForegroundColor DarkGray

    # Legend
    Write-Host ""
    Write-Host " LEGEND" -ForegroundColor Cyan
    Write-Host " Red = OS / Boot - do not touch"        -ForegroundColor Red
    Write-Host " Yellow = GPT / RAID - needs attention"    -ForegroundColor Yellow
    Write-Host " DarkYellow = Likely hot spare [score/8]"      -ForegroundColor DarkYellow
    Write-Host " Green = RAW - ready for S2D"             -ForegroundColor Green
    Write-Host " Magenta = USB - exclude"                   -ForegroundColor Magenta
    Write-Host ""
    Write-Host " Score signals: Size mismatch=3 Usage=HotSpare=3 CanPool=False=2 Model mismatch=1" -ForegroundColor DarkGray
    Write-Host " (Offline not scored - normal behaviour on cluster nodes)" -ForegroundColor DarkGray
    Write-Host ""

    # Summary - pre-compute spare details to avoid inline if errors
    $candidates = $allDisks | Where-Object {
        $_.PartitionStyle -eq "RAW" -and $_.BusType -ne "USB" -and
        -not $_.IsSystem -and -not $_.IsBoot
    }
    $needsReset = $allDisks | Where-Object {
        $_.PartitionStyle -eq "GPT" -and $_.BusType -ne "USB" -and
        -not $_.IsSystem -and -not $_.IsBoot -and
        $_.FriendlyName -notmatch "LOGICAL VOLUME"
    }
    $spares = $allDisks | Where-Object {
        $s          = $_.SerialNumber.Trim()
        $pd         = $physLookup[$s]
        $szMismatch = ([math]::Round([math]::Round($_.Size/1GB,1),0)) -ne $dominantSizeGB
        $hotSpare   = if ($pd) { $pd.Usage -eq "Hot Spare" } else { $false }
        $noPool     = if ($pd) { $pd.CanPool -eq $false }    else { $false }
        $modelOdd   = $_.FriendlyName -ne $dominantName
        $sc         = ($(if ($szMismatch) {3} else {0})) + ($(if ($hotSpare) {3} else {0})) + ($(if ($noPool) {2} else {0})) + ($(if ($modelOdd) {1} else {0}))
        $sc -ge 3 -and -not $_.IsSystem -and -not $_.IsBoot -and $_.BusType -ne "USB" -and $_.FriendlyName -notmatch "LOGICAL VOLUME"
    }
    $excluded = $allDisks | Where-Object {
        $_.IsSystem -or $_.IsBoot -or $_.BusType -eq "USB" -or $_.FriendlyName -match "LOGICAL VOLUME"
    }

    Write-Host " S2D SUMMARY" -ForegroundColor Cyan
    Write-Host " ✔ Ready for S2D : " -NoNewline; Write-Host $candidates.Count -ForegroundColor Green
    Write-Host " ⚠ Need Reset-Disk : " -NoNewline; Write-Host $needsReset.Count -ForegroundColor Yellow
    Write-Host " ⚠ Likely Hot Spares : " -NoNewline; Write-Host $spares.Count     -ForegroundColor DarkYellow

    foreach ($sp in $spares) {
        $s       = $sp.SerialNumber.Trim()
        $pd      = $physLookup[$s]
        $usage   = if ($pd) { $pd.Usage }        else { "Unknown" }
        $cp      = if ($pd) { "$($pd.CanPool)" } else { "?" }
        $szMatch = ([math]::Round([math]::Round($sp.Size/1GB,1),0)) -ne $dominantSizeGB
        $model   = $sp.FriendlyName -replace "HPE ",""
        Write-Host (" Disk {0,-3} {1,-16} {2,-16} Usage:{3,-12} CanPool:{4,-5} SizeMismatch:{5} Model:{6}" -f
            [int]$sp.Number, $model, $s, $usage, $cp, $szMatch, $model
        ) -ForegroundColor DarkYellow
    }

    Write-Host " ✗ Excluded : " -NoNewline; Write-Host $excluded.Count -ForegroundColor DarkGray
    foreach ($ex in $excluded) {
        $reason = if ($ex.IsSystem -or $ex.IsBoot)              { "OS/Boot" }
                  elseif ($ex.BusType -eq "USB")                { "USB" }
                  elseif ($ex.FriendlyName -match "LOGICAL VOLUME") { "RAID Volume" }
                  else                                           { "Other" }
        $name   = $ex.FriendlyName -replace "HPE ",""
        Write-Host (" Disk {0,-3} {1,-16} ({2})" -f [int]$ex.Number, $name, $reason) -ForegroundColor DarkGray
    }
    Write-Host ""
}

Show-DiskReport