Private/Get-HyperVMetadata.ps1
|
function Get-HyperVMetadata { <# .SYNOPSIS Collects all relevant metadata from a Hyper-V VM into a normalized object. .DESCRIPTION Pulls VM properties including OS information, hardware configuration, integration services status, network adapters, virtual hard disks, checkpoint (snapshot) information, creation date, power state, and notes. Returns a normalized PSCustomObject with the same structure as Get-VMMetadata (VMware), so the rest of the module (profiles, tag resolution, reports) works unchanged. This function expects a VM object from the Hyper-V Get-VM cmdlet. .PARAMETER VM A Hyper-V VM object from Get-VM. .PARAMETER IncludeCreationDate If specified, includes the VM creation date from the VM's CreationTime property. .EXAMPLE $vm = Hyper-V\Get-VM -Name "WebServer01" $meta = Get-HyperVMetadata -VM $vm $meta.NumCPU .EXAMPLE Hyper-V\Get-VM | ForEach-Object { Get-HyperVMetadata -VM $_ -IncludeCreationDate } .NOTES Author: Larry Roberts Part of the VM-AutoTagger module. #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline)] [object]$VM, [switch]$IncludeCreationDate ) process { $vmName = $VM.Name Write-Verbose "Collecting Hyper-V metadata for VM: $vmName" $powerState = switch ($VM.State) { 'Running' { 'PoweredOn' } 'Off' { 'PoweredOff' } 'Saved' { 'Suspended' } 'Paused' { 'Suspended' } default { 'PoweredOff' } } $numCPU = [int]$VM.ProcessorCount $memoryGB = [double]($VM.MemoryAssigned / 1GB) if ($memoryGB -eq 0 -and $VM.MemoryStartup) { $memoryGB = [double]($VM.MemoryStartup / 1GB) } $notes = if ($VM.Notes) { $VM.Notes } else { '' } # Guest OS info from integration services $guestFamily = '' $guestFullName = '' $guestId = '' # Try to get guest OS from WMI/CIM on the VM host try { $kvpItems = $VM | Get-VMIntegrationService | Where-Object { $_.Name -eq 'Key-Value Pair Exchange' } if ($kvpItems) { # Check OS data from KVP $guestOS = ($VM | Select-Object -ExpandProperty NetworkAdapters -ErrorAction SilentlyContinue | Select-Object -First 1) } } catch { } # Fall back to VM's GuestOperatingSystem property (from Hyper-V integration services) if ([string]::IsNullOrWhiteSpace($guestFullName)) { try { $vmInfo = Get-CimInstance -Namespace 'root\virtualization\v2' -ClassName 'Msvm_ComputerSystem' -Filter "ElementName='$vmName'" -ErrorAction SilentlyContinue if ($vmInfo) { $summary = $vmInfo | Invoke-CimMethod -MethodName 'GetSummaryInformation' -Arguments @{RequestedInformation=@(4)} -ErrorAction SilentlyContinue if ($summary -and $summary.SummaryInformation) { $guestFullName = $summary.SummaryInformation.GuestOperatingSystem } } } catch { } } # Simpler fallback: VM.Generation and OS name hints if ([string]::IsNullOrWhiteSpace($guestFullName)) { $guestFullName = if ($VM.PSObject.Properties['GuestOperatingSystem']) { $VM.GuestOperatingSystem } else { '' } } # Derive family from guest full name if (-not [string]::IsNullOrWhiteSpace($guestFullName)) { if ($guestFullName -match 'Windows') { $guestFamily = 'windowsGuest' } elseif ($guestFullName -match 'Linux|Ubuntu|CentOS|RHEL|Debian|SLES|Fedora') { $guestFamily = 'linuxGuest' } elseif ($guestFullName -match 'FreeBSD') { $guestFamily = 'freebsdGuest' } } # VMware Tools equivalent = Integration Services $toolsStatus = 'toolsNotInstalled' $toolsVersion = '' try { $icStatus = $VM | Get-VMIntegrationService -ErrorAction SilentlyContinue if ($icStatus) { $heartbeat = $icStatus | Where-Object { $_.Name -eq 'Heartbeat' } if ($heartbeat -and $heartbeat.Enabled) { $toolsStatus = if ($heartbeat.PrimaryStatusDescription -eq 'OK') { 'toolsOk' } else { 'toolsOld' } } $toolsVersion = 'IntegrationServices' } } catch { } # IP addresses from network adapters $ipAddresses = @() try { $netAdapters = $VM | Get-VMNetworkAdapter -ErrorAction SilentlyContinue if ($netAdapters) { foreach ($na in $netAdapters) { if ($na.IPAddresses) { $ipAddresses += @($na.IPAddresses | Where-Object { $_ -match '^\d+\.\d+\.\d+\.\d+$' }) } } } } catch { } # Networks (virtual switch names) $networks = @() try { $netAdapters = $VM | Get-VMNetworkAdapter -ErrorAction SilentlyContinue if ($netAdapters) { $networks = @($netAdapters | ForEach-Object { if ($_.SwitchName) { $_.SwitchName } else { 'Disconnected' } }) } } catch { } # Storage (VHD paths -> derive "datastores" as drive letters or CSV paths) $datastores = @() $provisionedSpaceGB = 0 $usedSpaceGB = 0 try { $vhds = $VM | Get-VMHardDiskDrive -ErrorAction SilentlyContinue foreach ($vhd in $vhds) { if ($vhd.Path) { $driveLetter = [System.IO.Path]::GetPathRoot($vhd.Path) if ($driveLetter -and $datastores -notcontains $driveLetter) { $datastores += $driveLetter } # Get VHD size info try { $vhdInfo = Get-VHD -Path $vhd.Path -ErrorAction SilentlyContinue if ($vhdInfo) { $provisionedSpaceGB += [math]::Round($vhdInfo.Size / 1GB, 2) $usedSpaceGB += [math]::Round($vhdInfo.FileSize / 1GB, 2) } } catch { } } } } catch { } # Snapshots (called "checkpoints" in Hyper-V) $snapshotCount = 0 $oldestSnapshotAge = $null $snapshotDetails = @() try { $checkpoints = $VM | Get-VMSnapshot -ErrorAction SilentlyContinue if ($checkpoints) { $snapshotCount = @($checkpoints).Count $snapshotDetails = @($checkpoints | ForEach-Object { [PSCustomObject]@{ Name = $_.Name Created = $_.CreationTime SizeGB = 0 # Hyper-V doesn't expose snapshot size directly AgeDays = [math]::Round(((Get-Date) - $_.CreationTime).TotalDays, 1) } }) $oldest = $checkpoints | Sort-Object CreationTime | Select-Object -First 1 if ($oldest) { $oldestSnapshotAge = [math]::Round(((Get-Date) - $oldest.CreationTime).TotalDays, 1) } } } catch { } # Creation date $createdDate = $null if ($IncludeCreationDate) { if ($VM.CreationTime -and $VM.CreationTime -ne [datetime]::MinValue) { $createdDate = $VM.CreationTime } } # Last powered on (Uptime) $lastPoweredOn = $null if ($VM.Uptime -and $VM.State -eq 'Running') { $lastPoweredOn = (Get-Date) - $VM.Uptime } # Host info $hostName = if ($VM.ComputerName) { $VM.ComputerName } else { $env:COMPUTERNAME } # Snapshot risk $snapshotRisk = 'Clean' if ($snapshotCount -gt 0) { if ($null -ne $oldestSnapshotAge -and $oldestSnapshotAge -gt 7) { $snapshotRisk = 'Stale Snapshots' } else { $snapshotRisk = 'Has Snapshots' } } # VM Age $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' } } $metadata = [PSCustomObject]@{ Name = $vmName PowerState = $powerState GuestFamily = $guestFamily GuestFullName = $guestFullName GuestId = $guestId NumCPU = $numCPU MemoryGB = [math]::Round($memoryGB, 2) 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 = @{} ProvisionedSpaceGB = $provisionedSpaceGB UsedSpaceGB = $usedSpaceGB Host = $hostName Cluster = '' Datacenter = '' Folder = '' VMId = if ($VM.Id) { [string]$VM.Id } else { '' } } $metadata.PSObject.TypeNames.Insert(0, 'VMAutoTagger.VMMetadata') return $metadata } } |