|
| 1 | +############################################################################################################################## |
| 2 | +# Point In Time Recovery - Using SQL Server 2022's T-SQL Snapshot Backup feature w. VMFS/VMDK datastore/files. |
| 3 | +# |
| 4 | +# Scenario: |
| 5 | +# Perform a point in time restore using SQL Server 2022's T-SQL Snapshot Backup |
| 6 | +# feature. This uses a FlashArray snapshot as the base of the restore, then restores |
| 7 | +# a log backup. |
| 8 | +# |
| 9 | +# IMPORTANT NOTE: |
| 10 | +# This example script is built for 1 database spanned across two VMDK files/volumes |
| 11 | +# from a single datastore. |
| 12 | +# |
| 13 | +# The granularity or unit of work for this workflow is a VMDK file(s) and the entirety |
| 14 | +# of its contents. Therefore, everything in the VMDK file(s) including files for other |
| 15 | +# databases will be impacted/overwritten. |
| 16 | +# |
| 17 | +# This example will need to be adapted if you wish to support multiple databases on |
| 18 | +# the same set of VMDK(s). |
| 19 | +# |
| 20 | +# Prerequisites: |
| 21 | +# 1. PowerShell Modules: dbatools & PureStoragePowerShellSDK2 |
| 22 | +# |
| 23 | +# Usage Notes: |
| 24 | +# * Each section of the script is meant to be run individually, one after another. |
| 25 | +# * The script is NOT meant to be executed all at once. |
| 26 | +# |
| 27 | +# Disclaimer: |
| 28 | +# This example script is provided AS-IS and is meant to be a building |
| 29 | +# block to be adapted to fit an individual organization's |
| 30 | +# infrastructure. |
| 31 | +############################################################################################################################## |
| 32 | + |
| 33 | + |
| 34 | + |
| 35 | +# Import powershell modules |
| 36 | +Import-Module dbatools |
| 37 | +Import-Module PureStoragePowerShellSDK2 |
| 38 | + |
| 39 | + |
| 40 | + |
| 41 | +# Declare all variables |
| 42 | +# VMware variables |
| 43 | +$VIServerName = 'vcenter.example.com' |
| 44 | +$SourceDatastoreName = 'source_sql_datastore' |
| 45 | +$SourceVMDKPaths = @('source_vm/sqldata.vmdk','source_vm/sqllog.vmdk') |
| 46 | + |
| 47 | + |
| 48 | + |
| 49 | +# FlashArray variables |
| 50 | +$ArrayName = 'flasharray1.example.com' # FlashArray FQDN |
| 51 | +$FAHostGroupName = 'FAHostGroupName' # HostGroup Name on FlashArray for the ESXi cluster |
| 52 | +$SourceVolumeName = 'volume_name' # Volume name on FlashArray containing database files |
| 53 | +$PGroupName = 'protection_group' # Name of the Protection Group on FlashArray1 |
| 54 | + |
| 55 | + |
| 56 | + |
| 57 | +# Windows/SQL Server variables |
| 58 | +$TargetSQLServer = 'target_sqlserver.example.com' # SQL Server Instance FQDN |
| 59 | +$TargetVM = 'target_sqlserver' # SQL Server VM name in VCenter |
| 60 | +$DbName = 'AdventureWorks' # Name of database |
| 61 | +$BackupShare = '\\flashblade1.example.com\backups' # File system location to write the backup metadata file |
| 62 | +$TargetDisks = @('1234c29689bc0888d32dcd2919a67z89', '1234c299721c4ba4a937552fb298a76') # The serial numbers of the Windows volume containing database files; use get-disk |
| 63 | + |
| 64 | + |
| 65 | + |
| 66 | +# Build a PowerShell Remoting Session to the Server |
| 67 | +$SqlServerSession = New-PSSession -ComputerName $TargetSQLServer |
| 68 | + |
| 69 | + |
| 70 | + |
| 71 | +# Build a persistent SMO connection |
| 72 | +$SqlInstance = Connect-DbaInstance -SqlInstance $TargetSQLServer -TrustServerCertificate -NonPooledConnection |
| 73 | + |
| 74 | + |
| 75 | + |
| 76 | +# Let's get some information about our database, take note of the size |
| 77 | +Get-DbaDatabase -SqlInstance $SqlInstance -Database $DbName | |
| 78 | + Select-Object Name, SizeMB |
| 79 | + |
| 80 | + |
| 81 | + |
| 82 | +# Connect to the FlashArray's REST API |
| 83 | +$Credential = Get-Credential -UserName "$env:USERNAME" -Message 'Enter your credential information...' |
| 84 | +$FlashArray = Connect-Pfa2Array –EndPoint $ArrayName -Credential $Credential -IgnoreCertificateError |
| 85 | + |
| 86 | + |
| 87 | + |
| 88 | +#### |
| 89 | +# Execute our backup |
| 90 | + |
| 91 | +# Freeze the database |
| 92 | +$Query = "ALTER DATABASE $DbName SET SUSPEND_FOR_SNAPSHOT_BACKUP = ON" |
| 93 | +Invoke-DbaQuery -SqlInstance $SqlInstance -Query $Query -Verbose |
| 94 | + |
| 95 | + |
| 96 | + |
| 97 | +# Take a snapshot of the Protection Group while the database is frozen |
| 98 | +$Snapshot = New-Pfa2ProtectionGroupSnapshot -Array $FlashArray -SourceName $PGroupName |
| 99 | + |
| 100 | + |
| 101 | + |
| 102 | +# Take a metadata backup of the database, this will automatically unfreeze |
| 103 | +# if successful |
| 104 | +# We'll use MEDIADESCRIPTION to hold some information about our snapshot and |
| 105 | +# the flasharray its held on |
| 106 | +$BackupFile = "$BackupShare\$DbName-$(Get-Date -Format FileDateTime).bkm" |
| 107 | + |
| 108 | +$Query = "BACKUP DATABASE $DbName |
| 109 | + TO DISK='$BackupFile' |
| 110 | + WITH METADATA_ONLY, MEDIADESCRIPTION='$($Snapshot.Name)|$($FlashArray.ArrayName)'" |
| 111 | +Invoke-DbaQuery -SqlInstance $SqlInstance -Query $Query -Verbose |
| 112 | + |
| 113 | +### |
| 114 | +# Backup completed |
| 115 | + |
| 116 | + |
| 117 | + |
| 118 | +### |
| 119 | +# Backup Verification |
| 120 | + |
| 121 | +# Let's check out the error log to see what SQL Server thinks happened |
| 122 | +Get-DbaErrorLog -SqlInstance $SqlInstance -LogNumber 0 -After (Get-Date).AddMinutes(-15) | Format-Table |
| 123 | + |
| 124 | + |
| 125 | + |
| 126 | +# The backup is recorded in MSDB as a Full backup with snapshot |
| 127 | +$BackupHistory = Get-DbaDbBackupHistory -SqlInstance $SqlInstance -Database $DbName -Last |
| 128 | +$BackupHistory |
| 129 | + |
| 130 | + |
| 131 | + |
| 132 | +# Let's explore the stuff in the backup header... |
| 133 | +# Remember, VDI is just a contract saying what's in the backup matches what SQL Server thinks is in the backup. |
| 134 | +Read-DbaBackupHeader -SqlInstance $SqlInstance -Path $BackupFile |
| 135 | + |
| 136 | + |
| 137 | + |
| 138 | +### |
| 139 | +# Take a Transaction Log backup |
| 140 | +# |
| 141 | +# NOTE: If you are testing this with a database in SIMPLE RECOVERY, there seems to be an occasional bug in |
| 142 | +# Backup-DbaDatabase that keeps a DataReader connection open. Subsequent dbatools cmdlet steps may fail. |
| 143 | +# Skip this step if your database in SIMPLE RECOVERY. |
| 144 | +$LogBackup = Backup-DbaDatabase -SqlInstance $SqlInstance -Database $DbName -Type Log -Path $BackupShare -CompressBackup |
| 145 | + |
| 146 | + |
| 147 | + |
| 148 | +### |
| 149 | +# DEMO - Delete a table |
| 150 | +Invoke-DbaQuery -SqlInstance $SqlInstance -Database $DbName -Query "SELECT TOP 10 * FROM Sales.Customer" |
| 151 | + |
| 152 | + |
| 153 | + |
| 154 | +# Delete a table |
| 155 | +Invoke-DbaQuery -SqlInstance $SqlInstance -Database $DbName -Query "DROP TABLE Sales.Customer" |
| 156 | + |
| 157 | + |
| 158 | + |
| 159 | +# Confirm it is gone |
| 160 | +Invoke-DbaQuery -SqlInstance $SqlInstance -Database $DbName -Query "SELECT TOP 10 * FROM Sales.Customer" |
| 161 | + |
| 162 | + |
| 163 | + |
| 164 | +### |
| 165 | +# Review State of Database and backup |
| 166 | + |
| 167 | +# Let's check out the state of the database, size, last full and last log |
| 168 | +Get-DbaDatabase -SqlInstance $SqlInstance -Database $DbName | |
| 169 | + Select-Object Name, Size, LastFullBackup, LastLogBackup |
| 170 | + |
| 171 | + |
| 172 | + |
| 173 | +# We can get the snapshot name from the $Snapshot variable above, but what if we didn't know this ahead of time? |
| 174 | +# We can also get the snapshot name from the MEDIADESCRIPTION in the backup file. |
| 175 | +$Query = "RESTORE LABELONLY FROM DISK = '$BackupFile'" |
| 176 | +$Labels = Invoke-DbaQuery -SqlInstance $SqlInstance -Query $Query -Verbose |
| 177 | +$SnapshotName = (($Labels | Select-Object MediaDescription -ExpandProperty MediaDescription).Split('|'))[0] |
| 178 | +$ArrayName = (($Labels | Select-Object MediaDescription -ExpandProperty MediaDescription).Split('|'))[1] |
| 179 | + |
| 180 | + |
| 181 | + |
| 182 | +### |
| 183 | +# Start the Restore Process |
| 184 | + |
| 185 | +# Connect to vCenter |
| 186 | +$VIServer = Connect-VIServer -Server $VIServerName -Protocol https -Credential $Credential |
| 187 | +$TargetSQLServerVM = Get-VM -Server $VIServer -Name $TargetVM |
| 188 | +$VMESXiHost = Get-VMhost -VM $TargetSQLServerVM |
| 189 | + |
| 190 | + |
| 191 | + |
| 192 | +# Create a new volume from the selected snapshot of the source |
| 193 | +$SnapshotSuffix = (Get-Date).ToString("yyyyMMdd-HHmmss") |
| 194 | +$NewClonedVolumeName = "$($SourceVolumeName)-clone-$($SnapshotSuffix)" |
| 195 | +$SnapshotSourceVolumeName = $SnapshotName + ".$SourceVolumeName" |
| 196 | +New-Pfa2Volume -Array $FlashArray -Name $NewClonedVolumeName -SourceName $SnapshotSourceVolumeName -Overwrite $true |
| 197 | + |
| 198 | + |
| 199 | + |
| 200 | +# Present the new volume to the ESXi host group |
| 201 | +New-Pfa2Connection -Array $FlashArray -HostGroupName $FAHostGroupName -VolumeName $NewClonedVolumeName |
| 202 | + |
| 203 | + |
| 204 | + |
| 205 | +# ESXi host must now rescan storage |
| 206 | +Get-VMHostStorage -RescanAllHba -RescanVmfs -VMHost $VMESXiHost |
| 207 | + |
| 208 | + |
| 209 | + |
| 210 | +# Connect to EsxCli |
| 211 | +$EsxCli = Get-EsxCli -VMHost $VMESXiHost |
| 212 | + |
| 213 | + |
| 214 | + |
| 215 | +### Diagnostic |
| 216 | +# Retrieve a list of the snapshots that have been presented to the host (our cloned volume should be present) |
| 217 | +# $snapInfo = $EsxCli.storage.vmfs.snapshot.list() |
| 218 | +# $snapInfo | where-object { ($_.VolumeName -match $SourceDatastoreName) } |
| 219 | +# $snapInfo |
| 220 | + |
| 221 | + |
| 222 | + |
| 223 | +# Resignature the cloned datastore |
| 224 | +$EsxCli.storage.vmfs.snapshot.resignature($SourceDatastoreName) |
| 225 | + |
| 226 | + |
| 227 | + |
| 228 | +# Find the newly resignatured datastore name |
| 229 | +# NOTE: |
| 230 | +# After a datastore is resignatured, its name will be "snap-[GUID chars]-[original DS name]" |
| 231 | +# This is why the wildcard match below is needed. |
| 232 | +$clonedDatastore = (Get-Datastore | ? { $_.name -match 'snap' -and $_.name -match $SourceDatastoreName }) |
| 233 | + |
| 234 | +while ($clonedDatastore -eq $null) { |
| 235 | + # We may have to wait a little bit before the datastore is fully operational |
| 236 | + Start-Sleep -Seconds 5 |
| 237 | + $clonedDatastore = (Get-Datastore | Where-Object { $_.name -match 'snap' -and $_.name -match $SourceDatastoreName }) |
| 238 | +} |
| 239 | + |
| 240 | + |
| 241 | + |
| 242 | +# Must rescan storage again so ESXi hosts(s) can see the new cloned datastore |
| 243 | +Get-VMHostStorage -RescanAllHba -RescanVmfs -VMHost $VMESXiHost |
| 244 | + |
| 245 | + |
| 246 | + |
| 247 | +######################################## |
| 248 | +# Prepare SQL Server & Windows for the |
| 249 | +# snapshot overlay operation |
| 250 | +######################################## |
| 251 | +# Offline the database, which we'd have to do anyway if we were restoring a full backup |
| 252 | +$Query = "ALTER DATABASE $DbName SET OFFLINE WITH ROLLBACK IMMEDIATE" |
| 253 | +Invoke-DbaQuery -SqlInstance $SqlInstance -Database master -Query $Query |
| 254 | + |
| 255 | + |
| 256 | + |
| 257 | +# Offline the volume(s) in Windows |
| 258 | +Foreach ($TargetDisk in $TargetDisks) { |
| 259 | + Invoke-Command -Session $SqlServerSession -ScriptBlock { Get-Disk | Where-Object { $_.SerialNumber -eq $using:TargetDisk } | Set-Disk -IsOffline $True } |
| 260 | +} |
| 261 | + |
| 262 | + |
| 263 | + |
| 264 | +# Remove the original VMDK(s), within the original datastore |
| 265 | +Foreach ($SourceVMDKPath in $SourceVMDKPaths) { |
| 266 | + $harddisk = Get-HardDisk -VM $TargetSQLServerVM | ? { $_.FileName -match $SourceVMDKPath } |
| 267 | + Remove-HardDisk -HardDisk $harddisk -Confirm:$false -DeletePermanently |
| 268 | +} |
| 269 | + |
| 270 | + |
| 271 | + |
| 272 | +# Attach the new VMDK(s) from the newly cloned datastore back to the target VM |
| 273 | +Foreach ($SourceVMDKPath in $SourceVMDKPaths) { |
| 274 | + $newlyAttachedDisk = New-HardDisk -VM $TargetSQLServerVM -DiskPath "[$($clonedDatastore.Name)] $SourceVMDKPath" |
| 275 | +} |
| 276 | + |
| 277 | + |
| 278 | + |
| 279 | +# Online the volume(s) in Windows |
| 280 | +Foreach ($TargetDisk in $TargetDisks) { |
| 281 | + Invoke-Command -Session $SqlServerSession -ScriptBlock { Get-Disk | Where-Object { $_.SerialNumber -eq $using:TargetDisk } | Set-Disk -IsOffline $False } |
| 282 | +} |
| 283 | + |
| 284 | + |
| 285 | + |
| 286 | +# Restore the database with no recovery, which means we can restore LOG native SQL Server backups |
| 287 | +$Query = "RESTORE DATABASE $DbName FROM DISK = '$BackupFile' WITH METADATA_ONLY, REPLACE, NORECOVERY" |
| 288 | +Invoke-DbaQuery -SqlInstance $SqlInstance -Database master -Query $Query -Verbose |
| 289 | + |
| 290 | + |
| 291 | + |
| 292 | +# Let's check the current state of the database...its RESTORING |
| 293 | +Get-DbaDbState -SqlInstance $SqlInstance -Database $DbName |
| 294 | + |
| 295 | + |
| 296 | + |
| 297 | +# Restore the log backup. |
| 298 | +Restore-DbaDatabase -SqlInstance $SqlInstance -Database $DbName -Path $LogBackup.BackupPath -NoRecovery -Continue |
| 299 | + |
| 300 | + |
| 301 | + |
| 302 | +# Online the database |
| 303 | +$Query = "RESTORE DATABASE $DbName WITH RECOVERY" |
| 304 | +Invoke-DbaQuery -SqlInstance $SqlInstance -Database master -Query $Query |
| 305 | + |
| 306 | + |
| 307 | + |
| 308 | +# Verify Restore |
| 309 | +Invoke-DbaQuery -SqlInstance $SqlInstance -Database $DbName -Query "SELECT TOP 10 * FROM dbo.Recipes" |
| 310 | + |
| 311 | + |
| 312 | + |
| 313 | +######################### |
| 314 | +# Begin Clean Up Steps |
| 315 | +######################### |
| 316 | +$destinationDatastore = Get-Datastore -Name $SourceDatastoreName |
| 317 | + |
| 318 | + |
| 319 | + |
| 320 | +# Perform Storage vMotion to move the new VMDK disk(s) to the original source datastore. Should be fast |
| 321 | +# thanks to XCOPY |
| 322 | +Foreach ($SourceVMDKPath in $SourceVMDKPaths) { |
| 323 | + $newlyAttachedDisk = Get-HardDisk -VM $TargetSQLServerVM | ? { $_.FileName -match $SourceVMDKPath } |
| 324 | + Move-HardDisk -HardDisk $newlyAttachedDisk -Datastore $destinationDatastore -Confirm:$false |
| 325 | +} |
| 326 | + |
| 327 | + |
| 328 | + |
| 329 | +# Now that the VMDKs have been moved back to the primary datastore, we can remove the temporary cloned |
| 330 | +# datastore - this can take a min or two. |
| 331 | +# First, removing from VCenter |
| 332 | +Remove-Datastore -Datastore $clonedDatastore -VMHost $VMESXiHost -Confirm:$false |
| 333 | + |
| 334 | + |
| 335 | + |
| 336 | +# On FlashArray, disconnect the cloned volume from the ESXi cluster |
| 337 | +Remove-Pfa2Connection -Array $FlashArray -HostGroupName $FAHostGroupName -VolumeName $NewClonedVolumeName |
| 338 | + |
| 339 | + |
| 340 | + |
| 341 | +# On FlashArray, destroy the cloned volume |
| 342 | +Remove-Pfa2Volume -Array $FlashArray -Name $NewClonedVolumeName |
| 343 | + |
| 344 | + |
| 345 | + |
| 346 | +# Clean up |
| 347 | +Remove-PSSession $SqlServerSession |
| 348 | + |
| 349 | + |
| 350 | + |
0 commit comments