Private/Get-VMMetadata.ps1

function Get-VMMetadata {
    <#
    .SYNOPSIS
        Collects all relevant metadata from a VMware VM into a normalized object.
    .DESCRIPTION
        Pulls VM properties including OS information, hardware configuration,
        VMware Tools status, network adapters, datastores, snapshot information,
        creation date, power state, notes, and custom attributes. Returns a
        normalized PSCustomObject for use by Sync-VMTags, Get-VMCompliance,
        and other module functions.
 
        This function expects a VM object from Get-VM (VMware.VimAutomation.ViCore).
        It makes additional API calls to retrieve snapshots, events (for creation
        date), and extended properties.
    .PARAMETER VM
        A VMware VM object from Get-VM.
    .PARAMETER IncludeCreationDate
        If specified, queries vCenter events to determine the VM creation date.
        This can be slow on large environments; omit for faster metadata collection.
    .EXAMPLE
        $vm = Get-VM -Name "WebServer01"
        $meta = Get-VMMetadata -VM $vm
        $meta.NumCPU
    .EXAMPLE
        Get-VM | ForEach-Object { Get-VMMetadata -VM $_ -IncludeCreationDate }
    .NOTES
        Author: Larry Roberts
        Part of the VM-AutoTagger module.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [object]$VM,

        [Parameter()]
        [switch]$IncludeCreationDate
    )

    process {
        Write-Verbose "Collecting metadata for VM: $($VM.Name)"

        # --- Basic properties ---
        $vmName       = $VM.Name
        $powerState   = [string]$VM.PowerState
        $numCPU       = [int]$VM.NumCpu
        $memoryGB     = [double]$VM.MemoryGB
        $notes        = if ($VM.Notes) { $VM.Notes } else { '' }
        $vmId         = if ($VM.Id) { $VM.Id } else { '' }

        # --- Guest OS info ---
        $guestFamily   = ''
        $guestFullName = ''
        $guestId       = ''

        if ($VM.Guest) {
            $guestFamily   = if ($VM.Guest.GuestFamily) { [string]$VM.Guest.GuestFamily } else { '' }
            $guestFullName = if ($VM.Guest.GuestFullName) { [string]$VM.Guest.GuestFullName } else { '' }
        }

        # Fall back to config-level GuestId for family detection
        if ($VM.ExtensionData -and $VM.ExtensionData.Config) {
            $guestId = if ($VM.ExtensionData.Config.GuestId) { [string]$VM.ExtensionData.Config.GuestId } else { '' }
            if ([string]::IsNullOrWhiteSpace($guestFullName) -and $VM.ExtensionData.Config.GuestFullName) {
                $guestFullName = [string]$VM.ExtensionData.Config.GuestFullName
            }
        }

        # If guest family is still empty, derive from GuestId
        if ([string]::IsNullOrWhiteSpace($guestFamily) -and -not [string]::IsNullOrWhiteSpace($guestId)) {
            if ($guestId -match 'windows') { $guestFamily = 'windowsGuest' }
            elseif ($guestId -match 'linux|ubuntu|centos|rhel|debian|sles|oracle|photon|amazon') { $guestFamily = 'linuxGuest' }
            elseif ($guestId -match 'darwin') { $guestFamily = 'darwinGuest' }
            elseif ($guestId -match 'freebsd') { $guestFamily = 'freebsdGuest' }
        }

        # --- VMware Tools ---
        $toolsStatus  = 'toolsNotInstalled'
        $toolsVersion = ''

        if ($VM.ExtensionData -and $VM.ExtensionData.Guest) {
            $toolsStatus  = if ($VM.ExtensionData.Guest.ToolsStatus) { [string]$VM.ExtensionData.Guest.ToolsStatus } else { 'toolsNotInstalled' }
            $toolsVersion = if ($VM.ExtensionData.Guest.ToolsVersion) { [string]$VM.ExtensionData.Guest.ToolsVersion } else { '' }
        }
        elseif ($VM.Guest) {
            # Some versions expose ToolsVersion on the Guest property
            if ($VM.Guest.PSObject.Properties['ToolsVersion']) {
                $toolsVersion = [string]$VM.Guest.ToolsVersion
            }
        }

        # --- IP Addresses ---
        $ipAddresses = @()
        if ($VM.Guest -and $VM.Guest.IPAddress) {
            $ipAddresses = @($VM.Guest.IPAddress | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
        }

        # --- Network adapters ---
        $networks = @()
        try {
            $netAdapters = Get-NetworkAdapter -VM $VM -ErrorAction SilentlyContinue
            if ($netAdapters) {
                $networks = @($netAdapters | ForEach-Object {
                    if ($_.NetworkName) { $_.NetworkName } else { 'Disconnected' }
                })
            }
        }
        catch {
            Write-Verbose "Could not retrieve network adapters for $vmName : $_"
        }

        # --- Datastores ---
        $datastores = @()
        try {
            $dsObjects = Get-Datastore -VM $VM -ErrorAction SilentlyContinue
            if ($dsObjects) {
                $datastores = @($dsObjects | ForEach-Object { $_.Name })
            }
        }
        catch {
            Write-Verbose "Could not retrieve datastores for $vmName : $_"
        }

        # --- Storage ---
        $provisionedSpaceGB = 0.0
        $usedSpaceGB        = 0.0
        if ($VM.ProvisionedSpaceGB) { $provisionedSpaceGB = [Math]::Round([double]$VM.ProvisionedSpaceGB, 2) }
        if ($VM.UsedSpaceGB) { $usedSpaceGB = [Math]::Round([double]$VM.UsedSpaceGB, 2) }

        # --- Snapshots ---
        $snapshotCount    = 0
        $oldestSnapshotAge = $null
        $snapshotDetails   = @()
        try {
            $snapshots = Get-Snapshot -VM $VM -ErrorAction SilentlyContinue
            if ($snapshots) {
                $snapshotCount = @($snapshots).Count
                $snapshotDetails = @($snapshots | ForEach-Object {
                    [PSCustomObject]@{
                        Name    = $_.Name
                        Created = $_.Created
                        SizeGB  = [Math]::Round($_.SizeGB, 2)
                        AgeDays = [Math]::Round(((Get-Date) - $_.Created).TotalDays, 1)
                    }
                })
                $oldest = $snapshots | Sort-Object Created | Select-Object -First 1
                if ($oldest -and $oldest.Created) {
                    $oldestSnapshotAge = [Math]::Round(((Get-Date) - $oldest.Created).TotalDays, 1)
                }
            }
        }
        catch {
            Write-Verbose "Could not retrieve snapshots for $vmName : $_"
        }

        # --- Creation date (from events) ---
        $createdDate = $null
        if ($IncludeCreationDate) {
            try {
                $events = Get-VIEvent -Entity $VM -MaxSamples 1000 -ErrorAction SilentlyContinue |
                    Where-Object { $_ -is [VMware.Vim.VmCreatedEvent] -or $_ -is [VMware.Vim.VmClonedEvent] -or $_ -is [VMware.Vim.VmDeployedEvent] } |
                    Sort-Object CreatedTime |
                    Select-Object -First 1

                if ($events) {
                    $createdDate = $events.CreatedTime
                }
            }
            catch {
                Write-Verbose "Could not retrieve creation events for $vmName : $_"
            }
        }

        # If creation date still unknown, fall back to ExtensionData
        if ($null -eq $createdDate -and $VM.ExtensionData -and $VM.ExtensionData.Config -and $VM.ExtensionData.Config.CreateDate) {
            $createDateVal = $VM.ExtensionData.Config.CreateDate
            if ($createDateVal -and $createDateVal -ne [datetime]::MinValue) {
                $createdDate = $createDateVal
            }
        }

        # --- Last powered on ---
        $lastPoweredOn = $null
        if ($VM.ExtensionData -and $VM.ExtensionData.Runtime -and $VM.ExtensionData.Runtime.BootTime) {
            $lastPoweredOn = $VM.ExtensionData.Runtime.BootTime
        }
        # For powered off VMs, check events
        if ($null -eq $lastPoweredOn -and $powerState -ne 'PoweredOn') {
            try {
                $powerEvents = Get-VIEvent -Entity $VM -MaxSamples 100 -ErrorAction SilentlyContinue |
                    Where-Object { $_.GetType().Name -match 'VmPoweredOffEvent|VmSuspendedEvent|VmPoweredOnEvent' } |
                    Sort-Object CreatedTime -Descending |
                    Select-Object -First 1

                if ($powerEvents) {
                    $lastPoweredOn = $powerEvents.CreatedTime
                }
            }
            catch {
                Write-Verbose "Could not retrieve power events for $vmName : $_"
            }
        }

        # --- Host, Cluster, Datacenter, Folder ---
        $hostName       = ''
        $clusterName    = ''
        $datacenterName = ''
        $folderPath     = ''

        if ($VM.VMHost) {
            $hostName = [string]$VM.VMHost.Name
        }

        try {
            $vmCluster = Get-Cluster -VM $VM -ErrorAction SilentlyContinue
            if ($vmCluster) { $clusterName = [string]$vmCluster.Name }
        }
        catch {
            Write-Verbose "Could not retrieve cluster for $vmName : $_"
        }

        try {
            $vmDC = Get-Datacenter -VM $VM -ErrorAction SilentlyContinue
            if ($vmDC) { $datacenterName = [string]$vmDC.Name }
        }
        catch {
            Write-Verbose "Could not retrieve datacenter for $vmName : $_"
        }

        if ($VM.Folder) {
            $folderPath = [string]$VM.Folder.Name
        }

        # --- Custom Attributes ---
        $customAttributes = @{}
        if ($VM.CustomFields) {
            foreach ($field in $VM.CustomFields) {
                if ($field.Key -and $field.Value) {
                    $customAttributes[$field.Key] = $field.Value
                }
            }
        }

        # --- Compute snapshot risk ---
        $snapshotRisk = 'Clean'
        if ($snapshotCount -gt 0) {
            if ($null -ne $oldestSnapshotAge -and $oldestSnapshotAge -gt 7) {
                $snapshotRisk = 'Stale Snapshots'
            }
            else {
                $snapshotRisk = 'Has Snapshots'
            }
        }

        # --- Compute VM age category ---
        $vmAgeCategory = 'Unknown'
        if ($null -ne $createdDate) {
            $ageDays = ((Get-Date) - $createdDate).TotalDays
            if ($ageDays -lt 30) { $vmAgeCategory = 'Less than 30d' }
            elseif ($ageDays -lt 90) { $vmAgeCategory = '30-90d' }
            elseif ($ageDays -lt 365) { $vmAgeCategory = '90d-1yr' }
            else { $vmAgeCategory = 'Over 1yr' }
        }

        # --- Build and return the metadata object ---
        $metadata = [PSCustomObject]@{
            Name               = $vmName
            PowerState         = $powerState
            GuestFamily        = $guestFamily
            GuestFullName      = $guestFullName
            GuestId            = $guestId
            NumCPU             = $numCPU
            MemoryGB           = $memoryGB
            ToolsStatus        = $toolsStatus
            ToolsVersion       = $toolsVersion
            IPAddresses        = $ipAddresses
            Networks           = $networks
            Datastores         = $datastores
            SnapshotCount      = $snapshotCount
            SnapshotDetails    = $snapshotDetails
            OldestSnapshotAge  = $oldestSnapshotAge
            SnapshotRisk       = $snapshotRisk
            CreatedDate        = $createdDate
            VMAge              = $vmAgeCategory
            LastPoweredOn      = $lastPoweredOn
            Notes              = $notes
            CustomAttributes   = $customAttributes
            ProvisionedSpaceGB = $provisionedSpaceGB
            UsedSpaceGB        = $usedSpaceGB
            Host               = $hostName
            Cluster            = $clusterName
            Datacenter         = $datacenterName
            Folder             = $folderPath
            VMId               = $vmId
        }

        $metadata.PSObject.TypeNames.Insert(0, 'VMAutoTagger.VMMetadata')
        return $metadata
    }
}