modules/AzStack.Storage/AzStack.Storage.psm1

<################################################################
# #
# Copyright (C) Microsoft Corporation. All rights reserved. #
# #
################################################################>


Import-LocalizedData -BindingVariable 'msg' -BaseDirectory "$PSScriptRoot\locale" -UICulture "en-US" -WarningAction SilentlyContinue
Import-Module $PSScriptRoot\..\AzStack.Utilities\AzStack.Utilities.psm1 -WarningAction SilentlyContinue
Import-Module $PSScriptRoot\..\AzStack.Common\AzStack.Common.psm1 -WarningAction SilentlyContinue

function Get-AzsSupportDiskSpaceReport {
    <#
        .SYNOPSIS
            Get available disk space report for all infra Hosts.
 
        .DESCRIPTION
            Utilizes Get-AzsSupportDiskSpace command to get available disk space information for all infra hosts, return with a report for better summary view.
 
        .PARAMETER Cluster
            The Cluster you want to run against.
 
        .PARAMETER DriveLetters
            The drive letters you want to get data from.
 
        .EXAMPLE
            PS> Get-AzsSupportDiskSpaceReport -DriveLetter C -Cluster contoso-cl | sort-object ComputerName
 
        .OUTPUTS
            Array of PSObject representing the disk space report
 
            infraHostDiskResults
            --------------------
            @{ComputerName=contoso-n02; Name=C; Used=104021139456; Free=490376728576; Provider=Microsoft.PowerShell.Core\FileSystem; Root=C:\}
            @{ComputerName=CONTOSO-N01; Name=C; Used=101640761344; Free=492757106688; Provider=Microsoft.PowerShell.Core\FileSystem; Root=C:\}
    #>


    [CmdletBinding()]
    param (
        [parameter(mandatory = $true, HelpMessage = "Please provide the name of Cluster you would like to check")]
        [string]$Cluster,
        [parameter(mandatory = $true, HelpMessage = "Please provide Drive Letters you would like to check")]
        [char]$DriveLetter
    )

    try {
        #Trace-AzsSupportCommand -Event OnEntry
        Trace-Output -Level:Information -Message ($msg.StorageCheckingDiskSpacePhysicalNodes)
        $infraHosts = Get-AzsSupportInfrastructureHost -Cluster $Cluster -State:Up
        $results = foreach ($infraHost in $infraHosts) {

            New-Object -TypeName PSCustomObject -Property @{
                infraHostDiskResults = Get-AzsSupportDiskSpace -ComputerName $infraHost -DriveLetter $DriveLetter | Select-Object -Property ComputerName, Name, Used, Free, Provider, Root
            }
        }

        #Trace-AzsSupportCommand -Event OnExit
        return $results
    }
    catch {
        #$formattedException = Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Get-AzsSupportDiskSpace {
    <#
        .SYNOPSIS
        Get available disk space on target computers.
 
        .DESCRIPTION
        Utilizes Get-PSDrive via Invoke-Command to remote computers to get available disk space information.
 
        .PARAMETER ComputerName
        The computer(s) that you want to get disk space information from.
 
        .PARAMETER DriveLetter
        The drive letter you want to get data from.
 
        .EXAMPLE
        PS> Get-AzsSupportDiskSpace -ComputerName $InfraHosts -DriveLetter C | format-table -AutoSize
 
        .OUTPUTS
        Array of PSObject representing the disk space
 
        ComputerName Name Used Free Provider Root
        ------------ ---- ---- ---- -------- ----
        contoso-n01 C 101630009344 492767858688 Microsoft.PowerShell.Core\FileSystem C:\
        contoso-n02 C 104016150528 490381717504 Microsoft.PowerShell.Core\FileSystem C:\
    #>


    param (
        [Parameter(Mandatory = $true)]
        [string[]]$ComputerName,

        [Parameter(Mandatory = $true)]
        [char]$DriveLetter
    )

    try {
        #Trace-AzsSupportCommand -Event OnEntry

        $session = New-AzsSupportPSSession -ComputerName $ComputerName
        if ($session) {
            Invoke-Command -Session $session -ScriptBlock { Get-PsDrive $using:DriveLetter | Select-Object @{Label = "ComputerName"; Expression = { $ENV:COMPUTERNAME } }, Name, Used, Free, Provider, Root } -AsJob -JobName ($Id = "$([guid]::NewGuid())") | Out-Null
            $diskResults = Wait-AzsSupportJob -JobName $Id -Activity "Get-AzsSupportDiskSpace" -ExecutionTimeOut 60 -PollingInterval 1 -PassThru
        }

        # Trace-AzsSupportCommand -Event OnExit
        if ($diskResults) {

            
            return ($diskResults | Select-Object ComputerName, Name, Used, Free, Provider, Root | Sort-Object -Property ComputerName)
        }
    }
    catch {
        #$formattedException = Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Get-AzsSupportPhysicalDisk {
    <#
        .SYNOPSIS
            Gets physical disks connected to the specified ComputerName.
 
        .DESCRIPTION
            Retrieves physical disks connected to the specified ComputerName. If no node is provided, all nodes are queried.
 
        .PARAMETER ComputerName
            Type the NetBIOS name, an IP address, or a fully qualified domain name of one or more remote computers.
 
        .PARAMETER UnhealthyDisks
            Will filter out all healthy disks and display only unhealthy disks.
 
        .PARAMETER SerialNumber
            The SerialNumber of the disk drive.
 
        .PARAMETER MSFT_StorageSubSystem
            The Storage SubSystem object.
 
        .PARAMETER MSFT_StoragePool
            The Storage Pool object.
 
        .EXAMPLE
            PS> Get-AzsSupportPhysicalDisk
 
        .EXAMPLE
            PS> Get-AzsSupportPhysicalDisk -ComputerName contoso-n01,contoso-n02 | Format-Table
 
        .EXAMPLE
            PS> Get-AzsSupportPhysicalDisk -SerialNumber ABC1_0000_0000_0001.
 
        .EXAMPLE
            PS> Get-AzsSupportPhysicalDisk -PD {aea78659-0c86-f12a-159e-8c6f799c79f7}
 
        .EXAMPLE
            PS> Get-AzsSupportPhysicalDisk -ComputerName contoso-n01,contoso-n02 -UnhealthyDisks | Format-Table
 
        .EXAMPLE
            PS> Get-AzsSupportPhysicalDisk -StorageSubSystem $StorageSubSystem
 
        .EXAMPLE
            PS> Get-AzsSupportPhysicalDisk -StoragePool $StoragePool | sort-object DeviceId
 
        .OUTPUTS
            Array of PSObject representing the physical disks
 
            Number FriendlyName SerialNumber MediaType CanPool OperationalStatus HealthStatus Usage Size
            ------ ------------ ------------ --------- ------- ----------------- ------------ ----- ----
            1000 Dell NVMe PE8110 RI M.2 3.84TB ABC1_0000_0000_0001. SSD False OK Healthy Auto-Select 3.49 TB
            1001 Dell NVMe PE8110 RI M.2 3.84TB ABC1_0000_0000_0002. SSD False OK Healthy Auto-Select 3.49 TB
            1002 Dell NVMe PE8110 RI M.2 3.84TB ABC1_0000_0000_0003. SSD False OK Healthy Auto-Select 3.49 TB
            1003 Dell NVMe PE8110 RI M.2 3.84TB ABC1_0000_0000_0004. SSD False OK Healthy Auto-Select 3.49 TB
            2000 Dell NVMe PE8110 RI M.2 3.84TB ABC1_0000_0000_0005. SSD False OK Healthy Auto-Select 3.49 TB
            2001 Dell NVMe PE8110 RI M.2 3.84TB ABC1_0000_0000_0006. SSD False OK Healthy Auto-Select 3.49 TB
            2002 Dell NVMe PE8110 RI M.2 3.84TB ABC1_0000_0000_0007. SSD False OK Healthy Auto-Select 3.49 TB
            2003 Dell NVMe PE8110 RI M.2 3.84TB ABC1_0000_0000_0008. SSD False OK Healthy Auto-Select 3.49 TB
    #>


    [CmdletBinding(DefaultParameterSetName = 'SerialNumber')]
    param(
        [Parameter(Mandatory = $false)]
        [System.String[]]$ComputerName = $((Get-AzsSupportInfrastructureHost).Name),

        [Parameter(Mandatory = $false)]
        [System.String]$SerialNumber,

        [Parameter(Mandatory = $false)]
        [System.String]$PD,

        [Parameter(Mandatory = $false)]
        [Switch]$UnhealthyDisks,

        [Parameter(Mandatory = $false)]
        [System.String]$UniqueId,

        [Parameter(Mandatory = $false)]
        [Microsoft.Management.Infrastructure.CimInstance]$StorageSubSystem,

        [Parameter(Mandatory = $false)]
        [Microsoft.Management.Infrastructure.CimInstance]$StoragePool


    )

    try {
        #Trace-AzsSupportCommand -Event OnEntry


        if (!$PSBoundParameters['StorageSubsystem'] -or !$PSBoundParameters['StoragePool'] -or !$PSBoundParameters['SerialNumber'] -or !$PSBoundParameters['PD'] -or !$PSBoundParameters['UniqueId']) {
            # The reason for doing against each Node is to protect against Cluster having issues and returning no data
            $localDisks = foreach ($nodeName in $ComputerName) {
                New-Object -TypeName PSCustomObject -Property @{
                    Disks = $( Get-AzsSupportStorageNode -Name $nodeName* `
                        | Get-PhysicalDisk -CimSession $nodeName -PhysicallyConnected `
                        | Select-Object `
                        @{Label = "ComputerName"; Expression = { $_.PSComputerName } }, `
                        @{Label = "Size"; Expression = { if ($_.Size -lt 1TB) { "{0:N2} GB" -f ($_.Size / 1GB) } else { "{0:N2} TB" -f ($_.Size / 1TB ) } } }, `
                            DeviceId, HealthStatus, FriendlyName, PhysicalLocation, SerialNumber, MediaType, BusType, CanPool, CannotPoolReason, OperationalStatus, Usage, Manufacturer, Model, FirmwareVersion, SlotNumber, IsIndicationEnabled, FruId, @{Label = "PD"; Expression = { [regex]::match($_.ObjectId, ':PD:(.*?)"').Groups[1].Value } }  `
                        | Sort-Object ComputerName, DeviceId, MediaType)
                } 
            }
        }

        # Control formatting based on the parameters passed
        switch ($PSBoundParameters) {
    
            { ($_.keys -contains "SerialNumber") } {
        
                Get-PhysicalDisk -SerialNumber $SerialNumber -CimSession $(Get-AzsSupportClusterName) 
                break
            }
            { ($_.keys -contains "PD") } {
        
                Get-PhysicalDisk -CimSession $(Get-AzsSupportClusterName) | Where-Object { $_.ObjectID -like "*$PD*" }
                break
            }
            { ($_.keys -contains "UniqueId") } {

                Get-PhysicalDisk -CimSession $(Get-AzsSupportClusterName) -UniqueId $UniqueId
                break
            }
            { ($_.keys -contains "UnhealthyDisks") } {
  
                return $localDisks.Disks | Where-Object { ($_.HealthStatus -ne 'Healthy' -or $_.OperationalStatus -ne 'OK') }
                break
            }
            { ($_.keys -contains "StorageSubsystem") } {
                $disks = Get-PhysicalDisk -CimSession $(Get-AzsSupportClusterName) -StorageSubsystem $StorageSubSystem
                return $disks
                break
            }   
            { ($_.keys -contains "StoragePool") } {
                $disks = Get-PhysicalDisk -CimSession $(Get-AzsSupportClusterName) -StoragePool $StoragePool
                return $disks
                break
            }      
            default {
                return $localDisks.Disks
                break
            }
        }
    }
    catch {
        #$formattedException= Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Get-AzsSupportPhysicalDiskIndicator {
    <#
    .SYNOPSIS
        To get the physical disks with light indicator on.
 
    .DESCRIPTION
        Retrieves physical disks with light indicator on that have been marked for attention.
 
    .PARAMETER SerialNumber
        Physical disk serial number.
 
    .PARAMETER Cluster
        Cluster name to get the physical disk indicator status.
 
    .EXAMPLE
        PS> Get-AzsSupportPhysicalDiskIndicator -Cluster contoso-cl -SerialNumber "ABC1_0000_0000_0001."
 
    .EXAMPLE
        PS> Get-AzsSupportPhysicalDiskIndicator -Cluster contoso-cl
 
    .OUTPUTS
        Array of PSObject representing the physical disks with light indicator on
 
      SerialNumber IsIndicationEnabled HealthStatus OperationalStatus
      ------------ ------------------- ------------ -----------------
      A1B0C2D4EFGH True Healthy OK
    #>


    param(
        [Parameter(Mandatory = $false)]
        [System.String]$SerialNumber,
        [Parameter(Mandatory = $true)]
        [System.String]$Cluster
    )

    try {
        #Trace-AzsSupportCommand -Event OnEntry

        if ($SerialNumber) {
            $disk = Get-AzsSupportPhysicalDisk -ComputerName $((Get-AzsSupportInfrastructureHost -Cluster $Cluster).Name) -SerialNumber $SerialNumber
            if ($disk) {
                #Normally we would invoke these against a persistent session from New-AzsSupportPSSession, however in this case the cluster owner may change, below command is run against Cluster owner
                $enabledDisks = Get-AzsSupportPhysicalDisk -ComputerName $((Get-AzsSupportInfrastructureHost -Cluster $Cluster).Name) -SerialNumber $SerialNumber | Where-Object { $_.IsIndicationEnabled -eq $true } 
            }
            else {
                Trace-Output -Level:Error -Message ($msg.StorageNoDiskWithSerialNumber -f $SerialNumber)
                break
            }
        }
        else {

            $enabledDisks = Get-AzsSupportPhysicalDisk -ComputerName $((Get-AzsSupportInfrastructureHost -Cluster $Cluster).Name) | Where-Object { $_.IsIndicationEnabled -eq $true } 
        }

        if ($enabledDisks) {
            $disksResult = foreach ($enabledDisk in $enabledDisks) {
                New-Object -TypeName PSCustomObject -Property @{
                    SerialNumber        = $enabledDisk.SerialNumber
                    OperationalStatus   = $enabledDisk.OperationalStatus
                    HealthStatus        = $enabledDisk.HealthStatus
                    IsIndicationEnabled = $enabledDisk.IsIndicationEnabled
                }
            }
            Trace-Output -Level:Information -Message ($msg.StorageLightIndicatorEnabled)
        }
        else {
            Trace-Output -Level:Information -Message ($msg.StorageLightIndicatorNotEnabled)
        }

        #Trace-AzsSupportCommand -Event OnExit
        return $disksResult
    }
    catch {
        #$formattedException= Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Get-AzsSupportStorageNode {
    <#
    .SYNOPSIS
        Gets specified storage node or all nodes if none are provided.
 
     .DESCRIPTION
        Retrieves storage nodes connected to the specified ComputerName. If no node is provided, all nodes are queried.
 
    .PARAMETER Node
        A physical node like contoso-n01.
 
    .PARAMETER Name
        Name of Physical Node.
 
    .EXAMPLE
        PS> Get-AzsSupportStorageNode
 
    .EXAMPLE
        PS> Get-AzsSupportStorageNode -Name contoso-n01
 
    .EXAMPLE
        PS> Get-AzsSupportStorageNode -Node contoso-n01
 
    .OUTPUTS
        Array of storage nodes based on name filter
 
        PS> Get-AzsSupportStorageNode -Name contoso-n01* -Node contoso-n01
 
        Name Manufacturer Model SerialNumber OperationalStatus PSComputerName
        ---- ------------ ----- ------------ ----------------- --------------
        contoso-n01.contoso.lab Dell Inc. AX-740xd 10000001 Up contoso-n01
        contoso-n02.contoso.lab Dell Inc. AX-740xd 10000002 Up contoso-n01
 
    #>


    [CmdletBinding()]
    param(
        [string]$Name,
        [string[]]$Node
    )
    try {
        #Trace-AzsSupportCommand -Event OnEntry
  
        $storageNode = switch ($PSBoundParameters) {
            { ($_.keys -contains "Name" -and $_.Keys -contains "Node") } {
                Get-StorageNode -name $Name* -CimSession $Node
                break
            }
            { ($_.keys -contains "Name") } {
                Get-StorageNode -name $Name* -CimSession (Get-AzsSupportClusterName) 
                break 
            }
            { ($_.keys -contains "Node") } {
                Get-StorageNode -CimSession $Node
                break
            }
            default {
                Get-StorageNode -CimSession (Get-AzsSupportClusterName) 
                break
            }
        }
           
        #Trace-AzsSupportCommand -Event OnExit
        return $storageNode
    }
    catch {
        #$formattedException= Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Get-AzsSupportStoragePool {
    <#
    .SYNOPSIS
        Gets specified Storage Pool or all pools if none are provided.
 
     .DESCRIPTION
        Retrieves storage pools connected to the specified ComputerName. If no node is provided, all nodes are queried.
 
    .PARAMETER FriendlyName
        Friendly name of Storage Pool.
 
    .PARAMETER IsPrimordial
        If set to $true, will return only primordial pools.
 
    .PARAMETER Cluster
        Cluster to target. Defaults to Cluster returned from Get-AzsSupportClusterName.
 
    .EXAMPLE
        PS> Get-AzsSupportStoragePool
 
    .EXAMPLE
        PS> Get-AzsSupportStoragePool -FriendlyName S2D_Pool
 
    .EXAMPLE
        PS> Get-AzsSupportStoragePool -IsPrimordial $False
 
    .EXAMPLE
        PS> Get-AzsSupportStoragePool -Cluster Contoso-cl
 
    .OUTPUTS
 
        PS> Get-AzsSupportStoragePool -FriendlyName S2D_Pool -Cluster Contoso-cl
 
        Array of Storage Pools based on filters selected
 
        FriendlyName OperationalStatus HealthStatus IsPrimordial IsReadOnly Size AllocatedSize PSComputerName
        ------------ ----------------- ------------ ------------ ---------- ---- ------------- --------------
        S2D_Pool OK Healthy False False 27.94 TB 1.79 TB Contoso-cl
    #>


    [CmdletBinding()]
    param(
        [string]$FriendlyName,
        [parameter(mandatory = $false, HelpMessage = "Please provide Name of Cluster")]
        [string]$Cluster, 
        [bool]$IsPrimordial
    )
    try {
        #Trace-AzsSupportCommand -Event OnEntry

        # If no cluster name is provided, attempt to get the cluster name
        if (!$PSBoundParameters['Cluster']) {
            $ClusterName = Get-AzsSupportClusterName 
        }
        else {
            #Ensure the cluster name is correct
            $ClusterName = Get-AzsSupportClusterName -Name $Cluster
        }

        # If no cluster name is found, return
        if ($null -eq $ClusterName) {
            Trace-Output -Level:Exception -Message ($msg.CommonClusterName)
            Return
        }
  
        $StoragePool = switch ($PSBoundParameters) {
            { ($_.keys -contains "Name" -and $_.Keys -contains "IsPrimordial") } {
                Get-StoragePool -Name $Name -CimSession $ClusterName -IsPrimordial $IsPrimordial
                break
            }
            { ($_.keys -contains "Name") } {
                Get-StoragePool -FriendlyName $FriendlyName -CimSession $ClusterName
                break 
            }
            { ($_.keys -contains "IsPrimordial") } {
                Get-StoragePool -CimSession $ClusterName -IsPrimordial $IsPrimordial
                break
            }
            default {
                Get-StoragePool -CimSession $ClusterName
                break
            }
        }
           
        #Trace-AzsSupportCommand -Event OnExit
        return $StoragePool
    }
    catch {
        #$formattedException= Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Get-AzsSupportStorageJob {
    <#
    .SYNOPSIS
        Gets all active storage jobs from the storage pool and virtual disks.
 
     .DESCRIPTION
        Retrieves all active storage jobs from the storage pool. If no node is provided, all nodes are queried. The optimisation job is excluded by default.
 
    .PARAMETER Cluster
        Cluster to target. Defaults to Cluster returned from Get-AzsSupportClusterName.
 
    .PARAMETER Wait
        Timed refresh for updating the output of any running storage jobs.
 
    .PARAMETER RefreshInSeconds
        How many seconds to wait before refreshing the output again. Defaults to 60 seconds.
 
    .EXAMPLE
        PS> Get-AzsSupportStorageJob
 
    .EXAMPLE
        PS> Get-AzsSupportStorageJob -Cluster contoso-cl
 
    .EXAMPLE
        PS> Get-AzsSupportStorageJob -Wait
 
    .EXAMPLE
        PS> Get-AzsSupportStorageJob -Wait -RefreshInSeconds 90
 
    .EXAMPLE
        PS> Get-AzsSupportStorageJob -Cluster contoso-cl -IncludeStoragePoolOptimizationJob
 
    .OUTPUTS
        Array of Storage Jobs based on selected filters
 
        PS> Get-AzsSupportStorageJob -Cluster Contoso-cl -IncludeStoragePoolOptimizationJob
 
        Name IsBackgroundTask ElapsedTime JobState PercentComplete BytesProcessed BytesTotal PSComputerName
        ---- ---------------- ----------- -------- --------------- -------------- ---------- --------------
        ClusterPerformanceHistory-Repair True 00:00:28 Suspended 0 0 B 1 Contoso-cl
        Infrastructure_1-Repair True 00:00:52 Suspended 0 0 B 9 GB Contoso-cl
        UserStorage_1-Repair True 00:00:52 Suspended 0 0 B 768 MB Contoso-cl
    #>


    [CmdletBinding(DefaultParameterSetName = "Cluster")]
    param(
        [Parameter(ParameterSetName = "Cluster")]
        [String]$Cluster = (Get-AzsSupportClusterName ),
        [Parameter(ParameterSetName = "Cluster")]
        [switch]$Wait,
        [Parameter(ParameterSetName = "Cluster")]
        [switch]$IncludeStoragePoolOptimizationJob,
        [Parameter(ParameterSetName = "Cluster")]
        [int]$RefreshInSeconds = 60
    )
    try {
        #Trace-AzsSupportCommand -Event OnEntry
        if (!($IncludeStoragePoolOptimizationJob.IsPresent)) {
            Trace-Output -Level:Information -Message ($msg.StorageOptimizationJobsFilteredOut)
        }
        while ($true) {
            $storageJobs = Get-StorageJob -CimSession $Cluster | Sort-Object JobState, Name
            # if storage jobs are null, we want to just log verbose message and break out of loop
            if ($null -eq $storageJobs) {
                Trace-Output -Level:Verbose -Message ($msg.StorageNoJobsFound)
                break
            }
            # check to see if storage pool optimization jobs should be included in the output
            # if set to false and we we filter some jobs out, then flag a message to operator to let them know
            if (!($IncludeStoragePoolOptimizationJob.IsPresent)) {
                $storageJobs = $storageJobs | Where-Object { $_.Name -notlike "*Pool-Optimize" }
            }
            # if user opted to wait, then output current results and then sleep for duration
            # else we want to break out of loop and return the storage jobs
            if ($Wait) {
                $storageJobs
                Trace-Output -Level:Information -Message ($msg.StorageRefreshingIn -f $RefreshInSeconds)
                Start-Sleep -Seconds $RefreshInSeconds
            }
            else {
                break
            }
        }
        #Trace-AzsSupportCommand -Event OnExit
        return $storageJobs
    }
    catch {
        #$formattedException= Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Get-AzsSupportStorageSubsystem {
    <#
    .SYNOPSIS
        Gets specified Storage Subsystem or all subsystems if none are provided.
     
    .DESCRIPTION
        Retrieves storage subsystems connected to the specified ComputerName. If no node is provided, all nodes are queried.
 
    .PARAMETER FriendlyName
        Name of Storage Pool.
     
    .PARAMETER CimSession
        CimSession to target.
 
    .EXAMPLE
        PS> Get-AzsSupportStorageSubsystem
    .EXAMPLE
        PS> Get-AzsSupportStorageSubsystem -FriendlyName Cluster* -Cimsession Contoso-cl
 
    .OUTPUTS
        Array of Storage Subsystems based on filters selected
 
        Get-AzsSupportStorageSubsystem -FriendlyName Cluster* -Cimsession Contoso-cl
 
        FriendlyName HealthStatus OperationalStatus PSComputerName
        ------------ ------------ ----------------- --------------
        Clustered Windows Storage on Contoso-cl Healthy OK Contoso-cl
    #>


    [CmdletBinding()]
    param(
        [string]$FriendlyName,
        [parameter(mandatory = $true, HelpMessage = "Please provide Name of Cluster or Cluster Node to connect to")]
        [CimSession]$CimSession
    )
    try {
        #Trace-AzsSupportCommand -Event OnEntry
  
        $StorageSubSystem = switch ($PSBoundParameters) {
            { ($_.keys -contains "FriendlyName") } {
                Get-StorageSubSystem -FriendlyName $FriendlyName -CimSession $CimSession
                break
            }
            default {
                Get-StorageSubSystem -CimSession $CimSession
                break
            }
        }
           
        #Trace-AzsSupportCommand -Event OnExit
        return $StorageSubSystem
    }
    catch {
        #$formattedException= Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Get-AzsSupportVirtualDisk {
    <#
    .SYNOPSIS
        Gets all virtual disks and their health states.
 
    .DESCRIPTION
        Retrieves virtual disks connected to the specified ComputerName. If no Cluster is provided, it will query all nonprimordial pools found.
 
    .EXAMPLE
        PS> Get-AzsSupportVirtualDisk
         
    .EXAMPLE
        PS> Get-AzsSupportVirtualDisk -Cluster "Contoso-cl"
     
    .EXAMPLE
        PS> Get-AzsSupportVirtualDisk -FriendlyName "UserStorage_1"
 
    .OUTPUTS
        Array of virtual disks based on filters selected
 
        FriendlyName ResiliencySettingName FaultDomainRedundancy OperationalStatus HealthStatus Size FootprintOnPool StorageEfficiency PSComputerName
        ------------ --------------------- --------------------- ----------------- ------------ ---- --------------- ----------------- --------------
        UserStorage_1 Mirror 1 OK Healthy 64 TB 1017 GB 49.95% Contoso-cl
    #>


    [CmdletBinding()]
    param (
        [parameter(mandatory = $false, HelpMessage = "Please provide Name of Cluster or Cluster Node to connect to")]
        [string]$Cluster,
        [parameter(mandatory = $false, HelpMessage = "Please provide friendly name of Virtual Disk to retrieve")]
        [string]$FriendlyName

    )

    try {
        #Trace-AzsSupportCommand -Event OnEntry


        # If no Cimsession provided, attempt to get the cluster name
        if (!$PSBoundParameters['Cluster']) {
            $ClusterName = Get-AzsSupportClusterName 
        }
        else {
            #Ensure the cluster name is correct
            $ClusterName = Get-AzsSupportClusterName -Name $Cluster
        }

        # If no cluster name is found, return
        if ($null -eq $ClusterName) {
            Trace-Output -Level:Exception -Message ($msg.StorageClusterName)
            Return
        }

        if ($PSBoundParameters['FriendlyName']) {
            $disks = Get-VirtualDisk -CimSession $ClusterName -FriendlyName $FriendlyName | Sort-Object HealthStatus, FriendlyName
        }
        else {
            $disks = Get-VirtualDisk -CimSession $ClusterName | Sort-Object HealthStatus, FriendlyName
        }

        #Trace-AzsSupportCommand -Event OnExit
        return $disks
    }
    catch {
        #$formattedException= Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Get-AzsSupportVolumeUtilization {
    <#
        .SYNOPSIS
            Reports the utilization for all Object Stores.
 
        .DESCRIPTION
            Utilizes Get-Volume to get the utilization for all Object Stores.
 
        .PARAMETER Filter
            Wildcard filter for FileSystemLabel.
 
        .PARAMETER Cluster
            The Cluster you want to run against.
 
        .EXAMPLE
            PS> Get-AzsSupportVolumeUtilization.
 
        .EXAMPLE
            PS> Get-AzsSupportVolumeUtilization -Filter User* -Cluster "Contoso-cl"
 
        .EXAMPLE
            PS> Get-AzsSupportVolumeUtilization -Cluster "Contoso-cl"
 
        .OUTPUTS
            Array representing the volume utilization
 
            Get-AzsSupportVolumeUtilization -Cluster Contoso-cl -Filter User* | Format-Table -AutoSize
 
            OperationalStatus FileSystemLabel SizeGB SizeRemainingGB UtilizationPercent HealthStatus
            ----------------- --------------- ------ --------------- ------------------ ------------
            OK UserStorage_1 65536 65038 0.76% Healthy
            OK UserStorage_2 65536 65179 0.55% Healthy
 
 
    #>


    param(
        [String]$Filter = [String]::Empty,
        [parameter(mandatory = $true, HelpMessage = "Please provide Name of Cluster")]
        [string]$Cluster
    )

    try {
        #Trace-AzsSupportCommand -Event OnEntry

        $objStores = Get-Volume -CimSession $Cluster | Where-Object { !([String]::IsNullOrEmpty($_.FileSystemLabel)) }

        if (![String]::IsNullOrEmpty($Filter)) {
            $objStores = $objStores `
            | Where-Object { $_.FileSystemLabel -like "*$Filter*" }
        }

        $results = foreach ($obj in $objStores) {
            New-Object -TypeName PSCustomObject -Property @{
                FileSystemLabel    = $obj.FileSystemLabel
                HealthStatus       = $obj.HealthStatus
                OperationalStatus  = $obj.OperationalStatus
                SizeGB             = ($obj.Size / 1GB) -as [int]
                SizeRemainingGB    = ($obj.SizeRemaining / 1GB) -as [int]
                UtilizationPercent = (1 - ($obj.SizeRemaining / $obj.Size)).ToString("P")
            }
        }

        #Trace-AzsSupportCommand -Event OnExit
        return $results | Sort-Object FileSystemLabel
    }
    catch {
        #$formattedException= Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Restart-AzsSupportClusterHealthService {
    <#
    .SYNOPSIS
        Restarts the cluster health service.
 
    .DESCRIPTION
        Restarts the cluster health service using appropriate methods and ensures resource correctly started.
 
    .PARAMETER Cluster
        Defaults to management cluster returned from Get-AzsSupportClusterName.
 
    .EXAMPLE
        PS> Restart-AzsSupportClusterHealthService
 
    .EXAMPLE
        PS> Restart-AzsSupportClusterHealthService -Cluster "Contoso-cl"
 
    .OUTPUTS
        Confirmation on restarting the cluster health service
 
        PS> Restart-AzsSupportClusterHealthService -Cluster "Contoso-cl"
 
        [Stopping health cluster resource]
        [Starting all resources in SDDC Group]
 
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [string]$Cluster = (Get-AzsSupportClusterName )
    )

    try {
        #Trace-AzsSupportCommand -Event OnEntry
        Trace-Output -Level:Information -Message ($msg.StorageStoppingHealthResource)
        $clusterResource = Get-ClusterResource -Cluster $Cluster -Name health | Stop-ClusterResource
        Trace-Output -Level:Information -Message ($msg.StorageStartingResourcesInGroup -f $($clusterResource.OwnerGroup.Name))
        # Starts all resources in the owning group as SDDC Management is dependant on the health service
        $null = Get-ClusterGroup -Cluster $Cluster -Name $clusterResource.OwnerGroup.Name | Start-ClusterGroup

        $stopWatch = [System.Diagnostics.Stopwatch]::StartNew()
        # ensure that we are able to start the health cluster resource and if not, try several times in attempt to get started before bailing out
        $clusterGroupState = Get-ClusterGroup -Cluster $Cluster -Name $clusterResource.OwnerGroup.Name | Get-ClusterResource
        while ([Microsoft.FailoverClusters.PowerShell.ClusterResourceState]::Offline -in $clusterGroupState.State) {
            if ($stopWatch.Elapsed.TotalSeconds -ge 60) {
                $stopWatch.Stop()
                throw New-Object System.TimeoutException("$($msg.StorageCHServiceResourcesThrow)" -f $($Cluster), $($clusterResource.OwnerGroup.Name), $($clusterResource[0].OwnerGroup.Name))
            }
            Trace-Output -Level:Warning -Message ($msg.StorageResourcesNotRunning -f $($clusterResource.OwnerGroup.Name))
            Trace-Output -Level:Information -Message ($msg.StorageReattemptingStartResources -f $($clusterResource.OwnerGroup.Name))
            Start-Sleep -Seconds 5
            $clusterGroupState = Get-ClusterGroup -Cluster $Cluster -Name $Cluster.OwnerGroup.Name | Start-ClusterGroup
        }
        $stopWatch.Stop()

        if ((Get-ClusterResource -Cluster $Cluster -Name health).State -ne [Microsoft.FailoverClusters.PowerShell.ClusterResourceState]::Online) {
            throw New-Object System.InvalidProgramException("$($msg.StorageClusterHealthServiceOffline) $($healthResource.State.ToString())")
        }
        if ([Microsoft.FailoverClusters.PowerShell.ClusterResourceState]::Offline -in (Get-ClusterGroup -Cluster $Cluster -Name $clusterResource.OwnerGroup.Name | Get-ClusterResource).State) {
            throw New-Object System.InvalidProgramException("$($msg.StorageCHServiceResourcesOfflineThrow)" -f $($clusterResource.OwnerGroup.Name), $Cluster, $($clusterResource[0].OwnerGroup.Name))
        }

        #Trace-AzsSupportCommand -Event OnExit
    }
    catch {
        #$formattedException= Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException

        throw $_
    }
}

function Set-AzsSupportPhysicalDiskIndicator {
    <#
    .SYNOPSIS
        To Enable or Disable physical disk indicator for spcific disk based on its serial number.
 
    .DESCRIPTION
        Enable physical disk indicator for specific disk based on its serial number to be marked for attention or disable.
 
    .PARAMETER SerialNumber
        Physical disk serial number.
 
    .PARAMETER Enable
        Enable physical disk indicator light.
 
    .PARAMETER Disable
        Disable physical disk indicator light.
 
    .PARAMETER ComputerName
        Computer name to get the physical disk indicator status.
 
    .EXAMPLE
        PS> Set-AzsSupportPhysicalDiskIndicator -ComputerName contoso-n01
 
    .EXAMPLE
        PS> Set-AzsSupportPhysicalDiskIndicator -SerialNumber A1B0C2D4EFGH -Enable -Cluster $(Get-AzsSupportClusterName)
 
    .OUTPUTS
        Enable physical disk indicator light :
 
        Set-AzsSupportPhysicalDiskIndicator -SerialNumber A1B0C2D4EFGH -Enable -Cluster contoso-cl
 
        [Enabling indicator light for physical disk with serial number A1B0C2D4EFGH]
        [Physical disk with serial number A1B0C2D4EFGH indicator light has been enabled]
        [Light indicator is enabled for some disks]
 
        SerialNumber IsIndicationEnabled HealthStatus OperationalStatus
        ------------ ------------------- ------------ -----------------
        A1B0C2D4EFGH True Healthy OK
 
        Disable physical disk indicator light :
 
        Set-AzsSupportPhysicalDiskIndicator -SerialNumber A1B0C2D4EFGH -Enable -Cluster contoso-cl
 
        [Disabling indicator light for physical disk with serial number A1B0C2D4EFGH]
        [Physical disk with serial number A1B0C2D4EFGH indicator light has been disabled]
        [Light indicator is enabled for some disks]
 
        SerialNumber IsIndicationEnabled HealthStatus OperationalStatus
        ------------ ------------------- ------------ -----------------
        A1B0C2D4EFGH False Healthy OK
 
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = "Enable")]
        [switch]$Enable,

        [Parameter(Mandatory = $true, ParameterSetName = "Disable")]
        [switch]$Disable,

        [Parameter(Mandatory = $true, ParameterSetName = "Enable")]
        [Parameter(Mandatory = $true, ParameterSetName = "Disable")]
        [string]$SerialNumber,

        [Parameter(Mandatory = $true)]
        [System.String]$Cluster
        
    )

    try {
        #Trace-AzsSupportCommand -Event OnEntry

  
        $disk = Get-AzsSupportPhysicalDisk -ComputerName $((Get-AzsSupportInfrastructureHost -Cluster $Cluster).Name) -SerialNumber $SerialNumber

        if ($disk) {
            switch ($PSCmdlet.ParameterSetName) {
                "Disable" {
                    Trace-Output -Level:Information -Message ($msg.StorageDisablingIndicatorLight -f $SerialNumber)
                    #Normally we would invoke these against a persistent session from New-AzsSupportPSSession, however in this case the cluster owner may change, below command is run against Cluster owner
                    Get-AzsSupportPhysicalDisk -ComputerName $((Get-AzsSupportInfrastructureHost -Cluster $Cluster).Name) -SerialNumber $SerialNumber | Disable-PhysicalDiskIdentification -CimSession $Cluster -ErrorAction SilentlyContinue
                    $disk = Get-AzsSupportPhysicalDisk -ComputerName $((Get-AzsSupportInfrastructureHost -Cluster $Cluster).Name) -SerialNumber $SerialNumber | Where-Object { $_.IsIndicationEnabled -eq $true }

                    if ($disk) {
                        Trace-Output -Level:Warning -Message ($msg.StorageDisableIndicatorFailure -f $SerialNumber)
                    }
                    else {
                        Trace-Output -Level:Information -Message ($msg.StorageDisableIndicatorSuccess -f $SerialNumber)
                    }
                }
                "Enable" {
                    Trace-Output -Level:Information -Message ($msg.StorageIndicatorEnable -f $SerialNumber)
                    Get-AzsSupportPhysicalDisk -ComputerName $((Get-AzsSupportInfrastructureHost -Cluster $Cluster).Name) -SerialNumber $SerialNumber | Enable-PhysicalDiskIdentification -CimSession $Cluster -ErrorAction SilentlyContinue

                    if ($disk) {
                        Trace-Output -Level:Information -Message ($msg.StorageEnableIndicatorSuccess -f $SerialNumber)
                    }
                    else {
                        Trace-Output -Level:Warning -Message ($msg.StorageEnableIndicatorFailure -f $SerialNumber)
                    }
                }
            }
        }
        else {
            Trace-Output -Level:Error -Message ($msg.StorageNoDiskSerialNumber -f $SerialNumber)
        }

        #Trace-AzsSupportCommand -Event OnExit

        # Protect against Cluster having issues and returning no data for enabled indicator lights
        Update-StorageProviderCache -CimSession $Cluster 
        $result = Get-AzsSupportPhysicalDiskIndicator -Cluster $Cluster
        if ($result) {
            $result | Format-Table | Out-String | Trace-Output
        }
    }
    catch {
        #$formattedException= Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Update-AzsSupportStorageHealthCache {
    <#
    .SYNOPSIS
        Refreshes the storage cache and health cluster resources.
 
    .DESCRIPTION
        Updates the cache instance and health cluster resources.
 
    .PARAMETER Cluster
        The cluster to perform the operation against.
 
    .EXAMPLE
        PS> Update-AzsSupportStorageHealthCache
 
    .EXAMPLE
        PS> Update-AzsSupportStorageHealthCache -Cluster "Contoso-cl"
 
    .OUTPUTS
        Confirmation on updating the storage cache
 
        PS> Update-AzsSupportStorageHealthCache -Cluster "Contoso-cl"
 
        [Stopping health cluster resource]
        [Starting all resources in SDDC Group]
        [Updating Storage Provider Cache]
         
 
    #>


    [cmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string]$Cluster = (Get-AzsSupportClusterName )
    )

    try {

        #Trace-AzsSupportCommand -Event OnEntry

        Restart-AzsSupportClusterHealthService -Cluster $Cluster

        Start-Sleep -Seconds 5

        Trace-Output -Level:Information -Message ($msg.StorageUpdatingCache)
        Update-StorageProviderCache -CimSession $Cluster

        #Trace-AzsSupportCommand -Event OnExit
    }
    catch {
        #$formattedException= Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Get-AzsSupportClusterUsage {

    <#
        .SYNOPSIS
            Calculate storage usage and capacity for the cluster
 
        .DESCRIPTION
            Allows quick understanding of the current environment and capacity
             
        .PARAMETER ClusterName
            The Cluster you want to run against
 
        .EXAMPLE
            PS> Get-AzsSupportClusterUsage -ClusterName "Contoso-cl"
 
        .OUTPUTS
            Write Cache Size : 20.95
            SupportedComponents : <Components><Disks><Disk><Manufacturer /><Model /><AllowedFirmware><Version /></AllowedFirmware></Disk></Disks><Cache /></Components>
            Used : 2.26
            Physical Disk Redundancy : 1
            Capacity Disk Size : 10.69
            Total Size : 256.61
            Total Drives : 36
            Drive Models : {@{Count=12; Model=KPM6WVUG1T92; Size (TB)=2}, @{Count=24; Model=MG07SCA12TEY; Size (TB)=11}}
            Reserve : 21.38
            Available : 254.35
    #>



    Param (
        [Parameter(Mandatory = $True)]
        [String]$ClusterName
    )

    try {
        #Trace-AzsSupportCommand -Event OnEntry

        $Nodes = Get-AzsSupportInfrastructureHost -Cluster $ClusterName
        $NonPrimordial = Get-AzsSupportStoragePool -Cluster $ClusterName -IsPrimordial $false
        $PoolDisks = Get-AzsSupportPhysicalDisk -ComputerName $((Get-AzsSupportInfrastructureHost -Cluster $ClusterName).Name) -StoragePool $NonPrimordial
        $TotalSize = [long](($PoolDisks | Where-Object { $_.Usage -ne "Journal" } | Measure-Object -sum Size).Sum)
        $TotalUsedSpace = (Get-AzsSupportVirtualDisk -Cluster $ClusterName | Measure-Object -sum Footprintonpool).Sum

        $StorageUsage = New-Object PSObject -Property @{
            'Capacity Disk Size'       = [math]::Floor((($PoolDisks | Where-Object { $_.Usage -ne "Journal" } | Select-Object -first 1).Size / 1TB) * 100) / 100
            'Total Size'               = [math]::Floor((($PoolDisks | Where-Object { $_.Usage -ne "Journal" } | Measure-Object -sum Size).Sum / 1TB) * 100) / 100
            'Used'                     = [math]::Floor(((Get-AzsSupportVirtualDisk | Measure-Object -sum Footprintonpool).Sum) / 1TB * 100) / 100
            'Available'                = [math]::Floor(((($TotalSize - $TotalUsedSpace) / 1TB)) * 100) / 100
            'Reserve'                  = [math]::Floor(((($PoolDisks | Where-Object { $_.Usage -ne "Journal" } | Select-Object -first 1 | Measure-Object -sum Size).Sum * $Nodes.count / 1TB)) * 100) / 100
            'Physical Disk Redundancy' = $(Get-AzsSupportVirtualDisk -Cluster $ClusterName | select-object -first 1).PhysicalDiskRedundancy
            'Write Cache Size'         = [math]::Floor((($PoolDisks | Where-Object { $_.Usage -eq "Journal" } | Measure-Object -sum Size).Sum / 1TB) * 100) / 100
            'Total Drives'             = $PoolDisks.Count
            'Drive Models'             = ($PoolDisks | Group-Object Model , Size) | Select-Object count, @{Name = 'Model'; Expression = { $_.Name.split(",")[0] } } , @{Name = 'Size (TB)'; Expression = { [long]($_.Name.split(",")[1] / 1TB) } }
            'SupportedComponents'      = $(Get-AzsSupportStorageSubsystem  -CimSession $ClusterName -FriendlyName cluster* | Get-StorageHealthSetting  -Cimsession $ClusterName -name System.Storage.SupportedComponents.Document).Value
        }

        #Trace-AzsSupportCommand -Event OnExit
        Return $StorageUsage
    }
    catch {

        #$formattedException= Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Get-AzsSupportStorageHealthFault {
    <#
        .SYNOPSIS
            Runs Get-HealthFault against the cluster.
 
        .DESCRIPTION
            See https://learn.microsoft.com/en-us/azure/azure-local/manage/health-service-faults?view=azloc-24112.
 
        .PARAMETER Cluster
            The Cluster you want to run against.
 
        .EXAMPLE
            PS> Get-AzsSupportStorageHealthFault
 
        .EXAMPLE
            PS> Get-AzsSupportStorageHealthFault -Cluster "Contoso-cl"
 
        .OUTPUTS
 
            Current Health Faults
 
            Get-AzsSupportStorageHealthFault -Cluster Contoso-cl
                     
            Severity: Degraded/Warning
 
 
            Reason : Parts of the virtual disk have one available copy of data. Failure of a disk, enclosure, node or rack will cause the volume to become unavailable and could lead to data loss. To see a list of physical disks that holding the last copy of data, please use Get-PhysicalDisk -NoRedundancy command.
            Recommendation : Bring online nodes and disks as soon as possible to bring back redundancy.
            Location : Not available
            Description : Virtual disk 'UserStorage_1'
            PSComputerName : Contoso-cl"
    #>


    param (
        [Parameter(Mandatory = $false)]
        $Cluster = (Get-AzsSupportClusterName)
    )

    try {
        #Trace-AzsSupportCommand -Event OnEntry
        Trace-Output -Level:Verbose -Message ($msg.StorageHealthFaultCheck)
        $Result = Get-HealthFault -CimSession $Cluster



        #Trace-AzsSupportCommand -Event OnExit
        return $Result
    }
    catch {
        #$formattedException= Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Get-AzsSupportStorageFirmwareDrift {

    <#
        .SYNOPSIS
            Checks for Firmware Drift in Storage Spaces
 
        .DESCRIPTION
            Checks for models of disk running different firmware
 
        .PARAMETER Cluster
            The Cluster you want to check for firmware drift
 
        .EXAMPLE
            Get-AzsSupportStorageFirmwareDrift -Cluster $ClusterName
 
        .OUTPUTS
            Array of disk models with different firmware versions
 
            Get-AzsSupportStorageFirmwareDrift -Cluster Contoso-cl
 
             Count Model FirmwareVersion
             ----- ----- ---------------
             2 MG07SCA12TEY {AB0C, DE48}
    #>


    param (
        [Parameter(Mandatory = $true)]
        $Cluster
    )

    try {

        #Trace-AzsSupportCommand -Event OnEntry
        $Subsystem = Get-AzsSupportStorageSubsystem -CimSession $Cluster -FriendlyName Cluster*
        $DiskHealth = Get-AzsSupportPhysicalDisk -ComputerName $((Get-AzsSupportInfrastructureHost -Cluster $Cluster).Name) -StorageSubsystem $Subsystem | Select-Object -unique Model, Firmwareversion
        $FWModels = $DiskHealth | Sort-Object Model | Group-Object Model, FW | Select-Object count, @{Name = 'Model'; Expression = { $_.Name.split(",")[0] } } , @{Name = 'FirmwareVersion'; Expression = { $_.Group.FirmwareVersion } }
        $FWModelsResult = $FWModels | Where-Object { $_.Count -gt 1 }

        #Trace-AzsSupportCommand -Event OnExit
        return $FWModelsResult

    }
    catch {

        #$formattedException= Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Get-AzsSupportStorageDiskInfoGraphicDisplay {

    <#
        .SYNOPSIS
            Outputs threshold based graphic for easy identification of disk space issues on CSVs
 
        .DESCRIPTION
            Allows quick ability to visualise an issue with disk space using pre-defined thresholds
         
        .PARAMETER ClusterName
            The Cluster you want to run against
 
        .EXAMPLE
            PS C:\> Get-AzsSupportStorageDiskInfoGraphicDisplay -ClusterName contoso-cl
 
        .OUTPUTS
 
            Graphical display of disk space usage
 
            Used Space < 80% Used Space > 80% Used Space > 90%
 
            Infrastructure_1 67.26% Free
            UserStorage_1 99.24% Free
            UserStorage_2 99.45% Free
    #>


    Param (
        [Parameter(Mandatory = $True)]
        [String]$ClusterName
    )

    try {

        #Trace-AzsSupportCommand -Event OnEntry
        $thresold = 40
        Write-Host "`n"
        Write-Host " " -BackgroundColor Green -NoNewline
        Write-Host $($msg.StorageUsedSpaceLt80Pct) -NoNewline " "
        Write-Host " " -BackgroundColor Yellow -NoNewline
        Write-Host $($msg.StorageUsedSpaceGt80Pct) -NoNewline " "
        Write-Host " " -BackgroundColor Red -NoNewline
        Write-Host $($msg.StorageUsedSpaceGt90Pct) -NoNewline " "
        Write-Host `n

        # Get Cluster Shared Volumes
        foreach ($v in Get-ClusterSharedVolume -cluster $ClusterName) {
            if ($v.State -match 'Online') {
                $usedSize = ($v.SharedVolumeInfo.Partition.size - $v.SharedVolumeInfo.Partition.FreeSpace) / $v.SharedVolumeInfo.Partition.Size
                $freeDisk = $v.SharedVolumeInfo.Partition.FreeSpace / $v.SharedVolumeInfo.Partition.Size
                $percentDisk = "{0:P2}" -f $freeDisk
                Write-Host ([regex]::match($v.name, '\((.*?)\)').Groups[1].Value).PadRight(20) -ForegroundColor White -NoNewline
                switch ($PercentDisk) {
                    { $_ -lt 10 } { Write-Host (" " * ($usedSize * $thresold)) -BackgroundColor Red -NoNewline }
                    { ($_ -gt 10) -and ($_ -lt 20) } { Write-Host (" " * ($usedSize * $thresold)) -BackgroundColor Yellow -NoNewline }
                    { $_ -gt 20 } { Write-Host (" " * ($usedSize * $thresold)) -BackgroundColor Green -NoNewline }
                }
                Write-Host (" " * ($freeDisk * $thresold))  -BackgroundColor White -NoNewline
                Write-Host " " $percentDisk "$($msg.StorageDGDisplayFree)" ""
            }
            else {
                Write-Host "`n"
               
                Write-Host  $($msg.StorageCSVUnexpectedState)
                $v
            }
        }
        #Trace-AzsSupportCommand -Event OnExit
    }
    catch {

        #$formattedException= Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException

    }
}

function Get-AzsSupportStorageDirtyCount {

    <#
        .SYNOPSIS
            Check Dirty Count for Cluster Shared Volumes and only returns if threshold is exceeded.
 
        .DESCRIPTION
            Early indicator check known issue - Virtual Disk is in Detached state with Unknown health status due to DRT full
 
        .PARAMETER ComputerName
            The computer(s) that you want to get dirty counts from that host virtual disks.
 
        .EXAMPLE
            Get-AzsSupportStorageDirtyCount -ComputerName contoso-n01
 
        .OUTPUTS
 
            Disk Dirty Count Threshold
            ---- ----------- ---------
            userstorage_2 - disk 19 1000 255
    #>


    param (
        [Parameter(Mandatory = $true)]
        $ComputerName
    )

    try {

        #Trace-AzsSupportCommand -Event OnEntry


        $DirtyLimit = (Get-Counter -ErrorAction SilentlyContinue -counter "\Storage Spaces Drt(*)\Limit" -ComputerName $ComputerName).countersamples  | Select-Object instancename , cookedvalue
        $DirtyCount = (Get-Counter -ErrorAction SilentlyContinue -counter "\Storage Spaces Drt(*)\Dirty Count" -ComputerName $ComputerName).countersamples | Select-Object instancename , cookedvalue

        $DirtyCountExceeded = foreach ($dirty in $DirtyCount) {

            $Check = $DirtyLimit | Where-Object { ($_.instancename -contains $dirty.InstanceName) -and ($dirty.CookedValue -gt $_.CookedValue) }
            if ($Check) {
                New-Object -TypeName PSCustomObject -Property @{
                    Output = (($dirty | Select-Object @{Name = "Disk"; Expression = { $_.InstanceName } }, @{Name = "Dirty Count"; Expression = { $_.CookedValue } } , @{Name = "Threshold"; Expression = { ($DirtyLimit | Where-Object { $_.instancename -contains $dirty.InstanceName }).cookedvalue } }))
                }
            }
        }
        Return $DirtyCountExceeded.Output
        #Trace-AzsSupportCommand -Event OnExit
    }
    catch {
        #$formattedException= Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Get-AzsSupportStorageCacheDetails {

    <#
        .SYNOPSIS
            Get detailed information on Cache drivers for usage and errors".
 
        .DESCRIPTION
            Retrieves values for Write Errors Total, Write Errors Timeout, Read Errors Total, Read Errors Timeout, Disk Transfers/sec for Cache Stores and Hybrid Disks.
 
        .PARAMETER ComputerName
            The computer(s) that you want to get SBL State information from.
 
        .EXAMPLE
            PS> Get-AzsSupportStorageCacheDetails -ComputerName $nodes
 
        .OUTPUTS
            List of cache disk usage and errors for given computer(s)
 
            Counter Path InstanceName Value
            ------- ---- ------------ -----
            read errors total contoso-n01 0 0
            read errors total contoso-n01 1 0
            read errors total contoso-n01 2 0
            read errors total contoso-n01 3 0
            read errors total contoso-n01 4 0
            read errors total contoso-n01 5 0
 
            ..... etc
    #>


    Param (
        [CmdletBinding()]

        [Parameter(Mandatory = $True)]
        [Array]$ComputerName
    )

    Try {

        #Trace-AzsSupportCommand -Event OnEntry

        $Counters = @(
            "\Cluster Storage Cache Stores(*)\Read Errors Total"
            "\Cluster Storage Cache Stores(*)\Read Errors Timeout"
            "\Cluster Storage Cache Stores(*)\Write Errors Total"
            "\Cluster Storage Cache Stores(*)\Write Errors Timeout"
            "\Cluster Storage Hybrid Disks(*:*)\Disk Transfers/sec"
            "\Cluster Storage Hybrid Disks(*:*)\Read Errors Total"
            "\Cluster Storage Hybrid Disks(*:*)\Read Errors Timeout"
            "\Cluster Storage Hybrid Disks(*:*)\Write Errors Total"
            "\Cluster Storage Hybrid Disks(*:*)\Write Errors Timeout"
        )
        $PerfCounters = (Get-Counter -Counter $Counters -ErrorAction SilentlyContinue -ComputerName $ComputerName  -MaxSamples 1).CounterSamples   | ForEach-Object {

            New-Object -TypeName PSCustomObject -Property @{

                Path         = ($_.Path).Split("\\")[2]
                Counter      = $([regex]::Match($_.Path , '([^\\]+$)').Value)
                InstanceName = $(if ($_.Path -match "Disk Transfers/sec") { $_.InstanceName }  else { $_.InstanceName -replace '\s*:.*' } )
                Value        = if ($_.Path -notmatch "Disk Transfers/sec") { $_.CookedValue }
            }

        }

        Return $PerfCounters

        #Trace-AzsSupportCommand -Event OnExit
    }
    catch {

        #$formattedException= Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Get-AzsSupportStorageDiskSBLState {
    <#
        .SYNOPSIS
            Gets SBL state for disks on given computer(s).
 
        .DESCRIPTION
            Gets SBL state from registry for disks on given computer(s), SBLAttributes , SBLDiskCacheState, SBLCacheUsageCurrent and SBLCacheUsageDesired.
 
        .PARAMETER ComputerName
            The computer(s) that you want to get SBL State information from.
 
        .EXAMPLE
            Get-AzsSupportStorageDiskSBLState -ComputerName $Nodes
 
        .OUTPUTS
            List of SBL state for given computer(s), filtered in example below for a specific disk
 
            $PD=(Get-AzsSupportPhysicalDisk -ComputerName contoso-cl | Where-Object {$_.SerialNumber -like "A1B0C2D4EFGH"} ).PD
            # ComputerName is the name of the computer hosting the disk
            (Get-AzsSupportStorageDiskSBLState -ComputerName contoso-n01)."contoso-n01".$PD
 
            Name Value
            ---- -----
            SBLDiskCacheState 3
            SBLCacheUsageCurrent 2
            SBLCacheUsageDesired
            SBLAttributes 0
    #>


    param (
        [Parameter(Mandatory = $true)]
        $ComputerName
    )


    $session = New-AzsSupportPSSession -ComputerName $ComputerName

    Try {

        #Trace-AzsSupportCommand -Event OnEntry

        Invoke-Command -Session $session -ScriptBlock {

            $ClusBFltDisk = @{ }
            $key = 'HKLM:SYSTEM\CurrentControlSet\Services\ClusBFlt\Parameters\Disks'

            $ClusBFltDisk["$($Env:Computername)"] = @{ }
            $registry = Get-Childitem -Path $key  -Recurse | where-object { $_.pspath -like "Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\ClusBFlt\Parameters\Disks\{*}\State" }


            ForEach ($sub in $registry ) {

                $DeviceGuid = ([regex]::Match($sub.Name, '\{([^]]+)\}').Groups[0].Value)

                $ClusBFlt = @{ }
                $ClusBFlt.SBLAttributes = ($sub | Get-ItemProperty).SblTargetMgrAttributes
                $ClusBFlt.SBLDiskCacheState = ($sub | Get-ItemProperty).DiskCacheState
                $ClusBFlt.SBLCacheUsageCurrent = ($sub | Get-ItemProperty).CacheUsageCurrent
                $ClusBFlt.SBLCacheUsageDesired = ($sub | Get-ItemProperty).CacheUsageDesired
                $ClusBFltDisk["$($Env:Computername)"][$($DeviceGuid)] = @{ }
                $ClusBFltDisk."$($Env:Computername)"."$DeviceGuid" += $ClusBFlt
            }

            return $ClusBFltDisk
        } -AsJob -JobName ($Id = "$([guid]::NewGuid())") | Out-Null

        $ClusBFltDisk = Wait-AzsSupportJob -JobName $Id -Activity "Get-AzsSupportStorageDiskSBLState" -ExecutionTimeOut 60 -PollingInterval 1 -PassThru
        #Trace-AzsSupportCommand -Event OnExit
        return $ClusBFltDisk
    }
    catch {
        #$formattedException= Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Get-AzsSupportStorageSNV {
    <#
        .SYNOPSIS
            Checks the Storage Node views for non healthy disks.
         
        .DESCRIPTION
            Checks the view of Storage Nodes for Storage Node views for non healthy disks.
 
        .EXAMPLE
            PS> Get-AzsSupportStorageSNV -Cluster contoso-cl
 
        .OUTPUTS
            Checks for any abnormalities with the Storage Node views for non healthy disks
 
            Get-AzsSupportStorageSNV -Cluster contoso-cl | Format-Table -AutoSize
 
            SerialNumber OperationalStatus Node Connected DiskNumber HealthStatus
            ------------ ----------------- ---- --------- ---------- ------------
            A1B0C2D4EFGH OK N:Contoso-N01 True 1003 Healthy
            A1B0C2D4EFGH In Maintenance Mode N:contoso-n02 False 1003 Warning
 
    #>


    Param (
        [CmdletBinding()]

        [Parameter(Mandatory = $True)]
        $Cluster
    )

    try {

        #Trace-AzsSupportCommand -Event OnEntry

        $Subsystem = Get-AzsSupportStorageSubsystem -Cimsession $Cluster -FriendlyName cluster*
        $Disks = Get-AzsSupportPhysicalDisk -ComputerName $((Get-AzsSupportInfrastructureHost -Cluster $Cluster).Name) -StorageSubsystem $Subsystem
        $DiskIssues = $Disks | Where-Object { $_.HealthStatus -notlike "Healthy" -or $_.OperationalStatus -notlike "OK" }

        If ($DiskIssues) {

            $DisksSNV = foreach ($Disk in $DiskIssues) {

                $SNVs = Get-PhysicalDiskSNV -CimSession $Cluster -PhysicalDisk $Disk

                foreach ($Obj in $SNVs) {

                    New-Object -TypeName PSCustomObject -Property @{

                        SerialNumber      = $($Disk.SerialNumber)
                        DiskNumber        = $($Obj.DiskNumber)
                        Node              = $($Obj.StorageNodeObjectId.Substring(130).replace('"', ""))
                        Connected         = $($Obj.IsPhysicallyConnected)
                        HealthStatus      = $($Obj.HealthStatus)
                        OperationalStatus = $($Obj.OperationalStatus)

                    }

                }
            }
            
            $SNVCheck = $DisksSNV | Select-Object OperationalStatus, HealthStatus, SerialNumber | Sort-Object | Get-Unique -AsString | Group-Object -Property SerialNumber

            # Look for mismatched states of disks with same serial number
            foreach ($Check in $SNVCheck) {
                if ($Check.count -gt 1) {
                    $DisksSNV | Where-Object { $_.SerialNumber -like $(($Check | where-object { $_.count -ge 2 }).Name) } | Sort-Object Connected -Descending
                } 
            }
            #Trace-AzsSupportCommand -Event OnExit
        }
    }
    catch {
        #$formattedException= Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Get-AzsSupportStoragePhysicalExtent {

    <#
        .SYNOPSIS
            Gets physical allocations for a virtual disk that is unhealty.
 
        .DESCRIPTION
            The "extent" (also known as "allocation" or "slab") is the area on a pooled disk containing one fragment of data for storage space.
 
        .PARAMETER Friendlyname
            The Friendly name of the Virtual Disk that you want to get non active extents for.
 
        .PARAMETER ClusterName
            The Cluster you want to run against.
 
        .EXAMPLE
            Get-AzsSupportStoragePhysicalExtent -Clustername contoso-cl -Friendlyname UserStorage_1
 
        .OUTPUTS
            Outputs key information for troubleshooting unhealthy Virtual Disk extents, providing VirtualDisk, Extents, UniqueDisks and Disks
 
            (Get-AzsSupportStoragePhysicalExtent -Clustername contoso-cl -Friendlyname Infrastructure_1).Extents
 
            PhysicalDiskUniqueId OperationalStatus OperationalDetails
            -------- ------------ -----------------
            eui.ABC52D0055692058 Stale Metadata {IO Error}
 
            (Get-AzsSupportStoragePhysicalExtent -Clustername contoso-cl -Friendlyname Infrastructure_1).UniqueDisks
 
            ColumnNumber : 1
            CopyNumber : 2
            Flags : 0x0000000000000000
            OperationalDetails : {IO Error}
            OperationalStatus : Stale Metadata
            PhysicalDiskOffset : 82141249536
            PhysicalDiskUniqueId : eui.ABC52D0055692058
            ReplacementCopyNumber :
            Size : 1073741824
            StorageTierUniqueId :
            VirtualDiskOffset : 154618822656
            VirtualDiskUniqueId : 66C74EBBE4A8954FB33F362DCBED4BA6
 
    #>


    Param (
        [Parameter(Mandatory = $True)]
        $Friendlyname,
        [Parameter(Mandatory = $true)]
        $ClusterName
    )

    try {

        #Trace-AzsSupportCommand -Event OnEntry

        $VirtualDisk = Get-AzsSupportVirtualDisk -Cluster $ClusterName -FriendlyName $Friendlyname

     
        # Running as job because cmdlet get-physicalextent doesn't filter before pulling data back wheen using CIM method GetPhysicalExtent
        $ScriptBlock = {
            Get-PhysicalExtent -Virtualdisk $Args[0] -CimSession $ClusterName | Where-Object { $_.OperationalStatus -notlike 'Active' } | Sort-Object PhysicalDiskUniqueId, OperationalStatus -unique
        }

        Start-Job -ScriptBlock $ScriptBlock -ArgumentList $VirtualDisk -Name ($Id = "$([guid]::NewGuid())") | Out-Null

        $JobOutput = Wait-AzsSupportJob -JobName $Id -Activity "Get-AzsSupportStorageDiskSBLState" -ExecutionTimeOut 60 -PollingInterval 1 -PassThru

        if ($JobOutput) {

            $VirtDiskFaultReason = "" | Select-Object   VirtualDisk, Extents, UniqueDisks, Disks
            $VirtDiskFaultReason.VirtualDisk = $VirtualDisk
            $VirtDiskFaultReason.Extents = $JobOutput | Select-Object PhysicalDiskUniqueId, OperationalStatus, OperationalDetails | Format-Table
            $VirtDiskFaultReason.UniqueDisks = $JobOutput | Sort-Object PhysicalDiskUniqueId -unique
            #This has to come from physical disk to ensure cables were not swapped
            $VirtDiskFaultReason.Disks = foreach ($drive in $($VirtDiskFaultReason.UniqueDisks.PhysicalDiskUniqueId)) {
 
                $Disk = Get-AzsSupportPhysicalDisk  -ComputerName $((Get-AzsSupportInfrastructureHost -Cluster $ClusterName).Name) -PD $drive | Select-Object SerialNumber, SlotNumber, FruId
    
                if ($null -eq $Disk) {

                    Get-AzsSupportPhysicalDisk  -ComputerName $((Get-AzsSupportInfrastructureHost -Cluster $ClusterName).Name) -UniqueId $drive | Select-Object SerialNumber, SlotNumber, FruId

                }
                else {
                    $Disk
                }
            }
                        

            #Trace-AzsSupportCommand -Event OnExit
            return $VirtDiskFaultReason
        }

        
        #Trace-AzsSupportCommand -Event OnExit
    }
    catch {

        #$formattedException= Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException

    }
}

function Get-AzsSupportStorageSupportedComponents {

    <#
        .SYNOPSIS
            Checks for supported firmware and hardware in Storage Spaces
 
        .DESCRIPTION
            Gets supported componants on storage spaces
 
        .PARAMETER Cluster
            The cluster you want to get rhe current supported commponents from
 
        .EXAMPLE
            Get-AzsSupportStorageSupportedComponents -Cluster contoso-cl
 
        .OUTPUTS
            Outputs supported components for the cluster
 
            (Get-AzsSupportStorageSupportedComponents -Cluster contoso-cl).OrigSupportedComponents
 
            Manufacturer : .*
            Model : Dell NVMe PE8010 RI M.2 1.92TB
            AllowedFirmware :
            TargetFirmware : 1.3.0
            Path : D:\CloudContent\Microsoft_Reserved\DriveFirmware\Hynix\1.3.0.bin
 
            ... etc
 
    #>


    param (
        [Parameter(Mandatory = $true)]
        $Cluster
    )

    try {

        # Trace-AzsSupportCommand -Event OnEntry

        $Missing = [System.Collections.ArrayList]::new()

        Trace-Output -Level:Verbose -Message ($msg.StorageSupportedComponents)
        [xml]$SupportedComponents = (Get-AzsSupportStorageSubsystem -CimSession $Cluster -FriendlyName cluster* | Get-StorageHealthSetting -name System.Storage.SupportedComponents.Document -CimSession $Cluster).Value
        $InputsNode = $SupportedComponents.Components.SelectNodes('//Disk')
        $OrigSupportedComponent = ($inputsNode | Select-Object Manufacturer, Model, @{N = 'AllowedFirmware'; E = { $_.AllowedFirmware.Version } }, @{N = 'TargetFirmware'; E = { $_.TargetFirmware.Version } }, @{N = 'Path'; E = { $_.TargetFirmware.BinaryPath } })


        $SuppCompResult = [PSCustomObject]@{
            OrigSupportedComponents = $OrigSupportedComponent

        }

        if ($($SupportedComponents.InnerXml) -contains "<Components><Disks><Disk><Manufacturer></Manufacturer><Model></Model><AllowedFirmware><Version></Version></AllowedFirmware></Disk></Disks><Cache></Cache></Components>" -or $null -eq $($SupportedComponents.InnerXml)) {
            Trace-Output -Level:Verbose -Message ($msg.StorageSupportedComponentsState)
        }
        else {

            Trace-Output -Level:Verbose -Message ($msg.StorageSupportedComponentsExtract)
            $SupportedModelFW = $SupportedComponents.SelectNodes('//Disk') | Select-Object Model, @{N = 'FirmwareVersion'; E = { $_.AllowedFirmware.Version } }
            Trace-Output -Level:Verbose -Message ($msg.StorageInstalledDisks)
           
            $Subsystem = Get-AzsSupportStorageSubsystem -CimSession $Cluster -FriendlyName cluster* 
            $InstalledDisks = Get-AzsSupportPhysicalDisk -ComputerName $((Get-AzsSupportInfrastructureHost -Cluster $Cluster).Name) -StorageSubsystem $Subsystem | Select-Object -unique Model, Firmwareversion

            if (-not ([string]::IsNullOrEmpty($SupportedModelFW.Model))) {
                foreach ($Installed in $InstalledDisks) {

                    Trace-Output -Level:Verbose -Message ($msg.StorageSupportedComponentsCheck)

                    if ($SupportedModelFW.model -notcontains $Installed.model -or $SupportedModelFW.Firmwareversion -notcontains $Installed.FirmwareVersion -and $SupportedModelFW.Firmwareversion -notmatch $null) {

                        Trace-Output -Level:Verbose -Message ($msg.StorageSupportedComponentsMissing)
                        [xml]$newNode = "<Disk><Manufacturer>.*</Manufacturer><Model>$($Installed.Model)</Model><AllowedFirmware><Version>$($Installed.FirmwareVersion)</Version></AllowedFirmware></Disk>"
                        $SupportedComponents.Components.disks.AppendChild($SupportedComponents.ImportNode(($Newnode.Disk), $true)) | Out-Null
                        $Missing.Add($Installed) | Out-Null

                        $SuppCompResult.Missing = $Missing
                        $SuppCompResult.SupportedComponents = $SupportedComponents

                    }
                }
            }
        }


        # Trace-AzsSupportCommand -Event OnExit
        Return $SuppCompResult
    }
    catch {
        #$formattedException = Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Get-AzsStorageDiskPnpId {

    <#
        .SYNOPSIS
            Gets PNP information for disks that should be in Storage Spaces
        .DESCRIPTION
            Gets Pnp Id for only disks that should be in Storage Spaces
        .PARAMETER ComputerName
            The computer(s) that you want to get Pnp Id information from
        .EXAMPLE
            Get-AzsStorageDiskPnpId -ComputerName $Nodes
        .OUTPUTS
 
        Data : SCSI\Disk&Ven_NVMe&Prod_Dell_NVMe_PE8110\5&115bc00c&0&000000
        HasProblem : False
        LastArrivalDate : 2/21/2025 10:33:44 AM
        DevNodeStatus : 25174026
        ComputerName : contoso-n01
        ProblemCode : 0
        IsPresent : True
        PSComputerName : contoso-n01.contoso.lab
 
        etc...
         
    #>


    param (
        [Parameter(Mandatory = $true)]
        $ComputerName
    )

    try {

        #Trace-AzsSupportCommand -Event OnEntry

        $session = New-AzsSupportPSSession -ComputerName $ComputerName

        Invoke-Command -Session $session -ScriptBlock {

            
            $OSDiskDiskIndex = Get-WMIObject Win32_LogicalDisk | Foreach-Object {
                Get-WmiObject -Query "Associators of {Win32_LogicalDisk.DeviceID='$($_.DeviceID)'} WHERE ResultRole=Antecedent"
            } | Select-Object -Unique -ExpandProperty Diskindex


            $OSDiskPNPs = foreach ($DiskIndex in $OSDiskDiskIndex) {

                New-Object -TypeName PSCustomObject -Property @{
                    Id = ((Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\disk\Enum).$DiskIndex)
                }
            }

            $PNPEnclosures = (Get-PnpDevice -class SCSIAdapter | Where-Object { $_.instanceid -like "PCI*" -and $_.Friendlyname -notlike "*RAID*" }).Instanceid

            if ($PNPEnclosures) {
                foreach ($PNPEnclosure in $PNPEnclosures) {

                    $Children = ((Get-PnpDevice -instanceid $PNPEnclosure | Get-PnpDeviceProperty -KeyName DEVPKEY_Device_children).Data | Where-Object { $Null -ne $_ -and $_ -notlike "*SCSI\Enclosure*" })

                    foreach ($PNPDisk in $Children) {
                   
                   
                        if ($PNPDisk -notlike "Virtual" `
                                -and $PNPDisk -notlike "Enclosure" `
                                -and $PNPDisk -notlike "Dummy" `
                                -and $PNPDisk -notlike "SCSI_COMMUNICATE" `
                                -and $PNPDisk -notlike "LOGICAL_VOLUME"  `
                                -and $PNPDisk -notlike $OSDiskPNPs.Id
                        ) {
                            $PNPDiskProperties = Get-PnpDeviceProperty -InstanceId $PNPDisk -KeyName DEVPKEY_Device_Address, DEVPKEY_Device_InstanceId , DEVPKEY_Device_LastArrivalDate, DEVPKEY_Device_IsPresent , DEVPKEY_Device_HasProblem, DEVPKEY_Device_ProblemCode , DEVPKEY_Device_DevNodeStatus
                    
                            New-Object -TypeName PSCustomObject -Property @{
                    
                                Data            = $PNPDisk
                                ComputerName    = $Env:ComputerName
                                LastArrivalDate = ($PNPDiskProperties | Where-Object { $_.KeyName -like "DEVPKEY_Device_LastArrivalDate" }).Data
                                IsPresent       = ($PNPDiskProperties | Where-Object { $_.KeyName -like "DEVPKEY_Device_IsPresent" }).Data
                                HasProblem      = ($PNPDiskProperties | Where-Object { $_.KeyName -like "DEVPKEY_Device_HasProblem" }).Data
                                ProblemCode     = ($PNPDiskProperties | Where-Object { $_.KeyName -like "DEVPKEY_Device_ProblemCode" }).Data
                                DevNodeStatus   = ($PNPDiskProperties | Where-Object { $_.KeyName -like "DEVPKEY_Device_DevNodeStatus" }).Data
                            }
                        }
                    }
                }
            }
            else {
                Trace-Output -Level:Verbose -Message ($msg.StorageControllerMissing)

                Trace-Output -Level:Verbose -Message ($msg.StorageVirtualDiskCheck)
                $ClusPortDeviceInformation = Get-WmiObject -ErrorAction Stop -Namespace root\wmi ClusPortDeviceInformation | Where-Object { $_.ConnectedNode -like $env:computername }

                $filteredDisks = $ClusPortDeviceInformation | Sort-Object ConnectedNode, ConnectedNodeDeviceNumber | Where-Object {
                    # non-default (enclosure) and non-virtual devices
                    $_.DeviceAttribute -and -not ($_.DeviceAttribute -band 0x1) }


                If ($filteredDisks) {
                    $FilteredPNPDisks = ForEach ($Disk in $filteredDisks) {

                        $Path = "HKLM:\SYSTEM\CurrentControlSet\Services\disk\enum"
                        $DRDisks = (Get-ItemProperty $path).psobject.properties | Where-Object { $_.name -notmatch 'Count|NextInstance|PS*' -and $OSDiskPNPs.id -notcontains $_.value } | Select-Object name, value


                        New-Object -TypeName PSCustomObject -Property @{

                            PNPDeviceid = (($DRDisks | Where-Object { $_.Name -eq $Disk.ConnectedNodeDeviceNumber }).Value)

                        }
                    }

                    $PNPDisks = forEach ($PNPDisk in $($FilteredPNPDisks.PNPDeviceid | Where-Object { $_ -notmatch "Enclosure|Dummy|SCSI_COMMUNICATE|LOGICAL_VOLUME" -and $OSDisksPNPs.id -notcontains $_ })) {

                        $PNPDiskProperties = Get-PnpDeviceProperty -InstanceId $PNPDisk -KeyName DEVPKEY_Device_Address, DEVPKEY_Device_InstanceId , DEVPKEY_Device_LastArrivalDate, DEVPKEY_Device_IsPresent , DEVPKEY_Device_HasProblem, DEVPKEY_Device_ProblemCode , DEVPKEY_Device_DevNodeStatus

                        New-Object -TypeName PSCustomObject -Property @{

                            Data            = $PNPDisk
                            ComputerName    = $Env:ComputerName
                            LastArrivalDate = ($PNPDiskProperties | Where-Object { $_.KeyName -like "DEVPKEY_Device_LastArrivalDate" }).Data
                            IsPresent       = ($PNPDiskProperties | Where-Object { $_.KeyName -like "DEVPKEY_Device_IsPresent" }).Data
                            HasProblem      = ($PNPDiskProperties | Where-Object { $_.KeyName -like "DEVPKEY_Device_HasProblem" }).Data
                            ProblemCode     = ($PNPDiskProperties | Where-Object { $_.KeyName -like "DEVPKEY_Device_ProblemCode" }).Data
                            DevNodeStatus   = ($PNPDiskProperties | Where-Object { $_.KeyName -like "DEVPKEY_Device_DevNodeStatus" }).Data

                        }

                    }
                    $PNPDisks
                }
                else {
                    Trace-Output -Level:Verbose -Message ($msg.StorageNoClusterDisks)
                }
            }


            #Trace-AzsSupportCommand -Event OnExit
        
        } -AsJob -JobName ($Id = "$([guid]::NewGuid())") | Out-Null

        $PNPDisksOutput = Wait-AzsSupportJob -JobName $Id -Activity "Get-AzsStorageDiskOSPnpId" -ExecutionTimeOut 60 -PollingInterval 1 -PassThru
        return   $PNPDisksOutput
    }
    catch {

        #$formattedException = Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException

    }
}

function Get-AzsSupportStorageMissingDisks {

    <#
        .SYNOPSIS
            Checks for Missing Disks in Storage Spaces
 
        .DESCRIPTION
            Checks for any disks that are in PNP that are eligible for Storage Spaces but not added
 
        .PARAMETER Cluster
            The Cluster you want to check for Missing Disks
 
        .EXAMPLE
            Get-AzsSupportStorageMissingDisks -Cluster contoso-cl
 
        .OUTPUTS
            Outputs a comparison of disks in PNP and Storage Spaces
 
            OS sees 7 disks and Storage Spaces sees 8 in Pool, please check disk output
    #>


    param (
        [Parameter(Mandatory = $true)]
        $Cluster
    )

    try {

        #Trace-AzsSupportCommand -Event OnEntry

        $NonPrimordial = Get-AzsSupportStoragePool -Cluster $Cluster -IsPrimordial $false
        $PoolDisks     = Get-AzsSupportPhysicalDisk -ComputerName $((Get-AzsSupportInfrastructureHost -Cluster $Cluster).Name) -StoragePool $NonPrimordial
        $PNPDisksonEnc = Get-AzsStorageDiskPnpId -ComputerName $((Get-AzsSupportInfrastructureHost -Cluster $Cluster).Name)  | Where-Object { $null -ne $_.Data -and $_.PSComputerName -notlike $ClusterName } | Select-Object Data, ComputerName, LastArrivalDate, IsPresent, HasProblem, ProblemCode, DevNodeStatus

        try {
            Trace-Output -Level:Verbose -Message ($msg.StorageCheckPNPDisksEligible -f $env:computername)
            if ($PNPDisksonEnc) {
                $CheckCount = $($PoolDisks.count) -match $PNPDisksonEnc.count
            }
        }
        Catch {
            Trace-Output -Level:Exception -Message ($msg.StoragePNPFailComparison -f $($_.Exception.Message))
    
        }

        if ($CheckCount -eq $False) {
            $MissingDisks = "OS sees $($PNPDisksonEnc.count) disks and Storage Spaces sees $($PoolDisks.count) in Pool, please check disk output"
        }

        #Trace-AzsSupportCommand -Event OnExit
        return $MissingDisks

    }
    catch {

        #$formattedException = Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Get-AzsSupportStorageDiskLatency {
    <#
        .SYNOPSIS
          Checks for disk latency over x seconds
 
        .DESCRIPTION
            Checks for disk latency over x seconds for a given time period for each IO operation. Restricted to above 2 seconds to control output
 
        .PARAMETER ClusterName
          Name of the cluster you want to run against
 
        .PARAMETER Latency
          The value you would like to check if breached, accepted values "2s", "6s", "10s", "20s", ">20s"
 
        .PARAMETER StartTime
          When you would like to start looking from
 
        .PARAMETER EndTime
          When you would like to end looking to
 
        .EXAMPLE
          PS> $LatencyBreach = "10s"
          PS> $StartTime = (Get-Date).AddDays(-2)
          PS> $EndTime = (Get-Date).AddDays(-1)
          PS> Get-AzsSupportStorageDiskLatency -ClusterName contoso-cl -Latency $LatencyBreach -StartTime $StartTime -EndTime $EndTime | Sort-Object OccurrenceTime | Format-Table -AutoSize
        
          .OUTPUTS
         Lists all disks that meet latency criteria set.
         
          MachineName Serial OccurrenceTime Vendor 128ms 256ms 2s 6s 10s 20s
          ----------- ------ -------------- ------ ----- ----- -- -- --- ---
          contoso-n02 A1B0C2D4EFGH 2/20/2025 6:10:14 PM stornvme 952 769 294 1 2 0
 
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $True)]
        [String]$ClusterName,
        [Parameter(Mandatory = $False)]
        [ValidateSet("2s", "6s", "10s", "20s", ">20s")]
        [String]$Latency,
        [Parameter(Mandatory = $True)]
        [DateTime]$StartTime,
        [Parameter(Mandatory = $True)]
        [DateTime]$Endtime
    )
    try {

        #Trace-AzsSupportCommand -Event OnEntry
        $Nodes = Get-AzsSupportInfrastructureHost -Cluster $ClusterName

        $StorportOpLogName = 'Microsoft-Windows-Storage-Storport/Operational'
        $StorPortOpSource = 'Microsoft-Windows-StorPort'
        $StorPortOpId = 505

        $LatencyLookup = @{
            '2s'   = "38"                            # latency in $_.Properties[38].value
            '6s'   = "39"                            # latency in $_.Properties[39].value
            '10s'  = "40"                            # latency in $_.Properties[40].value
            '20s'  = "41"                            # latency in $_.Properties[41].value
            '>20s' = "42"                            # latency in $_.Properties[42].value
        }

        $AllLatency = foreach ($node in $nodes) {

            # Microsoft-Windows-Storage-Storport/Operational
            $StorPortEvents = (Get-WinEvent -ComputerName $($Node.Name) -ErrorAction SilentlyContinue -FilterHashtable @{LogName = $StorportOpLogName; ProviderName = $StorPortOpSource; ID = $StorPortOpId; StartTime = $StartTime ; EndTime = $EndTime } | Select-Object MachineName, Properties, TimeCreated)
            
            # Check for anything for latencyBreach value
            $StorPortEvents = $StorPortEvents | where-object { ($_.Properties[$LatencyLookup[$Latency]].value) -gt 0 }
            Foreach ($StorPortEvent in $StorPortEvents) {

 
                New-Object -TypeName PSCustomObject -Property @{
                    MachineName    = ($StorPortEvent.MachineName).split('.')[0]
                    Serial         = (($StorPortEvent.Properties)[11].Value).trim()          
                    OccurrenceTime = $StorPortEvent.TimeCreated
                    'Vendor'       = ($StorPortEvent.Properties)[7].Value
                    '256us'        = ($StorPortEvent.Properties)[31].Value
                    '1ms'          = ($StorPortEvent.Properties)[32].Value
                    '4ms'          = ($StorPortEvent.Properties)[33].Value
                    '16ms'         = ($StorPortEvent.Properties)[34].Value
                    '64ms'         = ($StorPortEvent.Properties)[35].Value
                    '128ms'        = ($StorPortEvent.Properties)[36].Value
                    '256ms'        = ($StorPortEvent.Properties)[37].Value
                    '2s'           = ($StorPortEvent.Properties)[38].Value
                    '6s'           = ($StorPortEvent.Properties)[39].Value
                    '10s'          = ($StorPortEvent.Properties)[40].Value
                    '20s'          = ($StorPortEvent.Properties)[41].Value
                    '>20s'         = ($StorPortEvent.Properties)[42].Value
                }
            }
        }
        #Trace-AzsSupportCommand -Event OnExit
        return $AllLatency | Select-Object -Unique  MachineName, Serial, OccurrenceTime, Vendor, 128ms, 256ms, 2s, 6s, 10s, 20s, '>20s'
    }
    catch {

        #$formattedException = Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Start-AzsSupportStorageDiagnostic {

    <#
        .SYNOPSIS
            Runs a series of storage specific diagnostic tests and generates a storage report.
 
        .DESCRIPTION
            The script checks storage against known issues
 
        .EXAMPLE
            PS C:\>Start-AzsSupportStorageDiagnostic
 
        .EXAMPLE
            PS C:\>Start-AzsSupportStorageDiagnostic -PhysicalExtentCheck
 
        .EXAMPLE
            PS C:\>Start-AzsSupportStorageDiagnostic -CacheResultTable
 
 
        .PARAMETER ClusterName
            ClusterName you wish to run the Storage Diagnostics on. If not provided, the script will attempt to get the cluster name.
 
        .PARAMETER PhysicalExtentCheck
            Enables checking of the Virtual disks physical extents.
 
        .PARAMETER CacheResultTable
            Enables caching the Result Table for utilising data externally.
         
        .PARAMETER Include
            Allows user to specify which tests to run. By default all tests are run.
 
        .OUTPUTS
            Outputs a storage report with the results of the tests run.
    #>


    param
    (
        [parameter(mandatory = $false, HelpMessage = "Please provide Cluster to run Storage Diagnostics on")]
        [string]$ClusterName,
        [parameter(mandatory = $false, HelpMessage = "Check Virtual Disk for physical extent")]
        [string]$PhysicalExtentCheck,
        [parameter(mandatory = $false, HelpMessage = "Cache Result Table for utilising data externally")]
        [switch]$CacheResultTable,
        [parameter(Mandatory = $false, HelpMessage = "List of tests to include in the run. By default all tests are run.")]
        [ArgumentCompleter( { "CSVUsage", "DiskHealth", "StorageSummary", "StorageComponents", "DirtyCount", "VirtualDisks", "MissingDisks", "SNV", "FirmwareDrift", "SMPHost", "SMPHostIssue", "StorageHealth" })]
        [ValidateScript( { $_ -in "CSVUsage", "DiskHealth", "StorageSummary", "StorageComponents", "DirtyCount", "VirtualDisks", "MissingDisks", "SNV", "FirmwareDrift", "SMPHost", "SMPHostIssue", "StorageHealth" })]
        [string[]]$Include
    )


    try {

        $workingDir = Get-WorkingDirectory
        if (-NOT (Test-Path -Path $workingDir -PathType Container)) {
            $null = New-Item -Path $workingDir -ItemType Directory -Force
        }

        # build the trace file path and set global variable
        [System.String]$fileName = "Start-AzsSupportStorageDiagnostic_TraceOutput_{0}.csv" -f (Get-Date).ToString('yyyyMMdd')
        [System.IO.FileInfo]$filePath = Join-Path -Path $workingDir -ChildPath $fileName

        # Start telemetry at the beginning of the script
        Start-Transcript -path $filePath | Out-Null

        #Trace-AzsSupportCommand -Event OnEntry
        Trace-Output -Level:Verbose -Message ($msg.StorageSetupVariables)

        # If no cluster name is provided, attempt to get the cluster name
        if (!$PSBoundParameters['ClusterName']) {
            $ClusterName = Get-AzsSupportClusterName 
        }
        else {
            #Ensure the cluster name is correct
            $ClusterName = Get-AzsSupportClusterName -Name $ClusterName
        }

        # If no cluster name is found, return
        if ($null -eq $ClusterName) {
            Trace-Output -Level:Exception -Message ($msg.StorageClusterName)
            Return
        }

        $StorageCheck = @{ }
        $StorageCheck["Validation"] = @{ }
        $StorageHealth = @{ }
        Trace-Output -Level:Verbose -Message ($msg.StorageClusterNode -f $ClusterName)
        $Nodes = (Get-AzsSupportInfrastructureHost -Cluster $ClusterName).Name

        $ResultQuickPing = Test-AzsSupportQuickPing -ComputerName $Nodes -Status SuccessOnly
        $NodesReachable = ($ResultQuickPing).Name
        Trace-Output -Level:Verbose -Message ($msg.StorageNodesReachable -f $NodesReachable)

        #===================================================================
        # Data Gathering:
        #===================================================================


        # Get Cluster Shared Volume Usage
        if ($Include -contains "CSVUsage" -or !$Include) {
            Trace-Output -Level:Verbose -Message ($msg.StorageGetCSVUsage)
          
            Write-Host $($msg.StorageCSVUsage) -ForeGroundColor Yellow
            Trace-Output -Level:Verbose -Message ($msg.StorageCSVOutput)
            Get-AzsSupportStorageDiskInfoGraphicDisplay -ClusterName $ClusterName
            Write-Host `r `n
        }


        # Get Disks Per Node
        if ($Include -contains "DiskHealth" -or !$Include) {
            Trace-Output -Level:Verbose -Message ($msg.StorageDiskHealthDetails)
            $DiskHealth = Get-AzsSupportStorageDiskHealth -ComputerName $Nodes

        }


        # Storage Components Check
        if ($Include -contains "StorageComponents" -or !$Include) {
            Trace-Output -Level:Verbose -Message ($msg.StorageComponentsDetails)
            $SuppCompResult = Get-AzsSupportStorageSupportedComponents -Cluster $ClusterName
            $NewSuppComp = New-AzsSupportStorageSupportedComponents -Cluster $ClusterName
            $StorageHealth.Add('SupportComponentsChange', $($NewSuppComp.SupportedComponents))
            $StorageHealth.Add('SupportComponentsMissing', $($NewSuppComp.Missing))
            $StorageHealth.Add('SupportedComponents', $($SuppCompResult.OrigSupportedComponents))
        }

        # Dirty Count Check
        if ($Include -contains "DirtyCount" -or !$Include) {
            Trace-Output -Level:Verbose -Message ($msg.StoragePerformanceDirtyCount)
            $DirtyCountExceeded = Get-AzsSupportStorageDirtyCount -ComputerName $Nodes
            $StorageHealth.Add('Dirty Count', $DirtyCountExceeded)
        }


        # Virtual Disks Check
        if ($Include -contains "VirtualDisks" -or !$Include) {
            Trace-Output -Level:Verbose -Message ($msg.StorageUnhealthyVirtualDisks)
            $VirtualDisks = Get-AzsSupportVirtualDisk -Cluster $ClusterName | Where-Object { $_.HealthStatus -ne 'Healthy' }
            $StorageHealth.Add('Virtual Disk' , $VirtualDisks )
        }


        # Missing Disks Check
        if ($Include -contains "MissingDisks" -or !$Include) {
            Trace-Output -Level:Verbose -Message ($msg.StoragePNPDisks)
            $MissingDisks = Get-AzsSupportStorageMissingDisks -Cluster $ClusterName
            $StorageHealth.Add('Missing Disks' , $MissingDisks )
        }


        # Check Storage Node View
        if ($Include -contains "SNV" -or !$Include) {
            Trace-Output -Level:Verbose -Message ($msg.StorageSNVCheck)
            #Get count of nodes
            $NodeCount = (Get-AzsSupportClusterNode -Cluster $ClusterName).count
            #Get all disks showing as unhealthy
            $SNVFaultyDisks = Get-AzsSupportStorageSNV -Cluster $ClusterName | Where-Object { $_.OperationalStatus -notlike "OK" -or $_.HealthStatus -notlike "Healthy" } | Sort-Object SerialNumber
            #Get count of unique serial numbers`
            $CountofSerial = ($SNVFaultyDisks | Select-Object -Unique SerialNumber | Group-Object SerialNumber).count

            # Check if multiple disks have issues
            if ($CountofSerial -gt 1) {
                $FaultySerials = foreach ($SerialNumber in $(($SNVFaultydisks | Select-Object -unique SerialNumber).SerialNumber)) {
                    
                    if ($(($SNVFaultyDisks | Where-Object { $_.SerialNumber -eq $serialnumber }).SerialNumber).count -lt $NodeCount) {

                        $SerialNumber

                    }

                }
            }
            elseif ($(($SNVFaultyDisks | Where-Object { $_.SerialNumber -eq $serialnumber }).SerialNumber).count -lt $NodeCount) {

                $FaultySerials = ($SNVFaultyDisks | Select-Object -Unique SerialNumber).SerialNumber
    
            }
            
            if ($FaultySerials) {
                $StorageHealth.Add('SNVFaultyDisk', $($SNVFaultyDisks | Where-Object { $_.SerialNumber -in $FaultySerials }))
            }
            Clear-Variable -Name FaultySerials -ErrorAction SilentlyContinue
        }

        
        # Get Storage Summary
        if ($Include -contains "StorageSummary" -or !$Include) {
            Trace-Output -Level:Verbose -Message ($msg.StorageSummary)
            $BootTimes = $(Get-WmiObject -class win32_operatingsystem -ComputerName $NodesReachable | Select-Object  @{LABEL = 'ServerName'; EXPRESSION = { $_.csname } }, @{LABEL = 'LastBootUpTime'; EXPRESSION = { $_.ConverttoDateTime($_.lastbootuptime) } })
           
            # All Nodes are aware of all Storage Nodes so just use one to pull the data
            $StorageNodesReachable = Get-AzsSupportStorageNode -Node $NodesReachable[0] | Select-Object Name, Model, SerialNumber, Manufacturer -Unique


            $StorageNodesConfig = foreach ($BootTime in $BootTimes) {
                New-Object -TypeName PSCustomObject -Property @{
                    ServerName     = $BootTime.ServerName
                    LastBootUpTime = $BootTime.LastBootUpTime
                    Model          = ($StorageNodesReachable  | Where-Object { $_.Name -like "$($BootTime.ServerName)*" }).Model
                    Manufacturer   = ($StorageNodesReachable | Where-Object { $_.Name -like "$($BootTime.ServerName)*" }).Manufacturer 
                    SerialNumber   = ($StorageNodesReachable | Where-Object { $_.Name -like "$($BootTime.ServerName)*" }).SerialNumber 
                }
            }

            $StorageHealth.Add('StorageNodesConfig' , $(($StorageNodesConfig | Sort-Object ServerName)))     
            $StorageHealth.Add('StorageVolumeConfiguration', $( Get-AzsSupportStoragePool -Cluster $ClusterName -IsPrimordial $False | Get-Volume | Select-Object  FileSystemLabel, FileSystem))
            $StorageHealth.Add('StorageVirtualDiskConfiguration', $(Get-AzsSupportVirtualDisk -Cluster $ClusterName  | Select-Object  FriendlyName, Usage, ProvisioningType, NumberOfColumns, Interleave, NumberofDataCopies, MinimumLogicalDataCopies, ResiliencySettingName, @{LABEL = 'StorageEfficiency'; EXPRESSION = { ($_.AllocatedSize / $_.FootprintOnPool).ToString("P") } }))
            $StorageHealth.Add('StoragePoolConfiguration', $(Get-AzsSupportStoragePool -Cluster $ClusterName -IsPrimordial $false | Select-Object FriendlyName, ProvisioningTypeDefault, RetireMissingPhysicalDisks, RepairPolicy, ResiliencySettingNameDefault))
            $StorageHealth.Add('SpacesDirectConfiguration', $(Get-ClusterStorageSpacesDirect -Cimsession $ClusterName -WarningAction SilentlyContinue | Select-Object Name, CacheState, CacheModeHDD, CacheModeSSD, State))
            
        }


        # Check For Firmware Drift
        if ($Include -contains "FirmwareDrift" -or !$Include) {
            Trace-Output -Level:Verbose -Message ($msg.StorageCheckFirmwareDrift)
            $FWModelsResult = Get-AzsSupportStorageFirmwareDrift -Cluster $ClusterName
            $StorageHealth.Add('FWDrift' , $FWModelsResult)
        }


        # Check SMPHost
        if ($Include -contains "SMPHost" -or !$Include) {
            Trace-Output -Level:Verbose -Message ($msg.StorageCheckSMPHost)
            $ServiceObjects = Get-AzsSupportService -ComputerName $nodes -Name smphost | Select-Object ComputerName, ProcessId

            $SMPHost = foreach ($serviceObject in $ServiceObjects) {
                New-Object -TypeName PSCustomObject -Property @{
                    Process = ($(Get-AzsSupportProcess -ProcessId $($ServiceObject.ProcessId) -ComputerName $ServiceObject.ComputerName  | Select-Object WorkingSetMB, ComputerName ))
                }
            }

            $StorageHealth.Add('SMPHost Running', $SMPHost.Process)
        }


        # Check for SMPHost Issue
        if ($Include -contains "SMPHostIssue" -or !$Include) {
            Trace-Output -Level:Verbose -Message ($msg.StorageCheckSMPHostIssue)
            $detachedVirtualDisks = Get-AzsSupportVirtualDisk -Cluster $ClusterName | Where-Object { $_.OperationalStatus -eq 'Detached' }
            if ($detachedVirtualDisks) {
                $offlineClusterSharedVolumes = Get-ClusterSharedVolume -Cluster $clusterName | Where-Object { $_.State -ne 'Online' }
                if (!$offlineClusterSharedVolumes) {
                    $SMPHostIssue = $True
                }
            }
            $StorageHealth.Add('SMPHost Issue', $SMPHostIssue  )
        }


        # Add Extents Check
        if ($PhysicalExtentCheck) {
            Trace-Output -Level:Verbose -Message ($msg.StoragePhysicalExtentCheck)
            $StorageHealth.Add( 'VirtPhysicalExtents', $(Get-AzsSupportStoragePhysicalExtent -FriendlyName $PhysicalExtentCheck -ClusterName $ClusterName))
        }


        # Check Storage Health
        if ($Include -contains "StorageHealth" -or !$Include) {
            Trace-Output -Level:Verbose -Message ($msg.StorageCollatingStorageHealth)
            $StoragePoolDisks = (Get-AzsSupportPhysicalDisk -StoragePool $(Get-AzsSupportStoragePool -Cluster $ClusterName -IsPrimordial $false)).SerialNumber | Sort-Object $_

            $ClusterDisks = Get-AzsSupportPhysicalDisk | Where-Object { ($_.MediaType -notlike "Unspecified") } | Sort-Object SerialNumber

            if (!$ClusterDisks) {
                Trace-Output -Level:Verbose -Message ($msg.StorageNoPhysicalDisks)
                $ClusterDisks = Get-AzsSupportPhysicalDisk | Where-Object { ($_.Friendlyname -like 'Msft Virtual Disk') -and ($_.MediaType -notlike "Unspecified") -and ($_.Friendlyname -notlike '*LOGICAL VOLUME') -and ($_.Friendlyname -notlike '*ServeRAID*') -and ($_.Friendlyname -notlike '*LSI MegaSR*') }
                $StoragePoolDisks = (Get-AzsSupportPhysicalDisk -StoragePool $(Get-AzsSupportStoragePool -Cluster $ClusterName -IsPrimordial $false)) | Sort-Object $_
            }


            $StorageHealth.Add('Storage Pool' , $(Get-AzsSupportStoragePool -Cluster $ClusterName | Where-Object { $_.HealthStatus -notlike 'Healthy' }))
            $StorageHealth.Add('Storage Jobs' , $(Get-AzsSupportStorageJob -Cluster $ClusterName -IncludeStoragePoolOptimizationJob | Where-Object { $_.JobState -ne 'Completed' -and $_.Name -notlike "Format Volume" } | Select-Object Name, IsBackgroundTask, ElapsedTime, JobState, PercentComplete, BytesProcessed, BytesTotal ))
            $StorageHealth.Add('Cluster Nodes' , $(Get-AzsSupportInfrastructureHost -Cluster $ClusterName | Where-Object { $_.State -notlike 'Up' } | Select-Object Name, State))
            $StorageHealth.Add('CSV' , $(Get-ClusterSharedVolume -Cluster $ClusterName | Where-Object { $_.State -NotLike "Online" }))
            $StorageHealth.Add('Enclosures' , $(Get-StorageEnclosure -CimSession $ClusterName | Where-Object { $_.HealthStatus -notmatch "Healthy" } | Select-Object FriendlyName, SerialNumber, OperationalStatus, HealthStatus, NumberOfSlots, ObjectId, ElementsTypesInError, UniqueId))
            $StorageHealth.Add('EnclosureSNV' , $(Get-StorageEnclosureSNV -CimSession $ClusterName | Where-Object { $_.IsPhysicallyConnected -match "True" } | Select-Object Storagenodeobjectid, StorageenclosureObjectid))
            $StorageHealth.Add('Storage Health Action' , $(Get-AzsSupportStorageSubsystem -CimSession $ClusterName -FriendlyName cluster*  | Get-StorageHealthAction  -CimSession $ClusterName | Where-Object { $_.State -ne 'Succeeded' } | Select-Object Reason, State, Percentcomplete, uniqueid))
            $StorageHealth.Add('Storage Pool Disks' , $StoragePoolDisks)
            $StorageHealth.Add('Cluster Disks'  , $ClusterDisks)
            $StorageHealth.Add('Current Faults' , $(Get-AzsSupportStorageHealthFault -Cluster $ClusterName))

            if ($null -eq $ClusterDisks.SerialNumber) {
                $StorageHealth.Add('Disks Not In Pool', $((Compare-Object $ClusterDisks $StoragePoolDisks | Where-Object { $_.SideIndicator -like '=>' }).InputObject))
            }
            else {
                $StorageHealth.Add('Disks Not In Pool', $((Compare-Object $ClusterDisks.SerialNumber  $StoragePoolDisks | Where-Object { $_.SideIndicator -like '=>' }).InputObject))
            }

            $StorageHealth.Add('Health Running', $(Get-Process -ProcessName 'healthpih' -ErrorAction SilentlyContinue -ComputerName $Nodes | Select-Object Responding, ProcessName, MachineName))

        }


        # Analyse and Output Results
        Trace-Output -Level:Verbose -Message ($msg.StorageAnalyseResults)
        Complete-AzsSupportStorageChecks -StorageHealth  $StorageHealth -DiskHealth $DiskHealth -Nodes $Nodes -ClusterName $ClusterName -Include $Include
        Write-Host `r `n


        # Check if CacheResultTable switch stated
        Trace-Output -Level:Verbose -Message ($msg.StorageCheckCacheEnabled)
        if ($CacheResultTable) {
            Trace-Output -Level:Verbose -Message ($msg.StorageCacheEnabled)
            $Global:AzsSupport.Cache_StorageHealth = $StorageHealth        
            $Global:AzsSupport.Cache_StorageDiskHealth = $DiskHealth
        }
        else {
            Trace-Output -Level:Verbose -Message ($msg.StorageCacheClear)
            Clear-Variable StorageHealth, DiskHealth -ErrorAction SilentlyContinue
        }
        
        # Stop telemetry at the end of the script
        Stop-Transcript
        #Trace-AzsSupportCommand -Event OnExit
    }
    catch {
        #$formattedException = Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Get-AzsSupportDataIntegrityScanState {
    <#
    .SYNOPSIS
        Gets "Data Integrity Check And Scan" scheduled task's state on all reachable nodes from the failover cluster.
     
    .DESCRIPTION
        Gets detailed information on scheduled task "Data Integrity Check And Scan" on provided cluster nodes.
 
    .PARAMETER Cluster
        The Cluster you want to check for Missing Disks.
 
    .EXAMPLE
        Get-AzsSupportDataIntegrityScanState
 
    .EXAMPLE
        Get-AzsSupportDataIntegrityScanState -Cluster "Contoso-cl"
 
    .OUTPUTS
        Returns the state of the "Data Integrity Check And Scan" scheduled task on all reachable nodes from the failover cluster
 
        Get-AzsSupportDataIntegrityScanState -Cluster contoso-cl | Select PSComputerName,State,TaskName
 
        PSComputerName State TaskName
        -------------- ----- --------
        contoso-n01.contoso.la 3 Data Integrity Check And Scan
        contoso-n02.contoso.lab 3 Data Integrity Check And Scan
    #>


    [parameter(mandatory = $false, HelpMessage = "Please provide Cluster to run Storage Diagnostics on")]
    [string]$Cluster = Get-AzsSupportClusterName

    try {
        #Trace-AzsSupportCommand -Event OnEntry

        $clusterNodes = Get-AzsSupportClusterNode -Cluster $Cluster | Select-Object -ExpandProperty Name
        $result = [System.Collections.ArrayList]::new()
        # Obtain the scheduled task's state from every reachable cluster node
        $result = Invoke-AzsSupportCommand -ComputerName $clusterNodes -ScriptBlock { Get-ScheduledTask -TaskName 'Data Integrity Check And Scan' } -AsJob -PassThru

        #Trace-AzsSupportCommand -Event OnExit
        return $result
    }
    catch {
        #$formattedException = Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Get-AzsSupportStorageAutoPauseEvents {
    <#
        .SYNOPSIS
            Checks for volume autopause events
         
        .DESCRIPTION
            Checks for volume autopause events over a specified time period on specified nodes
 
        .PARAMETER Nodes
            Nodes you want to run the check on
 
        .PARAMETER StartTime
            When you would like to start looking from
 
        .PARAMETER EndTime
          When you would like to end looking to
 
        .EXAMPLE
            PS> $StartTime = (Get-Date).AddDays(-2)
            PS> $EndTime = (Get-Date).AddDays(-1)
            PS> $Nodes = Get-AzsSupportClusterNode
            PS> Get-AzsSupportStorageAutoPauseEvents -StartTime $StartTime -EndTime $EndTime -Nodes $Nodes | Select-Object TimeCreated, Node, Id, CSVFsEventIdName, VolumeName, FromDirectIo, Irp, Parameter1, Parameter2, LastUptime, CurrentDowntime, TimeSinceLastStateTransition, Lifetime, SourceName, StatusName | sort-Object TimeCreated | Format-Table -AutoSize
 
        .OUTPUTS
            AutoPause errors over time period specified
 
            TimeCreated Node Id CSVFsEventIdName VolumeName FromDirectIo Irp Parameter1 Parameter2 LastUptime
            ----------- ---- -- ---------------- ---------- ------------ --- ---------- ---------- ----------
            12/5/2024 12:21:16 AM contoso-n01 9296 VolumeAutopause Infrastructure_1 False 0 0 0 5922968750
            12/5/2024 12:21:16 AM contoso-n01 9296 VolumeAutopause UserStorage_1 False 0 0 0 5922968750
    #>


    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $True)]
        [DateTime]$StartTime,
        [Parameter(Mandatory = $True)]
        [DateTime]$EndTime,
        [Parameter(Mandatory = $True)]
        [array[]]$Nodes
    )
    try {
        #Trace-AzsSupportCommand -Event OnEntry
        $CsvFsClientLogName = 'Microsoft-Windows-FailoverClustering-CsvFs/Operational'
        $CsvFsClientSource = 'Microsoft-Windows-FailoverClustering-CsvFs-Diagnostic'
        $CSVFsEventIdName = 'VolumeAutopause'
        $CsvFsClientId = '9296'


        $CsvFsSourceName = @{
            0  = "Unknown"
            1  = "Tunnel"
            2  = "BRLReplayDownLevel"
            3  = "BRLUnlockAll"
            4  = "BRLUnlock"
            5  = "CADFOResumeComplete"
            6  = "CAPROResumeComplete"
            7  = "CASetBypass"
            8  = "CASuspendOnClose"
            9  = "UnregisterSCB"
            10 = "UnlockAllOnCleanup"
            11 = "UserRequest"
            12 = "OplockDowngradePurge"
            13 = "OplockAdvanceVDL"
            14 = "OplockFlush"
            15 = "OplockAllocate"
            16 = "StopLocalBuffering"
            17 = "SetMaxOplock"
            18 = "FltAckOplockBreak"
            19 = "AckOplockBreak"
            20 = "DowngradeBufferingAsync"
            21 = "UpgradeOplock"
            22 = "QueryOplockStatus"
            23 = "SCNComplete"
            24 = "SCNCompleteUnregisterScb"
            25 = "SCNStart"
            26 = "OplockCompleted"
            27 = "SetDownLevelDisposition"
            28 = "ReconnectSCB"
            29 = "ReconnectVCB"
            30 = "IoComplete"
            31 = "OplockUplockUpgradePurge"
            32 = "SetPurgeFailureMode"
            33 = "MarkHandleSkipCoherencySyncDIsallowWrites"
            34 = "OpenPagingFile"
        }
            
        

        $CsvFsStatusName = @{
            "0"        = "STATUS_SUCCESS"
            "c000009d" = "STATUS_DEVICE_NOT_CONNECTED"
            "c00000b5" = "STATUS_IO_TIMEOUT"
            "c00000c4" = "STATUS_UNEXPECTED_NETWORK_ERROR"
            "c0000203" = "STATUS_USER_SESSION_DELETED"
            "c000020c" = "STATUS_CONNECTION_DISCONNECTED"
            "c000020d" = "STATUS_CONNECTION_RESET"
            "c000026e" = "STATUS_VOLUME_DISMOUNTED"
            "c00004ab" = "STATUS_FT_READ_FAILURE"
            "c000000e" = "STATUS_NO_SUCH_DEVICE"
            "c000006d" = "STATUS_LOGON_FAILURE"
            "c00000be" = "STATUS_BAD_NETWORK_PATH"
            "c0e7000b" = "STATUS_SPACES_NOT_ENOUGH_DRIVES"
            "c0130026" = "STATUS_CLUSTER_CSV_VOLUME_DRAINING_SUCCEEDED_DOWNLEVEL"
            "c0130024" = "STATUS_CLUSTER_CSV_VOLUME_DRAINING"
        }


        $AllEvents = foreach ($node in $nodes) {

            (Get-WinEvent -ComputerName $($Node.Name) -ErrorAction SilentlyContinue -FilterHashtable @{LogName = $CsvFsClientLogName; ProviderName = $CsvFsClientSource; ID = $CsvFsClientId; StartTime = $StartTime; EndTime = $EndTime } | Select-Object TimeCreated, 
            @{Name = 'Node'; Expression = { (($_.MachineName).split("."))[0] } }, 
            Id , 
            @{Name = 'CSVFsEventIdName'; Expression = { $CSVFsEventIdName } }, 
            @{Name = 'Volume'; Expression = { (($_.Properties)[0].Value) } },
            @{Name = 'VolumeId'; Expression = { (($_.Properties)[1].Value) } },
            @{Name = 'VolumeName'; Expression = { (($_.Properties)[2].Value) } },
            @{Name = 'FromDirectIo'; Expression = { (($_.Properties)[3].Value) } },
            @{Name = 'Irp'; Expression = { (($_.Properties)[4].Value) } },
            @{Name = 'Source'; Expression = { (($_.Properties)[6].Value) } },
            @{Name = 'Status'; Expression = { ([int]($_.Properties)[5].Value) } },
            @{Name = 'Parameter1'; Expression = { (($_.Properties)[7].Value) } },
            @{Name = 'Parameter2'; Expression = { (($_.Properties)[8].Value) } },
            @{Name = 'LastUptime'; Expression = { (($_.Properties)[9].Value) } },
            @{Name = 'CurrentDowntime'; Expression = { (($_.Properties)[10].Value) } },
            @{Name = 'TimeSinceLastStateTransition'; Expression = { (($_.Properties)[11].Value) } },
            @{Name = 'Lifetime'; Expression = { (($_.Properties)[12].Value) } },
            @{Name = 'SourceName'; Expression = { $CsvFsSourceName[[int32](($_.Properties)[6].Value)] } },
            @{Name = 'StatusName'; Expression = { if (!$($CsvFsStatusName[('{0:X}' -f (([int]($_.Properties)[5].Value)))])) { ('{0:X}' -f (([int]($_.Properties)[5].Value))) }else { $CsvFsStatusName[('{0:X}' -f (([int]($_.Properties)[5].Value)))] } } })
                              

        }   
        #Trace-AzsSupportCommand -Event OnExit
        return $AllEvents
    }
    catch {
        #$formattedException = Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Get-AzsSupportStorPortDriverEvents {
    <#
       .SYNOPSIS
          Checks for StorPort Driver Events.
         
       .DESCRIPTION
            Checks for StorPort Driver Events over a specified time period on specified nodes.
 
       .PARAMETER ClusterName
          Name of the cluster you want to run against.
 
       .PARAMETER StartTime
          When you would like to start looking from .
 
        .PARAMETER EndTime
          When you would like to end looking to
.
       .PARAMETER Nodes
          Nodes you want to run the check on.
 
       .EXAMPLE
          PS> $StartTime = (Get-Date).AddDays(-2)
          PS> $EndTime = (Get-Date).AddDays(-1)
          PS> Get-AzsSupportStorPortDriverEvents -ClusterName contoso-cl -StartTime $StartTime -EndTime $EndTime | Sort-Object TimeCreated | Format-Table -AutoSize
 
        .OUTPUTS
         Lists specific errors from Storport drive resets and Storage Spaces Driver report disk errors.
 
         SerialNumber TimeCreated ComputerName Value ConnectedNode
         ------------ ----------- ------------ ----- -------------
         A1B0C2D4EFGH 2/25/2025 11:55:14 AM contoso-n02 DriveIoError contoso-n01
         A1B0C2D4EFGH 2/25/2025 11:55:14 AM contoso-n02 LostCommunication contoso-n01
         A1B0C2D4EFGH 2/25/2025 11:55:14 AM contoso-n01 LostCommunication contoso-n01
    #>


    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $True)]
        [String]$ClusterName,
        [Parameter(Mandatory = $True)]
        [DateTime]$StartTime,
        [Parameter(Mandatory = $True)]
        [DateTime]$EndTime
    )
     
    [regex]$guidRegex = '(?im)^[{(]?[0-9A-F]{8}[-]?(?:[0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?$'


    $StorPortDriverLogName = 'Microsoft-Windows-StorageSpaces-Driver/Operational'
    $StorPortDriverSource = 'Microsoft-Windows-StorageSpaces-Driver'
    $StorPortDriverID = 200, 202, 203, 204, 205
    $StorPortDriverlookup = @{
        200           = "DriveHeaderReadError"                  # Diskid in $_.Properties[0].value
        PropSerial200 = 0
        202           = "DriveHeaderSplit"                      # Diskid in $_.Properties[0].value
        PropSerial202 = 0
        203           = "DriveIoError"                          # Serial in $_.Properties[5].value
        PropSerial203 = 5
        204           = "DriveImpendingFailure"                 # Serial in $_.Properties[7].value
        PropSerial204 = 7
        205           = "LostCommunication"                     # Serial in $_.Properties[3].value
        PropSerial205 = 3
    }
       
    try {
        #Trace-AzsSupportCommand -Event OnEntry

        $Nodes = Get-AzsSupportInfrastructureHost -Cluster $ClusterName

        # Get information for disks connected to Cluster
        $ClusPortDeviceInformation = Get-WmiObject -ErrorAction Stop -Namespace root\wmi ClusPortDeviceInformation -ComputerName $ClusterName

        $AllEvents = foreach ($node in $nodes) {

            # Microsoft-Windows-StorageSpaces-Driver/Operational
            $SpacesDriverEvents = (Get-WinEvent -ComputerName $($Node.Name) -ErrorAction SilentlyContinue -FilterHashtable @{LogName = $StorportDriverLogName; ProviderName = $StorPortDriverSource; ID = $StorPortDriverId; StartTime = $StartTime } | Select-Object @{Name = 'ComputerName'; Expression = { ($_.MachineName).split('.')[0] } }, LogName, TimeCreated, Id, LevelDisplayName, OpcodeDisplayName, @{Name = 'Value'; Expression = { $StorPortDriverlookup[$_.Id] } }, @{Name = 'SerialNumber'; Expression = {
                                    
                        # Lookup properties that contain either Diskid or SerialNumber
                        $Serial = ($_.Properties[$StorPortDriverlookup["PropSerial" + $_.id]].value) 
                         
                        # Check if Diskid and needs translation to SerialNumber
                        if ($Serial -notmatch $guidRegex -and $null -ne $Serial ) {

                            $Serial
                        }
                        else {
                            # Translate Diskid to SerialNumber
                            $ClusPortDeviceInformation = $ClusPortDeviceInformation | Where-Object { $_.DeviceGuid -like "*$Serial*" }

                            if ($null -ne $ClusPortDeviceInformation) {
                                $ClusPortDeviceInformation.SerialNumber.trim()
                            }
        
                        }
                    }
                })
            Clear-Variable Serial -ErrorAction SilentlyContinue 
          
            if ($null -ne $SpacesDriverEvents) {
                Foreach ($SpacesDriverEvent in $SpacesDriverEvents) {

                    
                    $ConnectedNode = ($ClusPortDeviceInformation | Where-Object { $_.SerialNumber -match $($SpacesDriverEvent.SerialNumber) })
                    
                    if ($null -ne $ConnectedNode) {
                        $ConnectedNode = $ConnectedNode.ConnectedNode.trim()
                    }

                    New-Object -TypeName PSCustomObject -Property @{

                        ComputerName  = $(if ($SpacesDriverEvent.ComputerName) { $SpacesDriverEvent.ComputerName }else { "missing" })
                        TimeCreated   = $(if ($SpacesDriverEvent.TimeCreated) { $SpacesDriverEvent.TimeCreated }else { "missing" })             
                        Value         = $(if ($SpacesDriverEvent.Value) { $SpacesDriverEvent.Value }else { "missing" })
                        SerialNumber  = $(if ($SpacesDriverEvent.SerialNumber) { $SpacesDriverEvent.SerialNumber }else { "missing" })
                        ConnectedNode = $(if ($ConnectedNode) { $ConnectedNode }else { "missing" })

                    }
                }  
            }    
        }    
        #Trace-AzsSupportCommand -Event OnExit
        return $AllEvents
    }    
    catch {
        #$formattedException = Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Get-AzsSupportStorPortOpEvents {
    <#
       .SYNOPSIS
          Checks for Storport Operational Events.
 
        .DESCRIPTION
          Checks for Storport Operational Events over a specified time period on specified nodes.
 
       .PARAMETER ClusterName
          Name of the cluster you want to run against.
 
       .PARAMETER StartTime
          When you would like to start looking from .
 
        .PARAMETER EndTime
          When you would like to end looking to.
 
       .EXAMPLE
          PS> $StartTime = (Get-Date).AddDays(-2)
          PS> $EndTime = (Get-Date).AddDays(-1)
          PS> Get-AzsSupportStorPortOpEvents -ClusterName contoso-cl -StartTime $StartTime -EndTime $EndTime | Sort-Object TimeCreated | Format-Table | Format-Table -AutoSize
 
        .OUTPUTS
          Lists specific errors from Storport drive resets and Storage Spaces Driver report disk errors.
    #>


    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $True)]
        [String]$ClusterName,
        [Parameter(Mandatory = $True)]
        [DateTime]$StartTime,
        [Parameter(Mandatory = $True)]
        [DateTime]$EndTime
    )
     
    [regex]$guidRegex = '(?im)^[{(]?[0-9A-F]{8}[-]?(?:[0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?$'

    # Microsoft-Windows-Storage-Storport/Operational Events
    $StorportOpLogName = 'Microsoft-Windows-Storage-Storport/Operational'
    $StorPortOpSource = 'Microsoft-Windows-StorPort'
    $StorPortOpId = 500, 501, 502, 507, 508, 550
    $StorPortOplookup = @{
        500           = "HwTimeout"                              # Serial in $_.Properties[9].value
        PropSerial500 = 9
        501           = "BusReset"                               # Serial in $_.Properties[7].value
        PropSerial501 = 7
        502           = "MarkedUnresponsive"                     # Serial in $_.Properties[9].value
        PropSerial502 = 9
        507           = "ResetBroken"                            # Serial in $_.Properties[9].value
        PropSerial507 = 9
        508           = "AbortBroken"                            # Diskid in $_.Properties[5].value
        PropSerial508 = 5
        550           = "HierarchicalReset"                      # Serial in $_.Properties[9].value
        PropSerial550 = 9
    }

       
    try {
        #Trace-AzsSupportCommand -Event OnEntry

        $Nodes = Get-AzsSupportInfrastructureHost -Cluster $ClusterName
         
        # Get information for disks connected to Cluster
        $ClusPortDeviceInformation = Get-WmiObject -ErrorAction Stop -Namespace root\wmi ClusPortDeviceInformation -ComputerName $ClusterName

     
        $AllEvents = foreach ($node in $nodes) {


            # Microsoft-Windows-Storage-Storport/Operational
            $StorPortEvents = (Get-WinEvent -ComputerName $($Node.Name) -ErrorAction SilentlyContinue -FilterHashtable @{LogName = $StorportOpLogName; ProviderName = $StorPortOpSource; ID = $StorPortOpId; StartTime = $StartTime; EndTime = $EndTime } | Select-Object @{Name = 'ComputerName'; Expression = { ($_.MachineName).split('.')[0] } }, LogName, TimeCreated, Id, LevelDisplayName, OpcodeDisplayName, @{Name = 'Value'; Expression = { $StorPortoplookup[$_.Id] } }, @{Name = 'SerialNumber'; Expression = {
                              
                        # Lookup properties that contain either Diskid or SerialNumber
                        $Serial = ($_.Properties[$StorPortOplookup["PropSerial" + $_.id]].value) 
                                        
                        # Check if Diskid and needs translation to SerialNumber
                        if ($Serial -notmatch $guidRegex -and $null -ne $Serial ) {
  
                            $Serial
                        }
                        else {
                            # Translate Diskid to SerialNumber
                            $ClusPortDeviceInformation = $ClusPortDeviceInformation | Where-Object { $_.DeviceGuid -like "*$Serial*" } 

                            if ($null -ne $ClusPortDeviceInformation) {
                                $ClusPortDeviceInformation.SerialNumber.trim()
                            } 
                        } 
                    }
                })
           
            Clear-Variable Serial -ErrorAction SilentlyContinue 
            
            If ($null -ne $StorPortEvents) {
                Foreach ($StorPortEvent in $StorPortEvents) {

                    $ConnectedNode = $ClusPortDeviceInformation | Where-Object { $_.SerialNumber -match $($StorPortEvent.SerialNumber) }

                    if ($ConnectedNode) {
                        $ConnectedNode = $ConnectedNode.ConnectedNode.trim()
                    }

                    New-Object -TypeName PSCustomObject -Property @{
                        ComputerName  = $(if ($StorPortEvent.ComputerName) { $StorPortEvent.ComputerName }else { "missing" })
                        TimeCreated   = $(if ($StorPortEvent.TimeCreated) { $StorPortEvent.TimeCreated }else { "missing" })             
                        Value         = $(if ($StorPortEvent.Value) { $StorPortEvent.Value }else { "missing" })
                        SerialNumber  = $(if ($StorPortEvent.SerialNumber) { $StorPortEvent.SerialNumber }else { "missing" })
                        ConnectedNode = $(if ($ConnectedNode) { $ConnectedNode }else { "missing" })
                    }
                }
            }
        }
        #Trace-AzsSupportCommand -Event OnExit
        return $AllEvents
    }
    catch {
        #$formattedException = Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}


#Internal Only Functions

function Get-AzsSupportStorageEventLogErrors {
    <#
    .SYNOPSIS
        Gets errors from event logs for a specified node. If no node is specified, lists errors from all nodes.
 
    .PARAMETER Node
        A physical node like contoso-n01.
 
    .EXAMPLE
        PS> Get-AzsSupportStorageEventLogErrors -Node Azs-Node01.
 
    .OUTPUTS
        List of disk errors found on a given node, Internal only.
    #>

    [CmdletBinding()]
    param(
        [String]$Node
    )

    try {
        #Trace-AzsSupportCommand -Event OnEntry

        $nodes = @()
        if ($PSBoundParameters['Node']) {
            $nodes += $Node
        }
        else {
            $nodes += Get-AzsSupportInfrastructureHost | Select-Object -ExpandProperty Name
        }

        $errors = @()
        foreach ($nodeToQuery in $nodes) {
            $errors += Invoke-Command -ComputerName $nodeToQuery -ScriptBlock {
                Get-EventLog -LogName "System" -EntryType Error | Where-Object { $_.Source -Contains "disk" }
            }
        }

        #Trace-AzsSupportCommand -Event OnExit
        return $errors
    }
    catch {
        #$formattedException= Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Convert-AzsSupportStorageAttributes {

    <#
        .SYNOPSIS
            Translates the array passed value provided for SBLAttribute, SBLDiskCacheState, SBLCacheUsageCurrent and SBLCacheUsageDesired.
 
        .DESCRIPTION
            Turns array passed value into readable text for quick analysis and comparision.
 
        .PARAMETER DiskHealth
           Output from the Get-AzsSupportStorageDiskHealth function.
 
        .EXAMPLE
        PS C:\> Convert-AzsSupportStorageAttributes -DiskHealth $DiskHealth
 
    #>


    Param (
        [Parameter(Mandatory = $True)]
        $DiskHealth
    )

    try {

        #Trace-AzsSupportCommand -Event OnEntry

        $DiskHealth | Get-Member -type NoteProperty | foreach-object {

            switch -Wildcard ($_.name) {

                'SBLAttribute' {
                    $i = -1
                    switch -Wildcard ($DiskHealth.$_) {
                        '0' { $i++; $DiskHealth[$i].SBLAttribute = 'Default' }
                        '1' { $i++; $DiskHealth[$i].SBLAttribute = 'Cache_Disabled' }
                        '2' { $i++; $DiskHealth[$i].SBLAttribute = 'Missing' }
                        '4' { $i++; $DiskHealth[$i].SBLAttribute = 'Bad' }
                        '8' { $i++; $DiskHealth[$i].SBLAttribute = 'ReadOnly' }
                        '16' { $i++; $DiskHealth[$i].SBLAttribute = 'Set_Maintenance' }
                        '32' { $i++; $DiskHealth[$i].SBLAttribute = 'In_Maintenance' }
                        '64' { $i++; $DiskHealth[$i].SBLAttribute = 'ReadOnly_Due_To_Flash' }
                        '56' { $i++; $DiskHealth[$i].SBLAttribute = 'RO_Set-InMaint' }
                        '120' { $i++; $DiskHealth[$i].SBLAttribute = 'RO-DueToFlash_Set-InMaint' }
                        Default { $i++ }
                    } # End of switch SBLAttribute
                }

                'SBLDiskCacheState' {
                    $i = -1
                    switch -Wildcard ($DiskHealth.$_) {
                        '0' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'Default' }
                        '1' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'CacheDiskStateConfiguring' }
                        '2' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'CacheDiskStateInitialized' }
                        '3' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'CacheDiskStateInitializedAndBound' }
                        '4' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'CacheDiskStateDraining' }
                        '5' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'CacheDiskStateDisabling' }
                        '6' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'CacheDiskStateDisabled' }
                        '7' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'CacheDiskStateMissing' }
                        '8' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'CacheDiskStateOrphanedWaiting' }
                        '9' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'CacheDiskStateOrphanedRecovering' }
                        '10' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'CacheDiskStateFailedMediaError' }
                        '11' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'CacheDiskStateFailedProvisioning' }
                        '12' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'CacheDiskStateReset' }
                        '13' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'CacheDiskStateRepairing' }
                        '2000' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'CacheDiskStateIneligibleDataPartition' }
                        '2001' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'CacheDiskStateIneligibleNotGPT' }
                        '2002' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'CacheDiskStateIneligibleNotEnoughSpace' }
                        '2003' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'CacheDiskStateIneligibleUnsupportedSystem' }
                        '2004' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'CacheDiskStateIneligibleExcludedFromS2D' }
                        '2999' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'CacheDiskStateIneligibleForS2D' }
                        '3000' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'CacheDiskStateSkippedBindingNoFlash' }
                        '3001' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'CacheDiskStateIgnored' }
                        '3002' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'CacheDiskStateNonHybrid' }
                        '9000' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'CacheDiskStateInternalErrorConfiguring' }
                        '9001' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'CacheDiskStateMarkedBad' }
                        '9002' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'CacheDiskStateMarkedMissing' }
                        '9003' { $i++; $DiskHealth[$i].SBLDiskCacheState = 'CacheDiskStateInStorageMaintenance' }
                        Default { $i++ }
                    } # End of switch SBLDiskCacheState
                }

                'SBLCacheUsageCurrent' {
                    $i = -1
                    switch -Wildcard ($DiskHealth.$_) {
                        '0' { $i++; $DiskHealth[$i].SBLCacheUsageCurrent = 'NonHybrid' }
                        '1' { $i++; $DiskHealth[$i].SBLCacheUsageCurrent = 'Data' }
                        '2' { $i++; $DiskHealth[$i].SBLCacheUsageCurrent = 'Cache' }
                        '3' { $i++; $DiskHealth[$i].SBLCacheUsageCurrent = 'Auto' }
                        Default { $i++ }
                    } # End of switch SBLCacheUsageCurrent
                }

                'SBLCacheUsageDesired' {
                    $i = -1
                    switch -Wildcard ($DiskHealth.$_) {
                        '0' { $i++; $DiskHealth[$i].SBLCacheUsageDesired = 'NonHybrid' }
                        '1' { $i++; $DiskHealth[$i].SBLCacheUsageDesired = 'Data' }
                        '2' { $i++; $DiskHealth[$i].SBLCacheUsageDesired = 'Cache' }
                        '3' { $i++; $DiskHealth[$i].SBLCacheUsageDesired = 'Auto' }
                        Default { $i++ }
                    } # End of switch SBLCacheUsageDesired
                }

                'R/U' {
                    $i = -1
                    switch -Wildcard ($DiskHealth.$_) {
                        $null { $i++; $DiskHealth[$i].'R/U' = '0' }
                        Default { $i++ }
                    } # End of switch 'R/U'
                }

                'R/T' {
                    $i = -1
                    switch -Wildcard ($DiskHealth.$_) {
                        $null { $i++; $DiskHealth[$i].'R/T' = '0' }
                        Default { $i++ }
                    } # End of switch 'R/T'
                }

                'W/U' {
                    $i = -1
                    switch -Wildcard ($DiskHealth.$_) {
                        $null { $i++; $DiskHealth[$i].'W/U' = '0' }
                        Default { $i++ }
                    } # End of switch 'W/U'
                }

                'W/T' {
                    $i = -1
                    switch -Wildcard ($DiskHealth.$_) {
                        $null { $i++; $DiskHealth[$i].'W/T' = '0' }
                        Default { $i++ }
                    } # End of switch 'W/T'
                }

                'Cache' {
                    $i = -1
                    switch -Wildcard ($DiskHealth.$_) {
                        $null { $i++; $DiskHealth[$i].'Cache' = 'N/A' }
                        Default { $i++ }
                    } # End of switch 'W/T'
                }
            }
        }
        $DiskHealth | Select-Object -Property * -ExcludeProperty PSComputerName, RunspaceID
        #Trace-AzsSupportCommand -Event OnExit
    }
    catch {

        #$formattedException = Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException

    }
}

