commit 8ec5c02ffede6949679126dc9b2561dad894b528 Author: Kosta Mushkin Date: Fri Oct 31 13:25:37 2025 -0400 initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..deb92f5 --- /dev/null +++ b/README.md @@ -0,0 +1,147 @@ + +# 🧩 Zerto WSFC VPG Ownership Automation + +## Overview +This PowerShell script automates synchronization between **Windows Server Failover Cluster (WSFC)** node ownership and **Zerto Virtual Protection Groups (VPGs)**. + +When WSFC ownership changes, the script automatically: +- **Resumes** replication for the active node’s VPG +- **Pauses** replication for the standby node’s VPG +- Ensures only the current WSFC owner continues replication to maintain CBT consistency + +It supports two detection modes: **Disk-based** and **Group-based** ownership detection. + +--- + +## πŸ–ΌοΈ Architecture Overview + +### Owner: Node 1 +![WSFC Node1 Ownership](./Zerto%20Cluster%20Monitor-Node%201%20-%20Owner.drawio.png) + +- **WSFC Node1** owns the shared disk or owns the cluster group. +- **VPG Node1** is **Active** (replicating). +- **VPG Node2** is **Paused**. +- ZVM Prod and ZVM DR maintain CBT replication across sites. + +### Owner: Node 2 +![WSFC Node2 Ownership](./Zerto%20Cluster%20Monitor-Node%202%20-%20Owner.drawio.png) + +- Ownership of the **WSFC shared disk** transfers to **Node2**. +- The script detects the ownership change. +- **VPG Node2** is resumed; **VPG Node1** is paused. +- Zerto continues replication with no data collision. + +--- + +## βš™οΈ Features +- βœ… Disk-based and Group-based WSFC ownership detection +- βœ… Automatic VPG resume/pause alignment +- βœ… Detailed timestamped logging (`C:\Temp\PwshCompat`) +- βœ… Compatible with PowerShell 5.1+ and PowerShell 7 +- βœ… Safe retry and graceful error handling + +--- + +## πŸ”§ Configuration + +All configuration is centralized at the top of the script: + +```powershell +# --- HARD-CODED CONFIG --- +$ZvmHost = '192.168.222.20' +$ZvmUser = 'admin' +$ZvmPassPlain = 'password' + +$NodeUser = '.\wsfcadm' +$NodePassPlain = 'password' + +$ClusterFqdn = 'TOR-HV01.lab.local' +$WSFCGroupName = 'Cluster Group' +$Node1 = 'TOR-HV01-N1' +$Node2 = 'TOR-HV01-N2' + +# OWNERSHIP DETECTION MODE +# 'Disk' => determine active owner by shared disk ownership (preferred) +# 'Group' => determine active owner by Cluster Group ownership (legacy behavior) +$OwnershipSource = 'Disk' + +# DISK MONITORING CONFIGURATION (used when OwnershipSource = 'Disk') +$SharedDiskUniqueId = '6589CFC000000307DC30980C15CE8818' +$SharedDiskLabel = 'WSFCData1' +$DiskResourceName = 'Cluster Disk 1' + +# Zerto VPG names +$VpgName1 = 'Node1' +$VpgName2 = 'node2' +``` + +--- + +## 🧠 Detection Modes + +### Disk Mode (Preferred) +In this mode, the script determines the active WSFC node by checking: +1. The **Physical Disk Resource Owner** in the cluster (`Get-CimInstance root\MSCluster`). +2. If unavailable, it checks which node has the disk **Online and Read/Write**. + +This is the most reliable method for shared-disk clusters. + +### Group Mode (Legacy) +In this mode, the script determines ownership by checking the **Cluster Group owner** instead of a shared disk. +Useful when the cluster has no shared disks (e.g., cluster-only workloads). + +```powershell +$OwnershipSource = 'Group' +``` + +--- + +## πŸͺ΅ Logging + +Logs are created under: +``` +C:\Temp\PwshCompat\cluster-monitor-YYYYMMDD-HHMMSS.log +``` + +Each log line includes timestamp, component, and log level (`DEBUG`, `INFO`, `WARN`, `ERROR`). + +--- + +## 🧠 How It Works +1. Loads **FailoverClusters** and **Zerto.ZvmLinux.Commandlets** modules. +2. Connects to the ZVM using the provided credentials. +3. Retrieves both VPGs and their current states. +4. Determines the active WSFC owner (via disk or group). +5. Resumes or pauses VPGs to match ownership. +6. Logs all actions with timestamps. + +--- + +## πŸš€ Execution + +Run from a system with network access to both WSFC nodes and ZVM: + +```powershell +.\WSFC-Zerto-Monitor.ps1 +``` + +Administrator privileges are recommended. + +The script should run on a scheduled basis (i.e. every minute) + +--- + +## 🧾 Legal Disclaimer + +This script is provided as an **example only** and is **not supported** under any Zerto support program or service. + +> The author and Zerto disclaim all implied warranties, including merchantability and fitness for a particular purpose. +> In no event shall Zerto or the author be liable for damages arising from the use or inability to use this script. +> Use at your own risk. + +--- + +**Author:** Kosta Mushkin +**Company:** Zerto (HPE) +**Version:** 1.0 +**Date:** October 2025 diff --git a/Zerto Cluster Monitor-Node 1 - Owner.drawio.png b/Zerto Cluster Monitor-Node 1 - Owner.drawio.png new file mode 100644 index 0000000..3307332 Binary files /dev/null and b/Zerto Cluster Monitor-Node 1 - Owner.drawio.png differ diff --git a/Zerto Cluster Monitor-Node 2 - Owner.drawio.png b/Zerto Cluster Monitor-Node 2 - Owner.drawio.png new file mode 100644 index 0000000..c306da1 Binary files /dev/null and b/Zerto Cluster Monitor-Node 2 - Owner.drawio.png differ diff --git a/zerto_wsfc_monitor.ps1 b/zerto_wsfc_monitor.ps1 new file mode 100644 index 0000000..b37d8c2 --- /dev/null +++ b/zerto_wsfc_monitor.ps1 @@ -0,0 +1,456 @@ +# Legal Disclaimer +# This script is an example script and is not supported under any Zerto support program or service. +# The author and Zerto further disclaim all implied warranties including, without limitation, +# any implied warranties of merchantability or of fitness for a particular purpose. +# In no event shall Zerto, its authors or anyone else involved in the creation, +# production or delivery of the scripts be liable for any damages whatsoever (including, +# without limitation, damages for loss of business profits, business interruption, loss of business +# information, or other pecuniary loss) arising out of the use of or the inability to use the sample +# scripts or documentation, even if the author or Zerto has been advised of the possibility of such damages. +# The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. + +# Toggle Zerto VPGs to match WSFC owner. +# Enhanced version with comprehensive logging and disk/group ownership detection +# HARD-CODED VALUES (edit the block below to your env) + +# --- HARD-CODED CONFIG --- +$ZvmHost = 'XXXX' # ZVM hostname or IP address, i.e. 192.168.222.20 +$ZvmUser = 'admin' # ZVM username +$ZvmPassPlain = 'XXXX' # ZVM password + +$NodeUser = 'XXX' # WSFC admin username +$NodePassPlain = 'XXX' # WSFC admin password + +$ClusterFqdn = 'TOR-HV01.lab.local' # WSFC cluster FQDN +$WSFCGroupName = 'Cluster Group' # WSFC cluster group name, i.e. Cluster Group +$Node1 = 'TOR-HV01-N1' # WSFC node 1 FQDN, i.e. TOR-HV01-N1 +$Node2 = 'TOR-HV01-N2' # WSFC node 2 FQDN, i.e. TOR-HV01-N2 + +# OWNERSHIP DETECTION MODE +# 'Disk' => determine active owner by shared disk ownership (preferred) +# 'Group' => determine active owner by Cluster Group ownership (legacy behavior) +$OwnershipSource = 'Disk' # βœ… Set to 'Disk' or 'Group' + +# DISK MONITORING CONFIGURATION (used when OwnershipSource = 'Disk') +$SharedDiskUniqueId = '6589CFC000000307DC30980C15CE8818' # βœ… Your 10GB VMDK UniqueId +$SharedDiskLabel = 'WSFCData1' # Optional helper label +$DiskResourceName = 'Cluster Disk 1' # Optional cluster resource name + +$VpgName1 = 'Node1' # mapped to Node1 +$VpgName2 = 'node2' # mapped to Node2 (lowercase in your env) + +# --- LOGGING CONFIGURATION --- +$LogLevel = 'INFO' # DEBUG, INFO, WARN, ERROR +$LogFile = "C:\Temp\PwshCompat\cluster-monitor-$(Get-Date -Format 'yyyyMMdd-HHmmss').log" + +# --- LOGGING FUNCTIONS --- +function Write-Log { + param( + [string]$Message, + [ValidateSet('DEBUG','INFO','WARN','ERROR')]$Level = 'INFO', + [string]$Component = 'MAIN' + ) + + $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff' + $logEntry = "[$timestamp] [$Level] [$Component] $Message" + + # Write to console with color coding + switch ($Level) { + 'DEBUG' { Write-Host $logEntry -ForegroundColor Gray } + 'INFO' { Write-Host $logEntry -ForegroundColor White } + 'WARN' { Write-Host $logEntry -ForegroundColor Yellow } + 'ERROR' { Write-Host $logEntry -ForegroundColor Red } + } + + # Write to log file + try { + Add-Content -Path $LogFile -Value $logEntry -ErrorAction SilentlyContinue + } catch { + Write-Host "Failed to write to log file: $($_.Exception.Message)" -ForegroundColor Red + } +} + +function Write-SectionHeader { + param([string]$Title) + $separator = "=" * 60 + Write-Log $separator 'INFO' + Write-Log " $Title" 'INFO' + Write-Log $separator 'INFO' +} + +# --- HOUSEKEEPING (keeps WinPS compat/temp out of locked profile paths) --- +Write-SectionHeader "INITIALIZATION" +Write-Log "Creating temporary directory and setting environment variables" 'INFO' 'INIT' +New-Item -ItemType Directory -Path "C:\Temp\PwshCompat" -Force | Out-Null +$env:TEMP = 'C:\Temp\PwshCompat'; $env:TMP = 'C:\Temp\PwshCompat' +Write-Log "Temporary directory created: C:\Temp\PwshCompat" 'DEBUG' 'INIT' + +Write-Log "Log file will be written to: $LogFile" 'INFO' 'INIT' +Write-Log "Ownership detection mode: $OwnershipSource" 'INFO' 'INIT' + +# --- MODULES --- +Write-SectionHeader "MODULE LOADING" +try { + Write-Log "Loading FailoverClusters module..." 'INFO' 'MODULES' + Import-Module FailoverClusters -SkipEditionCheck -ErrorAction Stop + Write-Log "FailoverClusters module loaded successfully" 'INFO' 'MODULES' +} catch { + Write-Log "Failed to load FailoverClusters module: $($_.Exception.Message)" 'ERROR' 'MODULES' + throw +} + +try { + Write-Log "Loading Zerto.ZvmLinux.Commandlets module..." 'INFO' 'MODULES' + Import-Module Zerto.ZvmLinux.Commandlets -Force -ErrorAction Stop + Write-Log "Zerto module loaded successfully" 'INFO' 'MODULES' +} catch { + Write-Log "Failed to load Zerto module: $($_.Exception.Message)" 'ERROR' 'MODULES' + throw +} + +try { + Write-Log "Disabling SSL certificate validation for ZVM connection..." 'INFO' 'MODULES' + Remove-ZvmSslCheck + Write-Log "SSL certificate validation disabled" 'INFO' 'MODULES' +} catch { + Write-Log "Failed to disable SSL check: $($_.Exception.Message)" 'WARN' 'MODULES' +} + +# --- CREDENTIALS (from hard-coded strings) --- +Write-SectionHeader "CREDENTIAL SETUP" +Write-Log "Creating credentials for ZVM and cluster nodes..." 'INFO' 'CREDS' +try { + $ZvmCredential = New-Object pscredential($ZvmUser, (ConvertTo-SecureString $ZvmPassPlain -AsPlainText -Force)) + Write-Log "ZVM credentials created for user: $ZvmUser" 'DEBUG' 'CREDS' + + $NodeCredential = New-Object pscredential($NodeUser, (ConvertTo-SecureString $NodePassPlain -AsPlainText -Force)) + Write-Log "Node credentials created for user: $NodeUser" 'DEBUG' 'CREDS' + + Write-Log "All credentials created successfully" 'INFO' 'CREDS' +} catch { + Write-Log "Failed to create credentials: $($_.Exception.Message)" 'ERROR' 'CREDS' + throw +} + +# --- CONNECT TO ZVM --- +Write-SectionHeader "ZVM CONNECTION" +Write-Log "Attempting to connect to ZVM at: $ZvmHost" 'INFO' 'ZVM' +try { + Connect-ZVM -HostName $ZvmHost -Credential $ZvmCredential | Out-Null + Write-Log "Successfully connected to ZVM at $ZvmHost" 'INFO' 'ZVM' +} catch { + Write-Log "Failed to connect to ZVM: $($_.Exception.Message)" 'ERROR' 'ZVM' + throw +} + +# --- LOOK UP VPGs & IDs --- +Write-SectionHeader "VPG DISCOVERY" +Write-Log "Retrieving VPG information from ZVM..." 'INFO' 'VPG' + +try { + Write-Log "Looking up VPG: $VpgName1" 'DEBUG' 'VPG' + $vpg1 = Get-ZvmVpg -VpgName $VpgName1 | Select-Object VpgName,Status,SubStatus,Link + if ($vpg1) { + Write-Log "Found VPG1: $($vpg1.VpgName) - Status: $($vpg1.Status) - SubStatus: $($vpg1.SubStatus)" 'INFO' 'VPG' + } else { + Write-Log "VPG1 not found: $VpgName1" 'ERROR' 'VPG' + throw "VPG '$VpgName1' not found" + } + + Write-Log "Looking up VPG: $VpgName2" 'DEBUG' 'VPG' + $vpg2 = Get-ZvmVpg -VpgName $VpgName2 | Select-Object VpgName,Status,SubStatus,Link + if ($vpg2) { + Write-Log "Found VPG2: $($vpg2.VpgName) - Status: $($vpg2.Status) - SubStatus: $($vpg2.SubStatus)" 'INFO' 'VPG' + } else { + Write-Log "VPG2 not found: $VpgName2" 'ERROR' 'VPG' + throw "VPG '$VpgName2' not found" + } + + $vpgid1 = $vpg1.Link.Identifier + $vpgid2 = $vpg2.Link.Identifier + Write-Log "VPG IDs retrieved - VPG1: $vpgid1, VPG2: $vpgid2" 'DEBUG' 'VPG' + +} catch { + Write-Log "Failed to retrieve VPG information: $($_.Exception.Message)" 'ERROR' 'VPG' + throw +} + +Write-Log "VPG Summary:" 'INFO' 'VPG' +Write-Log " Node1=$Node1 VPG=$($vpg1.VpgName) Id=$vpgid1 SubStatus=$($vpg1.SubStatus)" 'INFO' 'VPG' +Write-Log " Node2=$Node2 VPG=$($vpg2.VpgName) Id=$vpgid2 SubStatus=$($vpg2.SubStatus)" 'INFO' 'VPG' + +# --- OWNERSHIP DETECTION FUNCTIONS --- +Write-SectionHeader "OWNERSHIP DETECTION FUNCTIONS" + +function Get-ClusterDiskOwnerByResource { + param( + [string]$ResourceName, + [pscredential]$Cred, + [string]$ProbeNode1, + [string]$ProbeNode2 + ) + + Write-Log "Attempting to find disk owner by cluster resource: $ResourceName" 'DEBUG' 'DISK' + $opt = New-CimSessionOption -Protocol Dcom + + foreach ($n in @($ProbeNode1,$ProbeNode2)) { + try { + Write-Log "Checking cluster resources on node: $n" 'DEBUG' 'DISK' + $s = New-CimSession -ComputerName $n -Credential $Cred -SessionOption $opt -ErrorAction Stop + $r = Get-CimInstance -Namespace root\MSCluster -Class MSCluster_Resource -CimSession $s ` + -Filter "Type='Physical Disk'" + Remove-CimSession $s + + if ($r) { + $match = if ($ResourceName) { + $r | Where-Object { $_.Name -eq $ResourceName -or $_.Name -like "*$ResourceName*" } + } else { + $r | Select-Object -First 1 + } + if ($match -and $match.OwnerNode) { + Write-Log "Found disk owner via cluster resource: $($match.OwnerNode)" 'INFO' 'DISK' + return $match.OwnerNode + } + } + } catch { + Write-Log "Failed to check cluster resources on $n : $($_.Exception.Message)" 'DEBUG' 'DISK' + } + } + Write-Log "No disk owner found via cluster resource method" 'WARN' 'DISK' + return $null +} + +function Get-ActiveClusterNodeCim { + param([string]$PreferredNode,[string]$FallbackNode,[string]$GroupName,[pscredential]$Cred) + + Write-Log "Attempting CIM connection to determine cluster group owner..." 'DEBUG' 'GROUP' + $opt = New-CimSessionOption -Protocol Dcom + + foreach ($n in @($PreferredNode,$FallbackNode)) { + try { + Write-Log "Trying CIM connection to node: $n" 'DEBUG' 'GROUP' + $sess = New-CimSession -ComputerName $n -Credential $Cred -SessionOption $opt -ErrorAction Stop + Write-Log "CIM session established to $n" 'DEBUG' 'GROUP' + + $rg = Get-CimInstance -Namespace root\MSCluster -Class MSCluster_ResourceGroup -CimSession $sess -Filter "Name='$GroupName'" + Remove-CimSession $sess + + if ($rg -and $rg.OwnerNode) { + Write-Log "Cluster group owner determined via CIM: $($rg.OwnerNode)" 'INFO' 'GROUP' + return $rg.OwnerNode + } + } catch { + Write-Log "CIM connection failed to $n : $($_.Exception.Message)" 'DEBUG' 'GROUP' + } + } + Write-Log "CIM method failed to determine cluster group owner" 'WARN' 'GROUP' + return $null +} + +function Get-ActiveOwner { + param( + [ValidateSet('Disk','Group')]$Mode, + [string]$GroupName, + [pscredential]$Cred, + [string]$Node1,[string]$Node2, + [string]$DiskResourceName, + [string]$DiskUniqueId, + [string]$DiskLabel + ) + + Write-Log "Determining active owner using mode: $Mode" 'INFO' 'OWNER' + + if ($Mode -eq 'Disk') { + Write-Log "Using disk-based ownership detection" 'INFO' 'OWNER' + + # 1) Prefer cluster Physical Disk resource owner (if present) + Write-Log "Step 1: Checking cluster Physical Disk resource owner..." 'DEBUG' 'OWNER' + $owner = Get-ClusterDiskOwnerByResource -ResourceName $DiskResourceName ` + -Cred $Cred -ProbeNode1 $Node1 -ProbeNode2 $Node2 + if ($owner) { + Write-Log "Active owner determined via cluster disk resource: $owner" 'INFO' 'OWNER' + return $owner + } + + Write-Log "Disk-based detection failed, falling back to group-based detection" 'WARN' 'OWNER' + } + + # Group owner detection (CIM first, then WinRM fallback) + Write-Log "Using group-based ownership detection" 'INFO' 'OWNER' + + # Group owner (CIM) + $owner = Get-ActiveClusterNodeCim -PreferredNode $Node1 -FallbackNode $Node2 -GroupName $GroupName -Cred $Cred + if ($owner) { + Write-Log "Active owner determined via CIM group detection: $owner" 'INFO' 'OWNER' + return $owner + } + + # Group owner (WinRM fallback) + Write-Log "CIM method failed, trying WinRM fallback..." 'INFO' 'OWNER' + foreach ($n in @($Node1,$Node2)) { + try { + Write-Log "Attempting WinRM connection to: $n.lab.local" 'DEBUG' 'OWNER' + $owner = Invoke-Command -ComputerName "$n.lab.local" -Authentication Negotiate -Credential $Cred -ScriptBlock { + Import-Module FailoverClusters + (Get-ClusterGroup -Name $using:GroupName).OwnerNode.Name + } -ErrorAction Stop + + if ($owner) { + Write-Log "Active owner determined via WinRM group detection: $owner" 'INFO' 'OWNER' + return $owner + } + } catch { + Write-Log "WinRM connection failed to $n : $($_.Exception.Message)" 'DEBUG' 'OWNER' + } + } + + Write-Log "All ownership detection methods failed" 'ERROR' 'OWNER' + return $null +} + +# --- DETERMINE ACTIVE OWNER --- +Write-SectionHeader "ACTIVE OWNER DETERMINATION" +Write-Log "Ownership detection mode: $OwnershipSource" 'INFO' 'CLUSTER' + +$activeNode = Get-ActiveOwner -Mode $OwnershipSource ` + -GroupName $WSFCGroupName ` + -Cred $NodeCredential ` + -Node1 $Node1 -Node2 $Node2 ` + -DiskResourceName $DiskResourceName ` + -DiskUniqueId $SharedDiskUniqueId ` + -DiskLabel $SharedDiskLabel + +if (-not $activeNode) { + Write-Log "Unable to determine active owner using mode '$OwnershipSource'" 'ERROR' 'CLUSTER' + throw "Unable to determine active owner using mode '$OwnershipSource'." +} + +Write-Log "Active owner determined (mode=$OwnershipSource): $activeNode" 'INFO' 'CLUSTER' + +# --- VPG STATE ASSESSMENT --- +Write-SectionHeader "VPG STATE ASSESSMENT" +$desiredOwner = if ($activeNode -eq $Node1) { $VpgName1 } elseif ($activeNode -eq $Node2) { $VpgName2 } else { $null } +Write-Log "Owner '$activeNode' maps to VPG '$desiredOwner'" 'INFO' 'ASSESSMENT' + +Write-Log "Current VPG states:" 'INFO' 'ASSESSMENT' +Write-Log " VPG1 ($VpgName1): Status=$($vpg1.Status), SubStatus=$($vpg1.SubStatus)" 'INFO' 'ASSESSMENT' +Write-Log " VPG2 ($VpgName2): Status=$($vpg2.Status), SubStatus=$($vpg2.SubStatus)" 'INFO' 'ASSESSMENT' + +# Desired states: +# - If owner = Node1 => VPG1 should be active (NOT paused), VPG2 should be paused +# - If owner = Node2 => VPG2 should be active (NOT paused), VPG1 should be paused +$needResume1 = ($activeNode -eq $Node1) -and ($vpg1.SubStatus -eq 'ReplicationPausedUserInitiated') +$needPause1 = ($activeNode -eq $Node2) -and ($vpg1.SubStatus -ne 'ReplicationPausedUserInitiated') +$needResume2 = ($activeNode -eq $Node2) -and ($vpg2.SubStatus -eq 'ReplicationPausedUserInitiated') +$needPause2 = ($activeNode -eq $Node1) -and ($vpg2.SubStatus -ne 'ReplicationPausedUserInitiated') + +Write-Log "Required actions analysis:" 'INFO' 'ASSESSMENT' +Write-Log " VPG1 Resume needed: $needResume1" 'DEBUG' 'ASSESSMENT' +Write-Log " VPG1 Pause needed: $needPause1" 'DEBUG' 'ASSESSMENT' +Write-Log " VPG2 Resume needed: $needResume2" 'DEBUG' 'ASSESSMENT' +Write-Log " VPG2 Pause needed: $needPause2" 'DEBUG' 'ASSESSMENT' + +if (-not ($needResume1 -or $needPause1 -or $needResume2 -or $needPause2)) { + Write-Log "βœ… Owner matches active VPG; no change is needed." 'INFO' 'ASSESSMENT' +} else { + Write-Log "πŸ”§ Adjustments required to match owner:" 'INFO' 'ASSESSMENT' + if ($needResume1) { Write-Log " - Resume $($vpg1.VpgName)" 'INFO' 'ASSESSMENT' } + if ($needPause1) { Write-Log " - Pause $($vpg1.VpgName)" 'INFO' 'ASSESSMENT' } + if ($needResume2) { Write-Log " - Resume $($vpg2.VpgName)" 'INFO' 'ASSESSMENT' } + if ($needPause2) { Write-Log " - Pause $($vpg2.VpgName)" 'INFO' 'ASSESSMENT' } +} + +# --- ACTIONS (idempotent) --- +Write-SectionHeader "VPG ACTIONS" + +# VPG1 Actions +if ($needResume1) { + Write-Log "Resuming $($vpg1.VpgName)..." 'INFO' 'ACTION' + try { + Start-ZvmVpgResume -VpgId $vpgid1 + Write-Log "Successfully initiated resume for $($vpg1.VpgName)" 'INFO' 'ACTION' + + Write-Log "Waiting 10 seconds before force sync..." 'INFO' 'ACTION' + Start-Sleep 10 + + Write-Log "Initiating force sync for $($vpg1.VpgName)..." 'INFO' 'ACTION' + Start-ZvmVpgForceSync -VpgId $vpgid1 + Write-Log "Successfully initiated force sync for $($vpg1.VpgName)" 'INFO' 'ACTION' + } catch { + Write-Log "Failed to resume/sync $($vpg1.VpgName): $($_.Exception.Message)" 'ERROR' 'ACTION' + } +} elseif ($activeNode -eq $Node1) { + Write-Log "$($vpg1.VpgName) already replicating (no action needed)" 'INFO' 'ACTION' +} + +if ($needPause1) { + Write-Log "Pausing $($vpg1.VpgName)..." 'INFO' 'ACTION' + try { + Start-ZvmVpgPause -VpgId $vpgid1 + Write-Log "Successfully initiated pause for $($vpg1.VpgName)" 'INFO' 'ACTION' + } catch { + Write-Log "Failed to pause $($vpg1.VpgName): $($_.Exception.Message)" 'ERROR' 'ACTION' + } +} elseif ($activeNode -eq $Node2) { + Write-Log "$($vpg1.VpgName) already paused (no action needed)" 'INFO' 'ACTION' +} + +# VPG2 Actions +if ($needResume2) { + Write-Log "Resuming $($vpg2.VpgName)..." 'INFO' 'ACTION' + try { + Start-ZvmVpgResume -VpgId $vpgid2 + Write-Log "Successfully initiated resume for $($vpg2.VpgName)" 'INFO' 'ACTION' + + Write-Log "Waiting 10 seconds before force sync..." 'INFO' 'ACTION' + Start-Sleep 10 + + Write-Log "Initiating force sync for $($vpg2.VpgName)..." 'INFO' 'ACTION' + Start-ZvmVpgForceSync -VpgId $vpgid2 + Write-Log "Successfully initiated force sync for $($vpg2.VpgName)" 'INFO' 'ACTION' + } catch { + Write-Log "Failed to resume/sync $($vpg2.VpgName): $($_.Exception.Message)" 'ERROR' 'ACTION' + } +} elseif ($activeNode -eq $Node2) { + Write-Log "$($vpg2.VpgName) already replicating (no action needed)" 'INFO' 'ACTION' +} + +if ($needPause2) { + Write-Log "Pausing $($vpg2.VpgName)..." 'INFO' 'ACTION' + try { + Start-ZvmVpgPause -VpgId $vpgid2 + Write-Log "Successfully initiated pause for $($vpg2.VpgName)" 'INFO' 'ACTION' + } catch { + Write-Log "Failed to pause $($vpg2.VpgName): $($_.Exception.Message)" 'ERROR' 'ACTION' + } +} elseif ($activeNode -eq $Node1) { + Write-Log "$($vpg2.VpgName) already paused (no action needed)" 'INFO' 'ACTION' +} + +Write-Log "Waiting 10 seconds before final status check..." 'INFO' 'ACTION' +Start-Sleep 10 + +# --- FINAL STATUS --- +Write-SectionHeader "FINAL STATUS" +Write-Log "Retrieving final VPG status..." 'INFO' 'STATUS' + +try { + $finalVpg1 = Get-ZvmVpg -VpgName $VpgName1 | Select-Object VpgName,Status,SubStatus + $finalVpg2 = Get-ZvmVpg -VpgName $VpgName2 | Select-Object VpgName,Status,SubStatus + + Write-Log "Final VPG Status:" 'INFO' 'STATUS' + Write-Log " VPG1 ($($finalVpg1.VpgName)): Status=$($finalVpg1.Status), SubStatus=$($finalVpg1.SubStatus)" 'INFO' 'STATUS' + Write-Log " VPG2 ($($finalVpg2.VpgName)): Status=$($finalVpg2.Status), SubStatus=$($finalVpg2.SubStatus)" 'INFO' 'STATUS' + + Write-Log "Detailed VPG Status Tables:" 'INFO' 'STATUS' + $finalVpg1 | Format-Table VpgName,Status,SubStatus -Auto + $finalVpg2 | Format-Table VpgName,Status,SubStatus -Auto + +} catch { + Write-Log "Failed to retrieve final VPG status: $($_.Exception.Message)" 'ERROR' 'STATUS' +} + +Write-SectionHeader "SCRIPT COMPLETION" +Write-Log "Cluster monitoring script completed successfully" 'INFO' 'COMPLETE' +Write-Log "Ownership detection mode used: $OwnershipSource" 'INFO' 'COMPLETE' +Write-Log "Log file saved to: $LogFile" 'INFO' 'COMPLETE'