Public/New-VMTagProfile.ps1

function New-VMTagProfile {
    <#
    .SYNOPSIS
        Creates or exports a YAML tag profile configuration file for VM-AutoTagger.
    .DESCRIPTION
        Generates a YAML tag profile that defines how VMs should be tagged. The profile
        specifies tag categories, their data sources, matching rules (wildcards, tiers,
        regex patterns), and cardinality settings.
 
        Two modes of operation:
        1. Generate a default/example profile with all built-in categories and optionally
           custom category examples.
        2. Export the current tag structure from a live vCenter server as a profile.
 
        The generated YAML file can be customized and passed to Sync-VMTags via the
        -ProfilePath parameter to control tag behavior.
    .PARAMETER OutputPath
        The file path where the YAML profile will be saved.
    .PARAMETER FromServer
        If specified, connects to this vCenter server and exports the existing tag
        categories and tags as a profile. This is useful for capturing an existing
        tag structure as a starting point for customization.
    .PARAMETER Credential
        PSCredential for vCenter authentication when using -FromServer.
    .PARAMETER IncludeCustom
        Include example custom tag category definitions (Department, Environment,
        Backup-Policy, Cost-Center, SLA-Tier) in the generated profile. These
        demonstrate advanced matching features like regex extraction and VM name
        pattern matching.
    .PARAMETER Hypervisor
        The hypervisor platform to use when exporting from a live server via
        -FromServer. Valid values: 'VMware', 'HyperV'. Default: 'VMware'.
        When set to 'HyperV', reads existing tags from VM Notes across all VMs
        instead of reading vSphere tag categories.
    .EXAMPLE
        New-VMTagProfile -OutputPath .\my-profile.yml
 
        Creates a default profile with the 10 standard tag categories.
    .EXAMPLE
        New-VMTagProfile -OutputPath .\full-profile.yml -IncludeCustom
 
        Creates a profile with standard categories plus example custom categories.
    .EXAMPLE
        New-VMTagProfile -OutputPath .\exported-profile.yml -FromServer vcenter.contoso.com -Credential (Get-Credential)
 
        Exports the existing vCenter tag structure as a YAML profile.
    .NOTES
        Author: Larry Roberts
        Part of the VM-AutoTagger module.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$OutputPath,

        [Parameter()]
        [string]$FromServer,

        [Parameter()]
        [PSCredential]$Credential,

        [Parameter()]
        [switch]$IncludeCustom,

        [Parameter()]
        [ValidateSet('VMware', 'HyperV')]
        [string]$Hypervisor = 'VMware'
    )

    process {
        if ($FromServer) {
            # Export from live server
            Write-Verbose "Exporting tag profile from: $FromServer"
            $yaml = Export-TagProfileFromServer -Server $FromServer -Credential $Credential -Hypervisor $Hypervisor
        }
        else {
            # Generate default profile
            Write-Verbose "Generating default tag profile"
            $yaml = Build-DefaultProfileYaml -IncludeCustom:$IncludeCustom
        }

        # Save to file
        try {
            $outputDir = Split-Path -Path $OutputPath -Parent
            if ($outputDir -and -not (Test-Path $outputDir)) {
                New-Item -Path $outputDir -ItemType Directory -Force | Out-Null
            }
            $yaml | Set-Content -Path $OutputPath -Encoding UTF8 -Force -NoNewline
            Write-Host "Tag profile saved to: $OutputPath" -ForegroundColor Green
        }
        catch {
            Write-Error "Failed to save tag profile: $_"
            return
        }

        # Return profile info
        return [PSCustomObject]@{
            Path        = (Resolve-Path $OutputPath).Path
            Source      = if ($FromServer) { "Exported from $FromServer" } else { 'Generated default' }
            IncludeCustom = [bool]$IncludeCustom
        }
    }
}

