Private/IoDeviceCompatibility.ps1

# Copyright (c) 2026 Broadcom. All Rights Reserved.
# Broadcom Confidential. The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
#
# =============================================================================
#
# SOFTWARE LICENSE AGREEMENT
#
# Copyright (c) CA, Inc. All rights reserved.
#
# You are hereby granted a non-exclusive, worldwide, royalty-free license
# under CA, Inc.'s copyrights to use, copy, modify, and distribute this
# software in source code or binary form for use in connection with CA, Inc.
# products.
#
# This copyright notice shall be included in all copies or substantial
# portions of the software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#
# =============================================================================
#region IoDeviceCompatibility

function Get-VcfCheckVMHostCpuInfo {

    <#
        .SYNOPSIS
        Returns CPU series information and specifications for an ESX host.

        .OUTPUTS
        [PSCustomObject] with CpuSeries, TotalCores, TotalThreads, Sockets, MhzPerCpu.
    #>


    [CmdletBinding()]
    [OutputType([PSObject])]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$VMHost
    )

    $cpuInfo = $VMHost.ExtensionData.Summary.Hardware.CpuModel
    $numCpuCores = $VMHost.ExtensionData.Summary.Hardware.NumCpuCores
    $numCpuThreads = $VMHost.ExtensionData.Summary.Hardware.NumCpuThreads
    $numCpuPkgs = $VMHost.ExtensionData.Summary.Hardware.NumCpuPkgs
    $cpuHz = $VMHost.ExtensionData.Summary.Hardware.CpuMhz

    return [PSCustomObject]@{
        CpuSeries    = $cpuInfo
        TotalCores   = $numCpuCores
        TotalThreads = $numCpuThreads
        Sockets      = $numCpuPkgs
        MhzPerCpu    = $cpuHz
    }
}
function ConvertTo-VcfCheckPciHexId {

    <#
        .SYNOPSIS
        Normalizes a PCI VendorId/DeviceId/SubVendorId/SubDeviceId to a lowercase 4-hex-digit
        string, matching the id format used by the packaged vSAN HCL asset.

        .DESCRIPTION
        vSphere reports these ids as signed 16-bit values, so any id with the high bit set (>=
        0x8000, common for storage controller and NIC subsystem ids) arrives as a negative
        [Int]. Masking with 0xFFFF before formatting discards the sign-extended high bits so the
        result is always the correct 4-hex-digit code instead of an 8-digit two's-complement
        string that can never match a shipped HCL entry.

        .OUTPUTS
        [String] lowercase 4-hex-digit id, or '' if Value is $null or not convertible.
    #>


    [CmdletBinding()]
    [OutputType([String])]
    Param (
        [Parameter(Mandatory = $false)] [Object]$Value
    )

    if ($null -eq $Value) { return '' }
    try {
        return ('{0:x4}' -f ([Int]$Value -band 0xFFFF))
    } catch {
        return ''
    }
}
function Get-VcfCheckHbaPciIdentity {

    <#
        .SYNOPSIS
        Resolves an HBA's vendor/model strings and normalized PCI VendorId/DeviceId/SubVendorId/
        SubDeviceId from the host's PCI device list.

        .OUTPUTS
        [PSCustomObject] with Vendor, Model, VendorId, DeviceId, SubVendorId, SubDeviceId - all ''
        if the HBA has no matching PCI device.
    #>


    [CmdletBinding()]
    [OutputType([PSObject])]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$Hba,
        [Parameter(Mandatory = $false)] [PSObject[]]$PciDevices = @()
    )

    $identity = [PSCustomObject]@{ Vendor = ''; Model = ''; VendorId = ''; DeviceId = ''; SubVendorId = ''; SubDeviceId = '' }
    if (-not $Hba.ExtensionData.Pci -or -not $PciDevices) { return $identity }

    $pciDevice = $PciDevices | Where-Object { $_.Id -eq $Hba.ExtensionData.Pci } | Select-Object -First 1
    if (-not $pciDevice) { return $identity }

    $identity.Vendor = $pciDevice.VendorName
    $identity.Model = $pciDevice.DeviceName
    $identity.VendorId = ConvertTo-VcfCheckPciHexId -Value $pciDevice.VendorId
    $identity.DeviceId = ConvertTo-VcfCheckPciHexId -Value $pciDevice.DeviceId
    $identity.SubVendorId = ConvertTo-VcfCheckPciHexId -Value $pciDevice.SubVendorId
    $identity.SubDeviceId = ConvertTo-VcfCheckPciHexId -Value $pciDevice.SubDeviceId
    return $identity
}
function Get-VcfCheckHbaDriverVersion {

    <#
        .SYNOPSIS
        Resolves the installed driver version for an HBA via the loaded VMkernel module's esxcli
        details.

        .OUTPUTS
        [String] driver version, or '' if unavailable.
    #>


    [CmdletBinding()]
    [OutputType([String])]
    Param (
        [Parameter(Mandatory = $false)] [PSObject]$EsxCli,
        [Parameter(Mandatory = $true)] [String]$DriverName
    )

    if (-not $EsxCli -or -not $DriverName) { return '' }

    try {
        $moduleDetails = Invoke-VcfCheckWithTimeout -TimeoutSeconds 30 -ArgumentList $EsxCli, $DriverName -ScriptBlock {
            param($InnerEsxCli, $InnerDriverName)
            $InnerEsxCli.system.module.get.Invoke(@{module = $InnerDriverName })
        }
        if ($moduleDetails -and $moduleDetails.Version) { return $moduleDetails.Version }
        return ''
    } catch {
        return ''
    }
}
function Get-VcfCheckVMHostNetworkDevices {

    <#
        .SYNOPSIS
        Returns physical network adapters (vmnics) for an ESX host, including driver and firmware details.

        .OUTPUTS
        [PSCustomObject[]] with Name, Vendor, Model, VendorId, DeviceId, SubVendorId, SubDeviceId,
        Driver, DriverVersion, FirmwareVersion, DeviceType.
    #>


    [CmdletBinding()]
    [OutputType([PSObject[]])]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$VMHost
    )

    $devices = @()
    $nics = @($VMHost | Get-VMHostNetworkAdapter -Physical -ErrorAction SilentlyContinue)
    $pciDevices = @($VMHost | Get-VMHostPciDevice -ErrorAction SilentlyContinue)
    $esxcli = $null

    try {
        $esxcli = Get-EsxCli -VMHost $VMHost -V2 -ErrorAction SilentlyContinue
    } catch {
        $esxcli = $null
    }

    foreach ($nic in $nics) {
        if ($nic) {
            $vendor = ''
            $model = ''
            $vendorId = ''
            $deviceId = ''
            $subVendorId = ''
            $subDeviceId = ''
            $driver = ''
            $driverVersion = ''
            $firmwareVersion = ''

            if ($nic.ExtensionData.Pci -and $pciDevices) {
                $pciId = $nic.ExtensionData.Pci
                $pciDevice = $pciDevices | Where-Object { $_.Id -eq $pciId } | Select-Object -First 1
                if ($pciDevice) {
                    $vendor = $pciDevice.VendorName
                    $model = $pciDevice.DeviceName
                    $vendorId = ConvertTo-VcfCheckPciHexId -Value $pciDevice.VendorId
                    $deviceId = ConvertTo-VcfCheckPciHexId -Value $pciDevice.DeviceId
                    $subVendorId = ConvertTo-VcfCheckPciHexId -Value $pciDevice.SubVendorId
                    $subDeviceId = ConvertTo-VcfCheckPciHexId -Value $pciDevice.SubDeviceId
                }
            }

            if ($esxcli) {
                try {
                    $nicDetails = Invoke-VcfCheckWithTimeout -TimeoutSeconds 30 -ArgumentList $esxcli, $nic.Name -ScriptBlock {
                        param($EsxCli, $NicName)
                        $EsxCli.network.nic.get.Invoke(@{nicname = $NicName })
                    }
                    if ($nicDetails) {
                        $driver = if ($nicDetails.Driver) { $nicDetails.Driver } else { '' }
                        $driverVersion = if ($nicDetails.DriverVersion) { $nicDetails.DriverVersion } else { '' }
                        $firmwareVersion = if ($nicDetails.FirmwareVersion) { $nicDetails.FirmwareVersion } else { '' }
                    }
                } catch {
                    $driver = ''
                    $driverVersion = ''
                    $firmwareVersion = ''
                }
            }

            $devices += [PSCustomObject]@{
                Name            = $nic.Name
                Vendor          = $vendor
                Model           = $model
                VendorId        = $vendorId
                DeviceId        = $deviceId
                SubVendorId     = $subVendorId
                SubDeviceId     = $subDeviceId
                Driver          = $driver
                DriverVersion   = $driverVersion
                FirmwareVersion = $firmwareVersion
                DeviceType      = 'Network'
            }
        }
    }

    return $devices
}
function Get-VcfCheckVMHostStorageAdapters {

    <#
        .SYNOPSIS
        Returns storage Host Bus Adapters (SAS, Fibre Channel, SATA, NVMe) for an ESX host with human-readable type descriptions.

        .OUTPUTS
        [PSCustomObject[]] with Name, Vendor, Model, VendorId, DeviceId, SubVendorId, SubDeviceId,
        Type, Driver, DriverVersion, Status, DeviceType.
    #>


    [CmdletBinding()]
    [OutputType([PSObject[]])]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$VMHost
    )

    $devices = @()
    $hbas = @($VMHost | Get-VMHostHba -ErrorAction SilentlyContinue)
    $pciDevices = @($VMHost | Get-VMHostPciDevice -ErrorAction SilentlyContinue)
    $esxcli = $null

    try {
        $esxcli = Get-EsxCli -VMHost $VMHost -V2 -ErrorAction SilentlyContinue
    } catch {
        $esxcli = $null
    }

    foreach ($hba in $hbas) {
        if ($hba) {
            $pciIdentity = Get-VcfCheckHbaPciIdentity -Hba $hba -PciDevices $pciDevices
            $vendor = $pciIdentity.Vendor
            $model = $pciIdentity.Model
            $vendorId = $pciIdentity.VendorId
            $deviceId = $pciIdentity.DeviceId
            $subVendorId = $pciIdentity.SubVendorId
            $subDeviceId = $pciIdentity.SubDeviceId
            $driverVersion = Get-VcfCheckHbaDriverVersion -EsxCli $esxcli -DriverName $hba.Driver

            if (-not $vendor) {
                $vendor = $hba.Vendor
            }
            if (-not $model) {
                $model = if ($hba.Model -and $hba.Model -ne 'N/A') { $hba.Model } else { $hba.Name }
            }

            $rawType = switch ($hba.Type) {
                0 { 'Block' }
                1 { 'FibreChannel' }
                2 { 'iSCSI' }
                3 { 'ParallelScsi' }
                'FibreChannel' { 'FibreChannel' }
                'iSCSI' { 'iSCSI' }
                'SAS' { 'SAS' }
                'SATA' { 'SATA' }
                'ParallelScsi' { 'ParallelScsi' }
                'SoftwareNVMe' { 'SoftwareNVMe' }
                default { $hba.Type }
            }

            $humanType = switch -Wildcard ($rawType) {
                'FibreChannel' { 'Fibre Channel Host Bus Adapter (HBA)' }
                'iSCSI' {
                    if ($hba.IsSoftware) { 'Software iSCSI Initiator' } else { 'Hardware iSCSI Adapter' }
                }
                'Block' {
                    if ($model -like '*NVMe*') {
                        'NVMe Controller'
                    } elseif ($model -like '*RAID*' -or $model -like '*PERC*' -or $model -like '*Smart Array*') {
                        'Hardware RAID Controller'
                    } else {
                        'SAS / SATA Host Bus Adapter'
                    }
                }
                'SAS' { 'SAS Host Bus Adapter' }
                'SATA' { 'SATA Controller' }
                'ParallelScsi' { 'Parallel SCSI Controller' }
                'SoftwareNVMe' { 'Software NVMe over Fabrics (NVMe-oF)' }
                default {
                    if ($model -like '*NVMe*') { 'NVMe Controller' } else { $rawType }
                }
            }

            $devices += [PSCustomObject]@{
                Name          = $hba.Name
                Vendor        = $vendor
                Model         = $model
                VendorId      = $vendorId
                DeviceId      = $deviceId
                SubVendorId   = $subVendorId
                SubDeviceId   = $subDeviceId
                Type          = $humanType
                Driver        = $hba.Driver
                DriverVersion = $driverVersion
                Status        = $hba.Status
                DeviceType    = $humanType
            }
        }
    }

    return $devices
}
function Get-VcfCheckVMHostScsiDevices {

    <#
        .SYNOPSIS
        Returns SCSI logical units (storage devices) for an ESX host with protocol and media type information.

        .OUTPUTS
        [PSCustomObject[]] with Name, Vendor, Model, Type, Capacity, Status, DeviceType, Revision,
        HbaName (the owning vmhbaN parsed from RuntimeName - for an NVMe drive this is the drive's
        own PCIe endpoint and can be joined back to Get-VcfCheckVMHostStorageAdapters for its PCI
        identity; for a SAS/SATA drive it is a shared HBA and carries no drive-specific identity).
    #>


    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    Param (
        [Parameter(Mandatory = $true)]
        [PSObject]$VMHost
    )

    $luns = Get-ScsiLun -VMHost $VMHost -LunType disk -ErrorAction SilentlyContinue

    # Assign directly to array to avoid expensive array rebuilding (+=$)
    $devices = foreach ($lun in $luns) {
        if (-not $lun) { continue }

        $media = if ($lun.IsSsd) { 'SSD' } else { 'HDD' }

        # Determine protocol using switch statement
        $protocol = switch ($lun) {
            { $_.CanonicalName -like 'nvme.*' -or $_.CanonicalName -like 'eui.*' -or $_.Model -like '*NVMe*' } { 'NVMe'; break }
            { $_.CanonicalName -like 't10.ATA*' -or $_.Model -like '*SATA*' } { 'SATA'; break }
            { $_.CanonicalName -like 'naa.5*' } { 'SAS'; break }
            default { 'SCSI' }
        }

        # Trim vendor to handle fixed-width trailing spaces typical in vSphere SCSI inquiry data
        $trimmedVendor = "$($lun.Vendor)".Trim()

        # Check Vendor and override generic bus protocol names with 'Unknown'
        $vendor = switch -Exact ($trimmedVendor) {
            'ATA'     { 'Unknown' }
            'NVMe'    { 'Unknown' }
            'SAS'     { 'Unknown' }
            'SATA'    { 'Unknown' }
            'SCSI'    { 'Unknown' }
            default   { $trimmedVendor }
        }

        [PSCustomObject]@{
            Name       = $lun.CanonicalName
            Vendor     = $vendor
            Model      = $lun.Model
            Type       = "$protocol $media"
            Capacity   = [Math]::Round($lun.CapacityGB, 2)
            Status     = $lun.RuntimeStatus
            DeviceType = 'SCSI'
            Revision   = "$($lun.ExtensionData.Revision)".Trim()
            HbaName    = "$($lun.RuntimeName)".Split(':')[0]
        }
    }

    return $devices
}
function Get-VcfCheckVMHostVCenterFqdn {

    <#
        .SYNOPSIS
        Extracts the connected vCenter FQDN from a VMHost object's Uid.

        .OUTPUTS
        [String] the vCenter FQDN, or an empty string if it cannot be determined.
    #>


    [CmdletBinding()]
    [OutputType([String])]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$VMHost
    )

    if ($VMHost.Uid -match '@([^@/]+):\d+/') {
        return $Matches[1]
    }
    return ''
}
function Get-VcfCheckCachedHostHardwareDetail {

    <#
        .SYNOPSIS
        Returns network and storage adapter hardware detail for an ESX host, cached per run.

        .DESCRIPTION
        Wraps Get-VcfCheckVMHostNetworkDevices, Get-VcfCheckVMHostStorageAdapters, and
        Get-VcfCheckVMHostScsiDevices behind a per-run cache on
        $Context.EsxHostHardwareDetailCache, keyed by vCenter FQDN + host name, so multiple checks
        that need the same host's device data (e.g. Test-VcfEsxHardwareDetails and
        Test-VcfVsanHclCompliance) don't each pay for the expensive per-host esxcli/SCSI
        collection independently.

        .PARAMETER Context
        The VcfCheck.Context object.

        .PARAMETER VMHost
        A VMHost inventory object retrieved via Get-VcfCheckVMHostInventory.

        .OUTPUTS
        [PSCustomObject] with NetworkAdapters, StorageAdapters, and Drives - the raw arrays
        returned by Get-VcfCheckVMHostNetworkDevices / Get-VcfCheckVMHostStorageAdapters /
        Get-VcfCheckVMHostScsiDevices.
    #>


    [CmdletBinding()]
    [OutputType([PSObject])]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$Context,
        [Parameter(Mandatory = $true)] [PSObject]$VMHost
    )

    $vcenterFqdn = Get-VcfCheckVMHostVCenterFqdn -VMHost $VMHost
    $cacheKey = "$vcenterFqdn|$($VMHost.Name)"

    if ($Context.EsxHostHardwareDetailCache.ContainsKey($cacheKey)) {
        return $Context.EsxHostHardwareDetailCache[$cacheKey]
    }

    $detail = [PSCustomObject]@{
        NetworkAdapters = @(Get-VcfCheckVMHostNetworkDevices -VMHost $VMHost -ErrorAction SilentlyContinue)
        StorageAdapters = @(Get-VcfCheckVMHostStorageAdapters -VMHost $VMHost -ErrorAction SilentlyContinue)
        Drives          = @(Get-VcfCheckVMHostScsiDevices -VMHost $VMHost -ErrorAction SilentlyContinue)
    }
    $Context.EsxHostHardwareDetailCache[$cacheKey] = $detail
    return $detail
}

#endregion