<# .SYNOPSIS Read-only inventory of a vCenter estate before a hypervisor migration. .DESCRIPTION Exports one row per VM with the attributes that decide how (and whether) a VM can be converted: firmware, Secure Boot, vTPM, encryption, VMware Tools state, CBT, snapshots, disk modes, RDMs, shared/multi-writer disks, SCSI bus sharing, PCI passthrough, vSAN placement, networks and guest IPs. A "Blockers" column lists the findings that need a decision before conversion. The script only reads. It never changes a VM, a snapshot or a setting. .PARAMETER Server vCenter FQDN or IP. .PARAMETER OutFile CSV path. Defaults to .\vmware-inventory-.csv .EXAMPLE .\Get-VMwareMigrationInventory.ps1 -Server vcenter.contoso.local Prompts for credentials, then writes the CSV in the current folder. .NOTES Requires the PowerCLI core module VMware.VimAutomation.Core. PowerCLI 9 and later ship as VCF.PowerCLI (Install-Module VCF.PowerCLI -Scope CurrentUser); older estates use VMware.PowerCLI 13.x. Tested with VMware.VimAutomation.Core 13.5.1. A read-only vCenter role is enough. Source: ANYSYS ICT, https://www.anysys.be/articles/ #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Server, [pscredential]$Credential = (Get-Credential -Message "vCenter (read-only account)"), [string]$OutFile = (Join-Path (Get-Location) ("vmware-inventory-{0:yyyyMMdd-HHmm}.csv" -f (Get-Date))) ) $ErrorActionPreference = 'Stop' Import-Module VMware.VimAutomation.Core -ErrorAction Stop # Do not touch the global PowerCLI configuration: the setting below only lives in this session. Set-PowerCLIConfiguration -Scope Session -InvalidCertificateAction Warn -Confirm:$false | Out-Null $vi = Connect-VIServer -Server $Server -Credential $Credential try { $rows = foreach ($vm in Get-VM -Server $vi | Sort-Object Name) { $cfg = $vm.ExtensionData.Config $devices = @($cfg.Hardware.Device | Where-Object { $null -ne $_ }) $disks = @(Get-HardDisk -VM $vm) $nics = @(Get-NetworkAdapter -VM $vm) $snaps = @(Get-Snapshot -VM $vm) $scsi = @(Get-ScsiController -VM $vm) $dsNames = @($disks | ForEach-Object { ($_.Filename -split '\]')[0].TrimStart('[') } | Sort-Object -Unique) $dsTypes = @($dsNames | ForEach-Object { (Get-Datastore -Name $_ -Server $vi).Type } | Sort-Object -Unique) $independent = @($disks | Where-Object { $_.Persistence -like 'Independent*' }) $rdmPhysical = @($disks | Where-Object { $_.DiskType -eq 'RawPhysical' }) $rdmVirtual = @($disks | Where-Object { $_.DiskType -eq 'RawVirtual' }) $multiWriter = @($disks | Where-Object { $_.ExtensionData.Backing.Sharing -eq 'sharingMultiWriter' }) $busSharing = @($scsi | Where-Object { $_.BusSharingMode -ne 'NoSharing' }) $ideKeys = @($devices | Where-Object { $_ -is [VMware.Vim.VirtualIDEController] } | ForEach-Object Key) $ideDisks = @($disks | Where-Object { $ideKeys -contains $_.ExtensionData.ControllerKey }) $passthrough = @($devices | Where-Object { $_ -is [VMware.Vim.VirtualPCIPassthrough] }) $vtpm = @($devices | Where-Object { $_.GetType().Name -eq 'VirtualTPM' }) $encrypted = $null -ne $cfg.KeyId $blockers = [System.Collections.Generic.List[string]]::new() if ($snaps.Count) { $blockers.Add("snapshots($($snaps.Count))") } if ($independent.Count) { $blockers.Add('independent-disk') } if ($rdmPhysical.Count) { $blockers.Add('rdm-physical') } if ($rdmVirtual.Count) { $blockers.Add('rdm-virtual') } if ($multiWriter.Count) { $blockers.Add('multi-writer') } if ($busSharing.Count) { $blockers.Add('scsi-bus-sharing') } if ($ideDisks.Count) { $blockers.Add('ide-disk') } if ($passthrough.Count) { $blockers.Add('pci-passthrough') } if ($vtpm.Count) { $blockers.Add('vtpm') } if ($encrypted) { $blockers.Add('vm-encryption') } if ($dsTypes -contains 'vsan') { $blockers.Add('vsan') } if (-not $cfg.ChangeTrackingEnabled) { $blockers.Add('cbt-off') } if ($vm.ExtensionData.Guest.ToolsRunningStatus -ne 'guestToolsRunning') { $blockers.Add('tools-not-running') } [pscustomobject]@{ Name = $vm.Name PowerState = $vm.PowerState GuestOS = $vm.ExtensionData.Guest.GuestFullName ConfiguredOS = $cfg.GuestFullName HardwareVersion = $vm.HardwareVersion Firmware = $cfg.Firmware SecureBoot = [bool]$cfg.BootOptions.EfiSecureBootEnabled vTPM = [bool]$vtpm.Count Encrypted = $encrypted NumCpu = $vm.NumCpu MemoryGB = $vm.MemoryGB ProvisionedGB = [math]::Round($vm.ProvisionedSpaceGB, 1) UsedGB = [math]::Round($vm.UsedSpaceGB, 1) Disks = $disks.Count DiskDetail = ($disks | ForEach-Object { '{0}:{1}GB:{2}:{3}' -f $_.Name, [math]::Round($_.CapacityGB, 1), $_.StorageFormat, $_.Persistence }) -join ' | ' ScsiTypes = ($scsi.Type | Sort-Object -Unique) -join ',' Datastores = $dsNames -join ',' DatastoreTypes = $dsTypes -join ',' Snapshots = $snaps.Count CBT = [bool]$cfg.ChangeTrackingEnabled ToolsStatus = $vm.ExtensionData.Guest.ToolsRunningStatus ToolsVersion = $vm.ExtensionData.Guest.ToolsVersionStatus2 Networks = ($nics.NetworkName | Sort-Object -Unique) -join ',' NicTypes = ($nics.Type | Sort-Object -Unique) -join ',' MacAddresses = $nics.MacAddress -join ',' GuestIPs = (@($vm.Guest.IPAddress) -join ',') Host = $vm.VMHost.Name Folder = $vm.Folder.Name Notes = ($vm.Notes -replace '\r?\n', ' ') Blockers = $blockers -join ';' } } $rows | Export-Csv -Path $OutFile -NoTypeInformation -Encoding UTF8 -Delimiter ';' Write-Host ("{0} VMs exported to {1}" -f @($rows).Count, $OutFile) Write-Host ("{0} VMs carry at least one blocker" -f @($rows | Where-Object Blockers).Count) } finally { Disconnect-VIServer -Server $vi -Confirm:$false }