function Publish-AzsSupportStorageChecks {

    <#
        .SYNOPSIS
            Displays high level details of test result.
 
        .DESCRIPTION
            Displays high level details of test being performed and result.
 
        .PARAMETER Data
            Checks if there is no data and if gives a PASS.
 
        .PARAMETER CheckName
            Name of the check being made.
 
        .PARAMETER Message
            What is being performed for a check.
 
        .PARAMETER Level
            Logging Level.
 
        .PARAMETER ResultGrade
            Result grade PASS/FAIL/WARN/INFO.
         
        .EXAMPLE
            PS C:\> Publish-AzsSupportStorageChecks -Data $($BadDisks) -CheckName "SBL Disk Check" "[Checks for SBL Bad Disks]" -Level INFO -ResultGrade FAIL
 
    #>



    [CmdletBinding()]
    param(
        [Parameter()]
        $Data,
        [Parameter()]
        $CheckName,
        [Parameter()]
        $Message,
        [Parameter()]
        $Level,
        [Parameter()]
        $ResultGrade
    )

    try {

        #Trace-AzsSupportCommand -Event OnEntry

        if (!$data) {
            Write-Colour "$CheckName".PadRight(50), '[', ' PASS ', ']' -ForeGroundColor White, White, Green, White ; "`r"
        }
        elseif ($ResultGrade -match "FAIL") {
            Write-Colour "$CheckName".PadRight(50), '[', ' FAIL ', ']' -ForeGroundColor White, White, Red, White ; "`r"
            $StorageCheck["Validation"]["$CheckName"] = @{ }
        }
        elseif ($ResultGrade -match "WARN") {
            Write-Colour "$CheckName".PadRight(50), '[', ' WARN ', ']' -ForeGroundColor White, White, Yellow, White ; "`r"
            $StorageCheck["Validation"]["$CheckName"] = @{ }
        }
        elseif ($ResultGrade -match "INFO") {
            Write-Colour "$CheckName".PadRight(50), '[', ' INFO ', ']' -ForeGroundColor White, White, Cyan, White ; "`r"
            $StorageCheck["Validation"]["$CheckName"] = @{ }
        }

        #Trace-AzsSupportCommand -Event OnExit
    }
    catch {

        #$formattedException = Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException

    }
}

