Private/Resolve-TagValue.ps1
|
function Resolve-TagValue { <# .SYNOPSIS Determines the tag value(s) for a VM based on a category definition and VM metadata. .DESCRIPTION Core resolution engine for VM-AutoTagger. Given a VM metadata object and a category definition from the tag profile, determines what tag value(s) should be assigned. Resolution strategies (based on the category source): - guest_family: Match GuestFamily against wildcard values - guest_fullname: Use GuestFullName directly as the tag value (or pass-through) - cpu_count: Match NumCPU against numeric tiers - memory_gb: Match MemoryGB against numeric tiers - tools_status: Match ToolsStatus against wildcard values - power_state: Match PowerState against wildcard values - snapshot_risk: Use the pre-computed SnapshotRisk value directly - vm_age: Use the pre-computed VMAge category directly - network: Return all network names (multi-value) - datastore: Return all datastore names (multi-value) - vm_name: Match VM Name against wildcard values - annotation: Extract value from VM Notes using a regex pattern - custom_attribute: Read a vSphere custom attribute by name - folder: Map vSphere folder path to a tag value - direct: Direct match of a source value against values list .PARAMETER VMMetadata A VM metadata object from Get-VMMetadata. .PARAMETER CategoryDefinition A category definition object from the parsed tag profile, containing Name, Source, Cardinality, Values, Tiers, Pattern, AttributeName, etc. .EXAMPLE $tagValues = Resolve-TagValue -VMMetadata $meta -CategoryDefinition $catDef .NOTES Author: Larry Roberts Part of the VM-AutoTagger module. #> [CmdletBinding()] param( [Parameter(Mandatory)] [object]$VMMetadata, [Parameter(Mandatory)] [object]$CategoryDefinition ) process { $source = $CategoryDefinition.Source $catName = $CategoryDefinition.Name Write-Verbose "Resolving tag for category '$catName' (source: $source) on VM '$($VMMetadata.Name)'" switch ($source) { 'guest_family' { $sourceValue = Get-SourceValue -VMMetadata $VMMetadata -Source 'guest_family' return @(Resolve-WildcardValue -SourceValue $sourceValue -Values $CategoryDefinition.Values) } 'guest_fullname' { $sourceValue = Get-SourceValue -VMMetadata $VMMetadata -Source 'guest_fullname' # If values are defined, match against them; otherwise use the raw value if ($CategoryDefinition.Values -and $CategoryDefinition.Values.Count -gt 0) { return @(Resolve-WildcardValue -SourceValue $sourceValue -Values $CategoryDefinition.Values) } # Direct pass-through: use the OS version string as the tag name if (-not [string]::IsNullOrWhiteSpace($sourceValue)) { # Sanitize for tag name (max 256 chars, remove problematic characters) $sanitized = $sourceValue -replace '[^\w\s\.\-\(\)]', '' if ($sanitized.Length -gt 128) { $sanitized = $sanitized.Substring(0, 128) } if (-not [string]::IsNullOrWhiteSpace($sanitized)) { return @($sanitized) } } return @() } 'cpu_count' { $sourceValue = $VMMetadata.NumCPU return @(Resolve-TierValue -SourceValue $sourceValue -Tiers $CategoryDefinition.Tiers) } 'memory_gb' { $sourceValue = $VMMetadata.MemoryGB return @(Resolve-TierValue -SourceValue $sourceValue -Tiers $CategoryDefinition.Tiers) } 'tools_status' { $sourceValue = Get-SourceValue -VMMetadata $VMMetadata -Source 'tools_status' return @(Resolve-WildcardValue -SourceValue $sourceValue -Values $CategoryDefinition.Values) } 'power_state' { $sourceValue = Get-SourceValue -VMMetadata $VMMetadata -Source 'power_state' return @(Resolve-WildcardValue -SourceValue $sourceValue -Values $CategoryDefinition.Values) } 'snapshot_risk' { # Direct mapping from metadata $risk = $VMMetadata.SnapshotRisk if (-not [string]::IsNullOrWhiteSpace($risk)) { return @($risk) } return @('Clean') } 'vm_age' { $age = $VMMetadata.VMAge if (-not [string]::IsNullOrWhiteSpace($age) -and $age -ne 'Unknown') { return @($age) } return @() } 'network' { # Multi-value: return all network names if ($VMMetadata.Networks -and $VMMetadata.Networks.Count -gt 0) { $results = @() foreach ($net in $VMMetadata.Networks) { if (-not [string]::IsNullOrWhiteSpace($net) -and $net -ne 'Disconnected') { $results += $net } } if ($results.Count -gt 0) { return $results } } return @() } 'datastore' { # Multi-value: return all datastore names if ($VMMetadata.Datastores -and $VMMetadata.Datastores.Count -gt 0) { $results = @() foreach ($ds in $VMMetadata.Datastores) { if (-not [string]::IsNullOrWhiteSpace($ds)) { $results += $ds } } if ($results.Count -gt 0) { return $results } } return @() } 'vm_name' { # Match VM name against wildcard value patterns $sourceValue = $VMMetadata.Name return @(Resolve-WildcardValue -SourceValue $sourceValue -Values $CategoryDefinition.Values) } 'annotation' { # Extract from VM notes using regex pattern return @(Resolve-PatternValue -Notes $VMMetadata.Notes -Pattern $CategoryDefinition.Pattern -Values $CategoryDefinition.Values) } 'custom_attribute' { # Read from VM custom attributes $attrName = $CategoryDefinition.AttributeName if ([string]::IsNullOrWhiteSpace($attrName)) { Write-Verbose " Category '$catName' source is custom_attribute but no attribute_name defined." return @() } $attrValue = $null if ($VMMetadata.CustomAttributes -and $VMMetadata.CustomAttributes.ContainsKey($attrName)) { $attrValue = $VMMetadata.CustomAttributes[$attrName] } if (-not [string]::IsNullOrWhiteSpace($attrValue)) { # If values are defined, match against them; otherwise use raw attribute value if ($CategoryDefinition.Values -and $CategoryDefinition.Values.Count -gt 0) { return @(Resolve-WildcardValue -SourceValue $attrValue -Values $CategoryDefinition.Values) } return @($attrValue) } return @() } 'folder' { # Map folder path to a tag value if ($CategoryDefinition.FolderMapping) { $folderName = $VMMetadata.Folder if (-not [string]::IsNullOrWhiteSpace($folderName)) { foreach ($mapping in $CategoryDefinition.FolderMapping) { if ($folderName -like $mapping.match) { return @($mapping.tag) } } } } return @() } 'name_pattern' { # Extract from VM name using regex return @(Resolve-NamePatternValue -VMName $VMMetadata.Name -Pattern $CategoryDefinition.NamePattern) } 'direct' { # Generic direct matching -- try values list with the category name as source key if ($CategoryDefinition.Values -and $CategoryDefinition.Values.Count -gt 0) { # Use the first non-null field that makes sense $testValue = $VMMetadata.Name return @(Resolve-WildcardValue -SourceValue $testValue -Values $CategoryDefinition.Values) } return @() } default { Write-Verbose " Unknown source type '$source' for category '$catName'." return @() } } } } function Get-SourceValue { <# .SYNOPSIS Retrieves the raw metadata value for a given source type. #> [CmdletBinding()] param( [Parameter(Mandatory)] [object]$VMMetadata, [Parameter(Mandatory)] [string]$Source ) switch ($Source) { 'guest_family' { return [string]$VMMetadata.GuestFamily } 'guest_fullname' { return [string]$VMMetadata.GuestFullName } 'guest_id' { return [string]$VMMetadata.GuestId } 'tools_status' { return [string]$VMMetadata.ToolsStatus } 'power_state' { return [string]$VMMetadata.PowerState } 'vm_name' { return [string]$VMMetadata.Name } 'snapshot_risk' { return [string]$VMMetadata.SnapshotRisk } 'vm_age' { return [string]$VMMetadata.VMAge } default { return '' } } } function Resolve-TierValue { <# .SYNOPSIS Resolves a numeric value against tier thresholds to determine a tag. .DESCRIPTION Iterates through tier definitions in order. Each tier has a Max threshold; if the source value is less than or equal to Max, that tier's tag is returned. Falls back to the default tier if no threshold matches. #> [CmdletBinding()] param( [Parameter()] [double]$SourceValue, [Parameter()] [array]$Tiers ) if (-not $Tiers -or $Tiers.Count -eq 0) { return @() } $defaultTag = '' foreach ($tier in $Tiers) { if ($tier.Default -eq $true) { $defaultTag = $tier.Tag continue } $max = if ($null -ne $tier.Max) { [double]$tier.Max } else { [double]::MaxValue } if ($SourceValue -le $max) { if (-not [string]::IsNullOrWhiteSpace($tier.Tag)) { return @($tier.Tag) } } } # If no tier matched, return default if (-not [string]::IsNullOrWhiteSpace($defaultTag)) { return @($defaultTag) } return @() } function Resolve-WildcardValue { <# .SYNOPSIS Matches a source string against wildcard patterns to determine a tag. .DESCRIPTION Iterates through value definitions in order. Each value has a Match pattern (supporting wildcards like * and ?). If the source value matches the pattern (-like), that value's tag is returned. Falls back to the default value if no pattern matches. #> [CmdletBinding()] param( [Parameter()] [string]$SourceValue, [Parameter()] [array]$Values ) if (-not $Values -or $Values.Count -eq 0) { return @() } $defaultTag = '' foreach ($v in $Values) { if ($v.Default -eq $true) { $defaultTag = $v.Tag continue } $pattern = $v.Match if ($null -eq $pattern -or [string]::IsNullOrWhiteSpace([string]$pattern)) { continue } if ($SourceValue -like $pattern) { if (-not [string]::IsNullOrWhiteSpace($v.Tag)) { return @($v.Tag) } } } # Fallback to default if (-not [string]::IsNullOrWhiteSpace($defaultTag)) { return @($defaultTag) } return @() } function Resolve-PatternValue { <# .SYNOPSIS Extracts a tag value from VM notes using a regex pattern. .DESCRIPTION If a regex pattern is defined on the category, applies it to the VM notes. If a capture group is present, returns the first capture group value. Otherwise returns the full match. If values are also defined, the extracted value is matched against them. #> [CmdletBinding()] param( [Parameter()] [string]$Notes, [Parameter()] [string]$Pattern, [Parameter()] [array]$Values ) if ([string]::IsNullOrWhiteSpace($Notes)) { return @() } if (-not [string]::IsNullOrWhiteSpace($Pattern)) { if ($Notes -match $Pattern) { $extracted = '' if ($Matches.Count -gt 1) { $extracted = $Matches[1].Trim() } else { $extracted = $Matches[0].Trim() } if (-not [string]::IsNullOrWhiteSpace($extracted)) { # If values list is provided, match against it if ($Values -and $Values.Count -gt 0) { return @(Resolve-WildcardValue -SourceValue $extracted -Values $Values) } return @($extracted) } } } elseif ($Values -and $Values.Count -gt 0) { # No pattern but values defined -- match notes content directly return @(Resolve-WildcardValue -SourceValue $Notes -Values $Values) } return @() } function Resolve-NamePatternValue { <# .SYNOPSIS Extracts a tag value from the VM name using a regex pattern. .DESCRIPTION Applies a regex pattern to the VM name and returns the first capture group or the full match if no capture groups are defined. #> [CmdletBinding()] param( [Parameter()] [string]$VMName, [Parameter()] [string]$Pattern ) if ([string]::IsNullOrWhiteSpace($VMName) -or [string]::IsNullOrWhiteSpace($Pattern)) { return @() } if ($VMName -match $Pattern) { $extracted = '' if ($Matches.Count -gt 1) { $extracted = $Matches[1].Trim() } else { $extracted = $Matches[0].Trim() } if (-not [string]::IsNullOrWhiteSpace($extracted)) { return @($extracted) } } return @() } |