-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathInitialize-ProtectionGroups.ps1
More file actions
181 lines (160 loc) · 9.78 KB
/
Copy pathInitialize-ProtectionGroups.ps1
File metadata and controls
181 lines (160 loc) · 9.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
#Requires -Version 7
param(
[switch]$WhatIf, # Show what would be created without making changes
[switch]$Prune, # Remove PG volume members whose volumes no longer correspond to any current cluster node
[switch]$Force # Skip the typed-token confirmation prompt for -Prune (destructive)
)
. "$PSScriptRoot/Config.ps1"
###############################################################################################################################
# Initialize Protection Groups - One-time setup for Pure Storage FlashArray PG-based snapshots
#
# Connects to the FA gateway, discovers current cluster nodes from Ops Manager (with .env fallback),
# resolves the FlashArray volume backing /data/mongo on each node via SCSI serial, creates a
# Protection Group named $ProtectionGroupName on each array, and adds each volume as a member.
# Safe to re-run - skips creation if the PG or membership already exists.
#
# Use -Prune to remove PG volume members that no longer correspond to any discovered cluster node.
# This is useful after scaling down the cluster or replacing a node's volume.
#
# Run this ONCE before using New-MongoSnapshot.ps1 with protection group snapshots.
# Re-run after adding nodes or replacing data volumes.
#
# Usage:
# pwsh ./Initialize-ProtectionGroups.ps1
# pwsh ./Initialize-ProtectionGroups.ps1 -WhatIf # dry run
# pwsh ./Initialize-ProtectionGroups.ps1 -Prune # remove orphaned PG members (typed confirmation required)
# pwsh ./Initialize-ProtectionGroups.ps1 -Prune -WhatIf # dry run with prune preview
# pwsh ./Initialize-ProtectionGroups.ps1 -Prune -Force # skip the typed confirmation (use only in automation)
###############################################################################################################################
Import-Module PureStoragePowerShellSDK2 -ErrorAction Stop
$ErrorActionPreference = 'Stop'
if ($WhatIf) {
Write-Host "`n[WhatIf] No changes will be made." -ForegroundColor DarkYellow
}
Write-Host "`n=== Initialize Protection Groups ===" -ForegroundColor Yellow
Write-Host " Protection group : $ProtectionGroupName" -ForegroundColor Cyan
Write-Host " Gateway : $FaEndpoint" -ForegroundColor Cyan
# Connect to the gateway. All FlashArray calls use this session with -ContextName.
$FA = Connect-Pfa2Array -EndPoint $FaEndpoint -Credential $FaCred -IgnoreCertificateError -ErrorAction Stop
Write-Host " Connected to gateway." -ForegroundColor Green
# Discover which fleet arrays have (or should have) the protection group.
# Use Get-Pfa2FleetMember to enumerate all fleet members from the gateway (matches snapshot script).
Write-Host " Enumerating fleet arrays..." -ForegroundColor Cyan
$FleetMembers = Get-Pfa2FleetMember -Array $FA -ErrorAction Stop
$AllArrays = @($FleetMembers | ForEach-Object { $_.Member } | Where-Object { $_.Name } | Select-Object -ExpandProperty Name | Select-Object -Unique)
Write-Host " Fleet arrays ($($AllArrays.Count)): $($AllArrays -join ', ')" -ForegroundColor Cyan
# Discover current cluster nodes (Ops Manager first, .env fallback).
Write-Host " Discovering cluster nodes..." -ForegroundColor Cyan
$ClusterNodes = Get-ClusterNodes
# Discover which FA volume backs /data/mongo on each node via SCSI serial.
# We need all fleet context names for the serial lookup; use all array names from the fleet.
$AllContextNames = @($AllArrays)
Write-Host " Discovering node-to-volume mappings via SCSI serial..." -ForegroundColor Cyan
$NodeVolumeMap = Resolve-NodeToArrayVolumeMap -FA $FA -Nodes $ClusterNodes `
-SshUserParam $SshUser -SshOptsParam $SshOpts -ContextNames $AllContextNames
$Errors = [System.Collections.Generic.List[string]]::new()
foreach ($NodeEntry in $NodeVolumeMap.GetEnumerator()) {
$Node = $NodeEntry.Key
$ShortName = $NodeEntry.Value.ShortName
$VolumeName = $NodeEntry.Value.VolumeName
Write-Host "`n [$Node -> $ShortName / $VolumeName]" -ForegroundColor White
try {
# Create the protection group if it does not exist on this array
$ExistingPg = Get-Pfa2ProtectionGroup -Array $FA -ContextName @($ShortName) -Name $ProtectionGroupName -ErrorAction SilentlyContinue
if ($ExistingPg) {
Write-Host " PG '$ProtectionGroupName' already exists - skipping creation" -ForegroundColor DarkGray
} else {
if ($WhatIf) {
Write-Host " [WhatIf] Would create PG '$ProtectionGroupName'" -ForegroundColor DarkYellow
} else {
$null = New-Pfa2ProtectionGroup -Array $FA -ContextName @($ShortName) -Name $ProtectionGroupName -ErrorAction Stop
Write-Host " Created PG '$ProtectionGroupName'" -ForegroundColor Green
}
}
# Add volume as a member of the protection group if not already a member
$ExistingMember = Get-Pfa2ProtectionGroupVolume -Array $FA -ContextName @($ShortName) -GroupName $ProtectionGroupName -MemberName $VolumeName -ErrorAction SilentlyContinue
if ($ExistingMember) {
Write-Host " Volume '$VolumeName' is already a member - skipping" -ForegroundColor DarkGray
} else {
if ($WhatIf) {
Write-Host " [WhatIf] Would add volume '$VolumeName' to PG '$ProtectionGroupName'" -ForegroundColor DarkYellow
} else {
$null = New-Pfa2ProtectionGroupVolume -Array $FA -ContextName @($ShortName) -GroupName $ProtectionGroupName -MemberName $VolumeName -ErrorAction Stop
Write-Host " Added '$VolumeName' to PG '$ProtectionGroupName'" -ForegroundColor Green
}
}
# Verify final state
if (-not $WhatIf) {
$Members = Get-Pfa2ProtectionGroupVolume -Array $FA -ContextName @($ShortName) -GroupName $ProtectionGroupName -ErrorAction SilentlyContinue
$MemberNames = $Members | ForEach-Object { $_.Member.Name }
Write-Host " PG members: $($MemberNames -join ', ')" -ForegroundColor Cyan
}
} catch {
$Errors.Add("$Node ($ShortName): $($_.Exception.Message)")
Write-Host " ERROR: $($_.Exception.Message)" -ForegroundColor Red
}
}
# Prune orphaned PG volume members: any volume currently in the PG that does not appear in
# the discovered $NodeVolumeMap should be removed. This handles node replacements and scale-down.
# Two-phase to support a typed-token confirmation: first enumerate all planned removals across
# the fleet, then (unless -WhatIf or -Force) require the operator to type the PG name.
if ($Prune) {
Write-Host "`n=== Pruning orphaned PG members ===" -ForegroundColor Yellow
$DiscoveredVolumeNames = @($NodeVolumeMap.Values | ForEach-Object { $_.VolumeName })
$PruneTargets = [System.Collections.Generic.List[object]]::new()
foreach ($Arr in $AllArrays) {
$Pg = Get-Pfa2ProtectionGroup -Array $FA -ContextName @($Arr) -Name $ProtectionGroupName -ErrorAction SilentlyContinue
if (-not $Pg) { continue }
$Members = @(Get-Pfa2ProtectionGroupVolume -Array $FA -ContextName @($Arr) -GroupName $ProtectionGroupName -ErrorAction SilentlyContinue)
foreach ($Member in $Members) {
$MemberVolumeName = $Member.Member.Name
if ($MemberVolumeName -notin $DiscoveredVolumeNames) {
$PruneTargets.Add([pscustomobject]@{ Array = $Arr; Volume = $MemberVolumeName })
}
}
}
if ($PruneTargets.Count -eq 0) {
Write-Host " No orphaned members found - nothing to prune." -ForegroundColor Green
} else {
Write-Host " Planned removals ($($PruneTargets.Count)):" -ForegroundColor Cyan
foreach ($T in $PruneTargets) {
Write-Host " $($T.Array): $($T.Volume)" -ForegroundColor White
}
# Typed-token confirmation: irreversible at the PG level (the volumes are not destroyed,
# but the PG snapshot of those volumes will no longer be coordinated). Require the
# operator to retype the PG name unless -WhatIf or -Force is set.
$Proceed = $WhatIf -or $Force
if (-not $Proceed) {
Write-Host "`n This will remove $($PruneTargets.Count) member(s) from PG '$ProtectionGroupName'." -ForegroundColor Yellow
Write-Host " Future PG snapshots will NOT include those volumes." -ForegroundColor Yellow
$Token = (Read-Host " Type the PG name '$ProtectionGroupName' to confirm prune").Trim()
if ($Token -ne $ProtectionGroupName) {
throw "Confirmation token did not match '$ProtectionGroupName'. Aborting prune."
}
$Proceed = $true
}
foreach ($T in $PruneTargets) {
if ($WhatIf) {
Write-Host " [WhatIf] Would remove orphaned member '$($T.Volume)' from PG '$ProtectionGroupName' on $($T.Array)" -ForegroundColor DarkYellow
} else {
try {
$null = Remove-Pfa2ProtectionGroupVolume -Array $FA -ContextName @($T.Array) -GroupName $ProtectionGroupName -MemberName $T.Volume -ErrorAction Stop
Write-Host " Removed orphaned member '$($T.Volume)' from $($T.Array)" -ForegroundColor Green
} catch {
$Errors.Add("prune $($T.Volume) on $($T.Array): $($_.Exception.Message)")
Write-Host " ERROR pruning '$($T.Volume)': $($_.Exception.Message)" -ForegroundColor Red
}
}
}
}
}
if ($Errors.Count -gt 0) {
throw "Initialization failed on $($Errors.Count) item(s):`n$($Errors -join "`n")"
}
Write-Host "`n=== Initialization Complete ===" -ForegroundColor Green
if ($WhatIf) {
Write-Host " WhatIf mode - no changes were made." -ForegroundColor DarkYellow
} else {
Write-Host " Protection group '$ProtectionGroupName' is ready on all arrays." -ForegroundColor Green
Write-Host " You can now run New-MongoSnapshot.ps1." -ForegroundColor White
}