|
| 1 | +function Test-HydrationPrerequisites { |
| 2 | + <# |
| 3 | + .SYNOPSIS |
| 4 | + Runs all pre-flight checks and returns a list of failure messages. |
| 5 | + .PARAMETER VMName |
| 6 | + Hyper-V VM name to validate. |
| 7 | + .PARAMETER RequireRunning |
| 8 | + If set, the VM must be in Running state (required for reconnect). |
| 9 | + .PARAMETER SkipClusterCheck |
| 10 | + Skip the HA/cluster check for non-clustered test environments. |
| 11 | + .OUTPUTS |
| 12 | + [System.Collections.Generic.List[string]] — empty means all passed. |
| 13 | + #> |
| 14 | + [CmdletBinding()] |
| 15 | + [OutputType([System.Collections.Generic.List[string]])] |
| 16 | + param( |
| 17 | + [Parameter(Mandatory)] |
| 18 | + [string]$VMName, |
| 19 | + [switch]$RequireRunning, |
| 20 | + [switch]$SkipClusterCheck |
| 21 | + ) |
| 22 | + |
| 23 | + $failures = [System.Collections.Generic.List[string]]::new() |
| 24 | + |
| 25 | + #region ── 1. stack-hci-vm Extension Version ────────────────────────────── |
| 26 | + Write-Step "Checking stack-hci-vm CLI extension version (>= 1.11.9)" |
| 27 | + if (-not (Test-AzCliExtensionVersion -MinVersion '1.11.9')) { |
| 28 | + $failures.Add("stack-hci-vm extension is missing or below 1.11.9. Upgrade: az extension add --upgrade --name stack-hci-vm --version 1.11.9") |
| 29 | + } else { |
| 30 | + Write-OK "stack-hci-vm extension >= 1.11.9" |
| 31 | + } |
| 32 | + #endregion |
| 33 | + |
| 34 | + #region ── 2. VM Exists in Hyper-V ─────────────────────────────────────── |
| 35 | + Write-Step "Checking VM '$VMName' exists in Hyper-V" |
| 36 | + $vm = $null |
| 37 | + try { |
| 38 | + $vm = Get-VM -Name $VMName -ErrorAction Stop |
| 39 | + Write-OK "VM found (State: $($vm.State), Generation: $($vm.Generation))" |
| 40 | + } catch { |
| 41 | + $failures.Add("VM '$VMName' not found in Hyper-V on this node.") |
| 42 | + return $failures |
| 43 | + } |
| 44 | + #endregion |
| 45 | + |
| 46 | + #region ── 3. VM Running State ──────────────────────────────────────────── |
| 47 | + if ($RequireRunning) { |
| 48 | + Write-Step "Checking VM is in Running state" |
| 49 | + if ($vm.State -ne 'Running') { |
| 50 | + $failures.Add("VM '$VMName' must be Running before reconnect. Current state: $($vm.State).") |
| 51 | + } else { |
| 52 | + Write-OK "VM is Running" |
| 53 | + } |
| 54 | + } |
| 55 | + #endregion |
| 56 | + |
| 57 | + #region ── 4. Highly Available ─────────────────────────────────────────── |
| 58 | + if (-not $SkipClusterCheck) { |
| 59 | + Write-Step "Checking VM is configured as Highly Available" |
| 60 | + try { |
| 61 | + $cr = Get-ClusterResource -ErrorAction Stop | |
| 62 | + Where-Object { $_.ResourceType -eq 'Virtual Machine' -and $_.Name -like "*$VMName*" } |
| 63 | + if (-not $cr) { |
| 64 | + $failures.Add("VM '$VMName' is not HA in Failover Cluster Manager. Configure HA before proceeding.") |
| 65 | + } else { |
| 66 | + Write-OK "VM is HA-configured (cluster resource: $($cr.Name))" |
| 67 | + } |
| 68 | + } catch { |
| 69 | + Write-Warn "Could not check cluster state. Skipping HA check." |
| 70 | + } |
| 71 | + } |
| 72 | + #endregion |
| 73 | + |
| 74 | + #region ── 5. No GPU Attached ──────────────────────────────────────────── |
| 75 | + Write-Step "Checking VM has no GPU attached (DDA or GPU-P)" |
| 76 | + try { |
| 77 | + $gpuDevices = Get-VMAssignableDevice -VMName $VMName -ErrorAction SilentlyContinue |
| 78 | + if ($gpuDevices) { |
| 79 | + $failures.Add("VM '$VMName' has $($gpuDevices.Count) DDA device(s) attached. Remove all GPUs before proceeding.") |
| 80 | + } else { |
| 81 | + Write-OK "No DDA GPU devices attached" |
| 82 | + } |
| 83 | + } catch { |
| 84 | + Write-Warn "Could not enumerate assignable devices. Verify no GPU is attached." |
| 85 | + } |
| 86 | + #endregion |
| 87 | + |
| 88 | + #region ── 6. Not a Trusted Launch VM ──────────────────────────────────── |
| 89 | + Write-Step "Checking VM is not a Trusted Launch VM" |
| 90 | + $secureBootEnabled = (Get-VMFirmware -VMName $VMName -ErrorAction SilentlyContinue).SecureBoot -eq 'On' |
| 91 | + $vtpmEnabled = (Get-VMSecurity -VMName $VMName -ErrorAction SilentlyContinue).TpmEnabled |
| 92 | + if ($secureBootEnabled -and $vtpmEnabled) { |
| 93 | + Write-Warn "VM has both Secure Boot and vTPM enabled. If this is a Trusted Launch VM, reconnect is not supported." |
| 94 | + } else { |
| 95 | + Write-OK "VM does not appear to be a Trusted Launch VM" |
| 96 | + } |
| 97 | + #endregion |
| 98 | + |
| 99 | + #region ── 7. KVP Integration Service ──────────────────────────────────── |
| 100 | + Write-Step "Checking Hyper-V Data Exchange (KVP) integration service" |
| 101 | + try { |
| 102 | + $kvp = Get-VMIntegrationService -VMName $VMName -Name 'Key-Value Pair Exchange' -ErrorAction Stop |
| 103 | + if (-not $kvp.Enabled) { |
| 104 | + $failures.Add("KVP (Data Exchange) integration service is disabled on '$VMName'. Enable it in Hyper-V Manager > Integration Services.") |
| 105 | + } else { |
| 106 | + Write-OK "Data Exchange (KVP) integration service is enabled" |
| 107 | + } |
| 108 | + } catch { |
| 109 | + $failures.Add("Could not check KVP integration service on '$VMName': $_") |
| 110 | + } |
| 111 | + #endregion |
| 112 | + |
| 113 | + #region ── 8. Guest Service Interface ──────────────────────────────────── |
| 114 | + Write-Step "Checking Hyper-V Guest Service Interface" |
| 115 | + try { |
| 116 | + $gsi = Get-VMIntegrationService -VMName $VMName -Name 'Guest Service Interface' -ErrorAction Stop |
| 117 | + if (-not $gsi.Enabled) { |
| 118 | + $failures.Add("Guest Service Interface is disabled on '$VMName'. Enable it in Hyper-V Manager > Integration Services.") |
| 119 | + } else { |
| 120 | + Write-OK "Guest Service Interface is enabled" |
| 121 | + } |
| 122 | + } catch { |
| 123 | + $failures.Add("Could not check Guest Service Interface on '$VMName': $_") |
| 124 | + } |
| 125 | + #endregion |
| 126 | + |
| 127 | + #region ── 9. GUID Folder ───────────────────────────────────────────────── |
| 128 | + Write-Step "Checking VM configuration path is under a GUID folder" |
| 129 | + if (-not (Test-GuidFolderPath -Path $vm.ConfigurationLocation)) { |
| 130 | + $failures.Add("VM '$VMName' config path '$($vm.ConfigurationLocation)' is not under a GUID folder. Move VM files into the correct GUID subfolder.") |
| 131 | + } else { |
| 132 | + Write-OK "Configuration path is under a GUID folder: $($vm.ConfigurationLocation)" |
| 133 | + } |
| 134 | + #endregion |
| 135 | + |
| 136 | + #region ── 10. Backup Services (advisory) ──────────────────────────────── |
| 137 | + Write-Step "Checking for backup services (advisory)" |
| 138 | + $running = @('VeeamBackupSvc', 'ArcasAgent', 'MSBackup', 'wbengine') | Where-Object { |
| 139 | + (Get-Service -Name $_ -ErrorAction SilentlyContinue)?.Status -eq 'Running' |
| 140 | + } |
| 141 | + if ($running) { |
| 142 | + Write-Warn "Backup service(s) detected: $($running -join ', '). Microsoft recommends no backup services run during reconnect." |
| 143 | + } else { |
| 144 | + Write-OK "No known backup services detected" |
| 145 | + } |
| 146 | + #endregion |
| 147 | + |
| 148 | + return $failures |
| 149 | +} |
0 commit comments