Private/ANF/New-SilkTCOANFList.ps1

<#
    .SYNOPSIS
    Discovers Azure NetApp Files accounts, capacity pools and volumes.

    .DESCRIPTION
    Provisioning only - no Azure Monitor, no time window. Everything here is what the
    environment has been built and paid for, which is what a costing comparison needs.
    Performance ceilings on ANF are pure arithmetic off service level and size.
#>


function New-SilkTCOANFList {
    param(
        [Parameter()]
        [array] $resourceGroupNames,
        [Parameter()]
        [switch] $includeSnapshots,
        [Parameter()]
        [switch] $includeBackups
    )

    $requiredModules = @('Az.NetAppFiles')
    foreach ($module in $requiredModules) {
        if (-not (Get-Module -ListAvailable -Name $module)) {
            throw "Required module '$module' is not installed. Install it with: Install-Module $module"
        }
        if (-not (Get-Module -Name $module)) {
            Import-Module $module -ErrorAction Stop
        }
    }

    # anf throughput is a flat rate per TiB by service level. thats the whole ceiling story,
    # no metrics needed. std 16 / prem 64 / ultra 128 MiB/s per TiB.
    $throughputPerTiB = @{
        'Standard' = 16
        'Premium'  = 64
        'Ultra'    = 128
    }

    # arm hands names back as 'account/pool' or 'account/pool/volume', we just want the leaf
    function Get-AnfLeafName {
        param([string] $name)
        if (-not $name) { return $null }
        return ($name -split '/')[-1]
    }

    $accounts = @()
    if ($resourceGroupNames) {
        foreach ($rg in $resourceGroupNames) {
            $accounts += Get-AnfAccount -ResourceGroupName $rg -ErrorAction SilentlyContinue
        }
    } else {
        $accounts = Get-AnfAccount -ErrorAction SilentlyContinue
    }

    if (-not $accounts) {
        Write-Warning "No Azure NetApp Files accounts found in the current context."
        return
    }

    $thelist = @()

    foreach ($acct in $accounts) {
        $acctName = Get-AnfLeafName $acct.Name
        $acctRg = $acct.ResourceGroupName

        Write-Verbose "-> Gathering ANF account - $acctName ($acctRg)" -Verbose

        $pools = @()
        try {
            $pools = @(Get-AnfPool -ResourceGroupName $acctRg -AccountName $acctName -ErrorAction Stop)
        } catch {
            Write-Warning "Could not list pools for account $acctName : $($_.Exception.Message)"
            continue
        }

        foreach ($pool in $pools) {
            $poolName = Get-AnfLeafName $pool.Name
            Write-Verbose "--> pool $poolName ($($pool.ServiceLevel), $([Math]::Round($pool.Size / 1TB, 2)) TiB)" -Verbose

            $poolSizeGiB = [Math]::Round($pool.Size / 1GB, 2)
            $poolSizeTiB = [Math]::Round($pool.Size / 1TB, 4)

            $rate = $throughputPerTiB[[string]$pool.ServiceLevel]
            if ($rate) {
                $poolCeiling = [Math]::Round($poolSizeTiB * $rate, 2)
            } elseif ($pool.TotalThroughputMibps) {
                # Flexible tier provisions throughput on its own, not off capacity
                $poolCeiling = [Math]::Round($pool.TotalThroughputMibps, 2)
            } else {
                $poolCeiling = $null
            }

            $volumes = @()
            try {
                $volumes = @(Get-AnfVolume -ResourceGroupName $acctRg -AccountName $acctName -PoolName $poolName -ErrorAction Stop)
            } catch {
                Write-Warning "Could not list volumes for pool $poolName : $($_.Exception.Message)"
            }

            # anf bills the POOL not the volumes. anything carved out short of pool size is
            # capacity the customer is paying for and not using - thats the number that matters
            $allocatedGiB = 0
            $volDetail = @()

            foreach ($vol in $volumes) {
                $volName = Get-AnfLeafName $vol.Name
                $quotaGiB = [Math]::Round($vol.UsageThreshold / 1GB, 2)
                $allocatedGiB += $quotaGiB

                # manual qos assigns throughput explicitly, auto derives it from quota
                $volCeiling = $null
                if ($vol.ThroughputMibps) {
                    $volCeiling = [Math]::Round($vol.ThroughputMibps, 2)
                } elseif ($rate) {
                    $volCeiling = [Math]::Round(($vol.UsageThreshold / 1TB) * $rate, 2)
                }

                $snaps = @()
                if ($includeSnapshots) {
                    try {
                        $snaps = @(Get-AnfSnapshot -ResourceGroupName $acctRg -AccountName $acctName -PoolName $poolName -VolumeName $volName -ErrorAction Stop)
                    } catch {
                        Write-Verbose "-> no snapshots readable for $volName" -Verbose
                    }
                }

                $v = New-Object psobject
                $v | Add-Member -MemberType NoteProperty -Name VolumeName -Value $volName
                $v | Add-Member -MemberType NoteProperty -Name ResourceId -Value $vol.Id
                $v | Add-Member -MemberType NoteProperty -Name Location -Value $vol.Location
                $v | Add-Member -MemberType NoteProperty -Name ServiceLevel -Value ([string]$vol.ServiceLevel)
                $v | Add-Member -MemberType NoteProperty -Name ProvisionedGiB -Value $quotaGiB
                $v | Add-Member -MemberType NoteProperty -Name ThroughputCeilingMiBps -Value $volCeiling
                $v | Add-Member -MemberType NoteProperty -Name AssignedThroughputMiBps -Value $vol.ThroughputMibps
                $v | Add-Member -MemberType NoteProperty -Name ProtocolTypes -Value ($vol.ProtocolTypes -join ',')
                $v | Add-Member -MemberType NoteProperty -Name NetworkFeatures -Value ([string]$vol.NetworkFeatures)
                $v | Add-Member -MemberType NoteProperty -Name CoolAccess -Value $vol.CoolAccess
                $v | Add-Member -MemberType NoteProperty -Name CoolnessPeriodDays -Value $vol.CoolnessPeriod
                $v | Add-Member -MemberType NoteProperty -Name IsLargeVolume -Value $vol.IsLargeVolume
                $v | Add-Member -MemberType NoteProperty -Name Zone -Value ($vol.Zones -join ',')
                $v | Add-Member -MemberType NoteProperty -Name SnapshotPolicy -Value (Get-AnfLeafName $vol.SnapshotPolicyId)
                $v | Add-Member -MemberType NoteProperty -Name ReplicationEndpointType -Value ([string]$vol.DataProtectionReplicationEndpointType)
                $v | Add-Member -MemberType NoteProperty -Name ReplicationSchedule -Value ([string]$vol.DataProtectionReplicationSchedule)
                $v | Add-Member -MemberType NoteProperty -Name EncryptionKeySource -Value ([string]$vol.EncryptionKeySource)
                $v | Add-Member -MemberType NoteProperty -Name Snapshots -Value @($snaps)
                $volDetail += $v
            }

            $strandedGiB = [Math]::Round($poolSizeGiB - $allocatedGiB, 2)
            $utilPct = if ($poolSizeGiB -gt 0) { [Math]::Round(($allocatedGiB / $poolSizeGiB) * 100, 2) } else { 0 }

            $p = New-Object psobject
            $p | Add-Member -MemberType NoteProperty -Name NetAppAccount -Value $acctName
            $p | Add-Member -MemberType NoteProperty -Name PoolName -Value $poolName
            $p | Add-Member -MemberType NoteProperty -Name ResourceGroupName -Value $acctRg
            $p | Add-Member -MemberType NoteProperty -Name ResourceId -Value $pool.Id
            $p | Add-Member -MemberType NoteProperty -Name Region -Value $pool.Location
            $p | Add-Member -MemberType NoteProperty -Name ServiceLevel -Value ([string]$pool.ServiceLevel)
            $p | Add-Member -MemberType NoteProperty -Name QosType -Value ([string]$pool.QosType)
            $p | Add-Member -MemberType NoteProperty -Name ProvisionedGiB -Value $poolSizeGiB
            $p | Add-Member -MemberType NoteProperty -Name ProvisionedTiB -Value $poolSizeTiB
            $p | Add-Member -MemberType NoteProperty -Name ThroughputCeilingMiBps -Value $poolCeiling
            $p | Add-Member -MemberType NoteProperty -Name TotalThroughputMiBps -Value $pool.TotalThroughputMibps
            $p | Add-Member -MemberType NoteProperty -Name UtilizedThroughputMiBps -Value $pool.UtilizedThroughputMibps
            $p | Add-Member -MemberType NoteProperty -Name AllocatedToVolumesGiB -Value $allocatedGiB
            $p | Add-Member -MemberType NoteProperty -Name StrandedGiB -Value $strandedGiB
            $p | Add-Member -MemberType NoteProperty -Name PoolUtilizationPct -Value $utilPct
            $p | Add-Member -MemberType NoteProperty -Name CoolAccess -Value $pool.CoolAccess
            $p | Add-Member -MemberType NoteProperty -Name EncryptionType -Value ([string]$pool.EncryptionType)
            $p | Add-Member -MemberType NoteProperty -Name Volumes -Value @($volDetail)
            $thelist += $p
        }

        # backups live under a vault, not the pool, and bill separately
        if ($includeBackups) {
            try {
                $vaults = @(Get-AnfBackupVault -ResourceGroupName $acctRg -AccountName $acctName -ErrorAction Stop)
                foreach ($vault in $vaults) {
                    $vaultName = Get-AnfLeafName $vault.Name
                    $backups = @(Get-AnfBackup -ResourceGroupName $acctRg -AccountName $acctName -BackupVaultName $vaultName -ErrorAction SilentlyContinue)

                    $b = New-Object psobject
                    $b | Add-Member -MemberType NoteProperty -Name NetAppAccount -Value $acctName
                    $b | Add-Member -MemberType NoteProperty -Name ResourceGroupName -Value $acctRg
                    $b | Add-Member -MemberType NoteProperty -Name BackupVault -Value $vaultName
                    $b | Add-Member -MemberType NoteProperty -Name Region -Value $vault.Location
                    $b | Add-Member -MemberType NoteProperty -Name Backups -Value @($backups)
                    $b | Add-Member -MemberType NoteProperty -Name IsBackupVault -Value $true
                    $thelist += $b
                }
            } catch {
                Write-Verbose "-> no backup vaults for $acctName" -Verbose
            }
        }
    }

    return $thelist
}