Get a pretty view of your S2D Storage Pools

I’d like to start with a shout out to Philip Elder, for he came up with the initial idea and script that I’ve used here.

One thing that’s not always obvious when dealing with S2D Clusters is how much of your Storage Pool has been provisioned and how much capacity, if any, is left.

To help with this, we came up with be script you’ll see at the bottom of this article.
It’s designed to be run both locally on a cluster host and against multiple clusters remotely.

So what can you expect to get out of this script?

  • Total Size - This is your RAW capacity in the pool
  • Allocated Size - This is the RAW amount of storage already allocated to Virtual Disks
  • RAW Free Space - This is your remaining capacity in the pool
  • Mirror Available - This is how much space you can provision as Mirror Volumes, it assumes 2-way mirror for 2-Nodes and 3-way mirror for 3+ Nodes
  • Parity Available - This is how much space you can provision as Parity Volumes
  • Reserved Space, this is how much space needs to be retained for rebuilds after disk failure

So how do you run this?
Well it’s very simple, you can just run the command with nothing else directly on a cluster host and get an output

Get-PoolStats | ft -au

Or you can run it remotely against multiple clusters

Get-PoolStats -CimSession Cluster-Gen13,Cluster-Gen14 | ft -au
Get-StoragePool -FriendlyName S2D* -CimSession Cluster-Gen13,Cluster-Gen14 | Get-PoolStats | ft -au

As always, I hope this script helps someone else out there

function Get-PoolStats {
    [CmdletBinding()]
    param(
        # Remote machines to query
        [alias("ComputerName")]
        [CimSession[]]$CimSession = "localhost",
        # Storage Pools you want to query
        [PSTypeName("Microsoft.Management.Infrastructure.CimInstance#ROOT/Microsoft/Windows/Storage/MSFT_StoragePool")]
        [parameter(
            Position=0,
            Mandatory=$false,
            ValueFromPipeline=$true
        )]
        $StoragePool = (Get-StoragePool -CimSession $CimSession | Where-Object IsPrimordial -eq $false)
    )

    begin{
        Write-Verbose "[$((get-date).TimeOfDay.ToString()) BEGIN  ] Starting: $($MyInvocation.Mycommand)"
        Write-Verbose "[$((get-date).TimeOfDay.ToString()) BEGIN  ] Initialising array"
        $Results = @()

        #region: Helper Functions
        Write-Verbose "[$((get-date).TimeOfDay.ToString()) BEGIN  ] Initialising helper functions"
        Function Format-Bytes {
            Param (
                $RawValue
            )
            $i = 0 ; $Labels = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
            Do { if ( $RawValue -Gt 1024 ) { $RawValue /= 1024 ; $i++ } } While ( $RawValue -Gt 1024 )
            # Return
            [String][Math]::Round($RawValue) + " " + $Labels[$i]
        }
        #endregion
    }

    process{
        Write-Verbose "[$((get-date).TimeOfDay.ToString()) PROCESS  ] Starting process block"
        Foreach($Pool in $StoragePool){
            Write-Verbose "[$((get-date).TimeOfDay.ToString()) PROCESS  ] Starting process $($Pool.FriendlyName)"
            Write-Verbose "[$((get-date).TimeOfDay.ToString()) PROCESS  ] Calculating Parity Efficiency"
            $PhysicalDisks = $Pool | Get-PhysicalDisk -CimSession $Pool.CimSystemProperties.ServerName
            $StorageNodes = ($Pool | Get-StorageSubSystem | Get-StorageNode).Count
            $SlowTierMediaType = ( $PhysicalDisks | Where-Object{ $_.Usage -ieq "Auto-Select" } ).MediaType | Sort | Get-Unique
            if($SlowTierMediaType -ieq "NVMe" -or $SlowTierMediaType -ieq "SSD"){
                # All-flash!
                if ($StorageNodes -lt 7) {
                    $ParityEfficiency = 2 / (2 + 2);
                }
                elseif ($StorageNodes -lt 9) {
                    $ParityEfficiency = 4 / (4 + 2);
                }
                elseif ($StorageNodes -lt 16) {
                    $ParityEfficiency = 6 / (6 + 2);
                }
                else {
                    $ParityEfficiency = 12 / (12 + 2 + 1); # LRC
                }
            }else{
                # Hybrid
                if ($StorageNodes -lt 7) {
                    $ParityEfficiency = 2 / (2 + 2);
                }
                elseif ($StorageNodes -lt 12) {
                    $ParityEfficiency = 4 / (4 + 2);
                }
                else {
                    $ParityEfficiency = 8 / (8 + 2 + 1); # LRC
                }
            }
            Write-Verbose "[$((get-date).TimeOfDay.ToString()) PROCESS  ] Parity Efficiency is $ParityEfficiency"
            Write-Verbose "[$((get-date).TimeOfDay.ToString()) PROCESS  ] Calculating Mirror Efficiency"
            $MirrorEfficiency = 1 / ( ( $Pool | Get-ResiliencySetting -Name Mirror ).PhysicalDiskRedundancyDefault + 1 )
            Write-Verbose "[$((get-date).TimeOfDay.ToString()) PROCESS  ] Mirror Efficiency is $MirrorEfficiency"
            Write-Verbose "[$((get-date).TimeOfDay.ToString()) PROCESS  ] Calculating Rebuild Space"
            $LargestDisk = $PhysicalDisks | Where-Object{ $_.Usage -ieq "Auto-Select" } | Sort-Object Size -Descending | Select-Object -First 1 -ExpandProperty Size
            If($StorageNodes -gt 4){
                $RebuildSpace = $LargestDisk * 4
            }else{
                $RebuildSpace = $LargestDisk * $StorageNodes
            }
            Write-Verbose "[$((get-date).TimeOfDay.ToString()) PROCESS  ] Calculating Pool Stats"
            $FriendlyName = $Pool.FriendlyName
            $Size = Format-Bytes -RawValue $Pool.Size
            $AllocatedSize = Format-Bytes -RawValue $Pool.AllocatedSize
            $RawFreeSpace = Format-Bytes -RawValue ($Pool.Size - $Pool.AllocatedSize)
            $RebuildSpaceNeeded = Format-Bytes -RawValue $RebuildSpace
            $MirrorAvailable = Format-Bytes -RawValue ( ( $Pool.Size - $Pool.AllocatedSize - $RebuildSpace ) * $MirrorEfficiency)
            $ParityAvailable = Format-Bytes -RawValue ( ( $Pool.Size - $Pool.AllocatedSize - $RebuildSpace ) * $ParityEfficiency)
            Write-Verbose "[$((get-date).TimeOfDay.ToString()) PROCESS  ] Adding output to results array"
            $Results += [pscustomobject][ordered]@{
                Name = $FriendlyName
                TotalSize = $Size
                AllocatedSize = $AllocatedSize
                RawFreeSpace = $RawFreeSpace
                MirrorAvailable = $MirrorAvailable
                ParityAvailable = $ParityAvailable
                ReservedSpace = $RebuildSpaceNeeded
            }
            Write-Verbose "[$((get-date).TimeOfDay.ToString()) PROCESS  ] Finished processing $($Pool.FriendlyName)"
        }
    }

    end{
        Write-Verbose "[$((get-date).TimeOfDay.ToString()) END  ] Returning results"
        Write-Output $Results
    }

}