Private/Test-VMExcluded.ps1

function Test-VMExcluded {
    <#
    .SYNOPSIS
        Decides whether a VM inventory record should be excluded from metric collection,
        returning the exclusion reason (string) or $null to keep it.

    .DESCRIPTION
        Pure, Azure-free classifier so the rules are unit-testable. The point is to skip
        managed / ephemeral / short-running compute (Databricks clusters, VM Scale Set
        members, Spot nodes, AKS node pools) using only cheap control-plane signals -- before
        any expensive Get-AzMetric call.

    .PARAMETER VM
        A normalized VM record with: Name, ResourceGroup, Location, SkuName, Tags (dict or
        PSObject), Vmss (scale-set id or empty), Priority, Created (datetime or $null).

    .PARAMETER IncludeScaleSetMember
        Keep VMs that belong to a VM Scale Set (Flex). Default: excluded.

    .PARAMETER IncludeSpot
        Keep Spot-priority VMs. Default: excluded.

    .PARAMETER ExcludeResourceGroupPattern
        Regex(es); a VM whose resource group matches any is excluded.

    .PARAMETER ExcludeNamePattern
        Regex(es); a VM whose name matches any is excluded.

    .PARAMETER ExcludeTag
        Hashtable of tag rules. A VM is excluded if it carries a matching tag. The value may
        be '*' (any value for that key) or a specific value (case-insensitive).

    .PARAMETER MinAgeDays
        When > 0 and the record has a Created date, exclude VMs created within the last N days
        (too little history to size). Requires -Now.

    .PARAMETER Now
        Reference time (UTC) for the MinAgeDays check.

    .OUTPUTS
        [string] reason to exclude, or $null to keep.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)] [object] $VM,
        [switch]    $IncludeScaleSetMember,
        [switch]    $IncludeSpot,
        [string[]]  $ExcludeResourceGroupPattern,
        [string[]]  $ExcludeNamePattern,
        [hashtable] $ExcludeTag,
        [int]       $MinAgeDays = 0,
        [datetime]  $Now
    )

    if (-not $IncludeScaleSetMember -and $VM.Vmss) {
        return 'VMSS member'
    }
    if (-not $IncludeSpot -and "$($VM.Priority)" -ieq 'Spot') {
        return 'Spot'
    }
    foreach ($pat in $ExcludeResourceGroupPattern) {
        if ($pat -and $VM.ResourceGroup -match $pat) { return "RG ~ /$pat/" }
    }
    foreach ($pat in $ExcludeNamePattern) {
        if ($pat -and $VM.Name -match $pat) { return "name ~ /$pat/" }
    }
    if ($ExcludeTag -and $ExcludeTag.Count -and $VM.Tags) {
        foreach ($key in $ExcludeTag.Keys) {
            $val = Get-TagValueCI -Tags $VM.Tags -Key $key
            if ($null -ne $val) {
                $want = [string]$ExcludeTag[$key]
                if ($want -eq '*' -or [string]$val -ieq $want) { return "tag $key=$val" }
            }
        }
    }
    if ($MinAgeDays -gt 0 -and $VM.Created -and $Now) {
        if ([datetime]$VM.Created -gt $Now.AddDays(-$MinAgeDays)) {
            return "younger than ${MinAgeDays}d"
        }
    }
    return $null
}

function Get-TagValueCI {
    <#
    .SYNOPSIS
        Case-insensitive tag lookup that works for both IDictionary and PSObject tag bags.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [object] $Tags,
        [Parameter(Mandatory)] [string] $Key
    )
    if ($null -eq $Tags) { return $null }
    if ($Tags -is [System.Collections.IDictionary]) {
        foreach ($k in $Tags.Keys) {
            if ([string]$k -ieq $Key) { return [string]$Tags[$k] }
        }
        return $null
    }
    foreach ($p in $Tags.PSObject.Properties) {
        if ($p.Name -ieq $Key) { return [string]$p.Value }
    }
    return $null
}