initial commit
This commit is contained in:
@@ -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** 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
|
||||||
|

|
||||||
|
|
||||||
|
- 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
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 131 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 131 KiB |
@@ -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'
|
||||||
Reference in New Issue
Block a user