Public/Test-DisasterRecoveryReadiness.ps1
|
function Test-DisasterRecoveryReadiness { <# .SYNOPSIS Evaluates VMware vSphere disaster recovery readiness for VMs across sites. .DESCRIPTION This advanced function uses Get-View to rapidly assess VMs in a source cluster against DR readiness criteria. Checks include CBT status, Snapshot age, Hardware Version (vmx-19+), vCPU/NUMA limits against a target cluster, Resource Pool constraints, Storage Blockers (pRDMs, Independent Disks, Multi-Writer, ISOs), Network configurations, DRS rules, and Encryption/vTPM status. Validates target Datastore Cluster capacity (>20% free). .PARAMETER SourceServer Connection to the source vCenter server. .PARAMETER TargetServer Connection to the target vCenter server. .PARAMETER SourceCluster Name of the source cluster. .PARAMETER TargetCluster Name of the target DR cluster. .PARAMETER TargetDatastoreCluster Name of the target Datastore Cluster. .PARAMETER VMName Optional array of critical VM names to test. If omitted, all applicable VMs in the source cluster are evaluated. .EXAMPLE Test-DisasterRecoveryReadiness -SourceServer $srcVc -TargetServer $tgtVc -SourceCluster "SiteA-Cl01" -TargetCluster "SiteB-Cl01" -TargetDatastoreCluster "SiteB-DSC01" .NOTES Author: The Any Stack Architect #> [CmdletBinding(SupportsShouldProcess=$true)] param( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] $SourceServer, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] $TargetServer, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$SourceCluster, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$TargetCluster, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$TargetDatastoreCluster, [Parameter(Mandatory=$false)] [string[]]$VMName ) begin { Write-Verbose "Initializing DR Readiness Validation..." } process { $ErrorActionPreference = 'Stop' try { # 1. Target Datastore Cluster Evaluation Write-Verbose "Evaluating Target Datastore Cluster: $TargetDatastoreCluster" $dsClusterView = Get-View -Server $TargetServer -ViewType StoragePod -Filter @{"Name"="^$TargetDatastoreCluster$"} -Property Name,Summary -ErrorAction Stop $targetDsHealthy = $false if ($dsClusterView) { $capacity = $dsClusterView.Summary.Capacity $freeSpace = $dsClusterView.Summary.FreeSpace if ($capacity -gt 0) { $freePct = ($freeSpace / $capacity) * 100 if ($freePct -gt 20) { $targetDsHealthy = $true } } } if (-not $targetDsHealthy) { Write-Warning "Target Datastore Cluster '$TargetDatastoreCluster' has less than 20% free space or was not found." } # 2. Target Cluster Compute Boundaries Write-Verbose "Evaluating Target Cluster: $TargetCluster" $tgtClusterView = Get-View -Server $TargetServer -ViewType ClusterComputeResource -Filter @{"Name"="^$TargetCluster$"} -Property Host -ErrorAction Stop $maxTargetCores = 0 if ($tgtClusterView -and $tgtClusterView.Host) { $tgtHosts = Get-View -Server $TargetServer -Id $tgtClusterView.Host -Property Hardware.CpuInfo.NumCores foreach ($h in $tgtHosts) { if ($h.Hardware.CpuInfo.NumCores -gt $maxTargetCores) { $maxTargetCores = $h.Hardware.CpuInfo.NumCores } } } # 3. Source Cluster rules (DRS Affinity) Write-Verbose "Fetching Source Cluster: $SourceCluster" $srcClusterView = Get-View -Server $SourceServer -ViewType ClusterComputeResource -Filter @{"Name"="^$SourceCluster$"} -Property ConfigurationEx.Rule,MoRef -ErrorAction Stop if (-not $srcClusterView) { throw "Source Cluster '$SourceCluster' not found." } # Map VM MoRef to DRS Rules $vmDrsRules = @{} if ($srcClusterView.ConfigurationEx.Rule) { foreach ($rule in $srcClusterView.ConfigurationEx.Rule) { if ($rule -is [VMware.Vim.ClusterAntiAffinityRuleSpec] -or $rule -is [VMware.Vim.ClusterAffinityRuleSpec]) { foreach ($ruleVm in $rule.Vm) { $vmStr = $ruleVm.Value if (-not $vmDrsRules[$vmStr]) { $vmDrsRules[$vmStr] = @() } $vmDrsRules[$vmStr] += $rule.Name } } } } # 4. Fetch VMs Write-Verbose "Fetching VMs from Source Cluster..." $vmProps = @( "Name", "Config.ManagedBy", "Config.FtInfo", "Config.ChangeTrackingEnabled", "Config.Version", "Config.Hardware.Device", "Config.Hardware.NumCPU", "Config.CpuAllocation", "Config.MemoryAllocation", "Config.KeyId", "Guest.ToolsStatus", "Snapshot.RootSnapshotList", "Parent", "Network" ) $allVms = Get-View -Server $SourceServer -ViewType VirtualMachine -SearchRoot $srcClusterView.MoRef -Property $vmProps # Filter vCLS and FT VMs, and by VMName if provided $filteredVms = $allVms | Where-Object { ($null -eq $_.Config.ManagedBy -or $_.Config.ManagedBy.ExtensionKey -ne "com.vmware.vcls") -and ($_.Name -notmatch "^vCLS") -and ($null -eq $_.Config.FtInfo) } if ($VMName -and $VMName.Count -gt 0) { $filteredVms = $filteredVms | Where-Object { $VMName -contains $_.Name } } # 5. Process each VM $snapThreshold = (Get-Date).AddHours(-24) # Pre-fetch Resource Pools $rpViews = Get-View -Server $SourceServer -ViewType ResourcePool -Property Name,Config $rpMap = @{} if ($rpViews) { foreach ($rp in $rpViews) { $rpMap[$rp.MoRef.Value] = $rp } } # Pre-fetch Networks for VSS check $netViews = Get-View -Server $SourceServer -ViewType Network -Property Name $vssMap = @{} if ($netViews) { foreach ($net in $netViews) { $vssMap[$net.MoRef.Value] = $net.Name } } $results = foreach ($vm in $filteredVms) { $isReady = $true # --- Data Protection Readiness --- $cbtEnabled = ($vm.Config.ChangeTrackingEnabled -eq $true) if (-not $cbtEnabled) { $isReady = $false } $toolsStatus = $vm.Guest.ToolsStatus if ($toolsStatus -ne "toolsOk") { $isReady = $false } $hwVersion = $vm.Config.Version $hwVerInt = 0 if ($hwVersion -match "\d+") { $hwVerInt = [int]$Matches[0] } if ($hwVerInt -lt 19) { $isReady = $false } $hasOrphanedSnaps = $false if ($vm.Snapshot -and $vm.Snapshot.RootSnapshotList) { $hasOrphanedSnaps = Get-OldSnapshots -SnapshotTree $vm.Snapshot.RootSnapshotList -OlderThan $snapThreshold if ($hasOrphanedSnaps) { $isReady = $false } } # --- Storage Edge Cases --- $storageFlags = @() $networkFlags = @() $encryptionFlags = @() if ($vm.Config.Hardware.Device) { foreach ($dev in $vm.Config.Hardware.Device) { # Connected ISO if ($dev.GetType().Name -eq "VirtualCdrom") { if ($dev.Backing.GetType().Name -eq "VirtualCdromIsoBackingInfo" -and $dev.Connectable.Connected) { $storageFlags += "Connected ISO" } } # Disks elseif ($dev.GetType().Name -eq "VirtualDisk") { if ($dev.Backing.DiskMode -match "independent") { $storageFlags += "Independent Disk ($($dev.Backing.DiskMode))" } if ($dev.Backing.Sharing -eq "sharingMultiWriter") { $storageFlags += "Multi-Writer VMDK" } if ($dev.Backing.CompatibilityMode -eq "physicalMode") { $storageFlags += "Physical RDM (pRDM)" } } # Network Adapters elseif ($dev.GetType().Name -match "Virtual(?:E1000|E1000e|PCNet32|Vmxnet|Vmxnet2|Vmxnet3)") { if (-not $dev.Connectable.Connected) { $networkFlags += "Disconnected vNIC" } if ($dev.AddressType -eq "Manual") { $networkFlags += "Static MAC Address" } } # vTPM elseif ($dev.GetType().Name -eq "VirtualTPM") { $encryptionFlags += "vTPM Present" } } } if ($storageFlags.Count -gt 0) { $isReady = $false } # --- Compute & NUMA --- $numaFlags = @() if ($maxTargetCores -gt 0 -and $vm.Config.Hardware.NumCPU -gt $maxTargetCores) { $numaFlags += "vCPUs ($($vm.Config.Hardware.NumCPU)) exceeds Target Host Physical Cores ($maxTargetCores)" $isReady = $false } # --- Resource Constraints --- $resourceFlags = @() if ($vm.Config.CpuAllocation.Reservation -gt 0) { $resourceFlags += "Hardcoded CPU Reservation" } if ($vm.Config.MemoryAllocation.Reservation -gt 0) { $resourceFlags += "Hardcoded Memory Reservation" } if ($vm.Parent) { $parentRp = $rpMap[$vm.Parent.Value] if ($parentRp) { if ($parentRp.Config.CpuAllocation.Limit -ne -1 -or $parentRp.Config.MemoryAllocation.Limit -ne -1) { $resourceFlags += "Restricted Resource Pool ($($parentRp.Name))" } } } # --- Networking & Cluster Rules --- if ($vm.Network) { foreach ($netRef in $vm.Network) { if ($vssMap.ContainsKey($netRef.Value)) { $networkFlags += "Standard vSwitch ($($vssMap[$netRef.Value]))" } } } if ($vmDrsRules.ContainsKey($vm.MoRef.Value)) { $networkFlags += "DRS Rules: $($vmDrsRules[$vm.MoRef.Value] -join '; ')" } if ($networkFlags.Count -gt 0) { $isReady = $false } # --- Security & Encryption --- if ($vm.Config.KeyId) { $encryptionFlags += "VM Encrypted" } if ($encryptionFlags.Count -gt 0) { $isReady = $false Write-Warning "VM $($vm.Name) has Encryption or vTPM active. Target vCenter requires a synced Key Provider." } # --- Output --- [PSCustomObject]@{ VMName = $vm.Name HardwareVersion = $hwVersion CBTEnabled = $cbtEnabled ToolsStatus = $toolsStatus HasOrphanedSnaps = $hasOrphanedSnaps StorageBlockers = ($storageFlags -join ' | ') NumaOrCoreViolations = ($numaFlags -join ' | ') ResourceConstraints = ($resourceFlags -join ' | ') NetworkOrRuleFlags = ($networkFlags -join ' | ') EncryptionFlags = ($encryptionFlags -join ' | ') TargetDatastoreHealthy = $targetDsHealthy DRReady = $isReady } } Write-Output $results } catch { Write-Error "Failed to evaluate DR Readiness: $($_.Exception.Message)" throw } } } |