PowerShell Script for Hyper-V VM Audit
This script must be run on the VM host from an elevated PowerShell window.
Note: The Hyper-V management tools must be installed for this script to work.
The results will be output to 2 files;
  • C:\VM_Audit\HyperV_VM_Audit_Disks.txt
  • C:\VM_Audit\HyperV_VM_Audit_Disks.csv

<--------------------------------- Script ----------------------------------->

<# .SYNOPSIS
    Hyper-V Host & VM Audit Script for Windows Server Migration (Detailed Per-Disk Rows).
   .DESCRIPTION
    Exports VM specs, Generation, Switch, MAC, IP, and outputs each physical
    and virtual disk as its own dedicated row in the CSV and TXT reports.
#>

# Output Directory
$OutputDir = "C:\VM_Audit"
if (-not (Test-Path $OutputDir)) { New-Item -ItemType Directory -Path $OutputDir | Out-Null }

$CSVPath = Join-Path $OutputDir "HyperV_VM_Audit_Disks.csv"
$TXTPath = Join-Path $OutputDir "HyperV_VM_Audit_Disks.txt"

# Get all VMs
$VMs = Get-VM

$AuditRows = foreach ($VM in $VMs) {
    # Network Adapters
    $Nics = Get-VMNetworkAdapter -VMName $VM.Name
    
    # Attempt to extract Guest IP Addresses via Hyper-V Integration Services (KVP)
    $GuestIPs = ($Nics.IPAddresses | Where-Object { $_ -match "\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" }) -join "; "
    if (-not $GuestIPs) { $GuestIPs = "Unknown / Guest Shut Down" }

    # Query hard disk drives directly attached to the VM
    $HardDrives = Get-VMHardDiskDrive -VMName $VM.Name

    if ($HardDrives.Count -eq 0) {
        # Edge case: VM has no attached drives
        [PSCustomObject]@{
            VMName             = $VM.Name
            State              = $VM.State
            Generation         = "Gen $($VM.Generation)"
            vCPUs              = $VM.ProcessorCount
            MemoryStartupGB    = [math]::Round($VM.MemoryStartup / 1GB, 2)
            DynamicMemory      = $VM.DynamicMemoryEnabled
            VirtualSwitch      = ($Nics.SwitchName) -join "; "
            MACAddress         = ($Nics.MacAddress) -join "; "
            GuestIPAddress     = $GuestIPs
            DiskType           = "No Disks Attached"
            ControllerType     = "N/A"
            ControllerLocation = "N/A"
            DiskPathOrNumber   = "N/A"
            Notes              = "Verify Static IPs inside OS upon migration"
        }
    }
    else {
        # Loop through each disk so it gets its own dedicated row
        foreach ($Drive in $HardDrives) {
            $ControllerLoc = "$($Drive.ControllerNumber),$($Drive.ControllerLocation)"
            
            if ($Drive.DiskNumber) {
                # Physical Pass-Through Disk
                $DiskType = "Physical Disk (Pass-Through)"
                $PathOrNum = "Physical Disk #$($Drive.DiskNumber)"
            }
            else {
                # Virtual Hard Disk (.vhdx / .vhd)
                $DiskType = "Virtual Disk (VHDX/VHD)"
                $PathOrNum = $Drive.Path
            }

            [PSCustomObject]@{
                VMName             = $VM.Name
                State              = $VM.State
                Generation         = "Gen $($VM.Generation)"
                vCPUs              = $VM.ProcessorCount
                MemoryStartupGB    = [math]::Round($VM.MemoryStartup / 1GB, 2)
                DynamicMemory      = $VM.DynamicMemoryEnabled
                VirtualSwitch      = ($Nics.SwitchName) -join "; "
                MACAddress         = ($Nics.MacAddress) -join "; "
                GuestIPAddress     = $GuestIPs
                DiskType           = $DiskType
                ControllerType     = $Drive.ControllerType
                ControllerLocation = $ControllerLoc
                DiskPathOrNumber   = $PathOrNum
                Notes              = "Verify Static IPs inside OS upon migration"
            }
        }
    }
}

# Export to CSV
$AuditRows | Export-Csv -Path $CSVPath -NoTypeInformation -Encoding UTF8

# Generate Human-Readable Text Summary
$TXTContent = @()
$TXTContent += "========================================================"
$TXTContent += "        HYPER-V VM DISK AUDIT REPORT - HOST: $env:COMPUTERNAME"
$TXTContent += "        Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
$TXTContent += "========================================================`n"

$GroupedVMs = $AuditRows | Group-Object VMName

foreach ($Group in $GroupedVMs) {
    $FirstRow = $Group.Group[0]
    $TXTContent += "VM NAME:          $($FirstRow.VMName)"
    $TXTContent += "--------------------------------------------------------"
    $TXTContent += "State:            $($FirstRow.State)"
    $TXTContent += "Generation:       $($FirstRow.Generation)"
    $TXTContent += "vCPUs:            $($FirstRow.vCPUs)"
    $TXTContent += "RAM (GB):         $($FirstRow.MemoryStartupGB) (Dynamic: $($FirstRow.DynamicMemory))"
    $TXTContent += "Virtual Switch:   $($FirstRow.VirtualSwitch)"
    $TXTContent += "MAC Address:      $($FirstRow.MACAddress)"
    $TXTContent += "Reported Guest IP:$($FirstRow.GuestIPAddress)"
    $TXTContent += "Attached Disks:"

    foreach ($DiskRow in $Group.Group) {
        $TXTContent += "  - [$($DiskRow.DiskType)] Bus: $($DiskRow.ControllerType) ($($DiskRow.ControllerLocation)) | Target: $($DiskRow.DiskPathOrNumber)"
    }
    
    $TXTContent += "`n"
}

$TXTContent | Out-File -FilePath $TXTPath -Encoding UTF8

Write-Host "Audit completed successfully!" -ForegroundColor Green
Write-Host "CSV saved to: $CSVPath" -ForegroundColor Yellow
Write-Host "TXT saved to: $TXTPath" -ForegroundColor Yellow