function New-AzsSupportStorageSupportedComponents {

    <#
        .SYNOPSIS
            Checks for supported firmware and hardware in Storage Spaces and suggests new config to support
 
        .DESCRIPTION
            Gets supported componants on storage spaces and builds new config recommended to apply
 
        .PARAMETER Cluster
            The cluster you want to get rhe current supported commponents from
 
        .EXAMPLE
            New-AzsSupportStorageSupportedComponents -Cluster $ClusterName
    #>


    param (
        [Parameter(Mandatory = $true)]
        $Cluster
    )

    try {

        #Trace-AzsSupportCommand -Event OnEntry

        Trace-Output -Level:Verbose -Message ($msg.StorageSupportedComponents)
        [xml]$SupportedComponents = (Get-AzsSupportStorageSubsystem -CimSession $Cluster -FriendlyName cluster* | Get-StorageHealthSetting -name System.Storage.SupportedComponents.Document -CimSession $Cluster).Value

        if ($($SupportedComponents.InnerXml) -contains "<Components><Disks><Disk><Manufacturer></Manufacturer><Model></Model><AllowedFirmware><Version></Version></AllowedFirmware></Disk></Disks><Cache></Cache></Components>" -or $null -eq $($SupportedComponents.InnerXml)) {
            Trace-Output -Level:Verbose -Message ($msg.StorageSupportedComponentsState)
        }
        else {
            Trace-Output -Level:Verbose -Message ($msg.StorageSupportedComponentsExtract)
            $SupportedModelFW = $SupportedComponents.SelectNodes('//Disk') | Select-Object Model, @{N = 'FirmwareVersion'; E = { $_.AllowedFirmware.Version } }
            Trace-Output -Level:Verbose -Message ($msg.StorageInstalledDisks)


            $Subsystem = Get-AzsSupportStorageSubsystem -CimSession $Cluster -FriendlyName cluster* 
            $InstalledDisks = Get-AzsSupportPhysicalDisk -ComputerName $((Get-AzsSupportInfrastructureHost -Cluster $Cluster).Name) -StorageSubsystem $Subsystem | Select-Object -unique Model, Firmwareversion

            if (-not ([string]::IsNullOrEmpty($SupportedModelFW.Model))) {
                $SuppCompResult = foreach ($Installed in $InstalledDisks) {
                    
                    Trace-Output -Level:Verbose -Message ($msg.StorageSupportedComponentsCheck)

                    if ($SupportedModelFW.model -notcontains $Installed.model -or $SupportedModelFW.Firmwareversion -notcontains $Installed.FirmwareVersion -and $SupportedModelFW.Firmwareversion -notmatch $null) {


                        Trace-Output -Level:Verbose -Message ($msg.StorageSupportedComponentsMissing)
                        [xml]$newNode = "<Disk><Manufacturer>.*</Manufacturer><Model>$($Installed.Model)</Model><AllowedFirmware><Version>$($Installed.FirmwareVersion)</Version></AllowedFirmware></Disk>"
                        $SupportedComponents.Components.disks.AppendChild($SupportedComponents.ImportNode(($Newnode.Disk), $true)) | Out-Null


                        New-Object -TypeName PSCustomObject -Property @{
                            Missing             = $Installed
                            SupportedComponents = $SupportedComponents
                        }
                    }
                }
            }
        }


        #Trace-AzsSupportCommand -Event OnExit
        Return $SuppCompResult
    }
    catch {
        #$formattedException= Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Complete-AzsSupportStorageChecks {

    <#
        .SYNOPSIS
            Checks the data from the collection
 
        .DESCRIPTION
            Gives full details of checks and failures and outputs to the screen next steps
 
        .PARAMETER StorageHealth
            StorageHealth you want to complete checks against
 
        .PARAMETER DiskHealth
            DiskHealth you want to complete checks against
 
        .PARAMETER Nodes
            Nodes of the cluster
 
        .PARAMETER ClusterName
            The ClusterName
 
        .PARAMETER Include
            The tests that have been selected in the report
 
        .EXAMPLE
            PS C:\> Complete-AzsSupportStorageChecks -StorageHealth $StorageHealth -DiskHealth $DiskHealth -Nodes $Nodes -ClusterName $ClusterName -Include $Include
 
    #>


    Param (
        [Parameter(Mandatory = $False)]
        [AllowEmptyCollection()]
        [array]$StorageHealth,

        [Parameter(Mandatory = $False)]
        [AllowEmptyCollection()]
        [array]$DiskHealth,

        [Parameter(Mandatory = $True)]
        $Nodes,

        [Parameter(Mandatory = $True)]
        $ClusterName,

        [Parameter(Mandatory = $False)]
        [AllowEmptyCollection()]
        $Include
    )

    try {

        #Trace-AzsSupportCommand -Event OnEntry

        #===================================================================
        # Storage Summary:
        #===================================================================

        # Calculate Storage Usage And Capacity
        if ($Include -contains "StorageSummary" -or !$Include) {

            # Output main system details and setup
            If ($StorageHealth) {
                $StorageHealth.Model
                Write-Host "`r`n$($msg.StorageNodesConfig)`r`n" -ForegroundColor Yellow | Format-List
                Write-Host ($StorageHealth.'StorageNodesConfig' | Sort-Object ServerName | Out-String)
                Write-Host "`r`n$($msg.StorageVolumeConfiguration)`r`n" -ForegroundColor Yellow | Format-List
                Write-Output ($StorageHealth).'StorageVolumeConfiguration' | Sort-Object FriendlyName | Format-Table -AutoSize
                Write-Host "`r`n$($msg.StorageVirtualDiskConfiguration)`r`n" -ForegroundColor Yellow | Format-List
                Write-Output ($StorageHealth).'StorageVirtualDiskConfiguration' | Sort-Object FriendlyName | Format-Table -AutoSize
                Write-Host "`r`n$($msg.StoragePoolConfiguration)`r`n" -ForegroundColor Yellow | Format-List
                Write-Output ($StorageHealth).'StoragePoolConfiguration' | Sort-Object FriendlyName | Format-Table -AutoSize
                Write-Host "`r`n$($msg.SpacesDirectConfiguration)`r`n" -ForegroundColor Yellow | Format-List
                Write-Output ($StorageHealth).'SpacesDirectConfiguration' | Sort-Object Namew | Format-Table -AutoSize
            }
            
            Write-Host "$($msg.StorageCapacityDetails)`r`n" -ForegroundColor Yellow | Format-List

            If ([array]$DiskHealth -ne $null ) {
                
                Write-Host "$($msg.StorageSSDDiskPerNode)".PadRight(30), :, $(($DiskHealth | Where-Object { $_.Media -like "SSD" }).count / $nodes.count)
                Write-Host "$($msg.StorageHDDDiskPerNode)".PadRight(30), :, $(($DiskHealth | Where-Object { $_.Media -like "HDD" }).count / $nodes.count)
            }

            # Calculate Storage Usage And Capacity
            Trace-Output -Level:Verbose -Message ($msg.StorageClusterStorageUsage)
            $StorageUsage = Get-AzsSupportClusterUsage -ClusterName $ClusterName

            # Check the value is not 0 or negative
            $ReserveUsed = $($([decimal]($StorageUsage.'Reserve' / $StorageUsage.'Capacity Disk Size')) - $([decimal]$StorageUsage.'Available' / $StorageUsage.'Capacity Disk Size'))
            if ($ReserveUsed -lt 1) {
                $ReserveUsed = 0

            }
            
            Write-Host "$($msg.StorageTotalSize)".PadRight(30), :, $StorageUsage.'Total Size' TB
            Write-Host "$($msg.StorageUsed)".PadRight(30), :, $StorageUsage.'Used' TB
            Write-Host "$($msg.StorageAvailable)".PadRight(30), :, $StorageUsage.'Available' TB "/ Repair for" ($([decimal]($StorageUsage.'Available' / $StorageUsage.'Capacity Disk Size')) -split '\.')[0].Trim() "capacity disk[s] currently"    
            Write-Host "$($msg.StorageReserve)".PadRight(30), :, $StorageUsage.'Reserve' TB   
            Write-Host "$($msg.StorageRepairUsed)".PadRight(30), : $ReserveUsed "disk[s]"
            Write-Host "$($msg.StorageWriteCacheSize)".PadRight(30), :, $StorageUsage.'Write Cache Size' TB
            Write-Host "$($msg.StoragePhysicalDiskRedundancy)".PadRight(30), :, $StorageUsage.'Physical Disk Redundancy'
            Write-Host "$($msg.StorageTotalDrives)".PadRight(30), : , $StorageUsage.'Total Drives'
            Write-Output "$($msg.StorageSupportedComponentsList)" $((Get-AzsSupportStorageSupportedComponents -Cluster $ClusterName).OrigSupportedComponents) | Format-Table -AutoSize
        }

        
        Write-Host "$($msg.StorageKnownIssuesCheck)`r`n" -ForegroundColor Yellow | Format-List

        #===================================================================
        # Data Checks:
        #===================================================================

        Trace-Output -Level:Verbose -Message ($msg.StoragePublishChecks)

        switch ($Include) {
            'MissingDisks' {
                Publish-AzsSupportStorageChecks $($StorageHealth.'Missing Disks') "Missing Disks From Storage Spaces" "[Check for disks only seen in OS]" INFO INFO
            }
            'StorageHealth' {
                Publish-AzsSupportStorageChecks $($StorageHealth.'Storage Pool'.Friendlyname) "Storage Pool Health Check" "[Check for Unhealthy Storage Pool]" INFO FAIL
                Publish-AzsSupportStorageChecks ($($StorageHealth.'Health Running' | Where-Object { $_ -notlike $null }).count -ne $Nodes.count) "Cluster Nodes Health Process Running" "[Check for Health Process not running]" INFO FAIL
                Publish-AzsSupportStorageChecks $($StorageHealth.'Storage Jobs'.Name) "Storage Job Check" "[Check For Storage Jobs]" INFO WARN
                Publish-AzsSupportStorageChecks $($StorageHealth.'Cluster Nodes'.Name) "Cluster Node Check" "[Cluster Node Check]" INFO FAIL
                Publish-AzsSupportStorageChecks $($StorageHealth.'CSV'.Name) "Cluster Shared Volumes Check" "[Cluster Shared Volumes Check]" INFO FAIL
                Publish-AzsSupportStorageChecks $($StorageHealth.'Enclosures'.FriendlyName) "Storage Enclosure Check" "[Storage Enclosure Check]" INFO FAIL
                Publish-AzsSupportStorageChecks $($StorageHealth.'Current Faults'.Reason) "Health Service Fault Check" "[Check for Health Service Fault]" INFO WARN
                Publish-AzsSupportStorageChecks $($StorageHealth.'Storage Health Action'.State) "Storage Health Action Check" "[Check for health-related system activities for Storage subsystems, file shares, and volumes]" INFO FAIL
                Publish-AzsSupportStorageChecks $($StorageHealth.'Disks Not In Pool') "Disks Not In Pool Check" "[Check for disks not in non primordial pool]" INFO FAIL
            }
            'VirtualDisks' {
                Publish-AzsSupportStorageChecks $($StorageHealth.'Virtual Disk'.Friendlyname) "Virtual Disk Check" "[Check for Virtual Disks in bad state]" INFO FAIL
            }
            'DirtyCount' {
                Publish-AzsSupportStorageChecks $($StorageHealth.'Dirty Count') "Dirty Count" "[Check if Dirty Count Exceeds Limit]" INFO FAIL
            }
            'StorageComponents' {
                Publish-AzsSupportStorageChecks $($StorageHealth.'SupportComponentsChange') "Support Components Change" "[Check if changes needed for Supported Components]" INFO INFO
                Publish-AzsSupportStorageChecks $($StorageHealth.'SupportComponentsMissing') "Support Components Missing" "[Check if there is missing Supported Components]" INFO FAIL
            }
            'FirmwareDrift' {
                Publish-AzsSupportStorageChecks $($StorageHealth.'FWDrift') "Firmware Drift" "[Firmware Drift]" INFO INFO
            }
            'SMPHost' {
                Publish-AzsSupportStorageChecks ($($StorageHealth.'SMPHost Running'  | Where-Object { $_ -notlike $null }).count -ne $Nodes.count) "Cluster Nodes SMPHost Running" "[Check for SMPHost Service not running]" INFO FAIL
            }
            'SMPHostIssue' {
                Publish-AzsSupportStorageChecks $($StorageHealth.'SMPHost Issue'  | Where-Object { $_ -notlike $null }) "SMPHost Issue Detected" "[Check for SMPHost Issue]" INFO FAIL
            }
            'DiskHealth' {
                Publish-AzsSupportStorageChecks $($DiskHealth.'Health' -ne 'Healthy') "Disk Health Check" "[Disks in state other than healthy]" INFO FAIL
                Publish-AzsSupportStorageChecks $($DiskHealth.'Opst' -like "Transient Error") "Transient Disk Check" "[Check For Disks In Transient State]" INFO FAIL
                Publish-AzsSupportStorageChecks $($DiskHealth | Where-Object { $_.Partitions -notlike "*Clus|Microsoft SBL Cache Store``], Space Protective*" -and $_.Partitions -notlike "*Clus|Microsoft SBL Cache Hdd``], Space Protective*" -and $_.Partitions -notlike "*Space Protective, ``[Clus|Microsoft SBL Cache Store``]*" -and $_.Partitions -notlike "*Space Protective, ``[Clus|Microsoft SBL Cache Hdd``]*" }) "Storage Spaces Partitions Check" "[Check for corrupt\missing partitions] " INFO FAIL
            }
            'CSVUsage' {
                # Check needs to be written for load imbalance

            }
            'StorageSummary' {
                # Check needs to be written for reserve being under required

            }
            'SNV' {
                Publish-AzsSupportStorageChecks $($StorageHealth.'SNVFaultyDisk') "Storage Node View Differs" "[Storage Node View Differs]" INFO FAIL

            }
            default {
                Publish-AzsSupportStorageChecks $($StorageHealth.'Missing Disks') "Missing Disks From Storage Spaces" "[Check for disks only seen in OS]" INFO INFO
                Publish-AzsSupportStorageChecks $($StorageHealth.'Storage Pool'.Friendlyname) "Storage Pool Health Check" "[Check for Unhealthy Storage Pool]" INFO FAIL
                Publish-AzsSupportStorageChecks ($($StorageHealth.'Health Running' | Where-Object { $_ -notlike $null }).count -ne $Nodes.count) "Cluster Nodes Health Process Running" "[Check for Health Process not running]" INFO FAIL
                Publish-AzsSupportStorageChecks $($StorageHealth.'Virtual Disk'.Friendlyname) "Virtual Disk Check" "[Check for Virtual Disks in bad state]" INFO FAIL
                Publish-AzsSupportStorageChecks $($StorageHealth.'Storage Jobs'.Name) "Storage Job Check" "[Check For Storage Jobs]" INFO WARN
                Publish-AzsSupportStorageChecks $($StorageHealth.'Cluster Nodes'.Name) "Cluster Node Check" "[Cluster Node Check]" INFO FAIL
                Publish-AzsSupportStorageChecks $($StorageHealth.'CSV'.Name) "Cluster Shared Volumes Check" "[Cluster Shared Volumes Check]" INFO FAIL
                Publish-AzsSupportStorageChecks $($StorageHealth.'Enclosures'.FriendlyName) "Storage Enclosure Check" "[Storage Enclosure Check]" INFO FAIL
                Publish-AzsSupportStorageChecks $($StorageHealth.'Current Faults'.Reason) "Health Service Fault Check" "[Check for Health Service Fault]" INFO WARN
                Publish-AzsSupportStorageChecks $($StorageHealth.'Storage Health Action'.State) "Storage Health Action Check" "[Check for health-related system activities for Storage subsystems, file shares, and volumes]" INFO FAIL
                Publish-AzsSupportStorageChecks $($StorageHealth.'Disks Not In Pool') "Disks Not In Pool Check" "[Check for disks not in non primordial pool]" INFO FAIL
                Publish-AzsSupportStorageChecks $($StorageHealth.'Dirty Count') "Dirty Count" "[Check if Dirty Count Exceeds Limit]" INFO FAIL
                Publish-AzsSupportStorageChecks $($StorageHealth.'SupportComponentsChange') "Support Components Change" "[Check if changes needed for Supported Components]" INFO INFO
                Publish-AzsSupportStorageChecks $($StorageHealth.'SupportComponentsMissing') "Support Components Missing" "[Check if there is missing Supported Components]" INFO FAIL
                Publish-AzsSupportStorageChecks $($StorageHealth.'SNVFaultyDisk') "Storage Node View Differs" "[Storage Node View Differs]" INFO FAIL
                Publish-AzsSupportStorageChecks $($StorageHealth.'FWDrift') "Firmware Drift" "[Firmware Drift]" INFO INFO
                Publish-AzsSupportStorageChecks ($($StorageHealth.'SMPHost Running'  | Where-Object { $_ -notlike $null }).count -ne $Nodes.count) "Cluster Nodes SMPHost Running" "[Check for SMPHost Service not running]" INFO FAIL
                Publish-AzsSupportStorageChecks $($StorageHealth.'SMPHost Issue'  | Where-Object { $_ -notlike $null }) "SMPHost Issue Detected" "[Check for SMPHost Issue]" INFO FAIL
                Publish-AzsSupportStorageChecks $($DiskHealth | Where-Object { $_.Partitions -notlike "*Clus|Microsoft SBL Cache Store``], Space Protective*" -and $_.Partitions -notlike "*Clus|Microsoft SBL Cache Hdd``], Space Protective*" -and $_.Partitions -notlike "*Space Protective, ``[Clus|Microsoft SBL Cache Store``]*" -and $_.Partitions -notlike "*Space Protective, ``[Clus|Microsoft SBL Cache Hdd``]*" }) "Storage Spaces Partitions Check" "[Check for corrupt\missing partitions] " INFO FAIL
                Publish-AzsSupportStorageChecks $($DiskHealth.'Health' -ne 'Healthy') "Disk Health Check" "[Disks in state other than healthy]" INFO FAIL
                Publish-AzsSupportStorageChecks $($DiskHealth.'Opst' -like "Transient Error") "Transient Disk Check" "[Check For Disks In Transient State]" INFO FAIL
            }
        }


        #===================================================================
        # Storage Known Issue Breakdown Of Analysis :
        #===================================================================

        if ([string]::IsNullOrEmpty($StorageCheck.Validation.keys)) { 
            Write-Host "$($msg.StorageNoIssuesFound)" -ForegroundColor Green | format-list
        }
        else {

            Write-Host "`r`n$($msg.StorageAnalysisBreakdown)`r`n"

            switch ($StorageCheck."Validation".Keys) {
                'Non Communicating Disks' {
                    Write-Colour "$PSitem".PadRight(50), '[', " $($msg.StorageReportFail) ", ']', `n -ForeGroundColor Yellow, White, Red, White
                    Write-Host "$($msg.StorageReason)".PadRight(50), "$($msg.StorageNCDReason)"
                    Write-Host "$($msg.StorageRecommendation)".PadRight(50), "$($msg.StorageNCDRecommendation)"
                    Write-Host ($DiskHealth.Lostdisks | Format-Table | Out-String) `r `n

                    if ($DiskHealth.LostDisks.LostDiskPNPcheck) {
                        Write-Host ($DiskHealth.LostDisks.LostDiskPNPcheck | Format-Table | Out-String) `r `n
                    }
                }
                'SBL Disk Check' {
                    Write-Colour "$PSitem".PadRight(50), '[', " $($msg.StorageReportFail) ", ']', `n -ForeGroundColor Yellow, White, Red, White
                    Write-Host "$($msg.StorageReason)".PadRight(50), "$($msg.StorageSDCReason)"
                    Write-Host "$($msg.StorageRecommendation)".PadRight(50), "$($msg.StorageSDCRecommendation)"
                    Write-Host ($($DiskHealth | Where-Object { ($_.SBLAttribute -notmatch 'Default') -or ($_.SBLDiskCacheState -notmatch 'CacheDiskStateInitializedAndBound') -and ($null -ne $_.Serialnumber) } | Select-Object SerialNumber, Health, Opst, SBLDiskCacheState, SBLAttribute, Slot, Media, Node ) | Format-Table | Out-String) `r `n
                }
                'Missing Disks From Storage Spaces' {
                    Write-Colour "$PSitem".PadRight(50), '[', " $($msg.StorageReportInfo) ", ']', `n -ForeGroundColor Yellow, White, Cyan, White
                    Write-Host "$($msg.StorageReason)".PadRight(50), "$($msg.StorageMDFSSReason)"
                    Write-Host "$($msg.StorageRecommendation)".PadRight(50), "$($msg.StorageMDFSSRecommendation)"
                    Write-Host ($StorageHealth.'Missing Disks' | Out-String) `r `n
                }
                'Storage Pool Health Check' {
                    Write-Colour "$PSitem".PadRight(50), '[', " $($msg.StorageReportFail) ", ']', `n  -ForeGroundColor Yellow, White, Red, White
                    Write-Host "$($msg.StorageReason)".PadRight(50), "$($msg.StorageSPHCReason)"
                    Write-Host "$($msg.StorageRecommendation)".PadRight(50), "$($msg.StorageSPHCRecommendation)"
                    Write-Host ($StorageHealth.'Storage Pool' | Format-Table | Out-String) `r `n
                }
                'Cluster Nodes Health Process Running' {
                    Write-Colour "$PSitem".PadRight(50), '[', " $($msg.StorageReportFail) ", ']', `n  -ForeGroundColor Yellow, White, Red, White
                    Write-Host "$($msg.StorageReason)".PadRight(50), "$($msg.StorageCNHPRReason)"
                    Write-Host "$($msg.StorageRecommendation)".PadRight(50), "$($msg.StorageCNHPRRecommendation)"
                    Write-Host ($StorageHealth.'Health Running' | Sort-Object MachineName | Format-Table | Out-String) `r `n
                }
                'Disk Health Check' {
                    Write-Colour "$PSitem".PadRight(50), '[', " $($msg.StorageReportFail) ", ']', `n  -ForeGroundColor Yellow, White, Red, White
                    Write-Host "$($msg.StorageReason)".PadRight(50), "$($msg.StorageDHCReason)"
                    Write-Host "$($msg.StorageRecommendation)".PadRight(50), "$($msg.StorageDHCRecommendation)"
                    $Columns = "SerialNumber", "Media", "Health", "OpSt", "Cache", "Slot", "SBLDiskCacheState", "SBLCacheUsageCurrent", "SBLCacheUsageDesired", "Node"
                    $UnhealthyDisks = $DiskHealth | Where-Object { $_.Health -notlike 'Healthy' -or $_.Opst -ne "OK" -and $null -ne $_.SerialNumber }
                    Foreach ($Disk in $UnhealthyDisks) {
                        $Disk | Format-Table $Columns | Out-String
                        Write-Host `r`n"$($msg.StorageDHCSourceDiskEvents)"`r`n  | Format-List
                        Write-Host ($Disk.EventLog | Format-Table | Out-String) `r `n
                    }
                }
                'Virtual Disk Check' {
                    Write-Colour "$PSitem".PadRight(50), '[', " $($msg.StorageReportFail) ", ']', `n  -ForeGroundColor Yellow, White, Red, White
                    Write-Host "$($msg.StorageReason)".PadRight(50), "$($msg.StorageVDCReason)"
                    Write-Host "$($msg.StorageRecommendation)".PadRight(50), "$($msg.StorageVDCRecommendation)"
                    Write-Host ($StorageHealth.'Virtual Disk' | Format-Table | Out-String) `r `n

                    foreach ($VirtDisk in $($StorageHealth.VirtPhysicalExtents.VirtualDisk)) {
                        Write-Host "$($msg.StorageVDCVDNotHealthy)" 
                        Write-Host ($VirtDisk | Format-Table | Out-String) `r `n

                        if ($($StorageHealth.VirtPhysicalExtents)) {
                            Write-Host "$($msg.StorageVDCExtentsHealthy)"
                            Write-Host ($StorageHealth.VirtPhysicalExtents.Extents | Format-Table | Out-String) `r `n
                            Write-Host "$($msg.StorageVDCRootCause)" 
                            Write-Host($($StorageHealth.VirtPhysicalExtents.Disks) | Format-Table  | Out-String) `r `n
                        }
                        else {
                            Write-Host "$($msg.StorageVDCExtentsTimeout)"
                            Write-Host "$($msg.StorageVDCExtentsTimeoutValue)" `r `n
                        }
                    }
                }
                'Transient Disk Check' {
                    Write-Colour "$PSitem".PadRight(50), '[', " $($msg.StorageReportFail) ", ']', `n  -ForeGroundColor Yellow, White, Red, White
                    Write-Host "$($msg.StorageReason)".PadRight(50), "$($msg.StorageTDCReason)"
                    Write-Host "$($msg.StorageRecommendation)".PadRight(50), "$($msg.StorageTDCRecommendation)"
                    $Columns = "SerialNumber", "Usage", "Media", "FW", "Model", "Health", "Opst", "Node" ; Write-Host ($DiskHealth | Where-Object { $_.Opst -like "Transient Error" } | Format-Table $Columns | Out-String)   `r `n
                }
                'Storage Job Check' {
                    Write-Colour "$PSitem".PadRight(50), '[', " $($msg.StorageReportWarn) ", ']', `n  -ForeGroundColor Yellow, White, Yellow, White
                    Write-Host "$($msg.StorageReason)".PadRight(50), "$($msg.StorageSJCReason)"
                    Write-Host "$($msg.StorageRecommendation)".PadRight(50), "$($msg.StorageSJCRecommendation)"
                    Write-Host ($StorageHealth.'Storage Jobs' | Format-Table | Out-String) `r `n
                }
                'Cluster Node Check' {
                    Write-Colour "$PSitem".PadRight(50), '[', " $($msg.StorageReportFail) ", ']', `n  -ForeGroundColor Yellow, White, Red, White
                    Write-Host "$($msg.StorageReason)".PadRight(50), "$($msg.StorageCNCReason)"
                    Write-Host "$($msg.StorageRecommendation)".PadRight(50), "$($msg.StorageCNCRecommendation)"
                    Write-Host ($StorageHealth.'Cluster Nodes' | Format-Table | Out-String) `r `n
                }
                'Cluster Shared Volumes Check' {
                    Write-Colour "$PSitem".PadRight(50), '[', " $($msg.StorageReportFail) ", ']', `n  -ForeGroundColor Yellow, White, Red, White
                    Write-Host "$($msg.StorageReason)".PadRight(50), "$($msg.StorageCSVCReason)"
                    Write-Host "$($msg.StorageRecommendation)".PadRight(50), "$($msg.StorageCSVCRecommendation)"
                    Write-Host ($StorageHealth.'CSV' | Out-String) `r `n  | Format-List
                }
                'Storage Enclosure Check' {
                    Write-Colour "$PSitem".PadRight(50), '[', " $($msg.StorageReportFail) ", ']', `n  -ForeGroundColor Yellow, White, Red, White
                    Write-Host "$($msg.StorageReason)".PadRight(50), "$($msg.StorageSECReason)"
                    Write-Host "$($msg.StorageRecommendation)".PadRight(50), "$($msg.StorageSECRecommendation)"
                    foreach ($Enclosure in $StorageHealth.'Enclosures') {
                        $SE = [regex]::Match($Enclosure.objectid, 'SE:{(.*?)}').Groups[1].Value
                        $SNV = ($StorageHealth.'EnclosureSNV' | Where-Object { $_.StorageEnclosureObjectId -match $SE }).StorageNodeObjectId
                        [regex]::match($SNV, 'SN:(.*?)"').Groups[1].Value
                        $Columns = "FriendlyName", "SerialNumber", "OperationalStatus", "HealthStatus", "NumberOfSlots", "Uniqueid" ; Write-Host ($Enclosure | Format-Table $Columns | Out-String)  `r `n
                    }
                }
                'Health Service Fault Check' {
                    Write-Colour "$PSitem".PadRight(50), '[', " $($msg.StorageReportWarn) ", ']', `n  -ForeGroundColor Yellow, White, Yellow, White
                    Write-Host "$($msg.StorageReason)".PadRight(50), "$($msg.StorageSSCReason)"
                    Write-Host "$($msg.StorageRecommendation)".PadRight(50), "$($msg.StorageSSCRecommendation)"
                    Write-Host ($StorageHealth.'Current Faults' | Out-String) `r `n  | Format-List
                }
                'Storage Health Action Check' {
                    Write-Colour "$PSitem".PadRight(50), '[', " $($msg.StorageReportFail) ", ']', `n  -ForeGroundColor Yellow, White, Red, White
                    Write-Host "$($msg.StorageReason)".PadRight(50), "$($msg.StorageSHACReason)"
                    Write-Host "$($msg.StorageRecommendation)".PadRight(50), "$($msg.StorageSHACRecommendation)"
                    Write-Host ($StorageHealth.'Storage Health Action' | Format-Table | Out-String) `r `n
                }
                'Storage Spaces Partitions Check' {
                    Write-Colour "$PSitem".PadRight(50), '[', " $($msg.StorageReportFail) ", ']', `n  -ForeGroundColor Yellow, White, Red, White
                    Write-Host "$($msg.StorageReason)".PadRight(50), "$($msg.StorageSSPCReason)"
                    Write-Host "$($msg.StorageRecommendation)".PadRight(50), "$($msg.StorageSSPCRecommendation)"
                    Write-Host "$($DiskHealth | Where-Object {$_.Partitions -notlike "*Clus|Microsoft SBL Cache Store], Space Protective*" -and $_.Partitions -notlike "*Clus|Microsoft SBL Cache Hdd``], Space Protective*" -and $_.Partitions -notlike "*Space Protective, ``[Clus|Microsoft SBL Cache Store``]*" -and $_.Partitions -notlike "*Space Protective, ``[Clus|Microsoft SBL Cache Hdd``]*"} | Format-Table Node, Slot, Media, Model , SerialNumber, Partitions | Out-String) `r `n "
                }
                'Disks Not In Pool Check' {
                    Write-Colour "$PSitem".PadRight(50), '[', " $($msg.StorageReportFail) ", ']', `n  -ForeGroundColor Yellow, White, Red, White
                    Write-Host "$($msg.StorageReason)".PadRight(50), "$($msg.StorageDNIPCReason)"
                    Write-Host "$($msg.StorageRecommendation)".PadRight(50), "$($msg.StorageDNIPCRecommendation)"
                    Write-Host ($StorageHealth.'Disks Not In Pool' | Out-String) `r `n | Format-List
                }
                'Dirty Count' {
                    Write-Colour "$PSitem".PadRight(50), '[', " $($msg.StorageReportFail) ", ']', `n  -ForeGroundColor Yellow, White, Red, White
                    Write-Host "$($msg.StorageReason)".PadRight(50), "$($msg.StorageDCReason)"
                    Write-Host "$($msg.StorageRecommendation)".PadRight(50), "$($msg.StorageDCRecommendation)"
                    Write-Host ($StorageHealth.'Dirty Count' | Out-String) `r `n | Format-List
                }
                'Support Components Missing' {
                    Write-Colour "$PSitem".PadRight(50), '[', " $($msg.StorageReportFail) ", ']', `n  -ForeGroundColor Yellow, White, Red, White
                    Write-Host "$($msg.StorageReason)".PadRight(50), "$($msg.StorageSCMReason)"
                    Write-Host "$($msg.StorageRecommendation)".PadRight(50), "$($msg.StorageSCMRecommendation)"
                    Write-Host ($StorageHealth.'SupportComponentsMissing' | Out-String) `r `n | Format-List
                }
                'Support Components Change' {
                    Write-Colour "$PSitem".PadRight(50), '[', " $($msg.StorageReportInfo) ", ']', `n  -ForeGroundColor Yellow, White, Cyan, White
                    Write-Host "$($msg.StorageReason)".PadRight(50), "$($msg.StorageSCCReason)"
                    Write-Host "$($msg.StorageRecommendation)".PadRight(50), "$($msg.StorageSCCRecommendation)"
                    Write-Host ($StorageHealth.'SupportComponentsChange' | Out-String) `r `n | Format-List
                }
                'Storage Node View Differs' {
                    Write-Colour "$PSitem".PadRight(50), '[', " $($msg.StorageReportFail) ", ']', `n  -ForeGroundColor Yellow, White, Red, White
                    Write-Host "$($msg.StorageReason)".PadRight(50), "$($msg.StorageSNVDReason)"
                    Write-Host "$($msg.StorageRecommendation)".PadRight(50), "$($msg.StorageSNVDRecommendation)"
                    Write-Host $($StorageHealth.'SNVFaultyDisk' | Out-String) `r `n | Sort-Object  ObjectId |  Format-Table -AutoSize
                }
                'Firmware Drift' {
                    Write-Colour "$PSitem".PadRight(50), '[', " $($msg.StorageReportInfo) ", ']', `n  -ForeGroundColor Yellow, White, Cyan, White
                    Write-Host "$($msg.StorageReason)".PadRight(50), "$($msg.StorageFDReason)"
                    Write-Host "$($msg.StorageRecommendation)".PadRight(50), "$($msg.StorageFDRecommendation)"
                    Write-Host $($StorageHealth.'FWDrift' | Out-String) `r `n | Sort-Object  Model |  Format-Table -AutoSize
                }
                'SMPHost Check' {
                    Write-Colour "$PSitem".PadRight(50), '[', " $($msg.StorageReportFail) ", ']', `n  -ForeGroundColor Yellow, White, Red, White
                    Write-Host "$($msg.StorageReason)".PadRight(50), "$($msg.StorageSCReason)"
                    Write-Host "$($msg.StorageRecommendation)".PadRight(50), "$($msg.StorageSCRecommendation)"
                    Write-Host $($StorageHealth. 'SMPHost Running' | Out-String) `r `n | Sort-Object ComputerName |  Format-Table -AutoSize
                }
                'SMPHost Issue Check' {
                    Write-Colour "$PSitem".PadRight(50), '[', " $($msg.StorageReportFail) ", ']', `n  -ForeGroundColor Yellow, White, Red, White
                    Write-Host "$($msg.StorageReason)".PadRight(50), "$($msg.StorageSICReason)"
                    Write-Host "$($msg.StorageRecommendation)".PadRight(50), "$($msg.StorageSICRecommendation)"
                    Write-Host "$($msg.StorageSICVirtualDisksCSV)"
                }
            }
        }
        if ($Include -contains "DiskHealth" -or !$Include) {
            Get-AzsSupportStorageDisksNode -DiskHealth $DiskHealth -Include $Include
        }

        #Trace-AzsSupportCommand -Event OnExit

    }
    catch {

        #$formattedException = Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException

    }
}

function Get-AzsSupportStorageDiskHealth {

    <#
        .SYNOPSIS
            Gets Disk Health From Storage Spaces Per Cluster Node
 
        .DESCRIPTION
            Pulls all needed information for Disks into a per Node sorted view
 
        .PARAMETER ComputerName
            The Cluster you want to check for Missing Disks
             
        .EXAMPLE
            Get-AzsSupportStorageDiskHealth -ComputerName $Nodes
    #>


    param (
        [Parameter(Mandatory = $true)]
        $ComputerName
    )

    try {

        #Trace-AzsSupportCommand -Event OnEntry
        Trace-Output -Level:Verbose -Message ($msg.StoragePerfCountersCacheDisks)
        $PerfCounters = Get-AzsSupportStorageCacheDetails -ComputerName $ComputerName

        Trace-Output -Level:Verbose -Message ($msg.StorageSBLStateDisks)
        $ClusBFltDisk = Get-AzsSupportStorageDiskSBLState -ComputerName   $ComputerName

        try {
            Trace-Output -Level:Verbose -Message ($msg.StorageNodeRemoteSession)
            $sessions = New-AzsSupportPSSession -ComputerName  $ComputerName

            $DiskHealth = Invoke-Command -Session $sessions -ScriptBlock {

                param (
                    $ClusBFltDisk,
                    $PerfCounters
                )

                function Get-PartitionType {
                    Param (

                        [Parameter(Mandatory = $True)]
                        [String[]]$Id,
                        [Parameter(Mandatory = $False)]
                        [String[]]$Name = $null
                    )
                    <#
 
                        .SYNOPSIS
                            Translates partition GUIDs to readable text from output for disk
 
                        .DESCRIPTION
                            Allows the engineer to view if partitions have been correctly setup for Storage Spaces by translating partition GUIDs
 
                        .EXAMPLE
                            PS C:\> Get-PartitionType $ptype
 
                        .PARAMETER Id
                            The partition id you wish to translate
 
                        .PARAMETER Name
                            This is set to $null to ensure each check is valid and not displaying prior data
                    #>


                    switch -Regex ($id) {
                        "c12a7328-f81f-11d2-ba4b-00a0c93ec93b" { "System" }
                        "e3c9e316-0b5c-4db8-817d-f92df00215ae" { "Reserved" }
                        "ebd0a0a2-b9e5-4433-87c0-68b6b72699c7" { "Basic" }
                        "5808c8aa-7e8f-42e0-85d2-e1e90434cfb3" { "LDM Metadata" }
                        "af9b60a0-1431-4f62-bc68-3311714a69ad" { "LDM Data" }
                        "de94bba4-06d1-4d40-a16a-bfd50179d6ac" { "Recovery" }
                        "e75caf8f-f680-4cee-afa3-b001e56efc2d" { "Space Protective" }
                        "PARTITION_SPACES_GUID" { "Space Protective" }
                        "eeff8352-dd2a-44db-ae83-bee1cf7481dc" { "Microsoft SBL Cache Store" }
                        "03aaa829-ebfc-4e7e-aac9-c4d76c63b24b" { "Microsoft SBL Cache Hdd" }
                        "db97dba9-0840-4bae-97f0-ffb9a327c7e1" { "[Clus|$name]" }
                        "PARTITION_CLUSTER_GUID" { "[Clus|$name]" }
                        Default { "[$id|$name]" }
                    }
                }

                function Get-AzsSupportStorageDiskErrors {
                    <#
                        .SYNOPSIS
                            Lists errors from event logs for a specified disk.
 
                        .PARAMETER Disk
                            A physical disk from $Disk
 
                        .EXAMPLE
                            PS> Get-AzsSupportStorageDiskErrors
 
                        .OUTPUTS
                            List of disk errors found for a given disk
                    #>


                    [CmdletBinding()]
                    Param (
                        [Parameter(Mandatory = $True)]
                        [array[]]$Disk
                    )


                    try {

                        # Microsoft-Windows-Storage-Storport/Operational Events
                        $LogName = 'Microsoft-Windows-Storage-Storport/Operational'
                        $Source = 'Microsoft-Windows-StorPort'
                        $ID = 500, 501, 502, 507, 508

                        $lookup = @{
                            500 = "Timeout"
                            501 = "Reset"
                            502 = "Unresponsive"
                            507 = "Reset is busted"
                            508 = "Abort is busted"
                        }


                        $Events = Get-WinEvent -ErrorAction SilentlyContinue -filterhashtable @{LogName = $LogName; ProviderName = $Source; ID = $ID } | Where-Object { $_.Properties[2].value -like "*$($disk.trim())*" } | Select-Object LogName, TimeCreated, Id, LevelDisplayName, OpcodeDisplayName, @{Name = 'Value'; Expression = { $Lookup[$_.Id] } } | Select-Object -first 1 -last 1

                        foreach ($Event in $Events) {

                            New-Object psobject -Property @{
                                LogName           = $Event.LogName
                                TimeCreated       = $Event.TimeCreated
                                Value             = $Event.Value
                                Id                = $Event.Id
                                LevelDisplayName  = $Event.LevelDisplayName
                                OpcodeDisplayName = $Event.OpcodeDisplayName

                            }
                        }


                        # Microsoft-Windows-StorageSpaces-Driver/Operational Events
                        $LogName = 'Microsoft-Windows-StorageSpaces-Driver/Operational'
                        $Source = 'Microsoft-Windows-StorageSpaces-Driver'
                        $ID = 200, 202, 203, 204, 205

                        $lookup = @{
                            200 = "DriveHeaderReadError"
                            202 = "DriveHeaderSplit"
                            203 = "DriveIoError"
                            204 = "DriveImpendingFailure"
                            205 = "LostCommunication"
                        }
                        
                        $Events = Get-WinEvent -ErrorAction SilentlyContinue -filterhashtable @{LogName = $LogName; ProviderName = $Source; ID = $ID } | Where-Object { $_.Properties[0].value -like "*$($disk.trim())*" } | Select-Object LogName, TimeCreated, Id, LevelDisplayName, OpcodeDisplayName, @{Name = 'Value'; Expression = { $Lookup[$_.Id] } } | Select-Object -first 1 -last 1

                        foreach ($Event in $Events) {
                            New-Object psobject -Property @{
                                LogName           = $Event.LogName
                                TimeCreated       = $Event.TimeCreated
                                Value             = $Event.Value
                                Id                = $Event.Id
                                LevelDisplayName  = $Event.LevelDisplayName
                                OpcodeDisplayName = $Event.OpcodeDisplayName

                            }
                        }
                    }
                    catch {
                        $_.Exception.Message
                    }
                }


                $Assem = ('System.dll', 'System.Data.dll')                # Can't utilise $using in array so declaring here
                $PDDisks = Get-StorageNode | Get-PhysicalDisk -PhysicallyConnected | Where-Object { $_.FriendlyName -notmatch 'LOGICAL VOLUME|Msft|RAID|Virtual' }

                if (!$PDDisks) {
                    # Check for virtual as no Physical have been found
                    $PDDisks = Get-StorageNode | Get-PhysicalDisk -PhysicallyConnected | Where-Object { $_.FriendlyName -notmatch 'LOGICAL VOLUME|RAID' -and $_.MediaType -notlike "Unspecified" }

                }
                # Create C# code for Add-Type"

                $Source = @"
  using System;
  using System.Collections;
  using System.Collections.Generic;
  using System.Data;
  using System.Diagnostics;
  using Microsoft.Win32.SafeHandles;
  using System.ComponentModel;
  using System.Runtime.InteropServices;
  using System.Security;
 
  namespace PartitionFinder
  {
     public class IOCtl
     {
          private const int GENERIC_READ = unchecked((int)0x80000000);
          private const int FILE_SHARE_READ = 1;
          private const int FILE_SHARE_WRITE = 2;
          private const int OPEN_EXISTING = 3;
          private const int IOCTL_DISK_GET_DRIVE_LAYOUT_EX = unchecked((int)0x00070050);
          private const int ERROR_INSUFFICIENT_BUFFER = 122;
 
          private enum PARTITION_STYLE : int
          {
              MBR = 0,
              GPT = 1,
              RAW = 2
          }
 
          private enum Partition : byte
 
          {
              Fat12 = 0x01,
              XenixRoot = 0x02,
              Xenixusr = 0x03,
              Fat16Small = 0x04,
              Extended = 0x05,
              Fat16 = 0x06,
              Ntfs = 0x07,
              Fat32 = 0x0B,
              Fat32Lba = 0x0C,
              Fat16Lba = 0x0E,
              ExtendedLba = 0x0F,
              HiddenFAT12 = 0x11,
              WindowsDynamicVolume = 0x42,
              LinuxSwap = 0x82,
              LinuxNative = 0x83,
              LinuxLvm = 0x8E,
              GptProtective = 0xEE,
              EfiSystem = 0xEF,
              GptStorageProtective = 0xE7
          }
 
 
          [SuppressUnmanagedCodeSecurity()]
          private class NativeMethods
          {
             [DllImport("kernel32", CharSet = CharSet.Unicode, SetLastError = true)]
             public static extern SafeFileHandle CreateFile(
             string fileName,
             int desiredAccess,
             int shareMode,
             IntPtr securityAttributes,
             int creationDisposition,
             int flagsAndAttributes,
             IntPtr hTemplateFile);
 
 
             [DllImport("kernel32", SetLastError = true)]
             [return: MarshalAs(UnmanagedType.Bool)]
             public static extern bool DeviceIoControl(
             SafeFileHandle hVol,
             int controlCode,
             IntPtr inBuffer,
             int inBufferSize,
             IntPtr outBuffer,
             int outBufferSize,
             ref int bytesReturned,
             IntPtr overlapped);
          }
 
 
          // Needs to be explicit to do the union.
          [StructLayout(LayoutKind.Explicit)]
          private struct DRIVE_LAYOUT_INFORMATION_EX
          {
              [FieldOffset(0)]
              public PARTITION_STYLE PartitionStyle;
              [FieldOffset(4)]
              public int PartitionCount;
              [FieldOffset(8)]
              public DRIVE_LAYOUT_INFORMATION_MBR Mbr;
              [FieldOffset(8)]
              public DRIVE_LAYOUT_INFORMATION_GPT Gpt;
          }
 
 
          private struct DRIVE_LAYOUT_INFORMATION_MBR
          {
 
          }
 
 
          [StructLayout(LayoutKind.Sequential)]
          private struct DRIVE_LAYOUT_INFORMATION_GPT
          {
              public Guid DiskId;
              public long StartingUsableOffset;
              public long UsableLength;
              public int MaxPartitionCount;
          }
 
 
          [StructLayout(LayoutKind.Sequential)]
          private struct PARTITION_INFORMATION_MBR
          {
              public byte PartitionType;
              [MarshalAs(UnmanagedType.U1)]
              public bool BootIndicator;
              [MarshalAs(UnmanagedType.U1)]
              public bool RecognizedPartition;
              public UInt32 HiddenSectors;
 
 
              // helper method - is the hi bit valid - if so IsNTFT has meaning.
              public bool IsValidNTFT()
              {
                  return (PartitionType & 0xc0) == 0xc0;
              }
 
              // is this NTFT - i.e. an NTFT raid or mirror.
              public bool IsNTFT()
              {
                  return (PartitionType & 0x80) == 0x80;
              }
 
 
              // the actual partition type.
              public Partition GetPartition()
              {
                  const byte mask = 0x3f;
                  return (Partition)(PartitionType & mask);
              }
          }
 
 
          [StructLayout(LayoutKind.Explicit, CharSet = CharSet.Unicode)]
          private struct PARTITION_INFORMATION_GPT
          {
              [FieldOffset(0)]
              public Guid PartitionType;
              [FieldOffset(16)]
              public Guid PartitionId;
              [FieldOffset(32)]
              //DWord64
              public ulong Attributes;
              [FieldOffset(40)]
              [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 36)]
              public string Name;
          }
 
 
          [StructLayout(LayoutKind.Explicit)]
          private struct PARTITION_INFORMATION_EX
          {
              [FieldOffset(0)]
              public PARTITION_STYLE PartitionStyle;
              [FieldOffset(8)]
              public long StartingOffset;
              [FieldOffset(16)]
              public long PartitionLength;
              [FieldOffset(24)]
              public int PartitionNumber;
              [FieldOffset(28)]
              [MarshalAs(UnmanagedType.U1)]
              public bool RewritePartition;
              [FieldOffset(32)]
              public PARTITION_INFORMATION_MBR Mbr;
              [FieldOffset(32)]
              public PARTITION_INFORMATION_GPT Gpt;
          }
 
         public static void SendIoCtlDiskGetDriveLayoutEx(int PhysicalDrive)
          {
              DRIVE_LAYOUT_INFORMATION_EX lie = default(DRIVE_LAYOUT_INFORMATION_EX);
              PARTITION_INFORMATION_EX[] pies = null;
              using (SafeFileHandle hDevice =
              NativeMethods.CreateFile("\\\\.\\PHYSICALDRIVE" + PhysicalDrive, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, IntPtr.Zero, OPEN_EXISTING, 0, IntPtr.Zero))
              {
                  if (hDevice.IsInvalid)
                      throw new Win32Exception();
                  // Must run as administrator, otherwise we get "ACCESS DENIED"
                  // We don't know how many partitions there are, so we have to use a blob of memory...
 
            int numPartitions = 1;
            bool done = false;
            do {
                // 48 = the number of bytes in DRIVE_LAYOUT_INFORMATION_EX up to
                // the first PARTITION_INFORMATION_EX in the array.
                // And each PARTITION_INFORMATION_EX is 144 bytes.
                int outBufferSize = 48 + (numPartitions * 144);
                IntPtr blob = default(IntPtr);
                int bytesReturned = 0;
                bool result = false;
 
 
                try {
                    blob = Marshal.AllocHGlobal(outBufferSize);
                    result = NativeMethods.DeviceIoControl(hDevice, IOCTL_DISK_GET_DRIVE_LAYOUT_EX, IntPtr.Zero, 0, blob, outBufferSize, ref bytesReturned, IntPtr.Zero);
 
                    // We expect that we might not have enough room in the output buffer.
                    if (result == false) {
                        // if the buffer wasn't too small, then something else went wrong.
                              if (Marshal.GetLastWin32Error() != ERROR_INSUFFICIENT_BUFFER)
                                  throw new Win32Exception();
                              // We need more space on the next loop.
                              numPartitions += 1;
                          }
                          else
                          {
                              // We got the size right, so stop looping.
                              done = true;
 
                              // Do something with the data here - we'll free the memory before we leave the loop.
                        // First we grab the DRIVE_LAYOUT_INFORMATION_EX, it's at the start of the blob of memory:
                              lie = (DRIVE_LAYOUT_INFORMATION_EX)Marshal.PtrToStructure(blob, typeof(DRIVE_LAYOUT_INFORMATION_EX));
                              // Then loop and add the PARTITION_INFORMATION_EX structures to an array.
                              pies = new PARTITION_INFORMATION_EX[lie.PartitionCount];
 
                              for (int i = 0; i <= lie.PartitionCount - 1; i++)
                              {
                                  // Where is this structure in the blob of memory?
                                  IntPtr offset = new IntPtr(blob.ToInt64() + 48 + (i * 144));
                                  pies[i] = (PARTITION_INFORMATION_EX)Marshal.PtrToStructure(offset, typeof(PARTITION_INFORMATION_EX));
                              }
                          }
                      }
                      finally
                      {
                          Marshal.FreeHGlobal(blob);
                      }
                  } while (!(done));
              }
              DumpInfo(lie, pies);
          }
 
 
          private static bool IsPart0Aligned(PARTITION_INFORMATION_EX[] pies)
          {
              try
              {
                  if (pies[0].StartingOffset % 4096 == 0)
                  {
                      return true;
                  }
                  else
                  {
                      return false;
                  }
              }
              catch
              {
                  return false;
              }
          }
 
 
          private static void DumpInfo(DRIVE_LAYOUT_INFORMATION_EX lie, PARTITION_INFORMATION_EX[] pies)
          {
              if (IsPart0Aligned(pies) == true)
              {
                  Console.Write("True");
              }
              else
              {
                  Console.Write("false");
              }
 
 
              Console.WriteLine("Partition Style: {0}", lie.PartitionStyle);
              Console.WriteLine("Partition Count: {0}", lie.PartitionCount);
              switch (lie.PartitionStyle)
              {
                  case PARTITION_STYLE.MBR:
                      break;
 
                  case PARTITION_STYLE.GPT:
                      Console.WriteLine("Gpt DiskId: {0}", lie.Gpt.DiskId);
                      break;
 
                  default:
                      Console.WriteLine("RAW!");
                      break;
              }
 
 
              for (int i = 0; i <= lie.PartitionCount - 1; i++)
 
              {
                  Console.WriteLine();
                  Console.WriteLine();
                  var _with1 = pies[i];
                  Console.WriteLine("Partition style: {0}", _with1.PartitionStyle);
                  Console.WriteLine("Partition number: {0}", _with1.PartitionNumber);
                  switch (_with1.PartitionStyle)
                  {
                      case PARTITION_STYLE.MBR:
                          var _with2 = _with1.Mbr;
                          Console.WriteLine("\r\t PartitionType - raw value {0}\n", _with2.PartitionType);
                          Console.WriteLine("\r\t BootIndicator {0}\n", _with2.BootIndicator);
                          Console.WriteLine("\r\t RecognizedPartition {0}\n", _with2.RecognizedPartition);
                          Console.WriteLine("\r\t HiddenSectors {0}\n", _with2.HiddenSectors);
                          break;
 
                      case PARTITION_STYLE.GPT:
                          var _with3 = _with1.Gpt;
                          Console.WriteLine("\r\t PartitionType {0}\n", _with3.PartitionType);
                          Console.WriteLine("\r\t PartitionId {0}\n", _with3.PartitionId);
                          Console.WriteLine("\r\t Name {0}\n", _with3.Name);
                          break;
 
                      case PARTITION_STYLE.RAW:
                          Console.WriteLine("RAW!");
                          break;
 
                      default:
                          Console.WriteLine("Unknown!");
                          break;
                  }
              }
          }
      }
  }
"@


                # Check if Add-Type has already been run
                if ($null -eq ("PartitionFinder.IOCtl" -as [type])) {
                    Add-Type -ReferencedAssemblies $Assem -TypeDefinition $Source -Verbose -ErrorAction Continue
                }



                # Get all disks connected to Cluster Node
                $d = Get-WmiObject -ErrorAction Stop -Namespace root\wmi ClusPortDeviceInformation  | Where-Object { $_.ConnectedNode -like $Env:ComputerName }

                if ($null -eq $d) {
                    throw "$($msg.StorageNotStorageSpacesDirect)"
                }


                # "Filter Disks from Cluster to ensure non Virtual\Default"
                # Filter to non-default (0) non-virtual (0x1) devices; actually a bitmask
                $filteredDisks = $d | Sort-Object ConnectedNode, ConnectedNodeDeviceNumber | Where-Object {
                    # non-default (enclosure) and non-virtual devices
                    $_.DeviceAttribute -and -not ($_.DeviceAttribute -band 0x1) }


                # Get Storage Spaces Partition Info for Cluster Node disks
                $DiskInfo = foreach ($disk in $filteredDisks) {

                    $oldOut = [Console]::Out
                    $newOut = New-Object IO.StringWriter

                    try {
                        [Console]::SetOut($newOut)
                        [PartitionFinder.IOCtl]::SendIoCtlDiskGetDriveLayoutEx($disk.DeviceNumber)
                    }

                    finally {
                        [Console]::SetOut($oldOut)
                    }


                    $output = $newOut.ToString()
                    $parts = $output.Split([Environment]::NewLine) | foreach-Object {
                        # PartitionType and Name are paired for every partition, in this order
                        if ($_ -match 'PartitionType\s+(.*)$') {
                            # be willing to handle partitions which diskutil does not name
                            if ($null -ne $ptype) {
                                Get-PartitionType $ptype
                            }
                            $ptype = $matches[1]
                        }
                        elseif ($_ -match 'Name\s+(.*)$') {
                            Get-PartitionType $ptype $matches[1]
                            $ptype = $null
                        }
                    }


                    # Checking if PDDisk Match SerialDisk
                    $SerialDisk = ($PDdisks | Where-Object { $_.Serialnumber -like $($Disk.SerialNumber.trim()) -and $null -ne $_.SerialNumber })

                    if (!$SerialDisk) {
                        # Check if Virtual
                        $SerialDisk = ($PDdisks | Where-Object { $_.Deviceid -like $($Disk.DeviceNumber) })
                    }


                    # Checking if NVMe and Adapters
                    #if ($SerialDisk.BusType -match "NVMe" -and $null -notlike $SerialDisk.FruId) {
                    # $SerialNumber = $SerialDisk.FruId
                    # $SlotNumber = "NA"
                    # }
                    #else {
                        $SerialNumber = $SerialDisk.SerialNumber
                        $SlotNumber = $SerialDisk.SlotNumber

                    #}


                    if ($SerialDisk.HealthStatus -ne "Healthy") {

                        $SerialDiskID = [regex]::match($($SerialDisk.ObjectId), '{PD:(.*?)}').Groups[1].Value
                        $DiskEvent = $(Get-AzsSupportStorageDiskErrors -Disk $SerialDiskID | out-string)

                        if ($DiskEvent) {
                            $EventLog = $DiskEvent
                        }
                        else {
                            $EventLog = "No Events Found"
                        }
                    }


                    # Collating DiskPartition data
                    New-Object -TypeName PSCustomObject -Property @{

                        Node                 = $Disk.ConnectedNode
                        DeviceId             = $Disk.DeviceGuid
                        Model                = $SerialDisk.Model
                        Usage                = $SerialDisk.Usage
                        Slot                 = $SlotNumber
                        Media                = $SerialDisk.MediaType
                        OpSt                 = $SerialDisk.OperationalStatus
                        FW                   = $SerialDisk.FirmwareVersion
                        Health               = $SerialDisk.HealthStatus
                        Partitions           = ($parts -join ', ')
                        PDID                 = $Disk.DeviceGuid
                        VirtDskFoot          = ($SerialDisk | Where-Object { $_.MediaType -ne 'Unspecified' } | select-Object @{N = 'Percentage' ; E = { ([math]::Round($_.Virtualdiskfootprint) / ($_.Size)).tostring("P") } }).Percentage
                        LostDisks            = $SerialDisk | Where-Object { $_.Opst -match "Lost Communication" } | Select-Object SerialNumber, MediaType, Slot, OpSt, Health, PNPid , ServerName, R/M, R/T
                        SBLAttribute         = $ClusBFltDisk.$($Disk.ConnectedNode).$($Disk.Deviceguid).SBLAttributes
                        SBLDiskCacheState    = $ClusBFltDisk.$($Disk.ConnectedNode).$($Disk.Deviceguid).SBLDiskCacheState
                        SBLCacheUsageCurrent = $ClusBFltDisk.$($Disk.ConnectedNode).$($Disk.Deviceguid).SBLCacheUsageCurrent
                        SBLCacheUsageDesired = $ClusBFltDisk.$($Disk.ConnectedNode).$($Disk.Deviceguid).SBLCacheUsageDesired
                        DiskNumber           = $Disk.ConnectedNodeDeviceNumber
                        'R/U'                = ($PerfCounters | Where-Object { $_.instancename -eq $($Disk.ConnectedNodeDeviceNumber) -and $_.Counter -match "read errors timeout" -and $_.Path -Match $($Disk.ConnectedNode) }).Value
                        'R/T'                = ($PerfCounters | Where-Object { $_.instancename -eq $($Disk.ConnectedNodeDeviceNumber) -and $_.Counter -match "read errors total" -and $_.Path -Match $($Disk.ConnectedNode) }).Value
                        'W/U'                = ($PerfCounters | Where-Object { $_.instancename -eq $($Disk.ConnectedNodeDeviceNumber) -and $_.Counter -match "write errors timeout" -and $_.Path -Match $($Disk.ConnectedNode) }).Value
                        'W/T'                = ($PerfCounters | Where-Object { $_.instancename -eq $($Disk.ConnectedNodeDeviceNumber) -and $_.Counter -match "write errors total" -and $_.Path -Match $($Disk.ConnectedNode) }).Value
                        Cache                = ($PerfCounters | Where-Object { $_.instancename -like "$($Disk.ConnectedNodeDeviceNumber):*" -and $_.Counter -match "disk transfers/sec" -and $_.Path -Match $($Disk.ConnectedNode) }).InstanceName
                        SerialNumber         = $SerialNumber
                        EventLog             = $EventLog
                        FruId                = $SerialDisk.FruId
                    }

                }
                Return  $DiskInfo
            } -ArgumentList $ClusBFltDisk, $PerfCounters
        }
        catch {
            Trace-Output -Level:Exception -Message ($msg.StorageUnableToRetrievePartitions)
        }

        Trace-Output -Level:Verbose -Message ($msg.StorageConvertValuesToText)
        $DiskHealth = Convert-AzsSupportStorageAttributes -DiskHealth $DiskHealth

        #Trace-AzsSupportCommand -Event OnExit
        return $DiskHealth

    }
    catch {

        #$formattedException = Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }
}

function Get-AzsSupportStorageDisksNode {

    <#
 
        .SYNOPSIS
            Output the disks in order of Nodes
 
        .DESCRIPTION
            Outputs just the disk data per node
         
        .PARAMETER DiskHealth
            Outputs from the Start-AzsSupportStorageDiagnostic function for disk health
 
        .PARAMETER Include
            Allows user to specify which tests to run. By default all tests are run.
                     
        .EXAMPLE
            PS C:\> Get-AzsSupportStorageDisksNode -DiskHealth $DiskHealth -Include SMPHost
 
    #>


    Param (

        [Parameter(Mandatory = $True)]
        $DiskHealth,
        [Parameter(Mandatory = $False)]
        [AllowEmptyCollection()]
        $Include
    )

    try {

        #Trace-AzsSupportCommand -Event OnEntry
        Trace-Output -Level:Verbose -Message ($msg.StorageDiagReportOrder)
        $CollectedServers = ($DiskHealth | Select-Object Node -Unique).Node | Sort-Object $_
        foreach ($CollectedServer in $CollectedServers) {
            Write-Host `n
            Write-Host `n
            Write-Host $CollectedServer -ForegroundColor Yellow
            Write-Host `n
            Write-Host ($($msg.StorageAssetTag)).PadRight(30) ': ' -NoNewline
            Write-Host $((Get-CimInstance win32_bios -CimSession $CollectedServer).SerialNumber) -ForegroundColor Yellow
            Write-Host ($($msg.StorageSSDCount)).PadRight(30) ': ' -NoNewline
            Write-Host ($DiskHealth | Where-Object { $_.Node -like $CollectedServer -and $_.Media -like "SSD" }).count -ForegroundColor Yellow
            Write-Host ($($msg.StorageHDDCount)).PadRight(30) ': ' -NoNewline
            Write-Host ($DiskHealth | Where-Object { $_.Node -like $CollectedServer -and $_.Media -like "HDD" }).count -ForegroundColor Yellow        
            Write-Host ($($msg.StorageSystemDriveFreeSpace)).PadRight(30) ': ' -NoNewline
            Write-Host $(Get-AzsSupportDiskSpace -ComputerName $CollectedServer -DriveLetter C | Select-Object @{Name = "GB"; Expression = { [long]($_.Free / 1GB) } }).GB GB -ForegroundColor Yellow

            if ($Include -contains "SMPHost" -or !$Include) {
                Write-Host ($($msg.StorageSMPHostMemory)).PadRight(30) ': ' -NoNewline
                Write-Host $($StorageHealth.'SMPHost Running' | Where-Object { $_.ComputerName -match $CollectedServer }).WorkingSetMB -ForegroundColor Yellow
            }

            #Check if Virtual deployment of Storage Spaces
            if ($DiskHealth.Model -notlike "Virtual Disk") {
                $DiskHealth | Where-Object { $_.Node -like $CollectedServer } | Sort-Object Slot | Format-Table  Serialnumber, Usage, Media, FW, Model, Health, Opst, R/U, R/T, W/U, W/T, Slot, DiskNumber, Cache, SBLAttribute, SBLDiskCacheState, SBLCacheUsageCurrent, VirtDskFoot -AutoSize -force | out-string -Width 4000
            }
            else {
                $DiskHealth | Where-Object { $_.Node -like $CollectedServer } | Sort-Object Slot | Format-Table  Usage, Media, FW, Model, Health, Opst, R/U, R/T, W/U, W/T, DiskNumber, Cache, SBLAttribute, SBLDiskCacheState, SBLCacheUsageCurrent, VirtDskFoot -AutoSize -force | out-string -Width 4000
            }
        }

        #Trace-AzsSupportCommand -Event OnExit

    }
    catch {

        #$formattedException = Get-FormattedException -Exception $_.Exception
        $_.Exception.Message | Trace-Output -Level:Exception
        #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException
    }

}

# SIG # Begin signature block
# MIIoQgYJKoZIhvcNAQcCoIIoMzCCKC8CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCo7IR2Fp+lOhyp
# LtE8ksm7W+MoDyufezC7nG9iwuYyKaCCDXYwggX0MIID3KADAgECAhMzAAAEBGx0
# Bv9XKydyAAAAAAQEMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjQwOTEyMjAxMTE0WhcNMjUwOTExMjAxMTE0WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQC0KDfaY50MDqsEGdlIzDHBd6CqIMRQWW9Af1LHDDTuFjfDsvna0nEuDSYJmNyz
# NB10jpbg0lhvkT1AzfX2TLITSXwS8D+mBzGCWMM/wTpciWBV/pbjSazbzoKvRrNo
# DV/u9omOM2Eawyo5JJJdNkM2d8qzkQ0bRuRd4HarmGunSouyb9NY7egWN5E5lUc3
# a2AROzAdHdYpObpCOdeAY2P5XqtJkk79aROpzw16wCjdSn8qMzCBzR7rvH2WVkvF
# HLIxZQET1yhPb6lRmpgBQNnzidHV2Ocxjc8wNiIDzgbDkmlx54QPfw7RwQi8p1fy
# 4byhBrTjv568x8NGv3gwb0RbAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQU8huhNbETDU+ZWllL4DNMPCijEU4w
# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW
# MBQGA1UEBRMNMjMwMDEyKzUwMjkyMzAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci
# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG
# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0
# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAIjmD9IpQVvfB1QehvpC
# Ge7QeTQkKQ7j3bmDMjwSqFL4ri6ae9IFTdpywn5smmtSIyKYDn3/nHtaEn0X1NBj
# L5oP0BjAy1sqxD+uy35B+V8wv5GrxhMDJP8l2QjLtH/UglSTIhLqyt8bUAqVfyfp
# h4COMRvwwjTvChtCnUXXACuCXYHWalOoc0OU2oGN+mPJIJJxaNQc1sjBsMbGIWv3
# cmgSHkCEmrMv7yaidpePt6V+yPMik+eXw3IfZ5eNOiNgL1rZzgSJfTnvUqiaEQ0X
# dG1HbkDv9fv6CTq6m4Ty3IzLiwGSXYxRIXTxT4TYs5VxHy2uFjFXWVSL0J2ARTYL
# E4Oyl1wXDF1PX4bxg1yDMfKPHcE1Ijic5lx1KdK1SkaEJdto4hd++05J9Bf9TAmi
# u6EK6C9Oe5vRadroJCK26uCUI4zIjL/qG7mswW+qT0CW0gnR9JHkXCWNbo8ccMk1
# sJatmRoSAifbgzaYbUz8+lv+IXy5GFuAmLnNbGjacB3IMGpa+lbFgih57/fIhamq
# 5VhxgaEmn/UjWyr+cPiAFWuTVIpfsOjbEAww75wURNM1Imp9NJKye1O24EspEHmb
# DmqCUcq7NqkOKIG4PVm3hDDED/WQpzJDkvu4FrIbvyTGVU01vKsg4UfcdiZ0fQ+/
# V0hf8yrtq9CkB8iIuk5bBxuPMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq
# hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5
# IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg
# Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03
# a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr
# rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg
# OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy
# 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9
# sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh
# dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k
# A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB
# w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn
# Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90
# lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w
# ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o
# ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD
# VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa
# BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny
# bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG
# AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV
# HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG
# AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl
# AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb
# C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l
# hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6
# I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0
# wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560
# STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam
# ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa
# J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah
# XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA
# 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt
# Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr
# /Xmfwb1tbWrJUnMTDXpQzTGCGiIwghoeAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAAQEbHQG/1crJ3IAAAAABAQwDQYJYIZIAWUDBAIB
# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIJyD3DD5aOieAq4posCIpeSd
# /e8Xkuk8tU2LQ3F0gT0+MEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
# BQAEggEAg+N81no38UFYUG4K4UJ29h1XCwRzfs1/aAD1/ZJuaKKnsiU2jKKZbNEH
# Tf1QDxujWOwlHGZlx3iXw2tFfRUmP1tdaNFJfgIMji9bPxXVCRRw8nS0NGv14nus
# 5/NK9gmggQJ5mJqbxEhW+RcQlzjw4IbCvtja1v8vF/e7FmHeVd0A2VsU+NxwsB55
# qzzk6tY+bIXuF05tLutpUUs91YPL2qCEcWkhjwOuSwwIqHNq34t1zGWJMH0wOg8M
# R0kQ+HaF/Ij8Q5LKJpQqpxuiMmFKPg064XhMthDKeOBopZ2+O9HLZPHeZ9trJ0vG
# zGxTRnsJSsE7i7Z644qq2+oyKl1ZnqGCF6wwgheoBgorBgEEAYI3AwMBMYIXmDCC
# F5QGCSqGSIb3DQEHAqCCF4UwgheBAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFZBgsq
# hkiG9w0BCRABBKCCAUgEggFEMIIBQAIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCBa8Ow885T82Bwsm/fBNDFMUXf0fNyroX9Ug54opB2mDwIGZ7YrXfPN
# GBIyMDI1MDIyNzEzNDQ1NC45NVowBIACAfSggdmkgdYwgdMxCzAJBgNVBAYTAlVT
# MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVs
# YW5kIE9wZXJhdGlvbnMgTGltaXRlZDEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNO
# OjU3MUEtMDVFMC1EOTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBT
# ZXJ2aWNloIIR+zCCBygwggUQoAMCAQICEzMAAAH7y8tsN2flMJUAAQAAAfswDQYJ
# KoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcNMjQw
# NzI1MTgzMTEzWhcNMjUxMDIyMTgzMTEzWjCB0zELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxhbmQgT3Bl
# cmF0aW9ucyBMaW1pdGVkMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046NTcxQS0w
# NUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2Uw
# ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCowlZB5YCrgvC9KNiyM/RS
# +G+bSPRoA4mIwuDSwt/EqhNcB0oPqgy6rmsXmgSI7FX72jHQf3lDx+GhmrfH2XGC
# 5nJM4riXbG1yC0kK2NdGWUzZtOmM6DflFSsHLRwCWgFT0YkGzssE2txsfqsGI6+o
# NA2Jw9FnCrXrHKMyJ1TUnUAm5q33Iufu1qJ+gPnxuVgRwG+SPl0fWVr3NTzjpAN4
# 6hE7o1yocuwPHz/NUpnE/fSZbpjtEyyq0HxwYKAbBVW6s6do0tezfWpNFPJUdfym
# k52hKKEJd6p5uAkJHMbzMb97+TShoGMUUaX7y4UQvALKHjAr1nn5rNPN9rYYPinq
# KG2yRezeWdbTlQp8MmEAAO3q+I5zRGT9zzM6KrOHSUql/95ZRjaj+G9wM9k2Atoe
# /J8OpvwBZoq87fqJFlJeqFLDxLEmjRMKmxsKOa3HQukeeptvVQXtyrT2QJx9ZMM9
# w3XaltgupyTRsgh88ptzseeuQ1CSz+ZJtVlOcPJPc7zMX2rgMJ9Z6xKvVqTJwN24
# bEJ0oG+C0mHVjEOrWyRPB5jHmIBZecHsozKWzdZBltO5tMIsu3xefy36yVwqbkOS
# +hu5uYdKuK5MDfBPIjLgXFqZMqbRUO72ZZ2zwy2NRIlXA1VWUFdpDdkxxWOKPJWh
# Q1W4Fj0xzBhwhArrbBDbQQIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFEdVIZhQ1DdH
# A6XvXMgC5SMgqDUqMB8GA1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8G
# A1UdHwRYMFYwVKBSoFCGTmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMv
# Y3JsL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBs
# BggrBgEFBQcBAQRgMF4wXAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0
# LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUy
# MDIwMTAoMSkuY3J0MAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUH
# AwgwDgYDVR0PAQH/BAQDAgeAMA0GCSqGSIb3DQEBCwUAA4ICAQDDOggo5jZ2dSN9
# a4yIajP+i+hzV7zpXBZpk0V2BGY6hC5F7ict21k421Mc2TdKPeeTIGzPPFJtkRDQ
# N27Ioccjk/xXzuMW20aeVHTA8/bYUB5tu8Bu62QwxVAwXOFUFaJYPRUCe73HR+OJ
# 8soMBVcvCi6fmsIWrBtqxcVzsf/QM+IL4MGfe1TF5+9zFQLKzj4MLezwJintZZel
# nxZv+90GEOWIeYHulZyawHze5zj8/YaYAjccyQ4S7t8JpJihCGi5Y6vTuX8ozhOd
# 3KUiKubx/ZbBdBwUTOZS8hIzqW51TAaVU19NMlSrZtMMR3e2UMq1X0BRjeuucXAd
# PAmvIu1PggWG+AF80PeYvV55JqQp/vFMgjgnK3XlJeEd3mgj9caNKDKSAmtYDnus
# acALuu7f9lsU0Iwr8mPpfxfgvqYE5hrY0YrAfgDftgYOt5wn+pddZRi98tiocZ/x
# OFiXXZiDWvBIqlYuiUD8HV6oHDhNFy9VjQi802Lmyb7/8cn0DDo0m5H+4NHtfu8N
# eJylcyVE2AUzIANvwAUi9A90epxGlGitj5hQaW/N4nH/aA1jJ7MCiRusWEAKwnYF
# /J4vIISjoC7AQefnXU8oTx0rgm+WYtKgePtUVHc0cOTfNGTHQTGSYXxo52m+gqG7
# AELGhn8mFvNLOu9nvgZWMoojK3kUDTCCB3EwggVZoAMCAQICEzMAAAAVxedrngKb
# SZkAAAAAABUwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
# EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv
# ZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmlj
# YXRlIEF1dGhvcml0eSAyMDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIy
# NVowfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT
# B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UE
# AxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXI
# yjVX9gF/bErg4r25PhdgM/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjo
# YH1qUoNEt6aORmsHFPPFdvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1y
# aa8dq6z2Nr41JmTamDu6GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v
# 3byNpOORj7I5LFGc6XBpDco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pG
# ve2krnopN6zL64NF50ZuyjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viS
# kR4dPf0gz3N9QZpGdc3EXzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYr
# bqgSUei/BQOj0XOmTTd0lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlM
# jgK8QmguEOqEUUbi0b1qGFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSL
# W6CmgyFdXzB0kZSU2LlQ+QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AF
# emzFER1y7435UsSFF5PAPBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIu
# rQIDAQABo4IB3TCCAdkwEgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIE
# FgQUKqdS/mTEmr6CkTxGNSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWn
# G1M1GelyMFwGA1UdIARVMFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEW
# M2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5
# Lmh0bTATBgNVHSUEDDAKBggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBi
# AEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV
# 9lbLj+iiXGJo0T2UkFvXzpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3Js
# Lm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAx
# MC0wNi0yMy5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2
# LTIzLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv
# 6lwUtj5OR2R4sQaTlz0xM7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZn
# OlNN3Zi6th542DYunKmCVgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1
# bSNU5HhTdSRXud2f8449xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4
# rPf5KYnDvBewVIVCs/wMnosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU
# 6ZGyqVvfSaN0DLzskYDSPeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDF
# NLB62FD+CljdQDzHVG2dY3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/
# HltEAY5aGZFrDZ+kKNxnGSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdU
# CbFpAUR+fKFhbHP+CrvsQWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKi
# excdFYmNcP7ntdAoGokLjzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTm
# dHRbatGePu1+oDEzfbzL6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZq
# ELQdVTNYs6FwZvKhggNWMIICPgIBATCCAQGhgdmkgdYwgdMxCzAJBgNVBAYTAlVT
# MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVs
# YW5kIE9wZXJhdGlvbnMgTGltaXRlZDEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNO
# OjU3MUEtMDVFMC1EOTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBT
# ZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQAEcefs0Ia6xnPZF9VvK7BjA/KQFaCBgzCB
# gKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQH
# EwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNV
# BAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUA
# AgUA62qMdjAiGA8yMDI1MDIyNzA3MDIxNFoYDzIwMjUwMjI4MDcwMjE0WjB0MDoG
# CisGAQQBhFkKBAExLDAqMAoCBQDraox2AgEAMAcCAQACAibXMAcCAQACAhKHMAoC
# BQDra932AgEAMDYGCisGAQQBhFkKBAIxKDAmMAwGCisGAQQBhFkKAwKgCjAIAgEA
# AgMHoSChCjAIAgEAAgMBhqAwDQYJKoZIhvcNAQELBQADggEBAHE3gDyeuLlyJGf0
# CeDpSLtOnJdktBk+h9mJyMJJxbvqQrtnyrziERCnd4Hih8v44KmVR3KbIdRlbcmn
# qblukhAhq1mGoJqufp2y/RQJ9PlwKJP3wVVEI0zqAbN+g9D4gdRpYhlCdIs8O3Jg
# eeYDeV/9BCT3isFaZtqzvaE0TNZ0MmkV6qwNkEzIT5voqSlvxUxGcs110ouBMgNf
# ZbAZUVl99XL6yIRUb/hXXCaJYZNZ7yhQksuIzwGagJ4Mvxmps1wHbgqKSEaT3fRf
# Ht3G3/PqW1rGLFgYezFDGzSbdaOS/vwj0PGI2zg3PTRJln0YHgJe40++nP2PWJ7a
# gwNaPmkxggQNMIIECQIBATCBkzB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz
# aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv
# cnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAx
# MAITMwAAAfvLy2w3Z+UwlQABAAAB+zANBglghkgBZQMEAgEFAKCCAUowGgYJKoZI
# hvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8GCSqGSIb3DQEJBDEiBCBubuTF+t+pr0l/
# vF6RQs4bBR1k+tFQD5wOe8B/6ND0MDCB+gYLKoZIhvcNAQkQAi8xgeowgecwgeQw
# gb0EIDnbAqv8oIWVU1iJawIuwHiqGMRgQ/fEepioO7VJJOUYMIGYMIGApH4wfDEL
# MAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1v
# bmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWlj
# cm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAH7y8tsN2flMJUAAQAAAfsw
# IgQgUjwSVaN6emKuu8vKqck7TCjNqB+4efIX46VmcOQYXMUwDQYJKoZIhvcNAQEL
# BQAEggIAHlliz63y/g4z+Tt3UTl2B4jxmTT3qrJmXa/VXlIQcb7aXihCaqW/LqcW
# 8cmXupz+sF9CCvAFiEer15SCC5qCT6sQsUspD9AKEz82kP72w5HxP1QeqFSZ4gsc
# kA3KqNzLIIduYT5focIAI165JdXLdFLnaeAemhYdeOaTKvnqkMog/AzXYZPee8hY
# vN5fUQs8kB8cvG9RSDNMyth7zHNeC7zfbXxCh2QLzo9VRX61wDzD3NV5ISfEmKtk
# H/0/tVSmDic5Q98aUeF7nkTiSbGT5H+fTe4+hdJmq6QEFOlpkRtvu2529e60guyI
# +KXlYfcgyYDyPYthNDslXE5cx08aUZPRakn242uCCKk65vahFz1DQW/f493X6pGt
# cX9K88CpgSnzr80Dvl+V2GyBAFtzTNM1GMI15iH8YhDT93dQXZySCoxVwLMXPoHl
# c/nXd3LJYYdik7UxJhllv2Rodnv7c+iKUmoh3SCLY7gu3yDfOxU7j/OHwk1KN6S3
# Mo2E8XqroM+nq1M4N+JLNbaW0g4WlKFsoOyVfxVydRL+ALV7ytdB1DiyXgoD/pis
# oP4FPaZpX4JYjNq8kwGnIiSTmByFBFs3ZWtA2K9NnDoNza+r4cLdqo5F8nEfyuEr
# 2/FMqDjOWEdF8/OEkYN97t1OnrLgr9pIQ4R8WdK5PzEGl3HUvLU=
# SIG # End signature block