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 |