function Export-TagProfileFromServer {
    <#
    .SYNOPSIS
        Exports existing tag categories and tags as a YAML profile string.
    .DESCRIPTION
        For VMware, exports vSphere tag categories and their tags.
        For Hyper-V, scans all VMs' Notes fields for VM-AutoTagger tag blocks
        and extracts the unique categories and values found.
    #>

    [CmdletBinding()]
    param(
        [string]$Server,
        [PSCredential]$Credential,
        [string]$Hypervisor = 'VMware'
    )

    $viConnection = $null
    try {
        $viConnection = Connect-Hypervisor -Server $Server -Credential $Credential -Hypervisor $Hypervisor
    }
    catch {
        Write-Error "Failed to connect to $Hypervisor host '$Server': $_"
        return ''
    }

    try {
        $yamlLines = [System.Collections.ArrayList]::new()

        [void]$yamlLines.Add("# VM-AutoTagger Tag Profile")
        [void]$yamlLines.Add("# Exported from: $Server ($Hypervisor)")
        [void]$yamlLines.Add("# Export date: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
        [void]$yamlLines.Add("")
        [void]$yamlLines.Add("name: exported-$($Server -replace '[^a-zA-Z0-9]', '-')")
        [void]$yamlLines.Add("description: Tag profile exported from $Server")
        [void]$yamlLines.Add("")
        [void]$yamlLines.Add("categories:")

        if ($Hypervisor -eq 'HyperV') {
            # Scan all VMs for existing tags in Notes
            $getVMParams = @{ ErrorAction = 'Stop' }
            if ($Server -ne 'localhost' -and $Server -ne $env:COMPUTERNAME -and $Server -ne '.') {
                $getVMParams['ComputerName'] = $Server
            }
            $allVMs = @(Hyper-V\Get-VM @getVMParams)

            # Collect all unique category -> tag value mappings
            $categoryTags = [ordered]@{}
            foreach ($vm in $allVMs) {
                $hvTags = Get-HyperVNotesTags -VM $vm
                foreach ($key in $hvTags.Keys) {
                    if (-not $categoryTags.Contains($key)) {
                        $categoryTags[$key] = [System.Collections.ArrayList]::new()
                    }
                    $val = $hvTags[$key]
                    if ($val -and $categoryTags[$key] -notcontains $val) {
                        [void]$categoryTags[$key].Add($val)
                    }
                }
            }

            foreach ($catName in ($categoryTags.Keys | Sort-Object)) {
                [void]$yamlLines.Add(" - name: $catName")
                [void]$yamlLines.Add(" source: direct")
                [void]$yamlLines.Add(" cardinality: Single")
                [void]$yamlLines.Add(" description: `"Exported from Hyper-V VM Notes`"")

                $tagValues = @($categoryTags[$catName] | Sort-Object)
                if ($tagValues.Count -gt 0) {
                    [void]$yamlLines.Add(" values:")
                    foreach ($tv in $tagValues) {
                        [void]$yamlLines.Add(" - match: `"$tv`"")
                        [void]$yamlLines.Add(" tag: `"$tv`"")
                    }
                }
                [void]$yamlLines.Add("")
            }
        }
        else {
            $categories = @(Get-TagCategory -Server $Server -ErrorAction Stop)

            foreach ($cat in ($categories | Sort-Object Name)) {
                [void]$yamlLines.Add(" - name: $($cat.Name)")
                [void]$yamlLines.Add(" source: direct")
                [void]$yamlLines.Add(" cardinality: $($cat.Cardinality)")
                if ($cat.Description) {
                    [void]$yamlLines.Add(" description: `"$($cat.Description -replace '"', '\"')`"")
                }

                $tags = @(Get-Tag -Category $cat -Server $Server -ErrorAction SilentlyContinue)
                if ($tags.Count -gt 0) {
                    [void]$yamlLines.Add(" values:")
                    foreach ($tag in ($tags | Sort-Object Name)) {
                        [void]$yamlLines.Add(" - match: `"$($tag.Name)`"")
                        [void]$yamlLines.Add(" tag: `"$($tag.Name)`"")
                    }
                }
                [void]$yamlLines.Add("")
            }
        }

        return ($yamlLines -join "`n")
    }
    finally {
        if ($viConnection) {
            try { Disconnect-Hypervisor -Server $Server -Hypervisor $Hypervisor } catch { }
        }
    }
}

function Build-DefaultProfileYaml {
    <#
    .SYNOPSIS
        Generates the default YAML profile content string.
    #>

    [CmdletBinding()]
    param(
        [switch]$IncludeCustom
    )

    $yaml = @"
# VM-AutoTagger Tag Profile
# Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
# Documentation: https://github.com/larro1991/VM-AutoTagger
 
name: default
description: Default tag profile with standard VM categories
 
categories:
  # ──────────────────────────────────────────────────────────────
  # OS-Family: Detected from VMware Tools guest family or GuestId
  # ──────────────────────────────────────────────────────────────
  - name: OS-Family
    source: guest_family
    cardinality: Single
    description: "Operating system family detected from VMware Tools"
    values:
      - match: "*Windows*"
        tag: Windows
      - match: "*Linux*"
        tag: Linux
      - match: "*linux*"
        tag: Linux
      - match: "*darwin*"
        tag: macOS
      - match: "*freebsd*"
        tag: FreeBSD
      - default: true
        tag: Other
 
  # ──────────────────────────────────────────────────────────────
  # OS-Version: Full guest OS name from VMware Tools
  # ──────────────────────────────────────────────────────────────
  - name: OS-Version
    source: guest_fullname
    cardinality: Single
    description: "Full OS version string from VMware Tools"
 
  # ──────────────────────────────────────────────────────────────
  # CPU-Tier: vCPU count tiers for capacity planning
  # ──────────────────────────────────────────────────────────────
  - name: CPU-Tier
    source: cpu_count
    cardinality: Single
    description: "vCPU count tier for capacity planning"
    tiers:
      - max: 2
        tag: "1-2 vCPU"
      - max: 8
        tag: "4-8 vCPU"
      - max: 16
        tag: "9-16 vCPU"
      - default: true
        tag: "16+ vCPU"
 
  # ──────────────────────────────────────────────────────────────
  # Memory-Tier: RAM allocation tiers for capacity planning
  # ──────────────────────────────────────────────────────────────
  - name: Memory-Tier
    source: memory_gb
    cardinality: Single
    description: "Memory allocation tier for capacity planning"
    tiers:
      - max: 4
        tag: "Small (<=4GB)"
      - max: 16
        tag: "Medium (8-16GB)"
      - max: 64
        tag: "Large (32-64GB)"
      - default: true
        tag: "XLarge (64GB+)"
 
  # ──────────────────────────────────────────────────────────────
  # Tools-Status: VMware Tools installation and currency
  # ──────────────────────────────────────────────────────────────
  - name: Tools-Status
    source: tools_status
    cardinality: Single
    description: "VMware Tools installation and currency status"
    values:
      - match: "toolsOk"
        tag: Current
      - match: "toolsOld"
        tag: Outdated
      - match: "toolsNotInstalled"
        tag: Not Installed
      - match: "toolsNotRunning"
        tag: Not Running
      - default: true
        tag: Not Installed
 
  # ──────────────────────────────────────────────────────────────
  # Power-State: VM power state
  # ──────────────────────────────────────────────────────────────
  - name: Power-State
    source: power_state
    cardinality: Single
    description: "VM power state"
    values:
      - match: "PoweredOn"
        tag: Running
      - match: "PoweredOff"
        tag: Stopped
      - match: "Suspended"
        tag: Suspended
      - default: true
        tag: Stopped
 
  # ──────────────────────────────────────────────────────────────
  # Snapshot-Risk: Snapshot presence and age assessment
  # ──────────────────────────────────────────────────────────────
  - name: Snapshot-Risk
    source: snapshot_risk
    cardinality: Single
    description: "Snapshot presence and age risk assessment"
 
  # ──────────────────────────────────────────────────────────────
  # VM-Age: Age since VM creation
  # ──────────────────────────────────────────────────────────────
  - name: VM-Age
    source: vm_age
    cardinality: Single
    description: "VM age since creation"
 
  # ──────────────────────────────────────────────────────────────
  # Network: Port group assignments (multiple allowed)
  # ──────────────────────────────────────────────────────────────
  - name: Network
    source: network
    cardinality: Multiple
    description: "Network port group assignments"
 
  # ──────────────────────────────────────────────────────────────
  # Datastore: Storage locations (multiple allowed)
  # ──────────────────────────────────────────────────────────────
  - name: Datastore
    source: datastore
    cardinality: Multiple
    description: "Datastore locations for VM files"
"@


    if ($IncludeCustom) {
        $yaml += @"
 
 
  # ══════════════════════════════════════════════════════════════
  # CUSTOM CATEGORIES (examples for enterprise use)
  # ══════════════════════════════════════════════════════════════
 
  # ──────────────────────────────────────────────────────────────
  # Department: Extracted from VM annotation/notes via regex
  # Expects notes to contain "Dept: <value>" or "Department: <value>"
  # ──────────────────────────────────────────────────────────────
  - name: Department
    source: annotation
    cardinality: Single
    description: "Department extracted from VM notes"
    pattern: "(?:Dept|Department):\\s*(.+)"
 
  # ──────────────────────────────────────────────────────────────
  # Environment: Determined from VM name prefix convention
  # Expects naming like PRD-WebServer01, DEV-AppSrv02, etc.
  # ──────────────────────────────────────────────────────────────
  - name: Environment
    source: vm_name
    cardinality: Single
    description: "Environment derived from VM naming convention"
    values:
      - match: "PRD-*"
        tag: Production
      - match: "PROD-*"
        tag: Production
      - match: "DEV-*"
        tag: Development
      - match: "STG-*"
        tag: Staging
      - match: "TST-*"
        tag: Testing
      - match: "QA-*"
        tag: QA
      - match: "DR-*"
        tag: Disaster Recovery
      - default: true
        tag: Unclassified
 
  # ──────────────────────────────────────────────────────────────
  # Backup-Policy: From vSphere custom attribute "BackupTier"
  # ──────────────────────────────────────────────────────────────
  - name: Backup-Policy
    source: custom_attribute
    attribute_name: BackupTier
    cardinality: Single
    description: "Backup policy tier from custom attribute"
 
  # ──────────────────────────────────────────────────────────────
  # Cost-Center: Extracted from VM annotation/notes via regex
  # Expects notes to contain "Cost Center: <value>"
  # ──────────────────────────────────────────────────────────────
  - name: Cost-Center
    source: annotation
    cardinality: Single
    description: "Cost center extracted from VM notes"
    pattern: "Cost[- ]?Center:\\s*(\\w+)"
 
  # ──────────────────────────────────────────────────────────────
  # SLA-Tier: From custom attribute or folder path
  # ──────────────────────────────────────────────────────────────
  - name: SLA-Tier
    source: custom_attribute
    attribute_name: SLATier
    cardinality: Single
    description: "SLA tier from custom attribute"
"@

    }

    return $yaml
}